├── .gitignore ├── LICENSE ├── desigh.drawio ├── gui.py ├── pack.bat ├── pack ├── quick.json └── setting.json ├── quick_mgr.py └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.spec 2 | __pycache__ 3 | /setting.json 4 | /quick.json 5 | /pack/gui.exe 6 | pack.rar 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2023] [intmian] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /desigh.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /gui.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import sys 4 | import time 5 | import tkinter as tk 6 | from tkinter import ttk,simpledialog,messagebox 7 | from quick_mgr import QuickCastManager 8 | from sys import exit 9 | import ctypes 10 | 11 | def InputToShow(key): 12 | if key[0] == '`': 13 | return f"空{key[1:]}ms" 14 | elif len(key) >= 3 and (key[:2] == "lp" or key[:2] == "rp" or key[:2] == "x1" or key[:2] == "x2"): 15 | s = "" 16 | if key[:2] == "lp": 17 | s = "按住左键" 18 | elif key[:2] == "rp": 19 | s = "按住右键" 20 | elif key[:2] == "x1": 21 | s = "按住侧后键" 22 | elif key[:2] == "x2": 23 | s = "按住侧前键" 24 | return f"{s}({key[2:]}ms)" 25 | else: 26 | return key 27 | 28 | # 界面管理 29 | class ui: 30 | def __init__(self): 31 | self.window = tk.Tk() 32 | self.window.title("快速施法") 33 | 34 | # 窗口大小 35 | window_x = 300 36 | window_y = 300 37 | # 基础控件大小,为了避免官方的gird布局的问题,这里自己实现一份 38 | weight_base = 30 39 | height_base = 30 40 | # 禁止调整大小 41 | self.window.resizable(False, False) 42 | 43 | # DPI 44 | ctypes.windll.shcore.SetProcessDpiAwareness(1) 45 | scale = ctypes.windll.shcore.GetScaleFactorForDevice(0) / 100 46 | window_x = int(window_x * scale) 47 | window_y = int(window_y * scale) 48 | weight_base = int(weight_base * scale) 49 | height_base = int(height_base * scale) 50 | 51 | # 移动到屏幕中央 52 | self.window.geometry(f"{window_x}x{window_y}") 53 | screen_width = self.window.winfo_screenwidth() 54 | screen_height = self.window.winfo_screenheight() 55 | self.window.geometry("%dx%d+%d+%d" % (window_x, window_y, (screen_width - window_x) / 2, (screen_height - window_y) / 2)) 56 | self.window.focus_force() 57 | 58 | left_space = weight_base * 0.5 59 | top_space = height_base * 0.5 60 | 61 | combobox_width = weight_base * 3 62 | combobox_height = height_base * 0.7 63 | combobox_x = 0 + left_space 64 | combobox_y = 0 + top_space 65 | 66 | start_button_width = weight_base * 2 67 | start_button_height = height_base * 1 68 | start_button_x = weight_base * 7 + left_space 69 | start_button_y = height_base * 1 + top_space 70 | 71 | stop_button_width = weight_base * 2 72 | stop_button_height = height_base * 1 73 | stop_button_x = weight_base * 7 + left_space 74 | stop_button_y = height_base * 2 + top_space 75 | 76 | clear_button_width = weight_base * 2 77 | clear_button_height = height_base * 1 78 | clear_button_x = weight_base * 7 + left_space 79 | clear_button_y = height_base * 4 + top_space 80 | 81 | self.combobox = ttk.Combobox(self.window) 82 | self.combobox.place(x=combobox_x, y=combobox_y, width=combobox_width, height=combobox_height) 83 | self.combobox["values"] = ("<新增方案>") 84 | self.combobox.current(0) 85 | 86 | listbox_width = weight_base * 6 87 | listbox_height = int(height_base * 8) 88 | listbox_x = 0 + left_space 89 | listbox_y = int(float(height_base) * 1 + top_space) 90 | 91 | self.listbox = tk.Listbox(self.window, width=listbox_width, height=listbox_height) 92 | self.listbox.place(x=listbox_x, y=listbox_y, width=listbox_width, height=listbox_height) 93 | 94 | self.start_button = tk.Button(self.window, text="开始") 95 | self.start_button.place(x=start_button_x, y=start_button_y, width=start_button_width, height=start_button_height) 96 | self.stop_button = tk.Button(self.window, text="停止") 97 | self.stop_button.place(x=stop_button_x, y=stop_button_y, width=stop_button_width, height=stop_button_height) 98 | self.clear_button = tk.Button(self.window, text="删除方案") 99 | self.clear_button.place(x=clear_button_x, y=clear_button_y,width=clear_button_width, height=clear_button_height) 100 | 101 | key_interval_label_width = weight_base * 2 102 | key_interval_label_height = height_base * 0.7 103 | key_interval_label_x = weight_base * 7 + left_space 104 | key_interval_label_y = height_base * 5 + top_space 105 | self.key_interval_label = tk.Label(self.window, text="按键间隔秒") 106 | self.key_interval_label.place(x=key_interval_label_x, y=key_interval_label_y, width=key_interval_label_width, height=key_interval_label_height) 107 | 108 | key_interval_entry_width = weight_base * 2 109 | key_interval_entry_height = height_base * 0.7 110 | key_interval_entry_x = weight_base * 7 + left_space 111 | key_interval_entry_y = height_base * 6 + top_space 112 | self.key_interval_entry = tk.Entry(self.window) 113 | self.key_interval_entry.place(x=key_interval_entry_x, y=key_interval_entry_y, width=key_interval_entry_width, height=key_interval_entry_height) 114 | 115 | key_up_interval_label_width = weight_base * 2 116 | key_up_interval_label_height = height_base * 0.7 117 | key_up_interval_label_x = weight_base * 7 + left_space 118 | key_up_interval_label_y = height_base * 7 + top_space 119 | self.key_up_interval_label = tk.Label(self.window, text="抬起间隔秒") 120 | self.key_up_interval_label.place(x=key_up_interval_label_x, y=key_up_interval_label_y, width=key_up_interval_label_width, height=key_up_interval_label_height) 121 | 122 | key_up_interval_entry_width = weight_base * 2 123 | key_up_interval_entry_height = height_base * 0.7 124 | key_up_interval_entry_x = weight_base * 7 + left_space 125 | key_up_interval_entry_y = height_base * 8 + top_space 126 | self.key_up_interval_entry = tk.Entry(self.window) 127 | self.key_up_interval_entry.place(x=key_up_interval_entry_x, y=key_up_interval_entry_y, width=key_up_interval_entry_width, height=key_up_interval_entry_height) 128 | # 主窗口销毁后,必须销毁整个程序,不然会出现已经开始的监听阻塞住主线程关闭,倒是无法真正关闭程序 129 | self.window.protocol("WM_DELETE_WINDOW", exit) 130 | 131 | def set_select(self, f): 132 | self.combobox.bind("<>", f) 133 | 134 | def set_click_start(self, f): 135 | # 如果绑定按下事件,切鼠标弹起时没有在按钮上,就会出现按键陷下去的情况……所以绑定鼠标弹起事件 136 | self.start_button.bind("", f) 137 | 138 | def set_click_stop(self, f): 139 | self.stop_button.bind("", f) 140 | 141 | def set_click_clear(self, f): 142 | self.clear_button.bind("", f) 143 | 144 | def set_listbox_select(self, f): 145 | self.listbox.bind("<>", f) 146 | 147 | def set_listbox_double_click(self, item_f): 148 | self.listbox.bind("", item_f) 149 | 150 | # 一个方案开始监听后页面相关 151 | def on_start(self): 152 | self.key_interval_entry.config(state="disabled") 153 | self.key_up_interval_entry.config(state="disabled") 154 | self.combobox.config(state="disabled") 155 | self.start_button.config(state="disabled") 156 | self.stop_button.config(state="normal") 157 | self.clear_button.config(state="disabled") 158 | 159 | # 一个方案停止监听后页面相关 160 | def on_stop(self): 161 | self.key_interval_entry.config(state="normal") 162 | self.key_up_interval_entry.config(state="normal") 163 | self.combobox.config(state="normal") 164 | self.start_button.config(state="normal") 165 | self.stop_button.config(state="disabled") 166 | self.clear_button.config(state="normal") 167 | 168 | # 选择某个方案后页面相关 169 | def on_select_cast(self,cast_name,cast_list): 170 | self.key_interval_entry.config(state="normal") 171 | self.key_up_interval_entry.config(state="normal") 172 | self.combobox.config(state="normal") 173 | self.start_button.config(state="normal") 174 | self.stop_button.config(state="disabled") 175 | self.clear_button.config(state="normal") 176 | str_list = [] 177 | for combo in cast_list: 178 | trigger_key = combo["trigger_key"] 179 | if trigger_key == "x1": 180 | trigger_key = "鼠标侧后键" 181 | elif trigger_key == "x2": 182 | trigger_key = "鼠标侧前键" 183 | elif trigger_key == "MLeft": 184 | trigger_key = "鼠标左键" 185 | elif trigger_key == "MRight": 186 | trigger_key = "鼠标右键" 187 | sequence = "" 188 | for key in combo["sequence"]: 189 | sequence += InputToShow(key) 190 | sequence += " " 191 | str_list.append(f"{trigger_key}: {sequence}") 192 | str_list.append("<双击新增|双击已有项删除>") 193 | self.combobox.set(cast_name) 194 | self.update_list(str_list) 195 | 196 | # 外部添加某个快捷键 197 | def on_add_combo(self,combo): 198 | trigger_key = combo["trigger_key"] 199 | if trigger_key == "x1": 200 | trigger_key = "鼠标侧后键" 201 | elif trigger_key == "x2": 202 | trigger_key = "鼠标侧前键" 203 | elif trigger_key == "MLeft": 204 | trigger_key = "鼠标左键" 205 | elif trigger_key == "MRight": 206 | trigger_key = "鼠标右键" 207 | sequence = "" 208 | for key in combo["sequence"]: 209 | sequence += InputToShow(key) 210 | self.listbox.insert(tk.END, f"{trigger_key}: {sequence}") 211 | # 将<双击新增|双击已有项删除>移到最后 212 | for i in range(len(self.listbox.get(0, tk.END)) - 1): 213 | if self.listbox.get(i) == "<双击新增|双击已有项删除>": 214 | self.listbox.delete(i) 215 | self.listbox.insert(tk.END, "<双击新增|双击已有项删除>") 216 | 217 | # 外部删除某个快捷键 218 | def on_delete_combo(self,trigger_key): 219 | if trigger_key == "x1": 220 | trigger_key = "鼠标侧后键" 221 | elif trigger_key == "x2": 222 | trigger_key = "鼠标侧前键" 223 | elif trigger_key == "MLeft": 224 | trigger_key = "鼠标左键" 225 | elif trigger_key == "MRight": 226 | trigger_key = "鼠标右键" 227 | self.listbox.delete(self.listbox.get(0, tk.END).index(trigger_key)) 228 | 229 | # 外部设置按键间隔 230 | def on_set_key_interval(self, interval,up_interval): 231 | self.key_interval_entry.delete(0, tk.END) 232 | self.key_interval_entry.insert(0, interval) 233 | self.key_up_interval_entry.delete(0, tk.END) 234 | self.key_up_interval_entry.insert(0, up_interval) 235 | 236 | def add_select(self, text): 237 | values = self.combobox["values"] 238 | new_values = values + (text,) 239 | self.combobox["values"] = new_values 240 | self.combobox.current(len(new_values) - 1) 241 | # 将<新增方案>移到最后 242 | for i in range(len(self.combobox["values"]) - 1): 243 | if self.combobox["values"][i] == "<新增方案>": 244 | self.combobox["values"] = self.combobox["values"][:i] + self.combobox["values"][i+1:] 245 | self.combobox["values"] += ("<新增方案>",) 246 | break 247 | 248 | def remove_select(self, text): 249 | self.combobox["values"] = tuple(filter(lambda x: x != text, self.combobox["values"])) 250 | 251 | def update_list(self, list): 252 | self.listbox.delete(0, tk.END) 253 | for i in list: 254 | self.listbox.insert(tk.END, i) 255 | 256 | def start(self): 257 | self.window.mainloop() 258 | 259 | class mgr: 260 | def __init__(self,ui_mgr:ui,quick_mgr:QuickCastManager) -> None: 261 | self.ui_mgr = ui_mgr 262 | self.quick_mgr = quick_mgr 263 | 264 | self.now_choose_cast = "" 265 | self.is_start = False 266 | 267 | for i,cast_name in enumerate(self.quick_mgr.quick_casts): 268 | self.ui_mgr.add_select(cast_name) 269 | if i == len(self.quick_mgr.quick_casts) - 1: 270 | self.select_cast(cast_name) 271 | # 添加一下事件 272 | self.ui_mgr.set_select(self.on_ui_select) 273 | self.ui_mgr.set_listbox_double_click(self.on_ui_item_double_click) 274 | self.ui_mgr.set_click_start(self.on_ui_start) 275 | self.ui_mgr.set_click_stop(self.on_ui_stop) 276 | self.ui_mgr.set_click_clear(self.on_ui_clear) 277 | 278 | # 如果没有方案则在打开时必须新增 279 | if len(self.quick_mgr.quick_casts) == 0: 280 | self.add_cast(True) 281 | self.select_default_cast() 282 | 283 | self.ui_mgr.on_set_key_interval(self.quick_mgr.settings["key_interval"],self.quick_mgr.settings["key_up_interval"]) 284 | 285 | def select_default_cast(self): 286 | default = "" 287 | for i,cast_name in enumerate(self.quick_mgr.quick_casts): 288 | if i == 0: 289 | default = cast_name 290 | self.select_cast(default) 291 | 292 | def on_ui_start(self,event): 293 | if self.is_start: 294 | return 295 | self.is_start = True 296 | self.quick_mgr.settings["key_interval"] = float(self.ui_mgr.key_interval_entry.get()) 297 | self.quick_mgr.settings["key_up_interval"] = float(self.ui_mgr.key_up_interval_entry.get()) 298 | self.quick_mgr.save_settings() 299 | self.quick_mgr.run_listener(self.now_choose_cast) 300 | self.ui_mgr.on_start() 301 | 302 | def on_ui_stop(self,event): 303 | if not self.is_start: 304 | return 305 | self.is_start = False 306 | self.quick_mgr.stop_listener() 307 | self.ui_mgr.on_stop() 308 | 309 | def on_ui_clear(self,event): 310 | if self.is_start: 311 | tk.messagebox.showerror("删除方案", "请先停止监听") 312 | return 313 | self.on_del_casts() 314 | 315 | def select_cast(self,cast_name): 316 | self.ui_mgr.on_select_cast(cast_name,self.quick_mgr.quick_casts[cast_name]) 317 | self.now_choose_cast = cast_name 318 | 319 | def on_ui_select(self,event:tk.Event): 320 | if self.ui_mgr.combobox.get() == "<新增方案>": 321 | self.add_cast() 322 | return 323 | self.select_cast(self.ui_mgr.combobox.get()) 324 | 325 | def on_ui_item_double_click(self,event): 326 | selected_index = self.ui_mgr.listbox.nearest(event.y) 327 | if selected_index >= 0: 328 | selected_item = self.ui_mgr.listbox.get(selected_index) 329 | if selected_item == "<双击新增|双击已有项删除>": 330 | self.add_combo() 331 | else: 332 | yes = simpledialog.messagebox.askyesno("删除快捷键", "是否删除该快捷键") 333 | if yes: 334 | # 提取出触发键 335 | trigger_key = selected_item.split(":")[0] 336 | if trigger_key == "鼠标侧后键": 337 | trigger_key = "x1" 338 | elif trigger_key == "鼠标侧前键": 339 | trigger_key = "x2" 340 | elif trigger_key == "鼠标左键": 341 | trigger_key = "MLeft" 342 | elif trigger_key == "鼠标右键": 343 | trigger_key = "MRight" 344 | self.del_comb(trigger_key,selected_item) 345 | 346 | def del_comb(self, trigger_key,selected_item): 347 | self.quick_mgr.delete_combo_from_cast(self.now_choose_cast,trigger_key) 348 | self.ui_mgr.on_delete_combo(selected_item) 349 | self.quick_mgr.stop_listener() 350 | self.quick_mgr.run_listener(self.now_choose_cast) 351 | 352 | def on_del_casts(self): 353 | yes = simpledialog.messagebox.askyesno("删除方案", "是否删除该方案") 354 | if yes: 355 | # 删除当前方案,选择第一个方案,如果没有方案则触发新增 356 | self.quick_mgr.delete_cast(self.now_choose_cast) 357 | self.ui_mgr.remove_select(self.now_choose_cast) 358 | if len(self.quick_mgr.quick_casts) == 0: 359 | self.add_cast(True) 360 | self.select_default_cast() 361 | 362 | def add_combo(self): 363 | # 以下非常扭曲的写法是为了避免第二个窗口不能获得焦点的bug,具体见https://stackoverflow.com/questions/54043323/tkinter-simpledialog-boxes-not-getting-focus-in-windows-10-with-python3 364 | root = tk.Tk() 365 | root.withdraw() 366 | root.update_idletasks() 367 | trigger_key = simpledialog.askstring("新增快捷键", "请输入触发键(快捷键使用+连接,鼠标后侧键x1前侧键x2,鼠标左键MLeft、右键MRight)",parent=root) 368 | if trigger_key == None or trigger_key == "": 369 | return 370 | # 检测是否包含空格 371 | if " " in trigger_key: 372 | simpledialog.messagebox.showerror("新增方案", "触发键不能包含空格") 373 | return 374 | # 检测是否已经存在 375 | for combo in self.quick_mgr.quick_casts[self.now_choose_cast]: 376 | if combo["trigger_key"] == trigger_key: 377 | simpledialog.messagebox.showerror("新增方案", "已经存在该快捷键,请先删除旧的") 378 | return 379 | root.update_idletasks() 380 | sequence = simpledialog.askstring("新增快捷键", "请输入按键序列,支持方向键(上下左右),按住左右侧前侧后键键(lp|rp|x1|x2xx 按住xx毫秒),额外支持`n表示空n ms,对应技能后摇等,以空格分隔",parent=root) 381 | if sequence == None or sequence == "": 382 | return 383 | 384 | sequence = sequence.split() 385 | # 检查是否存在是否存在大于一个键的 386 | for key in sequence: 387 | if key[0] == "`": 388 | # 遍历下是否都为数字,且小于10000 389 | if not key[1:].isdigit() or int(key[1:]) > 10000: 390 | simpledialog.messagebox.showerror("新增方案", "无效的按键序列") 391 | return 392 | elif len(key) >= 3 and (key[:2] == "lp" or key[:2] == "rp" or key[:2] == "x1" or key[:2] == "x2"): 393 | if not key[2:].isdigit() or int(key[2:]) > 10000: 394 | simpledialog.messagebox.showerror("新增方案", "无效的按键序列") 395 | return 396 | elif len(key) > 1: 397 | simpledialog.messagebox.showerror("新增方案", "无效的按键序列") 398 | return 399 | self.quick_mgr.add_combo_to_cast(self.now_choose_cast,trigger_key,sequence,self.is_start) 400 | self.ui_mgr.on_add_combo({"trigger_key":trigger_key,"sequence":sequence}) 401 | 402 | def add_cast(self,first=False): 403 | # 弹窗,用户输入方案名 404 | last_casts_name = self.now_choose_cast 405 | self.ui_mgr.combobox.set("") 406 | self.ui_mgr.listbox.delete(0, tk.END) 407 | if not first: 408 | cast_name = simpledialog.askstring("新增方案", "请输入方案名") 409 | else: 410 | cast_name = simpledialog.askstring("新增方案", "没有方案,请输入方案名",initialvalue="默认方案") 411 | if cast_name == None or cast_name == "": 412 | if last_casts_name == "": 413 | tk.messagebox.showerror("新增方案", "没有方案时不能拒绝新增,程序即将推出") 414 | exit() 415 | self.select_cast(last_casts_name) 416 | return 417 | self.quick_mgr.create_new_cast(cast_name,[]) 418 | self.select_cast(cast_name) 419 | self.ui_mgr.add_select(cast_name) 420 | self.ui_mgr.on_select_cast(cast_name,[]) 421 | 422 | def start(self): 423 | self.ui_mgr.start() 424 | 425 | 426 | def main(): 427 | # 打包的程序的实际路径是一个临时目录,所以注释 428 | # os.chdir(os.path.dirname(os.path.realpath(__file__))) 429 | # 如果没有管理员权限就以管理员权限重新启动 430 | if ctypes.windll.shell32.IsUserAnAdmin() == 0: 431 | messagebox.showwarning("警告", "未以管理员身份运行,将以管理员权限重新启动") 432 | # ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, __file__, None, 1) 433 | ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1 | 0x40) 434 | return 435 | random.seed() 436 | m = mgr(ui(),QuickCastManager()) 437 | m.start() 438 | 439 | if __name__ == "__main__": 440 | main() -------------------------------------------------------------------------------- /pack.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | 4 | rem 定义文件和文件夹路径 5 | set PACK_FOLDER=pack 6 | set GUI_SCRIPT=gui.py 7 | 8 | @REM rem 删除现有的pack文件夹及其内容 9 | @REM if exist %PACK_FOLDER% ( 10 | @REM rmdir /s /q %PACK_FOLDER% 11 | @REM ) 12 | 13 | rem 新建pack文件夹 14 | mkdir %PACK_FOLDER% 15 | 16 | rem 执行打包操作 17 | pyinstaller --onefile --noconsole %GUI_SCRIPT% 18 | 19 | rem 移动生成的可执行文件到pack文件夹 20 | move dist\gui.exe %PACK_FOLDER% 21 | 22 | rem 删除临时生成的dist和build文件夹 23 | rmdir /s /q dist 24 | rmdir /s /q build 25 | 26 | rem 提示操作完成 27 | echo Packing completed. 28 | 29 | endlocal 30 | -------------------------------------------------------------------------------- /pack/quick.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pack/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "key_up_interval": 0.01, 3 | "key_interval": 0.08 4 | } -------------------------------------------------------------------------------- /quick_mgr.py: -------------------------------------------------------------------------------- 1 | import json 2 | import keyboard 3 | import random 4 | import time 5 | import os 6 | from pynput import mouse 7 | from ctypes import windll 8 | mouseController = mouse.Controller() 9 | user32 = windll.user32 10 | kernel32 = windll.kernel32 11 | psapi = windll.psapi 12 | # TODO: 后面加一个自动识别游戏窗口的功能,避免在外面误触 13 | 14 | class QuickCastManager: 15 | def __init__(self): 16 | self.settings = self.load_settings() 17 | self.quick_casts = self.load_quick_casts() 18 | self.save_settings() 19 | self.save_quick_casts() 20 | self.select_cast = None 21 | self.mouse_combo = {} 22 | self.lock = False 23 | mouse_listener = mouse.Listener(on_click=self.on_click) 24 | # 启动监听器 25 | mouse_listener.start() 26 | 27 | # 鼠标按键监听 28 | def on_click(self, x,y, button, pressed): 29 | """ 30 | 程序启动时就会启动鼠标监听,开始流程后,遍历所有方案,将鼠标相关注册到此处。 31 | 请注意除了输入处都是以汉字存储对应功能的。 32 | """ 33 | if not pressed: 34 | return 35 | print(self.mouse_combo) 36 | if button == mouse.Button.x1: 37 | if 'x1' not in self.mouse_combo: 38 | return 39 | self.run_combo(self.mouse_combo['x1']) 40 | elif button == mouse.Button.x2: 41 | if 'x2' not in self.mouse_combo: 42 | return 43 | self.run_combo(self.mouse_combo['x2']) 44 | elif button == mouse.Button.left: 45 | if 'MLeft' not in self.mouse_combo: 46 | return 47 | self.run_combo(self.mouse_combo['MLeft']) 48 | elif button == mouse.Button.right: 49 | if 'MRight' not in self.mouse_combo: 50 | return 51 | self.run_combo(self.mouse_combo['MRight']) 52 | 53 | def load_settings(self): 54 | try: 55 | with open("setting.json", "r") as file: 56 | return json.load(file) 57 | except FileNotFoundError: 58 | return {"key_up_interval": 0.01, "key_interval": 0.08} 59 | 60 | def load_quick_casts(self): 61 | try: 62 | with open("quick.json", "r") as file: 63 | return json.load(file) 64 | except FileNotFoundError: 65 | return {} 66 | 67 | def save_quick_casts(self): 68 | with open("quick.json", "w+") as file: 69 | json.dump(self.quick_casts, file, indent=2) 70 | 71 | def save_settings(self): 72 | with open("setting.json", "w+") as file: 73 | json.dump(self.settings, file, indent=2) 74 | 75 | def change_settings(self, key_interval, key_up_interval): 76 | self.settings["key_interval"] = key_interval 77 | self.settings["key_up_interval"] = key_up_interval 78 | self.save_settings() 79 | 80 | def create_new_cast(self, name, combos): 81 | self.quick_casts[name] = combos 82 | self.save_quick_casts() 83 | 84 | def delete_cast(self, name): 85 | if name not in self.quick_casts: 86 | return False 87 | del self.quick_casts[name] 88 | self.save_quick_casts() 89 | return True 90 | 91 | def delete_combo_from_cast(self, cast_name,trigger_key): 92 | if cast_name not in self.quick_casts: 93 | return False 94 | find = False 95 | for combo in self.quick_casts[cast_name]: 96 | if combo["trigger_key"] == trigger_key: 97 | self.quick_casts[cast_name].remove(combo) 98 | find = True 99 | break 100 | self.save_quick_casts() 101 | return find 102 | 103 | def add_combo_to_cast(self, cast_name,trigger_key,sequence, hotkey=False): 104 | if cast_name in self.quick_casts: 105 | # 如果存在就覆盖 106 | find = False 107 | for combo in self.quick_casts[cast_name]: 108 | if combo["trigger_key"] == trigger_key: 109 | combo["sequence"] = sequence 110 | find = True 111 | break 112 | if not find: 113 | self.quick_casts[cast_name].append({"trigger_key": trigger_key, "sequence": sequence}) 114 | else: 115 | return 116 | if hotkey: 117 | if trigger_key in ["x1","x2","MLeft","MRight"]: 118 | self.mouse_combo[trigger_key] = {"trigger_key": trigger_key, "sequence": sequence} 119 | else: 120 | keyboard.add_hotkey("alt+" + trigger_key, self.run_combo, args=({"trigger_key": trigger_key, "sequence": sequence},)) 121 | self.save_quick_casts() 122 | 123 | def run_listener(self,cast_name): 124 | # 选择方案 125 | if cast_name not in self.quick_casts: 126 | return False 127 | self.select_cast = cast_name 128 | for combo in self.quick_casts[cast_name]: 129 | if combo["trigger_key"] in ["x1","x2","MLeft","MRight"]: 130 | self.mouse_combo[combo["trigger_key"]] = combo 131 | else: 132 | keyboard.add_hotkey(combo["trigger_key"], self.run_combo, args=(combo,)) 133 | self.cast_name = cast_name 134 | return True 135 | 136 | def stop_listener(self): 137 | self.mouse_combo = {} 138 | try: 139 | keyboard.unhook_all_hotkeys() 140 | except Exception as e: 141 | find = False 142 | for combo in self.quick_casts[self.cast_name]: 143 | if combo["trigger_key"] not in ["x1","x2","MLeft","MRight"]: 144 | find = True 145 | break 146 | if find: 147 | return False 148 | return True 149 | 150 | 151 | def run_combo(self, combo): 152 | if self.lock: 153 | return 154 | self.lock = True 155 | # 如果当前存在alt按键被按下,等待按键释放 156 | while keyboard.is_pressed('alt'): 157 | time.sleep(0.001) 158 | # for key in combo['sequence']: 159 | # keyboard.press(key) 160 | # delay = self.settings["key_interval"] * random.uniform(0.66, 1.33) 161 | # time.sleep(delay) 162 | # keyboard.release(key) 163 | # delay = self.settings["key_up_interval"] * random.uniform(0.66, 1.33) 164 | # time.sleep(delay) 165 | timeline_now = 0 166 | keys = [] 167 | for key in combo['sequence']: 168 | if key[0] == "`": 169 | delay = float(0) 170 | # 为了提高准确度并且操作更加自然,随机延迟设定如下 171 | delay += random.uniform(0.95, 1.051) * 0.001 * int(key[1:]) 172 | timeline_now += delay 173 | continue 174 | if len(key) >= 3 and (key[:2] == "lp" or key[:2] == "rp" or key[:2] == "x1" or key[:2] == "x2"): 175 | delay = float(0) 176 | delay += random.uniform(0.99, 1.01) * 0.001 * int(key[2:]) 177 | if key[0] == "l": 178 | keys.append(("MLeft",True,timeline_now)) 179 | keys.append(("MLeft",False,timeline_now+delay)) 180 | if key[0] == "r": 181 | keys.append(("MRight",True,timeline_now)) 182 | keys.append(("MRight",False,timeline_now+delay)) 183 | if key[:2] == "x1": 184 | keys.append(("x1",True,timeline_now)) 185 | keys.append(("x1",False,timeline_now+delay)) 186 | if key[:2] == "x2": 187 | keys.append(("x2",True,timeline_now)) 188 | keys.append(("x2",False,timeline_now+delay)) 189 | timeline_now += delay 190 | continue 191 | real_key = key 192 | if key == "上": 193 | real_key = "up" 194 | elif key == "下": 195 | real_key = "down" 196 | elif key == "左": 197 | real_key = "left" 198 | elif key == "右": 199 | real_key = "right" 200 | keys.append((real_key,True,timeline_now)) 201 | delay = self.settings["key_up_interval"] * random.uniform(0.952, 1.05) 202 | keys.append((real_key,False,timeline_now+delay)) 203 | timeline_now += self.settings["key_interval"] * random.uniform(0.82, 1.19) 204 | keys.sort(key=lambda x: x[2]) 205 | for i,key in enumerate(keys): 206 | print(key) 207 | if i != 0: 208 | time.sleep(key[2]-keys[i-1][2]) 209 | if key[0] == "MLeft": 210 | if key[1]: 211 | mouseController.press(mouse.Button.left) 212 | else: 213 | mouseController.release(mouse.Button.left) 214 | continue 215 | elif key[0] == "MRight": 216 | if key[1]: 217 | mouseController.press(mouse.Button.right) 218 | else: 219 | mouseController.release(mouse.Button.right) 220 | continue 221 | elif key[0] == "x1": 222 | if key[1]: 223 | mouseController.press(mouse.Button.x1) 224 | else: 225 | mouseController.release(mouse.Button.x1) 226 | continue 227 | elif key[0] == "x2": 228 | if key[1]: 229 | mouseController.press(mouse.Button.x2) 230 | else: 231 | mouseController.release(mouse.Button.x2) 232 | continue 233 | if key[1]: 234 | keyboard.press(key[0]) 235 | else: 236 | keyboard.release(key[0]) 237 | self.lock = False -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # quick_skill 简易的键盘宏程序 2 | 3 | ## 项目简介 4 | 5 | `quick_skill`,如一把翩跹的琴箫,倾听你指尖的节奏,是一款崭新的键盘宏管理工具。在这个光怪陆离的数字王国里,它为你创造了一个轻盈的旋律,让你尽享键盘之美。此刻,让我们一同漫游在`quick_skill`的音符之海。 6 | 7 | ## 功能特点 8 | 9 | 1. **多方案编曲**:如诗如画,每个键盘宏方案都是一曲独特的旋律。轻松切换,让你在数字乐章中穿梭自如。 10 | 2. **触发键旋律**:鼠标的侧影,键盘的回响,一键释放,连招流畅,使操作更富韵味。 11 | 3. **随机韵律**:程序智能穿插随机延迟,犹如飘逸的花瓣在风中摇曳,细腻且不失精准,黑夜中独自绽放,不为监测所知。 12 | 4. **延时花语**:按键的缄默,抬起的轻盈,延时的花语在数字之间流淌,延迟的钟摆,让你完美掌握每个旋律绽放的时机。 13 | 5. **技能前奏**:在连招的交响中,可以自由设置技能前奏的延迟,如同交响乐的序曲,为你在游戏中创造更丰富的表演。 14 | 6. **自由调音**:每个键盘宏方案都如一支自由的旋律,可以调整的按键延迟和抬起延迟,为你打造个性化的数字旋律。 15 | 7. **节拍定制**:在数字舞台上,插入自定义延迟,适配技能前后摇,让你的演奏更具个性,更得心应手。 16 | 17 | ## 项目速览 18 | 19 | ![image](https://github.com/intmian/quick_skill/assets/38103855/9b42afbd-6836-4eab-ad3d-2e27f6b58448) 20 | 21 | ![image](https://github.com/intmian/quick_skill/assets/38103855/d293064c-7407-4ad9-b87d-50f29263111a) 22 | 23 | ![image](https://github.com/intmian/quick_skill/assets/38103855/51709ee0-d170-417a-8d17-5ca861895ce6) 24 | 25 | ## 使用方法 26 | 从[正式版本](https://github.com/intmian/quick_skill/releases/)中下载最新版本。 27 | 28 | 新建自己的方案和连招。 29 | 30 | ### 例子 31 | 地狱潜兵2,一键搓招与磁轨炮不安全模式定时最大威力释放 32 | 33 | ![image](https://github.com/user-attachments/assets/4d968bbd-84d8-4e9a-8504-e67685043530) 34 | 35 | 36 | 某些游戏一键破红甲接羊刀 37 | 38 | ![image](https://github.com/intmian/quick_skill/assets/38103855/d0e9b57c-5ead-4b56-843b-43639871d05e) 39 | 40 | ### 反馈 41 | 如果您在使用中发现了一些问题或有什么改进的点,请通过issue进行反馈。您的反馈,就是对本项目的最大的支持。 42 | 43 | 常见问题可以参考下[issue置顶](https://github.com/intmian/quick_skill/issues/11)。 44 | 45 | ## 注意事项 46 | 47 | - 请注意,`quick_skill` 仅推荐在自动化办公操作或在征得开发者同意的前提下,在游戏内进行自动化操作。如行为违规,后果自负。 48 | - 在使用本程序时,请确保严格遵守游戏或应用的用户协议,以免触犯相关规定。 49 | - 开发者慎重提示,对于用户在使用过程中可能产生的任何问题或后果,概不负责。 50 | 51 | ## 交响之旅 52 | 53 | 携手`Quick_Skill`,让我们共赴一场数字交响之旅。如风轻拂琴弦,键盘在指尖舞动,创造属于你独特的数字旋律。愿你在这场音符的舞台上,找到数字世界的和谐与美妙。 54 | --------------------------------------------------------------------------------