├── .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 | 
20 |
21 | 
22 |
23 | 
24 |
25 | ## 使用方法
26 | 从[正式版本](https://github.com/intmian/quick_skill/releases/)中下载最新版本。
27 |
28 | 新建自己的方案和连招。
29 |
30 | ### 例子
31 | 地狱潜兵2,一键搓招与磁轨炮不安全模式定时最大威力释放
32 |
33 | 
34 |
35 |
36 | 某些游戏一键破红甲接羊刀
37 |
38 | 
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 |
--------------------------------------------------------------------------------