Skip to content

修复 macOS 圆角、x 关闭崩溃、托盘图标 - #1

Merged
30xX-i merged 23 commits into
30xX-i:mainfrom
mel0nyrame:main
Jun 19, 2026
Merged

30xX-i merged 23 commits into
30xX-i:mainfrom
mel0nyrame:main

Conversation

@mel0nyrame

@mel0nyrame mel0nyrame commented Jun 18, 2026 •

Copy link
Copy Markdown
Contributor

本 fork 适配 macOS,做以下修改:

  • 圆角不可见:title_widget 和 styles.py 全局 QWidget 背景改 transparent
  • x 关闭 SQLite 崩溃:NotificationWorker 改可中断 msleep(100) 循环,closeEvent 统一收尾,check_same_thread=False
  • x 按钮不退出进程:closeEvent 末尾调 app.quit()
  • 托盘图标走 NSStatusBar + template NSImage + NSMenu(绕过 QSystemTrayIcon)
  • 右下角斜线视觉突兀:删 ResizeHandle.paintEvent,保留 resize 功能
  • README 加 macOS 支持说明

mel0nyrame and others added 23 commits June 18, 2026 21:13
- 5 个跨平台函数:get_app_icon_path / setup_platform_app /
  get_default_font_family / show_notification / is_* 谓词
- macOS 上 NSApplication 激活策略修复(可选 pyobjc 依赖)
- pyobjc/win10toast 缺失时优雅降级
- 新增 _apply_platform_defaults 钩子,仅在 macOS 上生效
- 不覆盖用户在设置里显式改过的 font_family
- Windows 行为零回归
- main.py 删除顶层 ctypes 调用和写死 icon.ico
- MainWindow.__init__ 改用 platform_utils.get_app_icon_path()
- 修复 _add_category 弹窗图标相对路径在打包模式下找不到的 bug
- 资源缺失时统一回退到 Qt 标准图标
- _show_notification 改用 platform_utils.show_notification()
- requirements.txt 新增 pyobjc-framework-Cocoa 仅 darwin
- main.py 已在 Task 3 改用 setup_platform_app()(本 commit 不重复)
- platform_utils.show_notification duration 加 max(1, ...) 防截断为 0
- main_window.py 改用相对导入与其他 import 风格一致
- 删除 __init__ 内冗余的 QApplication import
- README macOS 章节加 emoji 前缀
- 新增 TrafficLightButtons 控件(红/黄/绿三圆 + hover 符号)
- 仅 macOS 启用,Windows/Linux 保留原有字符按钮
- 绿按钮复用 _toggle_ontop(切换始终置顶)
- 关闭/最小化信号连接到已有方法
- NotificationWorker.db.close() 从 stop() 移到 run() 退出循环后,
  保证 SQLite 连接只在创建它的线程里被 close(主线程调用会报
  ProgrammingError: SQLite objects created in a thread...)
- title_label 字体按平台分支:macOS 用 Helvetica Neue,
  Windows 保持 Forte(避免 Qt 启动时打 missing font family 警告)
- _toggle_ontop 改用 setWindowFlag(单数)替代 setWindowFlags(复数 |/~&),
  切换 WindowStaysOnTopHint 时不重建窗口,避免 macOS 上
  frameless hint 丢失 / 窗口位置错位
- macOS 上 QMainWindow.hide() 会让整个 application 进入 hidden 状态,
  showNormal() 无法恢复(窗口永远看不见)
- 新增 platform_utils.activate_application():
  在 macOS 上调 NSApplication.unhide_ + activateIgnoringOtherApps_
- TrayManager.show_window 在 showNormal 前调 activate_application()
- 其他平台 no-op,pyobjc 缺失时优雅降级
- 之前 unhide_ + activateIgnoringOtherApps_ 不够,某些 macOS
  上 NSWindow 仍 orderOut 不响应 show
- 遍历 NSApp.windows() 挨个 makeKeyAndOrderFront_ 拉到屏幕
- TrayManager.show_window 改用 show()(hide 的窗口不是 minimized,
  showNormal 不适用),仅当窗口已 minimized 时才用 showNormal
- FramelessWindowHint 窗口没有系统边框,鼠标无法拖边 resize
- 新增 QSizeGrip 子控件,16x16,放在 MainWindow 右下角
- resizeEvent 里重新定位 grip 到右下角
- setMinimumSize/setMaximumSize 保持 (320,480) / (600,900)
- Qt 自带的 QSizeGrip 在 FramelessWindowHint + 透明背景下
  会破坏 container 的 border-radius,导致窗口看起来变成方形
- 新增 src/ui/resize_handle.py:ResizeHandle QWidget
  - 16x16,自绘 3 条斜线(类似 macOS 原生 grip 样式)
  - 自己处理 mousePress/Move/Release 调父窗口 resize
  - 不影响其他 widget 的样式继承
- main_window.py 改用 ResizeHandle,移除 QSizeGrip import
- 同步删掉 fd2826b 残留的 is_macos unused import
圆角消失的真正根因(不是 QSizeGrip):
- styles.py 的 'QWidget { background: window }' 全局规则在 MainWindow 上
  setStyleSheet 时会覆盖 WA_TranslucentBackground,导致 MainWindow
  不透明,圆角之外的区域跟 container 同色,看起来'圆角没了'
- 修复:把全局 stylesheet 从 self(MainWindow)改设到 self.container
  - MainWindow 保持 WA_TranslucentBackground 透明
  - container 应用全局样式 + 圆角 + border
  - 圆角之外的区域透明,圆角内部 container 清晰可见
- 同时改 _setup_ui 和 _apply_theme

关闭时 SQLite 跨线程崩溃:
- NotificationWorker.stop() 在主线程调 db.close(),但 db 是子线程创建的,
  报 'SQLite objects created in a thread can only be used in that same thread'
- 修复:db.close() 移到 run() 退出 while 循环后(同线程 close)
- stop() 仅设置 running=False + wait()
根因:NotificationWorker.run() 用 time.sleep(30) 不可中断,
stop() 的 wait(1000) 必然超时返回,run() 收尾的 self.db.close()
在用户从 tray 退出前未执行;Python 进程退出时 gc 子线程
DatabaseManager.conn 触发跨线程 close。

修复:
- NotificationWorker.run() 用 100ms QThread.msleep 循环替代
  time.sleep(30),stop() 100ms 内即可生效
- stop() 改用 self.wait() 无超时,确保 run() 真正退出后才返回
- MainWindow.closeEvent 先 stop refresh_timer 再 stop worker,
  避免 refresh_timer 在 db close 后触发 _load_tasks
- TrayManager.quit_app 让 MainWindow 走 closeEvent 路径,
  统一收尾 refresh_timer + worker + db
- DatabaseManager 用 check_same_thread=False 作为安全网,
  每个连接仍只在创建它的线程里使用,功能不变
真实根因(Agent 1a 像素级验证):
- title_widget (main_window.py:193-200) 的 stylesheet 包含 QWidget { background: window },
  自身在 (0,0) 起点画不透明矩形,遮住 container 的 border-radius: 16px 圆角
- styles.py:8-15 的 QWidget { background: window } 全局规则通过 get_stylesheet()
  级联到所有 descendant widget,次要遮蔽源
- Qt 渲染层本质限制:border-radius 不裁切 layout-managed child widget 的 background 绘制
- 这是 Qt widget tree 通用行为,与 macOS / WA_NoSystemBackground / QMainWindow 无关

改动:
1. main_window.py:198 — title_widget 的 background: window 改为 background: transparent
2. styles.py:12 — QWidget 全局规则的 background: window 改为 background: transparent
   (container 自身的 QWidget#mainContainer 规则已显式设 background: window,不受影响)

验证:
- 应用前:4 角 30x30 区域 alpha=0 像素数 = 0/900
- 应用后:4 角 39-48/900,对应 16px 圆角弧线扫过的区域,4 角总和 181 透明像素
- (0,0) 从 (247, 249, 251, 255) 不透明变 (0, 0, 0, 0) 透明

引用: /Users/mac/Desktop/DeskNoteX/.git/sdd/investigate-round2-corners.md

Co-Authored-By: Claude <[email protected]>
用户反馈窗口右下角的 3 条灰色斜线看起来突兀。删掉 paintEvent 整个方法
让 ResizeHandle 不画任何东西(在 macOS 上完全看不到),但保留:

- enterEvent / leaveEvent:鼠标进入区域自动切换 SizeFDiagCursor,离开取消
- mousePressEvent / mouseMoveEvent / mouseReleaseEvent:按住拖动 resize 窗口
- 与父窗口 minimumSize / maximumSize 的边界处理

实际效果:用户看不到右下角任何东西,但鼠标移到右下角仍会变 resize 斜箭头,
按住拖动仍可调整窗口大小。

验证:
- ResizeHandle 实例仍存在(16x16,visible)
- 模拟 mousePress/Move 后窗口从 582x521 resize 到 600x551
- mouseRelease 正确清理 _dragging 状态
- corners 像素测试 PASS(alpha=0 count = 193,圆角仍可见)
- WA_TranslucentBackground + WA_NoSystemBackground 仍生效

Co-Authored-By: Claude <[email protected]>
用户反馈 menu bar(俗称"docker 栏")上 DeskNoteX 的 tray icon 是完整方形图片,
不像其他软件那样被 macOS 自动裁剪成圆形。

原因:之前用 QApplication.style().standardIcon(SP_ComputerIcon),这是彩色
Qt 内置图标,macOS 不会应用 menu bar 的圆形遮罩。

macOS menu bar 对 tray icon 的标准规则(template image):
1. 必须只含黑色 + alpha(其他颜色被系统忽略)
2. 系统自动反色(深色菜单栏下显示白色,浅色下显示黑色)
3. 系统自动应用圆形遮罩,让 icon 融入菜单栏视觉

修复:在 platform_utils.py 加 make_tray_icon(size=18),程序化绘制一个简单
的便签本图形(纯黑底 + 3 条白色横线),TrayManager 改用它替代 SP_ComputerIcon。

smoke test 验证:
- make_tray_icon(18) 返回 18x18 QIcon
- 中心像素纯黑(#000000)、边缘像素透明 —— 符合 template 规则
- TrayManager tray icon 正常设置,show() 不崩
- quit_app + MainWindow close 干净退出
- corners 像素测试仍 PASS(alpha=0 count = 193)

Co-Authored-By: Claude <[email protected]>
用户反馈 commit 45b326d 之后 menu bar 上还是方形图片。

根因:PyQt5 的 QIcon 在 macOS 上默认不会传 template 标记。即使 QPixmap
画的是纯黑 + alpha 像素,NSImage 包装时仍然是彩色标记,macOS 不会应用
圆形遮罩。

唯一可靠的方案:直接用 NSImage 创建,setTemplate_(True) 必须在画图前调
用,然后 lockFocus + NSBezierPath 绘制,最后 unlockFocus 拿到 TIFF 数据
转回 QPixmap 给 QIcon。

实现:
- platform_utils.py:make_tray_icon 分两条路径
  - macOS + pyobjc 可用:_make_tray_icon_nsimage(size) 创建 NSImage
    并 setTemplate_(True),绘制便签本图形,TIFFRepresentation 转 QPixmap
  - 其他平台 / pyobjc 缺失:fallback _make_tray_icon_qpixmap(size)

smoke test 验证:
- img.isTemplate() = True(关键证据,说明 setTemplate_(True) 生效)
- 18x18 QIcon 非空
- tray.tray.setIcon(make_tray_icon(18)) 正常
- corners 像素测试仍 PASS(alpha=0 count = 193)
- AppKit 缺失时 fallback 仍能工作

Co-Authored-By: Claude <[email protected]>
用户反馈点右上角 x 按钮无法完全关闭,窗口关了但进程还在(系统托盘残留)。

根因:main.py:34 设了 QApplication.setQuitOnLastWindowClosed(False)
让关闭窗口不退出进程(托盘常驻),但 closeEvent 没主动调 app.quit()
所以 x 按钮只触发关闭逻辑,不退出应用。

用户期望:x 按钮 = 完全退出整个应用(不依赖 tray)。

修复:closeEvent 末尾加 QApplication.instance().quit(),主动 quit
绕开 setQuitOnLastWindowClosed(False) flag。TrayManager.quit_app
走相同路径(window.close() 触发 closeEvent → 触发 quit),无需单独
修改。

smoke test 验证:
- monkey-patch app.quit,调 w.close(),quit 被调
- corners 像素测试仍 PASS
- NSImage tray icon 仍生效

Co-Authored-By: Claude <[email protected]>
用户上一轮 commit 后 menu bar 上仍是方形图片,反馈"还是正方形图片"。

根因(在 macOS 14+ 实测确认):
- PyQt5 QSystemTrayIcon.setIcon(QIcon) 内部把 QIcon 转 NSImage 时,
  强制把 isTemplate 设为 False,即使用户传的 NSImage.isTemplate = True
- macOS 14+ 的 NSKVONotifying_NSSceneStatusItem 没有 setTemplateImage: selector
  只有 setImage:,且 setImage_ 看到 isTemplate=False 就当彩色图片处理
- 结果:用户看到的 menu bar tray icon 是完整方形图片,而不是圆形遮罩

唯一可靠的方案:完全不用 QSystemTrayIcon,直接用 NSStatusBar 创建
statusItem,然后 setImage_(template_NSImage)+ setMenu_(NSMenu)。

实现:
- platform_utils.py:
  - _make_macos_target_class() 动态创建 NSObject 子类,
    用模块级 _macos_target_class_cache 缓存(pyobjc 不允许重复
    定义同名 Objective-C class)
  - create_macos_status_item(on_show, on_hide, on_quit, tooltip)
    创建 NSStatusBar statusItem + 纯黑 + alpha 便签本 NSImage(template)
    + NSMenu(显示/隐藏/退出)+ target-action 桥接
  - remove_macos_status_item(item, bar) 清理
  - show_macos_notification(title, message, duration_ms) 用 NSUserNotification
  - show_notification 改:macOS 走 show_macos_notification,Windows/Linux 不变
- managers.py TrayManager:
  - 加 is_macos() 分流:macOS 走 _init_macos(NSStatusBar),
    其他平台 _init_qsystemtray(原 QSystemTrayIcon 行为)
  - 加 backend 属性标识当前 backend('nsstatus' / 'qsystemtray')
  - 加 cleanup() 退出时清理 NSStatusItem

smoke test 验证:
- 创建 2 次 NSStatusBar 不冲突(模块级 cache)
- status_item.image.isTemplate() = True(关键证据,真 template)
- TrayManager backend = 'nsstatus'(确认 macOS 走新路径)
- corners 像素测试 PASS(alpha=0 count = 193)
- closeEvent 触发 app.quit()

Co-Authored-By: Claude <[email protected]>
以 c66a956 引入的 README 为基础(标题 + 简介 + 核心功能表 + 快速启动,
26 行简洁结构),不改任何章节,只在第二行简介末尾追加跨平台说明。

Co-Authored-By: Claude <[email protected]>
@30xX-i
30xX-i merged commit 424cac8 into 30xX-i:main Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants