# 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 中键退出工具
一款简洁实用的 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