diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..63156ed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,49 @@ +name: 🐛 创建错误报告 +description: 此处只受理 Bug 报告 +title: "[Bug] 描述问题的标题" +labels: bug +body: + - type: markdown + attributes: + value: | + > 💡 请务必遵循标题格式。例如:[Bug] 窗口无法隐藏 + - type: checkboxes + attributes: + label: 议题条件 + description: 在你开始之前,请花几分钟时间确保你已如实完成以下工作,以便让我们更高效地沟通。 + options: + - label: 我确认即使在最新正式版中存在该问题。 + required: true + - label: 我确认已在 [Issues](/IvanHanloth/Boss-Key/issues) 进行搜索并确认没有人反馈过相同的Bug。 + required: true + - label: 我确认已经总结议题内容并按规范设置此Issue的标题 + required: true + - type: input + attributes: + label: 系统环境 + description: 在哪个平台(Windows/Linux/MacOS)上运行?如果是直接使用Python运行,则尽量同时提供Python版本 + placeholder: 如:Windows 11 Python 3.11.9 + validations: + required: true + - type: input + attributes: + label: 使用版本 + description: 请提供您当前使用的 Boss Key 版本号。 + placeholder: 如:v2.0.3 + validations: + required: true + - type: textarea + attributes: + label: 问题描述 + description: 请提供详细的问题描述和操作步骤等信息,以便我们也能够更轻松地将问题复现。 + validations: + required: true + - type: textarea + attributes: + label: 错误日志 + description: 如果有错误日志,请提供以便更好地定位问题。 + render: auto + - type: textarea + attributes: + label: 截图补充 + description: 如上述仍然无法准确地表述问题,可提供必要的截图(可直接粘贴上传) \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..f4da8a8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: ❓提出问题 + url: https://github.com/IvanHanloth/BossKey/discussions + about: 提问项目相关的其他问题 + - name: 📄官方文档 + url: https://ivanhanloth.github.io/Boss-Key/ + about: 在创建 Issue 之前,请仔细查阅 Bili23 Downloader 使用手册以确保正确使用。 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..08dc145 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,30 @@ +name: ✨ 提出建议 +description: 此处支持提出建议、请求新功能 +title: "[Feature] 简要描述你的建议或请求" +labels: enhancement +body: + - type: markdown + attributes: + value: | + > 💡 请务必遵循标题格式。例如:[Feature] 希望支持鼠标隐藏 + - type: checkboxes + attributes: + label: 议题条件 + description: 在你开始之前,请花几分钟时间确保你已如实完成以下工作,以便让我们更高效地沟通。 + options: + - label: 我确认即使在最新正式版中存在该问题。 + required: true + - label: 我确认已在 [Issues](/IvanHanloth/Boss-Key/issues) 进行搜索并确认没有人反馈过相同的Bug。 + required: true + - label: 我确认已经总结议题内容并按规范设置此Issue的标题 + required: true + - type: textarea + attributes: + label: 详细描述 + description: 请详细描述你的建议或需求。 + validations: + required: true + - type: textarea + attributes: + label: 其他 + description: 如上述仍然无法准确地表述问题,可提供必要的截图(可直接粘贴上传) \ No newline at end of file diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml index 6768da1..f42ac38 100644 --- a/.github/workflows/tag-release.yml +++ b/.github/workflows/tag-release.yml @@ -82,6 +82,7 @@ jobs: draft: false prerelease: false generate_release_notes: true + discussion_category_name: announcements compile-to-installer: runs-on: windows-latest diff --git a/README.md b/README.md index a049f33..019f039 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ - multifile - 多文件版,标准的程序版本,所有依赖文件被压缩到一个压缩包里,解压后可用 - installer - 安装程序(推荐),完整封装的Boss-Key程序安装程序,提供一键安装、更新、卸载,可以更高效的管理Boss-Key程序 +部分版本会提供win7系统的软件包,带有win7标识的可以在Windows 7系统上运行 ### 基础使用 @@ -75,6 +76,10 @@ 注意!此功能仍在测试,启用后会导致窗口隐藏出现延时 +#### 文件路径匹配功能 +Boss Key默认使用窗口标题等多因素进行窗口匹配以确保精确隐藏窗口,但是有时可能出现希望根据可执行文件路径来进行匹配: +即只要是由同一个程序启动的窗口都隐藏,此时就可以启用“文件路径匹配”选项进行模糊匹配 + #### 同时隐藏当前活动窗口 启用该功能后,按下隐藏窗口热键时,除了会隐藏绑定的窗口,还会隐藏当前被激活的窗口。 @@ -91,6 +96,8 @@ 如果你安装了python环境,也可以尝试克隆仓库后,运行Boss-Key.py文件来启动窗口 +如果你是windows7系统,部分版本会提供win7系统的软件包,可以下载带有win7标识的软件使用 + **为什么我一直没办法检查更新** 检查更新的服务依赖Github提供的Github Page,如果你的电脑无法访问[https://ivanhanloth.github.io/Boss-Key](https://ivanhanloth.github.io/Boss-Key),则无法检查更新 @@ -118,7 +125,8 @@ Boss-Key │   │   ├── about.py 关于页面 │   │   ├── record.py 录制热键页面 │   │   ├── setting.py 设置页面 -│   │   └── taskbar.py 托盘图标 +│   │   ├── taskbar.py 托盘图标 +│   │   └── window_restore.py 窗口恢复工具页面 │   └── Boss-Key.py 项目入口文件 ├── src 网站相关目录 │   └── static 静态文件目录 @@ -129,11 +137,33 @@ Boss-Key └── requirements.txt 项目依赖文件 ``` +## 分支说明及规范 +为了高效、安全的完成开发,我们对分支、合并等有一定限制。 + +仓库有两个主要分支`main`和`dev`分支: +- `main`包含正式发布版本的源代码,此分支不能直接提交,原则上只能已完成版本功能开发且决定发布新版本并通过测试后,从`dev`分支合并 +- `dev`用于存储所有正在开发的功能的源代码,此分支不能直接提交,原则上只能包含已经完成开发并通过测试的功能 + +其他分支则用于正常功能开发、提交等。 + +新功能、Bug修复等操作应该通过PR提交至`dev`分支,经过代码审查后合并。最后经过测试后,统一通过PR合并至`main`分支。 + +分支命名没有强制规范,但是为了方便维护,我们推荐使用类似`类型/功能`的命名方式,例如`feat/checkUpdate`、`fix/hideWindow`等。 + +## Project规范 +我们通常使用github提供的Project功能进行统一项目规划管理。 + +应该尽量将Issue-PullRequest-Project三者相互关联,以便实现统一管理。 ## 已知问题 - 无法隐藏部分游戏窗口,可能由于游戏窗口加密导致 ## 更新日志 +**V2.0.3 (更新于2025/4/5)** +- 新增文件路径匹配选项 +- 新增窗口恢复工具 +- 修复状态列表问题 +- 优化运行速度和占用 **V2.0.2 (更新于2025/2/2)** - 新增全选窗口选项 @@ -141,6 +171,7 @@ Boss-Key - 修复无法使用热键关闭程序的问题 - 优化界面UI设计 - 优化配置文件读取 + **V2.0.1 (更新于2025/1/17)** - 修改已在运行时的提醒 - 修复窗口销毁后再打开隐藏失败的问题 diff --git a/main/Boss-Key.py b/main/Boss-Key.py index 0e0c780..6b3b122 100644 --- a/main/Boss-Key.py +++ b/main/Boss-Key.py @@ -1,12 +1,13 @@ # nuitka-project: --onefile -# nuitka-project: --windows-console-mode=disable # nuitka-project: --standalone +# nuitka-project: --assume-yes-for-downloads # nuitka-project: --follow-import-to=core -# nuitka-project: --windows-icon-from-ico=icon.ico -# nuitka-project: --windows-product-name="Boss Key" -# nuitka-project: --windows-file-description="Boss Key Application" +# nuitka-project: --follow-import-to=GUI # nuitka-project: --copyright="Copyright (C) 2025 Ivan Hanloth All Rights Reserved. " -# nuitka-project: --windows-company-name="Ivan Hanloth" +# nuitka-project: --product-name="Boss Key" +# nuitka-project: --file-description="Boss Key Application" +# nuitka-project: --windows-icon-from-ico=icon.ico +# nuitka-project: --windows-console-mode=disable from GUI import setting, taskbar from core import listener @@ -16,19 +17,32 @@ import psutil import wx from core.config import Config +import platform +import atexit -ctypes.windll.shcore.SetProcessDpiAwareness(2) # Win10 and Win8 -ctypes.windll.user32.SetProcessDPIAware() #Win7 and below +if platform.system() == "Windows": + if platform.release() == "7": + ctypes.windll.user32.SetProcessDPIAware() + else: + ctypes.windll.shcore.SetProcessDpiAwareness(2) class APP(wx.App): def __init__(self): wx.App.__init__(self) - + def clean(): + try: + Config.HotkeyListener.Close() + except: + pass + atexit.register(clean) + # 设置语言环境为中文 self.locale = wx.Locale(wx.LANGUAGE_CHINESE_SIMPLIFIED) + self.SetAppName(Config.AppName) self.SetAppDisplayName(Config.AppName) self.SetVendorName(Config.AppAuthor) + lock=os.path.join(os.path.dirname(sys.argv[0]),"Boss-Key.lock") if self.is_already_running(lock): ask=wx.MessageBox("Boss Key 可能已在运行\n点击“确定”继续运行新的Boss-Key程序\n点击“取消”直接关闭此窗口","Boss Key", wx.OK | wx.ICON_INFORMATION | wx.CANCEL | wx.CANCEL_DEFAULT) @@ -42,37 +56,41 @@ def write_pid(self,name): with open(name, "w") as f: f.write(str(psutil.Process().pid)) - def is_already_running(self,name): - if os.path.exists(name): - with open(name, "r") as f: - pid=f.read() - if pid == "": + def is_already_running(self, name): + if not os.path.exists(name): + self.write_pid(name) + return False + + with open(name, "r") as f: + pid = f.read() + + if pid == "": + self.write_pid(name) + return False + + try: + process = psutil.Process(int(pid)) + if not process.is_running(): self.write_pid(name) - else: - try: - process=psutil.Process(int(pid)) - if process.is_running(): - this_name=psutil.Process(psutil.Process().pid).name() #获取当前进程名 - if this_name==process.name(): - return True - else: - self.write_pid(name) - return False - else: - self.write_pid(name) - return False - except: - self.write_pid(name) - return False - else: + return False + + this_name = psutil.Process(psutil.Process().pid).name() # 获取当前进程名 + if this_name == process.name(): + return True + self.write_pid(name) + return False + except: + self.write_pid(name) + return False + if __name__ == '__main__': app = APP() + Config.SettingWindowId = wx.NewIdRef() Config.TaskBarIcon=taskbar.TaskBarIcon() Config.HotkeyListener=listener.HotkeyListener() - Config.SettingWindow=setting.SettingWindow() + setting.SettingWindow(Config.SettingWindowId) if Config.first_start: - Config.SettingWindow.Show() + wx.FindWindowById(Config.SettingWindowId).Show() app.MainLoop() - diff --git a/main/GUI/about.py b/main/GUI/about.py index ba278e8..64187cd 100644 --- a/main/GUI/about.py +++ b/main/GUI/about.py @@ -22,8 +22,8 @@ def Show(self): wx.adv.AboutBox(self.info) class UpdateWindow(wx.Dialog): - def __init__(self): - super().__init__(None, title="检查更新 - Boss Key", style=wx.DEFAULT_DIALOG_STYLE | wx.STAY_ON_TOP | wx.RESIZE_BORDER) + def __init__(self,id=None): + super().__init__(None,id=id, title="检查更新 - Boss Key", style=wx.DEFAULT_DIALOG_STYLE | wx.STAY_ON_TOP | wx.RESIZE_BORDER) self.SetIcon(wx.Icon(wx.Image(Config.icon).ConvertToBitmap())) self.init_Load_UI() @@ -52,15 +52,15 @@ def init_Load_UI(self): self.panel.SetSizer(self.sizer) def onCheckUpdate(self): - def checkUpdate(): + def check(): try: - info = check_update() + info = checkUpdate() except: wx.CallAfter(self.init_error_UI) return wx.CallAfter(self.init_real_UI,info) - threading.Thread(target=checkUpdate).start() + threading.Thread(target=check).start() def init_real_UI(self,info): ## 清空原有元素 @@ -114,4 +114,4 @@ def Btn_click(self,url,is_latest): if ask == wx.CANCEL: return webbrowser.open(url) - self.Hide() + self.Hide() \ No newline at end of file diff --git a/main/GUI/record.py b/main/GUI/record.py index 0b35ae0..fd135e8 100644 --- a/main/GUI/record.py +++ b/main/GUI/record.py @@ -12,6 +12,11 @@ class RecordedHotkey: confirm = False class RecordWindow(wx.Dialog): + # 定义组件ID常量 + ID_CONFIRM_BTN = wx.NewIdRef() + ID_STATUS_TEXT = wx.NewIdRef() + ID_KEY_TEXT = wx.NewIdRef() + def __init__(self): wx.Dialog.__init__(self, None, title="录制热键 - Boss Key", style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER) self.SetIcon(wx.Icon(wx.Image(Config.icon).ConvertToBitmap())) @@ -28,7 +33,7 @@ def __init__(self): RecordedHotkey.recording = True RecordedHotkey.keys_recorded = set() RecordedHotkey.keys_pressing = set() - keyboard.hook(self.onKeyEvent,suppress=True) + keyboard.hook(self.onKeyEvent, suppress=True) def init_UI(self): # 创建面板 @@ -39,45 +44,50 @@ def init_UI(self): # 创建一个静态文本,居中对齐 hbox1 = wx.BoxSizer(wx.HORIZONTAL) - self.text = wx.StaticText(panel, label="正在录制热键",style=wx.ALIGN_CENTER) - self.text.SetFont(wx.Font(16, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)) - self.text.SetSize(self.text.GetBestSize()) - hbox1.Add(self.text, proportion=1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT, border=20) + status_text = wx.StaticText(panel, self.ID_STATUS_TEXT, label="正在录制热键", style=wx.ALIGN_CENTER) + status_text.SetFont(wx.Font(16, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)) + status_text.SetSize(status_text.GetBestSize()) + hbox1.Add(status_text, proportion=1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT, border=20) vbox.Add(hbox1, proportion=1, flag=wx.EXPAND|wx.TOP|wx.BOTTOM, border=20) # 创建一个静态文本,居中对齐,字号变大,加粗 hbox2 = wx.BoxSizer(wx.HORIZONTAL) - self.text2 = wx.StaticText(panel,style=wx.ALIGN_CENTER, label="请按下热键") - self.text2.SetFont(wx.Font(22, wx.FONTFAMILY_SCRIPT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)) - self.text2.SetSize(self.text2.GetBestSize()) - hbox2.Add(self.text2, proportion=1, flag=wx.LEFT|wx.RIGHT, border=20) + key_text = wx.StaticText(panel, self.ID_KEY_TEXT, style=wx.ALIGN_CENTER, label="请按下热键") + key_text.SetFont(wx.Font(22, wx.FONTFAMILY_SCRIPT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)) + key_text.SetSize(key_text.GetBestSize()) + hbox2.Add(key_text, proportion=1, flag=wx.LEFT|wx.RIGHT, border=20) vbox.Add(hbox2, proportion=1, flag=wx.EXPAND|wx.TOP|wx.BOTTOM, border=20) # 创建一个按钮,居中对齐 hbox3 = wx.BoxSizer(wx.HORIZONTAL) - self.record_btn = wx.Button(panel, -1, label="确定") - hbox3.Add(self.record_btn, proportion=1, flag=wx.LEFT|wx.RIGHT, border=20) + confirm_btn = wx.Button(panel, self.ID_CONFIRM_BTN, label="确定") + hbox3.Add(confirm_btn, proportion=1, flag=wx.LEFT|wx.RIGHT, border=20) vbox.Add(hbox3, proportion=1, flag=wx.EXPAND|wx.TOP|wx.BOTTOM, border=20) - self.record_btn.Bind(wx.EVT_BUTTON, self.Confirm) + + # 使用ID绑定事件 + self.Bind(wx.EVT_BUTTON, self.Confirm, id=self.ID_CONFIRM_BTN) self.Bind(wx.EVT_CLOSE, self.onClose) panel.SetSizer(vbox) def onKeyEvent(self, event): - key=keyMux(event) + key = keyMux(event) if event.event_type == 'down': - if len(RecordedHotkey.keys_pressing)==0: + if len(RecordedHotkey.keys_pressing) == 0: RecordedHotkey.keys_recorded.clear() RecordedHotkey.keys_pressing.add(key) RecordedHotkey.keys_recorded.add(key) elif event.event_type == 'up': RecordedHotkey.keys_pressing.discard(key) - if len(RecordedHotkey.keys_pressing)<=2: + if len(RecordedHotkey.keys_pressing) <= 2: self.stopRecording() else: RecordedHotkey.finishOneTime = False - self.text2.SetLabel("+".join(RecordedHotkey.keys_recorded)) - self.text2.SetExtraStyle(wx.ALIGN_CENTER) + + # 使用FindWindowById获取组件 + text = self.FindWindowById(self.ID_KEY_TEXT) + text.SetLabel("+".join(RecordedHotkey.keys_recorded)) + text.SetExtraStyle(wx.ALIGN_CENTER) def stopRecording(self): RecordedHotkey.finishOneTime = True @@ -90,9 +100,9 @@ def Confirm(self, event): self.Destroy() RecordedHotkey.confirm = True - def onClose(self,event): + def onClose(self, event): self.stopRecording() RecordedHotkey.recording = False keyboard.unhook_all() self.Destroy() - RecordedHotkey.confirm= False \ No newline at end of file + RecordedHotkey.confirm = False \ No newline at end of file diff --git a/main/GUI/setting.py b/main/GUI/setting.py index f612c9e..4bab24d 100644 --- a/main/GUI/setting.py +++ b/main/GUI/setting.py @@ -1,15 +1,33 @@ import wx import wx.dataview as dataview -import wx.dataview from core.config import Config import GUI.record as record import core.tools as tool -import json import wx.lib.buttons as buttons +from core.model import WindowInfo class SettingWindow(wx.Frame): - def __init__(self): - super().__init__(None, title="设置 - Boss Key", style=wx.DEFAULT_FRAME_STYLE | wx.RESIZE_BORDER) + # 定义组件ID常量 + ID_LEFT_TREELIST = wx.NewIdRef() + ID_RIGHT_TREELIST = wx.NewIdRef() + ID_ADD_BINDING_BTN = wx.NewIdRef() + ID_REMOVE_BINDING_BTN = wx.NewIdRef() + ID_REFRESH_BTN = wx.NewIdRef() + ID_HIDE_SHOW_HOTKEY_TEXT = wx.NewIdRef() + ID_HIDE_SHOW_HOTKEY_BTN = wx.NewIdRef() + ID_CLOSE_HOTKEY_TEXT = wx.NewIdRef() + ID_CLOSE_HOTKEY_BTN = wx.NewIdRef() + ID_MUTE_AFTER_HIDE_CHECKBOX = wx.NewIdRef() + ID_SEND_BEFORE_HIDE_CHECKBOX = wx.NewIdRef() + ID_HIDE_CURRENT_CHECKBOX = wx.NewIdRef() + ID_CLICK_TO_HIDE_CHECKBOX = wx.NewIdRef() + ID_HIDE_ICON_AFTER_HIDE_CHECKBOX = wx.NewIdRef() + ID_PATH_MATCH_CHECKBOX = wx.NewIdRef() + ID_RESET_BTN = wx.NewIdRef() + ID_SAVE_BTN = wx.NewIdRef() + + def __init__(self,id=None): + super().__init__(None,id=id, title="设置 - Boss Key", style=wx.DEFAULT_FRAME_STYLE | wx.RESIZE_BORDER) self.SetIcon(wx.Icon(wx.Image(Config.icon).ConvertToBitmap())) self.init_UI() @@ -32,7 +50,7 @@ def init_UI(self): # 左边列表 left_staticbox = wx.StaticBox(panel, label="现有窗口进程") left_sizer = wx.StaticBoxSizer(left_staticbox, wx.VERTICAL) - self.left_treelist = dataview.TreeListCtrl(panel, style=wx.dataview.TL_CHECKBOX) + self.left_treelist = dataview.TreeListCtrl(panel, self.ID_LEFT_TREELIST, style=dataview.TL_CHECKBOX) self.left_treelist.AppendColumn('窗口标题', width=300) self.left_treelist.AppendColumn('窗口句柄', width=100) self.left_treelist.AppendColumn('进程PID', width=150) @@ -41,17 +59,17 @@ def init_UI(self): # 中键按钮 middle_sizer = wx.BoxSizer(wx.VERTICAL) - self.add_binding_btn = buttons.GenButton(panel, label="添加绑定-->") - self.remove_binding_btn = buttons.GenButton(panel, label="<--删除绑定") - self.refresh_btn = buttons.GenButton(panel, label="刷新进程") - middle_sizer.Add(self.add_binding_btn, 0, wx.EXPAND | wx.ALL, 5) - middle_sizer.Add(self.remove_binding_btn, 0, wx.EXPAND | wx.ALL, 5) - middle_sizer.Add(self.refresh_btn, 0, wx.EXPAND | wx.ALL, 5) + add_binding_btn = buttons.GenButton(panel, self.ID_ADD_BINDING_BTN, label="添加绑定-->") + remove_binding_btn = buttons.GenButton(panel, self.ID_REMOVE_BINDING_BTN, label="<--删除绑定") + refresh_btn = buttons.GenButton(panel, self.ID_REFRESH_BTN, label="刷新进程") + middle_sizer.Add(add_binding_btn, 0, wx.EXPAND | wx.ALL, 5) + middle_sizer.Add(remove_binding_btn, 0, wx.EXPAND | wx.ALL, 5) + middle_sizer.Add(refresh_btn, 0, wx.EXPAND | wx.ALL, 5) # 右边列表 right_staticbox = wx.StaticBox(panel, label="已绑定进程") right_sizer = wx.StaticBoxSizer(right_staticbox, wx.VERTICAL) - self.right_treelist = dataview.TreeListCtrl(panel, style=wx.dataview.TL_CHECKBOX) + self.right_treelist = dataview.TreeListCtrl(panel, self.ID_RIGHT_TREELIST, style=dataview.TL_CHECKBOX) self.right_treelist.AppendColumn('窗口标题', width=300) self.right_treelist.AppendColumn('窗口句柄', width=100) self.right_treelist.AppendColumn('进程PID', width=150) @@ -71,21 +89,21 @@ def init_UI(self): #设置隐显窗口热键 hide_show_hotkey_sizer = wx.BoxSizer(wx.HORIZONTAL) hide_show_hotkey_label = wx.StaticText(panel, label="隐藏/显示窗口热键:") - self.hide_show_hotkey_text = wx.TextCtrl(panel, -1, value=Config.hide_hotkey) - self.hide_show_hotkey_btn = wx.Button(panel, -1, label="录制热键") + hide_show_hotkey_text = wx.TextCtrl(panel, self.ID_HIDE_SHOW_HOTKEY_TEXT, value=Config.hide_hotkey) + hide_show_hotkey_btn = wx.Button(panel, self.ID_HIDE_SHOW_HOTKEY_BTN, label="录制热键") hide_show_hotkey_sizer.Add(hide_show_hotkey_label, proportion=1, flag=wx.EXPAND| wx.ALL, border=10) - hide_show_hotkey_sizer.Add(self.hide_show_hotkey_text, proportion=1, flag=wx.EXPAND| wx.ALL, border=10) - hide_show_hotkey_sizer.Add(self.hide_show_hotkey_btn, proportion=1, flag=wx.EXPAND|wx.ALL, border=10) + hide_show_hotkey_sizer.Add(hide_show_hotkey_text, proportion=1, flag=wx.EXPAND| wx.ALL, border=10) + hide_show_hotkey_sizer.Add(hide_show_hotkey_btn, proportion=1, flag=wx.EXPAND|wx.ALL, border=10) hotkey_sizer.Add(hide_show_hotkey_sizer, proportion=1, flag=wx.EXPAND|wx.ALL, border=10) #设置关闭热键 close_hotkey_sizer = wx.BoxSizer(wx.HORIZONTAL) close_hotkey_label = wx.StaticText(panel, label="一键关闭程序热键:") - self.close_hotkey_text = wx.TextCtrl(panel, -1, value=Config.close_hotkey) - self.close_hotkey_btn = wx.Button(panel, -1, label="录制热键") + close_hotkey_text = wx.TextCtrl(panel, self.ID_CLOSE_HOTKEY_TEXT, value=Config.close_hotkey) + close_hotkey_btn = wx.Button(panel, self.ID_CLOSE_HOTKEY_BTN, label="录制热键") close_hotkey_sizer.Add(close_hotkey_label, proportion=1, flag=wx.EXPAND| wx.ALL, border=10) - close_hotkey_sizer.Add(self.close_hotkey_text, proportion=1, flag=wx.EXPAND| wx.ALL, border=10) - close_hotkey_sizer.Add(self.close_hotkey_btn, proportion=1, flag=wx.EXPAND| wx.ALL, border=10) + close_hotkey_sizer.Add(close_hotkey_text, proportion=1, flag=wx.EXPAND| wx.ALL, border=10) + close_hotkey_sizer.Add(close_hotkey_btn, proportion=1, flag=wx.EXPAND| wx.ALL, border=10) hotkey_sizer.Add(close_hotkey_sizer, proportion=1, flag=wx.EXPAND| wx.ALL, border=10) bottom_sizer.Add(hotkey_sizer, flag=wx.EXPAND| wx.ALL) @@ -95,40 +113,48 @@ def init_UI(self): mute_after_hide_sizer=wx.BoxSizer(wx.HORIZONTAL) mute_after_hide_label = wx.StaticText(panel, label="隐藏窗口后静音") - self.mute_after_hide_checkbox = wx.CheckBox(panel, -1, "") + mute_after_hide_checkbox = wx.CheckBox(panel, self.ID_MUTE_AFTER_HIDE_CHECKBOX, "") mute_after_hide_sizer.Add(mute_after_hide_label,proportion=1, flag=wx.EXPAND| wx.ALL) - mute_after_hide_sizer.Add(self.mute_after_hide_checkbox,proportion=1, flag=wx.EXPAND| wx.ALL) + mute_after_hide_sizer.Add(mute_after_hide_checkbox,proportion=1, flag=wx.EXPAND| wx.ALL) settings_checkbox_sizer.Add(mute_after_hide_sizer,proportion=1,flag=wx.EXPAND| wx.ALL, border=10) send_before_hide_sizer=wx.BoxSizer(wx.HORIZONTAL) send_before_hide_label = wx.StaticText(panel, label="隐藏前发送暂停键(Beta)") send_before_hide_label.SetToolTip(wx.ToolTip("隐藏窗口前发送暂停键,用于关闭弹出的输入框等,隐藏窗口会存在一定的延迟")) - self.send_before_hide_checkbox = wx.CheckBox(panel, -1, "") + send_before_hide_checkbox = wx.CheckBox(panel, self.ID_SEND_BEFORE_HIDE_CHECKBOX, "") send_before_hide_sizer.Add(send_before_hide_label,proportion=1, flag=wx.EXPAND| wx.ALL) - send_before_hide_sizer.Add(self.send_before_hide_checkbox,proportion=1, flag=wx.EXPAND| wx.ALL) + send_before_hide_sizer.Add(send_before_hide_checkbox,proportion=1, flag=wx.EXPAND| wx.ALL) settings_checkbox_sizer.Add(send_before_hide_sizer, proportion=1,flag=wx.EXPAND| wx.ALL, border=10) hide_current_sizer=wx.BoxSizer(wx.HORIZONTAL) hide_current_label = wx.StaticText(panel, label="同时隐藏当前活动窗口") - self.hide_current_checkbox = wx.CheckBox(panel, -1, "") + hide_current_checkbox = wx.CheckBox(panel, self.ID_HIDE_CURRENT_CHECKBOX, "") hide_current_sizer.Add(hide_current_label,proportion=1, flag=wx.EXPAND| wx.ALL) - hide_current_sizer.Add(self.hide_current_checkbox,proportion=1, flag=wx.EXPAND| wx.ALL) + hide_current_sizer.Add(hide_current_checkbox,proportion=1, flag=wx.EXPAND| wx.ALL) settings_checkbox_sizer.Add(hide_current_sizer, proportion=1,flag=wx.EXPAND| wx.ALL, border=10) click_to_hide_sizer=wx.BoxSizer(wx.HORIZONTAL) click_to_hide_label = wx.StaticText(panel, label="点击托盘图标切换隐藏状态") - self.click_to_hide_checkbox = wx.CheckBox(panel, -1, "") + click_to_hide_checkbox = wx.CheckBox(panel, self.ID_CLICK_TO_HIDE_CHECKBOX, "") click_to_hide_sizer.Add(click_to_hide_label,proportion=1, flag=wx.EXPAND| wx.ALL) - click_to_hide_sizer.Add(self.click_to_hide_checkbox,proportion=1, flag=wx.EXPAND| wx.ALL) + click_to_hide_sizer.Add(click_to_hide_checkbox,proportion=1, flag=wx.EXPAND| wx.ALL) settings_checkbox_sizer.Add(click_to_hide_sizer, proportion=1,flag=wx.EXPAND| wx.ALL, border=10) hide_icon_after_hide_sizer=wx.BoxSizer(wx.HORIZONTAL) hide_icon_after_hide_label = wx.StaticText(panel, label="隐藏窗口后隐藏托盘图标") - self.hide_icon_after_hide_checkbox = wx.CheckBox(panel, -1, "") + hide_icon_after_hide_checkbox = wx.CheckBox(panel, self.ID_HIDE_ICON_AFTER_HIDE_CHECKBOX, "") hide_icon_after_hide_sizer.Add(hide_icon_after_hide_label,proportion=1, flag=wx.EXPAND| wx.ALL) - hide_icon_after_hide_sizer.Add(self.hide_icon_after_hide_checkbox,proportion=1, flag=wx.EXPAND| wx.ALL) + hide_icon_after_hide_sizer.Add(hide_icon_after_hide_checkbox,proportion=1, flag=wx.EXPAND| wx.ALL) settings_checkbox_sizer.Add(hide_icon_after_hide_sizer, proportion=1,flag=wx.EXPAND| wx.ALL, border=10) + path_match_sizer=wx.BoxSizer(wx.HORIZONTAL) + path_match_label = wx.StaticText(panel, label="文件路径匹配") + path_match_label.SetToolTip(wx.ToolTip("启用文件路径匹配可以匹配同一程序的不同窗口")) + path_match_checkbox = wx.CheckBox(panel, self.ID_PATH_MATCH_CHECKBOX, "") + path_match_sizer.Add(path_match_label,proportion=1, flag=wx.EXPAND| wx.ALL) + path_match_sizer.Add(path_match_checkbox,proportion=1, flag=wx.EXPAND| wx.ALL) + settings_checkbox_sizer.Add(path_match_sizer, proportion=1,flag=wx.EXPAND| wx.ALL, border=10) + bottom_sizer.Add(settings_checkbox_sizer, flag=wx.EXPAND| wx.ALL, border=10) #设置提示 @@ -138,10 +164,10 @@ def init_UI(self): # 创建按钮 button_sizer = wx.BoxSizer(wx.HORIZONTAL) - self.reset_btn = wx.Button(panel,size=(100,60), label="重置设置") - self.save_btn = wx.Button(panel,size=(100,60), label="保存设置") - button_sizer.Add(self.reset_btn, proportion=1, flag=wx.LEFT, border=20) - button_sizer.Add(self.save_btn, proportion=1, flag=wx.RIGHT, border=20) + reset_btn = wx.Button(panel, self.ID_RESET_BTN, size=(100,60), label="重置设置") + save_btn = wx.Button(panel, self.ID_SAVE_BTN, size=(100,60), label="保存设置") + button_sizer.Add(reset_btn, proportion=1, flag=wx.LEFT, border=20) + button_sizer.Add(save_btn, proportion=1, flag=wx.RIGHT, border=20) bottom_sizer.Add(button_sizer, proportion=1, flag=wx.EXPAND|wx.BOTTOM, border=10) # Add top and bottom sizers to the main sizer @@ -152,41 +178,61 @@ def init_UI(self): main_sizer.Fit(self) def Bind_EVT(self): - self.save_btn.Bind(wx.EVT_BUTTON, self.OnSave) - self.reset_btn.Bind(wx.EVT_BUTTON,self.OnReset) - self.hide_show_hotkey_btn.Bind(wx.EVT_BUTTON, self.OnRecordSW) - self.close_hotkey_btn.Bind(wx.EVT_BUTTON, self.OnRecordCL) - self.send_before_hide_checkbox.Bind(wx.EVT_CHECKBOX, self.OnSendBeforeHide) - self.refresh_btn.Bind(wx.EVT_BUTTON, self.RefreshLeftList) - self.add_binding_btn.Bind(wx.EVT_BUTTON, self.OnAddBinding) - self.remove_binding_btn.Bind(wx.EVT_BUTTON, self.OnRemoveBinding) - # self.left_treelist.Bind(dataview.EVT_TREELIST_SELECTION_CHANGED, self.OnToggleCheck) - # self.right_treelist.Bind(dataview.EVT_TREELIST_SELECTION_CHANGED, self.OnToggleCheck) + self.Bind(wx.EVT_BUTTON, self.OnSave, id=self.ID_SAVE_BTN) + self.Bind(wx.EVT_BUTTON, self.OnReset, id=self.ID_RESET_BTN) + self.Bind(wx.EVT_BUTTON, self.OnRecordSW, id=self.ID_HIDE_SHOW_HOTKEY_BTN) + self.Bind(wx.EVT_BUTTON, self.OnRecordCL, id=self.ID_CLOSE_HOTKEY_BTN) + self.Bind(wx.EVT_CHECKBOX, self.OnSendBeforeHide, id=self.ID_SEND_BEFORE_HIDE_CHECKBOX) + self.Bind(wx.EVT_BUTTON, self.RefreshLeftList, id=self.ID_REFRESH_BTN) + self.Bind(wx.EVT_BUTTON, self.OnAddBinding, id=self.ID_ADD_BINDING_BTN) + self.Bind(wx.EVT_BUTTON, self.OnRemoveBinding, id=self.ID_REMOVE_BINDING_BTN) self.left_treelist.Bind(dataview.EVT_TREELIST_ITEM_CHECKED, self.OnToggleCheck) self.right_treelist.Bind(dataview.EVT_TREELIST_ITEM_CHECKED, self.OnToggleCheck) - self.Bind(wx.EVT_CLOSE,self.OnClose) + self.Bind(wx.EVT_CLOSE, self.OnClose) def SetData(self): Config.load() - self.hide_show_hotkey_text.SetValue(Config.hide_hotkey) - self.close_hotkey_text.SetValue(Config.close_hotkey) - self.mute_after_hide_checkbox.SetValue(Config.mute_after_hide) - self.send_before_hide_checkbox.SetValue(Config.send_before_hide) - self.hide_current_checkbox.SetValue(Config.hide_current) - self.click_to_hide_checkbox.SetValue(Config.click_to_hide) - self.hide_icon_after_hide_checkbox.SetValue(Config.hide_icon_after_hide) + hide_show_hotkey_text = self.FindWindowById(self.ID_HIDE_SHOW_HOTKEY_TEXT) + close_hotkey_text = self.FindWindowById(self.ID_CLOSE_HOTKEY_TEXT) + mute_after_hide_checkbox = self.FindWindowById(self.ID_MUTE_AFTER_HIDE_CHECKBOX) + send_before_hide_checkbox = self.FindWindowById(self.ID_SEND_BEFORE_HIDE_CHECKBOX) + hide_current_checkbox = self.FindWindowById(self.ID_HIDE_CURRENT_CHECKBOX) + click_to_hide_checkbox = self.FindWindowById(self.ID_CLICK_TO_HIDE_CHECKBOX) + hide_icon_after_hide_checkbox = self.FindWindowById(self.ID_HIDE_ICON_AFTER_HIDE_CHECKBOX) + path_match_checkbox = self.FindWindowById(self.ID_PATH_MATCH_CHECKBOX) + + hide_show_hotkey_text.SetValue(Config.hide_hotkey) + close_hotkey_text.SetValue(Config.close_hotkey) + mute_after_hide_checkbox.SetValue(Config.mute_after_hide) + send_before_hide_checkbox.SetValue(Config.send_before_hide) + hide_current_checkbox.SetValue(Config.hide_current) + click_to_hide_checkbox.SetValue(Config.click_to_hide) + hide_icon_after_hide_checkbox.SetValue(Config.hide_icon_after_hide) + path_match_checkbox.SetValue(Config.path_match) self.InsertTreeList(Config.hide_binding, self.right_treelist, True) self.RefreshLeftList() - def OnSave(self,e): - Config.hide_hotkey = self.hide_show_hotkey_text.GetValue() - Config.close_hotkey = self.close_hotkey_text.GetValue() - Config.mute_after_hide = self.mute_after_hide_checkbox.GetValue() - Config.send_before_hide = self.send_before_hide_checkbox.GetValue() - Config.hide_current = self.hide_current_checkbox.GetValue() - Config.click_to_hide = self.click_to_hide_checkbox.GetValue() - Config.hide_icon_after_hide = self.hide_icon_after_hide_checkbox.GetValue() + def OnSave(self, e): + hide_show_hotkey_text = self.FindWindowById(self.ID_HIDE_SHOW_HOTKEY_TEXT) + close_hotkey_text = self.FindWindowById(self.ID_CLOSE_HOTKEY_TEXT) + mute_after_hide_checkbox = self.FindWindowById(self.ID_MUTE_AFTER_HIDE_CHECKBOX) + send_before_hide_checkbox = self.FindWindowById(self.ID_SEND_BEFORE_HIDE_CHECKBOX) + hide_current_checkbox = self.FindWindowById(self.ID_HIDE_CURRENT_CHECKBOX) + click_to_hide_checkbox = self.FindWindowById(self.ID_CLICK_TO_HIDE_CHECKBOX) + hide_icon_after_hide_checkbox = self.FindWindowById(self.ID_HIDE_ICON_AFTER_HIDE_CHECKBOX) + path_match_checkbox = self.FindWindowById(self.ID_PATH_MATCH_CHECKBOX) + + Config.hide_hotkey = hide_show_hotkey_text.GetValue() + Config.close_hotkey = close_hotkey_text.GetValue() + Config.mute_after_hide = mute_after_hide_checkbox.GetValue() + Config.send_before_hide = send_before_hide_checkbox.GetValue() + Config.hide_current = hide_current_checkbox.GetValue() + Config.click_to_hide = click_to_hide_checkbox.GetValue() + Config.hide_icon_after_hide = hide_icon_after_hide_checkbox.GetValue() + Config.path_match = path_match_checkbox.GetValue() + + # 获取Windows对象列表 Config.hide_binding = self.ItemsData(self.right_treelist, only_checked=False) Config.HotkeyListener.ShowWindows(load=False) @@ -197,26 +243,37 @@ def OnSave(self,e): except: wx.MessageDialog(None, u"热键绑定失败,请重试", u"Boss Key", wx.OK | wx.ICON_ERROR).ShowModal() - def OnAddBinding(self,e): + def OnAddBinding(self, e): left_checked = self.ItemsData(self.left_treelist, only_checked=True) self.InsertTreeList(left_checked, self.right_treelist, False) for item in left_checked: self.RemoveItem(self.left_treelist, item) - - def OnRemoveBinding(self,e): + def OnRemoveBinding(self, e): right_checked = self.ItemsData(self.right_treelist, only_checked=True) self.InsertTreeList(right_checked, self.left_treelist, False) for item in right_checked: self.RemoveItem(self.right_treelist, item) - def OnReset(self,e): - self.hide_show_hotkey_text.SetValue("Ctrl+Q") - self.close_hotkey_text.SetValue("Win+Esc") - self.mute_after_hide_checkbox.SetValue(True) - self.send_before_hide_checkbox.SetValue(False) - self.hide_current_checkbox.SetValue(True) - self.InsertTreeList([],self.right_treelist,True) + def OnReset(self, e): + hide_show_hotkey_text = self.FindWindowById(self.ID_HIDE_SHOW_HOTKEY_TEXT) + close_hotkey_text = self.FindWindowById(self.ID_CLOSE_HOTKEY_TEXT) + mute_after_hide_checkbox = self.FindWindowById(self.ID_MUTE_AFTER_HIDE_CHECKBOX) + send_before_hide_checkbox = self.FindWindowById(self.ID_SEND_BEFORE_HIDE_CHECKBOX) + hide_current_checkbox = self.FindWindowById(self.ID_HIDE_CURRENT_CHECKBOX) + click_to_hide_checkbox = self.FindWindowById(self.ID_CLICK_TO_HIDE_CHECKBOX) + hide_icon_after_hide_checkbox = self.FindWindowById(self.ID_HIDE_ICON_AFTER_HIDE_CHECKBOX) + path_match_checkbox = self.FindWindowById(self.ID_PATH_MATCH_CHECKBOX) + + hide_show_hotkey_text.SetValue("Ctrl+Q") + close_hotkey_text.SetValue("Win+Esc") + mute_after_hide_checkbox.SetValue(True) + send_before_hide_checkbox.SetValue(False) + hide_current_checkbox.SetValue(True) + click_to_hide_checkbox.SetValue(False) + hide_icon_after_hide_checkbox.SetValue(False) + path_match_checkbox.SetValue(False) + self.InsertTreeList([], self.right_treelist, True) self.RefreshLeftList() wx.MessageDialog(None, u"已重置选项,请保存设置以启用", u"Boss Key", wx.OK | wx.ICON_INFORMATION).ShowModal() @@ -225,43 +282,82 @@ def OnToggleCheck(self, e): treelist = e.GetEventObject() item = e.GetItem() is_checked = treelist.GetCheckedState(item) - treelist.CheckItemRecursively(item, is_checked) - # 检查父级是否需要修改状态 + + # 递归设置子节点状态 + self.CheckItemRecursively(treelist, item, is_checked) + + # 更新父节点状态 + self.UpdateParentCheckState(treelist, item) + + def CheckItemRecursively(self, treelist, item, check_state): + """递归设置项目及其子项的选中状态""" + treelist.CheckItem(item, check_state) + + # 处理所有子节点 + child = treelist.GetFirstChild(item) + while child.IsOk(): + self.CheckItemRecursively(treelist, child, check_state) + child = treelist.GetNextSibling(child) + + def UpdateParentCheckState(self, treelist, item): + """更新父节点的选中状态""" parent = treelist.GetItemParent(item) - if parent == treelist.GetRootItem(): - return - else: - if treelist.AreAllChildrenInState(parent, wx.CHK_CHECKED): + if parent != treelist.GetRootItem(): + # 检查所有兄弟节点状态 + all_checked = True + all_unchecked = True + + child = treelist.GetFirstChild(parent) + while child.IsOk(): + state = treelist.GetCheckedState(child) + if state != wx.CHK_CHECKED: + all_checked = False + if state != wx.CHK_UNCHECKED: + all_unchecked = False + child = treelist.GetNextSibling(child) + + # 根据子节点状态设置父节点状态 + if all_checked: treelist.CheckItem(parent, wx.CHK_CHECKED) - elif treelist.AreAllChildrenInState(parent, wx.CHK_UNCHECKED): + elif all_unchecked: treelist.CheckItem(parent, wx.CHK_UNCHECKED) + else: + treelist.CheckItem(parent, wx.CHK_UNDETERMINED) + + # 递归更新上层父节点 + self.UpdateParentCheckState(treelist, parent) - def OnSendBeforeHide(self,e): - if self.send_before_hide_checkbox.GetValue(): + def OnSendBeforeHide(self, e): + send_before_hide_checkbox = self.FindWindowById(self.ID_SEND_BEFORE_HIDE_CHECKBOX) + if send_before_hide_checkbox.GetValue(): wx.MessageDialog(None, u"隐藏窗口前向被隐藏的窗口发送空格,用于暂停视频等。启用此功能可能会延迟窗口的隐藏", u"Boss Key", wx.OK | wx.ICON_INFORMATION).ShowModal() def OnRecordSW(self, e): - self.recordHotkey(self.hide_show_hotkey_text, self.hide_show_hotkey_btn) + hide_show_hotkey_text = self.FindWindowById(self.ID_HIDE_SHOW_HOTKEY_TEXT) + hide_show_hotkey_btn = self.FindWindowById(self.ID_HIDE_SHOW_HOTKEY_BTN) + self.recordHotkey(hide_show_hotkey_text, hide_show_hotkey_btn) - def OnClose(self, e): + def OnClose(self, e): self.Hide() def OnRecordCL(self, e): - self.recordHotkey(self.close_hotkey_text, self.close_hotkey_btn) + close_hotkey_text = self.FindWindowById(self.ID_CLOSE_HOTKEY_TEXT) + close_hotkey_btn = self.FindWindowById(self.ID_CLOSE_HOTKEY_BTN) + self.recordHotkey(close_hotkey_text, close_hotkey_btn) - def RefreshLeftList(self,e=None): - windows=tool.getAllWindows() - right=self.ItemsData(self.right_treelist,only_checked=False) - list=[] + def RefreshLeftList(self, e=None): + windows = tool.getAllWindows() + right = self.ItemsData(self.right_treelist, only_checked=False) + list = [] for window in windows: - flag=0 + flag = 0 for i in right: - if tool.isSameWindow(window,i,True): - flag=1 + if tool.isSameWindow(window, i, True): + flag = 1 break if not flag: list.append(window) - self.InsertTreeList(list,self.left_treelist,True) + self.InsertTreeList(list, self.left_treelist, True) def InsertTreeList(self, data: list, treelist: dataview.TreeListCtrl, clear=True): if clear: @@ -269,20 +365,28 @@ def InsertTreeList(self, data: list, treelist: dataview.TreeListCtrl, clear=True root = treelist.GetRootItem() process_map = {} for window in data: - process = window['process'] + # 确保window是WindowInfo对象 + if isinstance(window, dict): + window = WindowInfo.from_dict(window) + + process = window.process if process not in process_map: - exists_node=self.SearchProcessNode(treelist, process) + exists_node = self.SearchProcessNode(treelist, process) if exists_node is None: process_map[process] = treelist.AppendItem(root, process) else: process_map[process] = exists_node - item = treelist.AppendItem(process_map[process], window['title']) - treelist.SetItemText(item, 1, str(window['hwnd'])) - treelist.SetItemText(item, 2, str(window['PID'])) - treelist.SetItemData(item, {"title":window['title'],"hwnd": window['hwnd'], "process": window['process'], "PID": window['PID']}) + item = treelist.AppendItem(process_map[process], window.title) + treelist.SetItemText(item, 1, str(window.hwnd)) + treelist.SetItemText(item, 2, str(window.PID)) + treelist.SetItemData(item, window) treelist.Expand(root) for process in process_map: treelist.Expand(process_map[process]) + + # 初始化所有父节点的选中状态 + for process in process_map: + self.UpdateParentCheckState(treelist, treelist.GetFirstChild(process_map[process])) def SearchProcessNode(self, treelist: dataview.TreeListCtrl, process): item = treelist.GetRootItem() @@ -291,15 +395,20 @@ def SearchProcessNode(self, treelist: dataview.TreeListCtrl, process): if not item.IsOk(): break data = treelist.GetItemData(item) - if data is not None and data and data['process'] == process: + if data is not None and hasattr(data, 'process') and data.process == process: return treelist.GetItemParent(item) def RemoveItem(self, treelist: dataview.TreeListCtrl, data): - node=item = self.SearchProcessNode(treelist, data['process']) + # 确保data是WindowInfo对象 + if isinstance(data, dict): + data = WindowInfo.from_dict(data) + + node = item = self.SearchProcessNode(treelist, data.process) if item is not None: item = treelist.GetFirstChild(item) while item.IsOk(): - if treelist.GetItemData(item) == data: + item_data = treelist.GetItemData(item) + if item_data and item_data == data: treelist.DeleteItem(item) break item = treelist.GetNextSibling(item) @@ -308,7 +417,7 @@ def RemoveItem(self, treelist: dataview.TreeListCtrl, data): # 如果没有子节点了,删除父节点 treelist.DeleteItem(node) - def ItemsData(self, treelist: dataview.TreeListCtrl, only_checked=False, item_object=False): + def ItemsData(self, treelist: dataview.TreeListCtrl, only_checked=False, item_object=False)->list[WindowInfo]: res = [] item = treelist.GetRootItem() while item.IsOk(): @@ -325,14 +434,14 @@ def ItemsData(self, treelist: dataview.TreeListCtrl, only_checked=False, item_ob res.append(data) return res - def recordHotkey(self, text_ctrl:wx.TextCtrl, btn:wx.Button): + def recordHotkey(self, text_ctrl: wx.TextCtrl, btn: wx.Button): try: Config.HotkeyListener.stop() except: pass btn.Disable() btn.SetLabel("录制中...") - record.RecordedHotkey.confirm=False + record.RecordedHotkey.confirm = False RecordWindow = record.RecordWindow() RecordWindow.ShowModal() btn.Enable() diff --git a/main/GUI/taskbar.py b/main/GUI/taskbar.py index 2bb7d67..ef6b75c 100644 --- a/main/GUI/taskbar.py +++ b/main/GUI/taskbar.py @@ -2,11 +2,13 @@ from core.config import Config import core.tools as tool from GUI import about +from GUI.window_restore import WindowRestoreDialog import sys class TaskBarIcon(wx.adv.TaskBarIcon): - MENU_SETTING,MENU_EXIT,MENU_STARTUP,MENU_UPDATE = wx.NewIdRef(count=4) + MENU_SETTING, MENU_EXIT, MENU_STARTUP, MENU_UPDATE, MENU_RESTORE = wx.NewIdRef(count=5) + ID_RESTORE = wx.NewIdRef(count=1) def __init__(self): super().__init__() @@ -22,59 +24,66 @@ def BindEVT(self): self.Bind(wx.EVT_MENU, self.onExit, id=self.MENU_EXIT) self.Bind(wx.EVT_MENU, self.onAbout, id=wx.ID_ABOUT) self.Bind(wx.EVT_MENU, self.onUpdate, id=self.MENU_UPDATE) + self.Bind(wx.EVT_MENU, self.onRestore, id=self.MENU_RESTORE) # 绑定任务栏图标单击事件 self.Bind(wx.adv.EVT_TASKBAR_LEFT_DOWN, self.onLeftClick) def CreatePopupMenu(self): - self.menu = wx.Menu() - self.menu.Append(self.MENU_SETTING, '设置') - self.menu.Append(self.MENU_STARTUP, '开机自启', kind=wx.ITEM_CHECK) - self.menu.Check(self.MENU_STARTUP, tool.checkStartup("Boss Key Application",Config.file_path)) - self.menu.AppendSeparator() - self.menu.Append(self.MENU_UPDATE, '检查更新') - self.menu.Append(wx.ID_ABOUT, '关于') - self.menu.AppendSeparator() - self.menu.Append(self.MENU_EXIT, '退出') - return self.menu + menu = wx.Menu() + menu.Append(self.MENU_SETTING, '设置') + menu.Append(self.MENU_STARTUP, '开机自启', kind=wx.ITEM_CHECK) + menu.Check(self.MENU_STARTUP, tool.checkStartup("Boss Key Application", Config.file_path)) + menu.AppendSeparator() + menu.Append(self.MENU_RESTORE, '窗口恢复工具') + menu.Append(self.MENU_UPDATE, '检查更新') + menu.Append(wx.ID_ABOUT, '关于') + menu.AppendSeparator() + menu.Append(self.MENU_EXIT, '退出') + return menu - def onLeftClick(self,e=''): + def onLeftClick(self, e=''): if Config.click_to_hide: - if Config.HotkeyListener!="": + if Config.HotkeyListener != "": Config.HotkeyListener.onHide() - def onStartup(self,e): - if tool.checkStartup("Boss Key Application",Config.file_path): + def onStartup(self, e): + if tool.checkStartup("Boss Key Application", Config.file_path): if tool.removeStartup("Boss Key Application"): - tool.sendNotify(title="开机自启状态变化",message="Boss Key开机自启已关闭") - self.menu.Check(self.MENU_STARTUP,False) + tool.sendNotify(title="开机自启状态变化", message="Boss Key开机自启已关闭") else: - tool.sendNotify(title="开机自启状态变化",message="Boss Key开机自启关闭失败") - self.menu.Check(self.MENU_STARTUP,True) + tool.sendNotify(title="开机自启状态变化", message="Boss Key开机自启关闭失败") else: - if tool.addStartup("Boss Key Application",Config.file_path): - tool.sendNotify(title="开机自启状态变化",message="Boss Key开机自启已开启") - self.menu.Check(self.MENU_STARTUP,True) + if tool.addStartup("Boss Key Application", Config.file_path): + tool.sendNotify(title="开机自启状态变化", message="Boss Key开机自启已开启") else: - tool.sendNotify(title="开机自启状态变化",message="Boss Key开机自启开启失败") - self.menu.Check(self.MENU_STARTUP,False) + tool.sendNotify(title="开机自启状态变化", message="Boss Key开机自启开启失败") - def onSetting(self,e): - Config.SettingWindow.RefreshLeftList() - Config.SettingWindow.Show() + def onSetting(self, e): + window=wx.FindWindowById(Config.SettingWindowId) + window.RefreshLeftList() + window.Show() - def onAbout(self,e): + def onAbout(self, e): about.AboutWindow().Show() - def onExit(self,e): + def onExit(self, e): Config.HotkeyListener.Close() - sys.exit(0) - def onUpdate(self,e): - if Config.UpdateWindow!="": - Config.UpdateWindow.Show() + def onUpdate(self, e): + if Config.UpdateWindowId != -1: + wx.FindWindowById(Config.UpdateWindowId).Show() else: - Config.UpdateWindow=about.UpdateWindow() - Config.UpdateWindow.Show() + about.UpdateWindow(Config.UpdateWindowId).Show() + + def onRestore(self, e): + """显示窗口恢复对话框""" + dialog = wx.FindWindowById(self.ID_RESTORE) + if dialog is None: + WindowRestoreDialog(self.ID_RESTORE).Show() + else: + dialog.Restore() + dialog.Raise() + dialog.RefreshLeftList() def HideIcon(self): wx.CallAfter(self.RemoveIcon) diff --git a/main/GUI/window_restore.py b/main/GUI/window_restore.py new file mode 100644 index 0000000..f4cccbc --- /dev/null +++ b/main/GUI/window_restore.py @@ -0,0 +1,73 @@ +import wx +import wx.dataview as dataview +import win32gui +import win32con +from .setting import SettingWindow +from core import tools as tool + +class WindowRestoreDialog(SettingWindow): + def __init__(self, id): + super().__init__(id=id) + self.SetSize((700, 600)) + + self.SetTitle("窗口恢复") + + self.Center() + + def init_UI(self): + self.window_info = [] + + # 创建界面 + panel = wx.Panel(self) + vbox = wx.BoxSizer(wx.VERTICAL) + + # 树形列表 + self.left_treelist = dataview.TreeListCtrl(panel, style=dataview.TL_CHECKBOX) + self.left_treelist.AppendColumn('窗口标题', width=300) + self.left_treelist.AppendColumn('窗口句柄', width=100) + self.left_treelist.AppendColumn('进程PID', width=150) + + # 按钮区域 + btn_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.show_btn = wx.Button(panel, label="显示窗口") + self.hide_btn = wx.Button(panel, label="隐藏窗口") + btn_sizer.Add(self.show_btn, proportion=1, flag=wx.RIGHT, border=5) + btn_sizer.Add(self.hide_btn, proportion=1, flag=wx.LEFT, border=5) + + # 布局 + vbox.Add(self.left_treelist, proportion=1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT, border=5) + vbox.Add(btn_sizer, flag=wx.EXPAND|wx.ALL, border=10) + + panel.SetSizer(vbox) + + def SetData(self): + self.RefreshLeftList() + + def RefreshLeftList(self, e=None): + windows = tool.getAllWindows() + list = [] + for window in windows: + list.append(window) + self.InsertTreeList(list, self.left_treelist, True) + + def Bind_EVT(self): + self.show_btn.Bind(wx.EVT_BUTTON, self.on_show_window) + self.hide_btn.Bind(wx.EVT_BUTTON, self.on_hide_window) + self.left_treelist.Bind(dataview.EVT_TREELIST_ITEM_CHECKED, self.OnToggleCheck) + + def on_show_window(self,e=None): + windows = self.ItemsData(self.left_treelist, only_checked=True) + result = wx.MessageBox(f"将恢复{len(windows)}个窗口\r\n恢复未知的窗口可能会导致窗口出错", "警告", wx.OK | wx.CANCEL | wx.ICON_WARNING) + if result != wx.OK: + return + for window in windows: + win32gui.ShowWindow(window.hwnd, win32con.SW_SHOW) + + def on_hide_window(self,e=None): + windows = self.ItemsData(self.left_treelist, only_checked=True) + result = wx.MessageBox(f"将隐藏{len(windows)}个窗口\r\n隐藏未知的窗口可能会导致窗口出错", "警告", wx.OK | wx.CANCEL | wx.ICON_WARNING) + if result != wx.OK: + return + for window in windows: + win32gui.ShowWindow(window.hwnd, win32con.SW_HIDE) + \ No newline at end of file diff --git a/main/core/config.py b/main/core/config.py index 4784bea..4f3dee2 100644 --- a/main/core/config.py +++ b/main/core/config.py @@ -4,11 +4,12 @@ from .icon import get_icon from configparser import ConfigParser from io import BytesIO +from .model import WindowInfo class Config: AppName = "Boss Key" - AppVersion = "v2.0.2.0" - AppReleaseDate = "2025-02-02" + AppVersion = "v2.0.3.0" + AppReleaseDate = "2025-04-05" AppAuthor = "IvanHanloth" AppDescription = "老板来了?快用Boss-Key老板键一键隐藏静音当前窗口!上班摸鱼必备神器" AppCopyRight = "Copyright © 2022-2025 Ivan Hanloth All Rights Reserved." @@ -48,6 +49,7 @@ class Config: click_to_hide = True hide_icon_after_hide = False + path_match = False hide_binding = [] @@ -57,11 +59,10 @@ class Config: # 判断是否为首次启动 first_start = not os.path.exists(config_path) - SettingWindow="" TaskBarIcon="" - UpdateWindow="" - HotkeyListener= "" + SettingWindowId = -1 + UpdateWindowId = -1 recording_hotkey = False recorded_hotkey = None @@ -87,13 +88,15 @@ def load(): Config.send_before_hide = config.get("setting", {}).get("send_before_hide", False) Config.hide_current = config.get("setting", {}).get("hide_current", True) Config.hide_icon_after_hide = config.get("setting", {}).get("hide_icon_after_hide", False) + Config.path_match = config.get("setting", {}).get("path_match", False) Config.click_to_hide= config.get("setting", {}).get("click_to_hide", True) Config.hide_hotkey = config.get("hotkey", {}).get("hide_hotkey", "Ctrl+Q") Config.close_hotkey = config.get("hotkey", {}).get("close_hotkey", "Win+Esc") - Config.hide_binding = config.get("hide_binding", []) + # 将hide_binding从字典列表转换为WindowInfo对象列表 + Config.hide_binding = [WindowInfo.from_dict(item) for item in config.get("hide_binding", [])] if config.get('version', '') != Config.AppVersion: Config.save() @@ -113,9 +116,11 @@ def save(): 'send_before_hide': Config.send_before_hide, 'hide_current': Config.hide_current, 'click_to_hide': Config.click_to_hide, - 'hide_icon_after_hide': Config.hide_icon_after_hide + 'hide_icon_after_hide': Config.hide_icon_after_hide, + 'path_match': Config.path_match }, - "hide_binding" : Config.hide_binding + # 将WindowInfo对象列表转换为字典列表用于JSON序列化 + "hide_binding": [item.to_dict() if isinstance(item, WindowInfo) else item for item in Config.hide_binding] } with open(Config.config_path, 'w', encoding='utf-8') as f: diff --git a/main/core/listener.py b/main/core/listener.py index 5447a17..1a40a21 100644 --- a/main/core/listener.py +++ b/main/core/listener.py @@ -7,6 +7,7 @@ import multiprocessing import threading import time +import wx class HotkeyListener(): def __init__(self): @@ -18,6 +19,7 @@ def __init__(self): self.Queue = multiprocessing.Queue() self.listener = None self.reBind() + self.end_flag=False threading.Thread(target=self.listenToQueue,daemon=True).start() def listenToQueue(self): @@ -26,37 +28,48 @@ def listenToQueue(self): try: msg = self.Queue.get() if msg == "showTaskBarIcon": - Config.TaskBarIcon.ShowIcon() + wx.CallAfter(Config.TaskBarIcon.ShowIcon()) elif msg == "hideTaskBarIcon": - Config.TaskBarIcon.HideIcon() + wx.CallAfter(Config.TaskBarIcon.HideIcon()) elif msg == "closeApp": print("收到关闭消息") - tool.sendNotify("Boss Key已停止服务", "Boss Key已成功退出") self.ShowWindows() - self.stop() + tool.sendNotify("Boss Key已停止服务", "Boss Key已成功退出") + self._stop() try: - Config.SettingWindow.Destroy() + wx.FindWindowById(Config.SettingWindowId).Destroy() Config.TaskBarIcon.Destroy() - Config.UpdateWindow.Destroy() - except: + wx.FindWindowById(Config.UpdateWindowId).Destroy() + except Exception as e: + print(e) pass exit_flag = True break except: pass - - if exit_flag: - sys.exit(0) + finally: + if exit_flag: + sys.exit(0) def reBind(self): - self.stop() + self._stop() self.BindHotKey() def ListenerProcess(self,hotkey): - with keyboard.GlobalHotKeys(hotkey) as listener: - while True: #避免意外退出 - listener.join() - print("线程意外退出") + try: + with keyboard.GlobalHotKeys(hotkey) as listener: + self.end_flag = False + while listener.running and not self.end_flag: + time.sleep(0.1) # 减少CPU使用率 + + # 如果是因为 end_flag 退出但监听器仍在运行 + if listener.running and self.end_flag: + listener.stop() + + print("热键监听已停止") + except Exception as e: + self.ShowWindows(False) + print(f"热键监听出错: {e}") def BindHotKey(self): hotkeys = { @@ -107,11 +120,11 @@ def HideWindows(self): for i in outer: for j in inner: - if tool.isSameWindow(i,j,False): + if tool.isSameWindow(i, j, False, not Config.path_match): if outer==Config.hide_binding: # 此时i是绑定的元素,j是窗口元素,需要隐藏j - needHide.append(j['hwnd']) + needHide.append(j.hwnd) else: - needHide.append(i['hwnd']) + needHide.append(i.hwnd) break if Config.hide_current: # 插入当前窗口的句柄 @@ -136,8 +149,12 @@ def HideWindows(self): def Close(self,e=""): self.Queue.put("closeApp") - def stop(self): + def _stop(self): + """ + 直接关闭listener,应该使用Close + """ if self.listener is not None: + self.end_flag=True try: self.listener.terminate() self.listener.join() diff --git a/main/core/model.py b/main/core/model.py new file mode 100644 index 0000000..1f42fa5 --- /dev/null +++ b/main/core/model.py @@ -0,0 +1,46 @@ +class WindowInfo: + """ + 窗口信息模型 + """ + title:str = None + hwnd:int = None + process:str = None + PID:int = None + path:str = None + + def __init__(self, title:str, hwnd:int, process:str, PID:int, path:str=None): + self.title = title + self.hwnd = hwnd + self.process = process + self.PID = PID + self.path = path + + def to_dict(self): + """将对象转为字典,用于JSON序列化""" + return { + "title": self.title, + "hwnd": self.hwnd, + "process": self.process, + "PID": self.PID, + "path": self.path + } + + @classmethod + def from_dict(cls, data): + """从字典创建对象,用于JSON反序列化""" + return cls( + title=data.get('title', '无标题窗口'), + hwnd=data.get('hwnd', 0), + process=data.get('process', ''), + PID=data.get('PID', 0), + path=data.get('path', '') + ) + + def __eq__(self, other): + """比较两个WindowInfo是否相等""" + if not isinstance(other, WindowInfo): + return False + return (self.hwnd == other.hwnd and + self.process == other.process and + self.PID == other.PID and + self.title == other.title) \ No newline at end of file diff --git a/main/core/tools.py b/main/core/tools.py index 4029d6e..9c0322f 100644 --- a/main/core/tools.py +++ b/main/core/tools.py @@ -9,7 +9,9 @@ import requests import json -def check_update(): +from core.model import WindowInfo + +def checkUpdate(): requests.packages.urllib3.disable_warnings() # 获取最新版本信息 try: @@ -146,44 +148,65 @@ def hwnd2windowName(hwnd): title=None return title -def getAllWindows(): +def getAllWindows()-> list[WindowInfo]: # 获取所有窗口信息 - def enumHandler(hwnd, windows:list): + def enumHandler(hwnd, windows:list[WindowInfo]): if win32gui.IsWindowVisible(hwnd): title = hwnd2windowName(hwnd) pid = win32process.GetWindowThreadProcessId(hwnd)[1] process_name = psutil.Process(pid).name() - windows.append({'title': title, 'hwnd': int(hwnd), 'process': process_name, 'PID':int(pid)}) + process_path = psutil.Process(pid).exe() + + windows.append(WindowInfo( + title=title, + hwnd=int(hwnd), + process=process_name, + PID=int(pid), + path=process_path + )) return True windows = [] win32gui.EnumWindows(enumHandler, windows) - windows.sort(key=lambda x: x['title']) + windows.sort(key=lambda x: x.title) return windows -def isSameWindow(w1:dict,w2:dict,strict=False): +def isSameWindow(w1:WindowInfo, w2:WindowInfo, auto=False, strict=True): """ 判断两个窗口的信息是否指向同一个窗口 - w1、w2: dict, 包含hwnd、title、process、PID - strict: 启用严格模式 + w1、w2: WindowInfo对象或字典 + auto: 智能匹配模式,默认False + strict: 严格模式,默认True,非严格模式下只判断进程名称是否相同 """ + # 转换可能的字典为WindowInfo对象 + if isinstance(w1, dict): + w1 = WindowInfo.from_dict(w1) + if isinstance(w2, dict): + w2 = WindowInfo.from_dict(w2) ## 一模一样的两个,肯定是同一个 - if w1==w2: + if w1 == w2: return True process_except=["explorer.exe"] - hwnd_same=w1['hwnd']==w2['hwnd'] - title_same=w1['title']==w2['title'] and w1['title']!="无标题窗口" - process_name_same=w1['process']==w2['process'] and w1 not in process_except - PID_same=w1['PID']==w2['PID'] - process_same=process_name_same or PID_same + hwnd_same = w1.hwnd == w2.hwnd + title_same = w1.title == w2.title and w1.title != "无标题窗口" + process_name_same = w1.process == w2.process and w1.process not in process_except + process_path_same = w1.path == w2.path + PID_same = w1.PID == w2.PID + process_same = process_name_same or PID_same ## 非严格模式下 if not strict: + ## 进程名称、路径相同则同一个 + if process_name_same and process_path_same: + return True + + ## 非智能模式下 + if not auto: ## 进程名称相同且标题名称相同则同一个 if process_name_same and title_same: return True diff --git a/requirements.txt b/requirements.txt index 47b015d..d39ea99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,16 +3,16 @@ charset-normalizer==3.4.1 comtypes==1.4.8 idna==3.10 keyboard==0.13.5 -Nuitka==2.5.9 -numpy==2.1.3 +Nuitka +numpy +urllib3 +winsdk==1.0.0b10 +wxPython==4.2.2 +zstandard==0.23.0 ordered-set==4.1.0 psutil==6.1.0 pycaw==20240210 pynput==1.7.7 pywin32==308 requests==2.32.3 -six==1.16.0 -urllib3==2.3.0 -winsdk==1.0.0b10 -wxPython==4.2.2 -zstandard==0.23.0 \ No newline at end of file +six==1.16.0 \ No newline at end of file