# MiddleQ **Repository Path**: ProgHub/MiddleQ ## Basic Information - **Project Name**: MiddleQ - **Description**: 在Mac上使用鼠标中键点击Dock栏的应用图标以快速退出该应用 - **Primary Language**: Swift - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-02-27 - **Last Updated**: 2026-03-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README

MiddleQ - macOS Dock 中键退出工具

MiddleQ Logo

一款简洁实用的 macOS 工具,让你可以通过鼠标中键快速退出 Dock 中的应用程序。
当前版本:v1.3.0 | 系统要求:macOS 13.0+

特性亮点安装使用方法配置开发故障排除

--- ## 🌟 特性亮点 - **⚡ 快捷高效**: 在 Dock 上中键点击即可秒退应用,无需右键菜单或 Cmd+Q - **🧠 智能识别**: 自动识别点击的 Dock 图标并终止对应应用进程 - **🔄 自动扫描**: 首次启动自动扫描所有已安装应用,智能建立映射关系 - **⚙️ 灵活配置**: 支持手动添加自定义应用映射,满足特殊需求 - **🛡️ 安全防护**: 智能保护 Finder(访达)不被意外退出 - **🚀 开机自启**: 支持开机自动启动,后台静默运行(macOS 13.0+) - **💻 双架构支持**: Universal Binary 同时支持 Intel 和 Apple Silicon - **🍃 轻量级**: 菜单栏常驻,资源占用极低,几乎无感知 --- ## 📦 安装 ### 方法一:下载预编译版本(推荐) 1. 从 [Releases](https://github.com/yourusername/MiddleQ/releases) 页面下载最新的 `.dmg` 文件 2. 双击打开 DMG 文件 3. 将 `MiddleQ.app` 拖拽到 `Applications` 文件夹 4. 首次运行时系统会提示安全性警告,前往 **系统偏好设置 > 隐私与安全性** 允许运行 ### 方法二:源码编译 ```bash # 克隆仓库 git clone https://github.com/yourusername/MiddleQ.git cd MiddleQ # 编译应用(自动同步版本号) ./build.sh # 打包为 DMG(可选) ./package_dmg.sh ``` 编译产物: - `MiddleQ.app` - 可直接运行的应用 - `MiddleQ_v1.3.0.dmg` - 安装包(用于分发) --- ## 🚀 使用方法 ### 基本操作 1. **启动 MiddleQ** - 在 Applications 文件夹中找到并运行 `MiddleQ.app` - 运行后会在菜单栏显示鼠标图标 🖱️ 2. **授予辅助功能权限** - 系统会弹出权限请求对话框 - 前往 **系统偏好设置 > 隐私与安全性 > 辅助功能** - 点击左下角锁图标解锁 - 添加并勾选 `MiddleQ.app` - 重启 MiddleQ 应用 3. **使用中键退出应用** - 在 Dock 中使用鼠标中键(滚轮按键)点击任意运行中的应用图标 - 该应用会立即退出 ### 菜单栏功能 点击菜单栏的 🖱️ 图标可以访问以下功能: | 功能 | 快捷键 | 说明 | |------|--------|------| | **开机自动启动** | - | 开启/关闭开机自启动 | | **扫描所有应用** | `⌘S` | 重新扫描并更新应用映射 | | **编辑配置文件** | `⌘E` | 打开配置文件进行编辑 | | **重新加载配置** | `⌘R` | 编辑完成后重新加载配置 | | **退出 MiddleQ** | `⌘Q` | 完全退出应用 | --- ## ⚙️ 配置 ### 配置文件位置 ``` ~/Library/Application Support/MiddleQ/config.json ``` ### 🆕 自动扫描功能 **MiddleQ v1.3.0 新增功能**:首次启动时会自动扫描以下目录的所有应用程序: - `/Applications`(系统应用目录) - `~/Applications`(用户应用目录) 自动为所有检测到的应用建立 **应用名称 → Bundle Identifier** 的映射关系,**无需手动配置**。 ### 手动触发扫描 如果需要更新应用映射,可以点击菜单栏图标,选择 **"扫描所有应用"**(或按 `⌘S`)。 ### 手动配置说明 对于某些不生效的应用,可以手动添加映射: #### 方法一:使用终端命令获取 Bundle ID ```bash # 方式 1:使用 mdls 命令(推荐) mdls -name kMDItemCFBundleIdentifier /Applications/应用名称.app # 方式 2:使用 defaults 命令 defaults read /Applications/应用名称.app/Contents/Info.plist CFBundleIdentifier ``` #### 方法二:通过菜单栏编辑 1. 点击菜单栏图标 > **"编辑配置文件"**(或按 `⌘E`) 2. 在打开的 JSON 文件中添加映射 3. 保存文件(`⌘S`) 4. 点击菜单栏图标 > **"重新加载配置"**(或按 `⌘R`) ### 配置示例 ``` { "Chrome": "com.google.Chrome", "微信": "com.tencent.xinWeChat", "QQ": "com.tencent.qq", "钉钉": "com.alibaba.DingTalkMac", "Visual Studio Code": "com.microsoft.VSCode" } ``` > 💡 **提示**: 大多数应用已被自动扫描添加,仅在特殊情况下需要手动配置。 --- ## 🔧 开发 ### 技术栈 - **语言**: Swift 5+ - **平台**: macOS 13.0+ - **架构支持**: Intel (x86_64) + Apple Silicon (arm64) 通用二进制 - **依赖框架**: - Cocoa (AppKit) - 应用界面 - ApplicationServices - 辅助功能 API - ServiceManagement - SMAppService 开机自启 - UserNotifications - 用户通知 ### 项目结构 ``` MiddleQ/ ├── src/ │ ├── main.swift # 程序入口 │ └── AppDelegate.swift # 应用委托类(核心逻辑,361 行) ├── config/ │ └── Info.plist # 应用配置文件(Bundle ID、版本等) ├── resource/ # 资源文件 │ ├── AppIcon.icns # 应用图标(723KB) │ ├── AppIcon.png # 应用图标 PNG 版本 │ ├── status_icon.png # 状态栏图标(1x) │ └── status_icon@2x.png # 状态栏图标(2x) ├── build.sh # 编译脚本(支持双架构) ├── package_dmg.sh # DMG 打包脚本 ├── version.sh # 版本管理脚本 └── README.md # 说明文档 ``` ### 编译构建 #### 快速开始 ```bash # 编译应用(自动同步版本号) ./build.sh ``` #### 编译流程详解 1. **同步版本号** - 执行 `version.sh` 更新各配置文件 2. **清理旧版本** - 删除之前的 `MiddleQ.app` 3. **分别编译双架构** - Intel x86_64: `swiftc -target x86_64-apple-macos11.0 ...` - Apple Silicon arm64: `swiftc -target arm64-apple-macos11.0 ...` 4. **合并为通用二进制** - 使用 `lipo` 工具 5. **复制资源文件** - Info.plist、图标等 6. **代码签名** - `codesign --force --deep --sign -` 输出信息示例: ``` ✨ 构建完成:MiddleQ.app 📁 架构信息:Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64] ``` ### 打包分发 ```bash # 打包为 DMG 安装包 ./package_dmg.sh ``` **打包流程**: 1. 创建临时 HFS+ 磁盘镜像 2. 复制 `MiddleQ.app` 到镜像 3. 创建 `/Applications` 快捷方式 4. 弹出 Finder 窗口供用户调整布局 5. 转换为只读压缩 DMG 输出:`MiddleQ_v1.3.0.dmg` ### 版本管理 #### 更新版本流程 ```bash # 1. 修改版本号 echo "1.3.0" > version.txt # 2. 同步版本到所有配置文件 ./version.sh # 3. 执行构建 ./build.sh ``` #### 版本同步机制 `version.sh` 会自动更新: - ✅ `version.txt` - 存储当前版本号 - ✅ `config/Info.plist` - CFBundleShortVersionString 和 CFBundleVersion - ✅ `package_dmg.sh` - DMG 文件名中的版本号 ### 代码说明 #### 💡 实现原理详解 **MiddleQ 如何实现中键点击 Dock 图标退出应用?** 整个流程可以分为四个核心步骤: ``` 用户中键点击 → 全局事件监听 → 坐标检测 → Dock 元素识别 → 应用匹配 → 终止进程 ``` ##### 步骤 1: 全局鼠标事件监听 MiddleQ 使用 macOS 的 `CGEvent.tapCreate` API 创建一个全局事件监听器,监听所有鼠标中键按下事件: ```swift func startEventTap() { // 监听其他鼠标按键按下事件(包括中键) let mask = (1 << CGEventType.otherMouseDown.rawValue) guard let tap = CGEvent.tapCreate( tap: .cgSessionEventTap, // 会话级事件监听 place: .headInsertEventTap, // 在事件队列头部插入(优先处理) options: .defaultTap, // 默认选项 eventsOfInterest: CGEventMask(mask), callback: { (proxy, type, event, refcon) -> Unmanaged? in // 回调函数:检查是否为中键 if event.getIntegerValueField(.mouseEventButtonNumber) == 2 { if let delegate = NSApp.delegate as? AppDelegate { // 处理 Dock 点击 if delegate.handleDockClick(at: event.location) { return nil // 拦截事件,不再传递给其他应用 } } } return Unmanaged.passUnretained(event) // 放行其他事件 }, userInfo: nil ) else { return } // 将事件监听器添加到 RunLoop eventTap = tap runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0) CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes) CGEvent.tapEnable(tap: tap, enable: true) } ``` **关键点**: - `.cgSessionEventTap`: 仅监听当前用户会话的事件 - `.headInsertEventTap`: 确保第一时间捕获事件 - 按钮编号 `2`: 代表鼠标中键(滚轮按键) - 返回 `nil`: 拦截事件,防止传递给目标应用 ##### 步骤 2: 坐标检测与 Dock 元素识别 当检测到中键点击后,使用 Accessibility API 获取点击位置的 UI 元素信息: ```swift func handleDockClick(at point: CGPoint) -> Bool { // 创建系统级辅助功能元素(代表整个屏幕) let systemWideElement = AXUIElementCreateSystemWide() var clickedElement: AXUIElement? // 获取点击坐标处的 UI 元素 AXUIElementCopyElementAtPosition( systemWideElement, Float(point.x), Float(point.y), &clickedElement ) guard let element = clickedElement else { return false } // 检查元素角色是否为 Dock 项 var role: CFTypeRef? AXUIElementCopyAttributeValue(element, kAXRoleAttribute as CFString, &role) guard (role as? String) == "AXDockItem" else { return false } // 获取 Dock 图标的标题(应用名称) var titleValue: CFTypeRef? AXUIElementCopyAttributeValue(element, kAXTitleAttribute as CFString, &titleValue) let dockTitle = titleValue as? String ?? "" if dockTitle.isEmpty { return false } print("📍 点击图标:[\(dockTitle)]") // ... 后续处理 } ``` **关键点**: - `AXUIElementCreateSystemWide()`: 创建系统范围的辅助功能上下文 - `kAXRoleAttribute`: 获取 UI 元素的角色(必须是 `AXDockItem`) - `kAXTitleAttribute`: 获取 Dock 图标的标题(应用名称) - 返回值 `Bool`: `true` 表示成功处理,`false` 表示忽略 ##### 步骤 3: 智能应用匹配 找到 Dock 图标后,需要匹配到实际运行的应用进程。MiddleQ 使用两层匹配策略: ```swift let runningApps = NSWorkspace.shared.runningApplications // 第一层:使用配置映射表精确匹配 if let mappedBundleId = appMapping[dockTitle] { if let app = runningApps.first(where: { $0.bundleIdentifier == mappedBundleId }) { print("✅ 映射命中:\(mappedBundleId)") app.terminate() return true } } // 第二层:兜底模糊匹配(通过应用名称) if let app = runningApps.first(where: { $0.localizedName?.lowercased() == dockTitle.lowercased() }) { print("✅ 自动命中:\(dockTitle.lowercased())") app.terminate() return true } return false ``` **匹配流程**: 1. **精确匹配**: 从 `appMapping` 字典中查找 Bundle ID(最准确) 2. **模糊匹配**: 直接比较应用名称(覆盖未配置的应用) 3. **大小写不敏感**: 使用 `.lowercased()` 提高匹配成功率 ##### 步骤 4: 安全保护与应用终止 确认目标应用后,调用 `terminate()` 方法安全退出: ```swift // 🛡️ 特殊保护 Finder(访达) if dockTitle == "访达" || dockTitle.lowercased() == "finder" { print("🛡️ 已阻止退出访达 (Finder)") return true // 返回 true 表示事件已处理,但不执行退出 } // 终止应用进程 app.terminate() ``` **为什么 Finder 需要特殊保护?** - Finder 是 macOS 的核心系统进程 - 意外退出会导致桌面、文件管理器等功能暂时失效 - Finder 会自动重启,但会造成用户体验中断 --- #### 完整流程图 ``` graph TB A[用户中键点击 Dock] --> B[CGEvent.tapCreate 捕获事件] B --> C{按钮编号 = 2?} C -->|是 | D[AXUIElementCopyElementAtPosition
获取点击位置元素] C -->|否 | E[放行事件] D --> F{元素角色 = AXDockItem?} F -->|否 | E F -->|是 | G[获取 Dock 图标标题
应用名称] G --> H{是否为 Finder?} H -->|是 | I[🛡️ 阻止退出
返回成功] H -->|否 | J[查询 appMapping 映射表] J --> K{找到 Bundle ID?} K -->|是 | L[通过 Bundle ID 精确匹配应用] K -->|否 | M[通过应用名称模糊匹配] L --> N{找到运行中的应用?} M --> N N -->|是 | O[调用 app.terminate
退出应用] N -->|否 | P[无操作,返回失败] O --> Q[返回成功,拦截事件] style I fill:#ff6b6b style O fill:#51cf66 style Q fill:#51cf66 ``` --- #### 关键技术点 1. **全局事件监听** ```swift CGEvent.tapCreate( tap: .cgSessionEventTap, place: .headInsertEventTap, eventsOfInterest: CGEventMask(1 << CGEventType.otherMouseDown.rawValue) ) ``` 2. **Accessibility API 使用** ```swift AXUIElementCopyElementAtPosition(systemWideElement, x, y, &element) AXUIElementCopyAttributeValue(element, kAXRoleAttribute, &role) ``` 3. **现代开机自启 (SMAppService)** ```swift if #available(macOS 13.0, *) { let service = SMAppService.mainApp try service.register() // 注册开机启动 } ``` --- ## 🔒 安全性和权限 ### 所需权限 | 权限类型 | 用途 | 授权方式 | |---------|------|---------| | **辅助功能权限** | 监听全局鼠标事件、访问 Dock 元素信息 | 系统弹窗引导至隐私设置 | | **文件系统访问** | 读写配置文件 (`~/Library/Application Support/MiddleQ/`) | 应用沙盒自动授权 | ### 隐私承诺 - ✅ **零数据收集**: 不收集、上传任何用户数据 - ✅ **零网络连接**: 纯本地应用,无任何网络请求 - ✅ **开源透明**: 代码完全开源,可随时审计 - ✅ **最小权限**: 仅请求必要权限,无过度索权 --- ## 🐛 故障排除 ### 常见问题 #### Q: 应用无法正常工作 **A**: 请按以下步骤检查: 1. **检查辅助功能权限** ``` 系统偏好设置 > 隐私与安全性 > 辅助功能 ``` 确保 `MiddleQ.app` 已勾选 2. **重启应用** - 退出 MiddleQ(菜单栏 > 退出 MiddleQ) - 重新启动 MiddleQ.app 3. **查看日志** ```bash log stream --predicate 'process == "MiddleQ"' --info ``` #### Q: 某些应用无法通过中键退出 **A**: 1. **刷新应用映射** - 点击菜单栏 > "扫描所有应用" - 等待扫描完成 2. **手动添加映射** ```bash # 获取 Bundle ID mdls -name kMDItemCFBundleIdentifier /Applications/问题应用.app # 编辑配置文件添加映射 # 菜单栏 > 编辑配置文件 ``` 3. **检查应用名称** - 确认 Dock 中显示的名称与配置 key 一致 - 注意大小写和空格 #### Q: Finder 被意外退出了 **A**: 此情况已被特别防护,正常情况下不会发生。如果确实遇到: 1. 检查是否是特殊情况(如 Finder 崩溃) 2. 查看日志确认是否为其他原因 3. 提交 Issue 报告详细情况 #### Q: 开机自启功能不可用 **A**: - **系统版本要求**: macOS 13.0 或更高版本 - **检查方法**:  > 关于本机 > 查看 macOS 版本 - **替代方案**: 旧版本系统可手动添加到登录项 #### Q: 配置文件格式错误 **A**: 1. **恢复默认配置** ```bash # 删除配置文件 rm ~/Library/Application\ Support/MiddleQ/config.json # 重启应用会自动创建新文件 ``` 2. **JSON 语法检查** - 确保使用双引号 `"` - 键值对用冒号 `:` 分隔 - 最后一项后不加逗号 ### 日志查看 #### 实时日志 ```bash # 查看 MiddleQ 的实时调试信息 log stream --predicate 'process == "MiddleQ"' --info ``` #### 历史日志 ```bash # 查看最近的日志 log show --predicate 'process == "MiddleQ"' --last 1h ``` #### 终端输出 如果从终端启动应用,会看到详细输出: ```bash # 从终端启动(用于调试) ./MiddleQ.app/Contents/MacOS/MiddleQ ``` 典型输出示例: ``` 🔍 开始扫描应用程序目录... ➕ 已添加:Chrome -> com.google.Chrome ➕ 已添加:微信 -> com.tencent.xinWeChat ✅ 扫描完成!新增 125 个映射,跳过 0 个已存在映射 ✅ 权限已就绪 📍 点击图标:[Chrome] ✅ 映射命中:com.google.Chrome ``` --- ## 📄 许可证 本项目采用 **MIT 许可证** - 查看 [LICENSE](LICENSE) 文件了解详情 **简要说明**: - ✅ 可自由使用、修改、分发 - ✅ 可用于商业用途 - ❗ 需保留原始版权声明 - ❗ 不提供任何担保 --- ## 🙏 致谢 - 感谢所有贡献者和用户的支持 - 特别感谢开源社区提供的各种工具和库 - 基于 macOS Accessibility API 实现 --- ## 📞 联系方式 如有问题或建议,请通过以下方式联系: - **GitHub Issues**: [提交问题](https://github.com/yourusername/MiddleQ/issues) - **邮箱**: wang.chaofan@foxmail.com - **项目主页**: https://github.com/yourusername/MiddleQ ---

Made with ❤️ for macOS users
版本:1.3.0 | 最后更新:2026-03-04