项目背景
说起来好笑,假设有一个奇怪需求 — 仅仅是假设,不代表我有这个需求,虽然可以想象有人会有这个需求,但是这个人不是我,我也不认识任何这样的人 — 疯狂向某个程序输出按键,比如,一会儿疯狂输入f
,一会儿疯狂输入q
。
如果是两个按键需求,我想要设置一个最简单最通用的开启和关闭办法,那么我就会考虑使用Capslock
和Numlock
.
Capslock | Numlock | 功能 |
---|---|---|
关闭 | * | 关闭疯狂按键 |
打开 | 关闭 | 疯狂输入q |
打开 | 打开 | 疯狂输入f |
画成一个状态图,就是这样的。
这里,可以看到Capslock
是一个总开关,Numlock
是一个切换开关。这样,我就可以通过Capslock
来控制是否疯狂输出按键,通过Numlock
来控制疯狂输出的按键是什么。这么考虑一方面是直觉,另一方面也是因为Capslock
在所有键盘上基本上都有,而且很容易用小手指按到。相对而言,Numlock
在一些键盘上可能没有,而且也不容易按到。
如果你要输入的键更多,那么你可以考虑使用ScrollLock
。同样,我们考虑使用Capslock
作为总开关,Numlock
和ScrollLock
作为切换开关。
Capslock | Numlock | ScrollLock | 功能 |
---|---|---|---|
关闭 | * | * | 关闭疯狂按键 |
打开 | 关闭 | 关闭 | 疯狂输入q |
打开 | 打开 | 关闭 | 疯狂输入f |
打开 | 关闭 | 打开 | 疯狂输入a |
打开 | 打开 | 打开 | 疯狂输入b |
这样的需求是不是很奇怪?这个人也真的很奇怪。再次强调,我不是这个人,也不认识这个人。但是,我可以用Python实现这个需求,毕竟,我是业余程序员,职业的程序员就不一定能实现这个需求了。
前置工作
工欲善其事必先利其器,我们要搞事情,就要先安装Python。
by 匿名社区大佬
安装 Python
首先,我们要安装一个赤裸裸的Python。作为一个小白,我从来不知道什么版本是最好的,我都是下一个最新的稳定版。所以,我建议你也下一个最新的稳定版。你可以在Python下载找到Python的安装包。
随便打开一个命令行窗口(不是PowerShell窗口),然后输入python
,如果你看到了Python的版本号。那么,恭喜你,你已经安装成功了。
在Windows系统中,你可以按Win + R
,然后输入cmd
,然后按回车键,这样就可以打开一个命令行窗口。
命令行窗口和PowerShell窗口是不一样的,PowerShell窗口是一个强大的窗口,但是我经常被它搞晕,所以我不建议你使用PowerShell窗口。
创建环境
一般来说,我们不推荐在系统的Python环境中安装第三方库,因为这样可能会导致系统的Python环境变得混乱。或者有人的Python安装在C盘,装一些全局的包可能会把C盘撑爆(别问我为什么知道,我只是听说而已)。
所以,我们一般会创建一个虚拟环境,然后在虚拟环境中安装第三方库。
python -m venv .venv
.venv\Scripts\activate
通过以上的命令,我们就在当前的目录下创建了一个虚拟环境,然后激活了这个虚拟环境。激活之后,对Python干的事情就全部停留在这个虚拟环境中了。What happens in the venv, stays in the venv.最后要始乱终弃哦不清理痕迹的时候,只需要把这个.venv
文件夹删除就可以了。
安装依赖
一般而言,我们工程素质比较高,会把依赖写在一个requirements.txt
文件中,然后通过pip
来安装依赖。
pydirectinput
pywinauto
高端人士可能还会精心指定一下版本,我们更高端的人士就不需要这样的底层操作,一旦哪个包不能用了,我们不干了就行了……
之后,我们就可以通过以下命令来安装依赖。
pip install -r requirements.txt
还有一种可能就是需要先升级pip
,然后再安装依赖。有时候我们在pypi
下载包的时候会很慢,这时候我们可以通过指定镜像来安装依赖。两个结合在一起,就是以下的命令。
python -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --upgrade pip
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
代码实现
需求分析
首先,内裤哦不需求分析:
- 交互需求:
- 显示状态
- 疯狂 vs 关闭
- 按键选择
- 操作
- 开启/关闭疯狂
- 选择不同按键
- 显示状态
- 数据需求:
- 无
我们尽量简化,那么该如何设计UI呢?我们这种高端人士,根本不需要知道怎么设计UI,只需要提意见就行,“五彩斑斓的黑”、“略带黑色的白”、“稍微透一点白色的黑”、“更加纯粹黑色的黑”……
按照我上面的设计思路,只需要看看键盘上的灯就能够知道状态,但是我们可能会有没有灯的键盘,而且,老师低头去看键盘,不就显得非常低端了吗?所以,我们还是需要一个小窗口来显示状态。
我们的UI设计就是这样的:
- 在屏幕上显示一个色块
- 在色块上显示文字
- 对应的三种状态
- 关闭:红色,显示当前按键
- 疯狂1:绿色,显示当前按键
- 疯狂2:黄色,显示当前按键
- 双击色块:退出程序
- 中键色块:切换疯狂和关闭(顺便切换Capslock)
- 右键色块:切换按键(顺便切换Numlock)
为什么还要增加两个鼠标操作呢?因为我们是高端人士,我们不需要按键盘,我们只需要鼠标就可以了。
我们的代码由三个部分组成:
- GUI主控程序
- 找准目标窗口
- 疯狂小手手按键盘
GUI主控程序
import sys
import tkinter as tk
import pydirectinput
import win32con
from pydirectinput import press
from win32api import GetKeyState as gks
import crazy
from wow import switch_to_wow
import pydirectinput
def setup_root_properties(r: tk.Tk, w, h, fs):
"""
Create a tkinter window with the following properties:
- The window is always on top.
- The window is not resizable.
- The window is not decorated.
- The window is centered on the screen.
- The window can be moved by dragging it.
- The window can be closed by double-clicking it.
- The window can toggle numlock by right-clicking it.
- The window can toggle capslock by middle-clicking it.
:param r: tk.Tk, the root window.
:param w: int, the width of the window.
:param h: int, the height of the window.
:param fs: int, the font size of the text.
"""
global label
def move_window(event):
r.geometry("+{0}+{1}".format(event.x_root, event.y_root))
r.wm_attributes("-topmost", True)
r.overrideredirect(True)
sw = r.winfo_screenwidth()
sh = r.winfo_screenheight()
left = (sw - WIDTH) / 2
top = (sh - HEIGHT) / 2
label = tk.Label(root,
text="AHK",
width=WIDTH,
height=HEIGHT,
bg="red",
font=("微软雅黑", fs))
label.pack()
r.geometry("%dx%d+%d+%d" % (WIDTH, HEIGHT, left, top))
label.bind("<B1-Motion>", move_window)
label.bind("<Double-Button-1>", lambda event: quit_program())
label.bind("<Button-2>", lambda event: toggle_capslock())
label.bind("<Button-3>", lambda event: toggle_num_lock())
return label
def get_color_text():
"""
Determine the color and text of the label.
- If capslock and numlock are on, the color is green and the text is ~AOE~.
- If capslock is on and numlock is off, the color is yellow and the text is ~SINGLE~.
"""
capslock = gks(win32con.VK_CAPITAL)
num_lock = gks(win32con.VK_NUMLOCK)
color = "green" if (capslock and num_lock) else "yellow" if (
capslock and not num_lock) else "red"
text = AOE if num_lock else SINGLE
return color, text
def quit_program():
"""
Quit the program.
"""
crazy.stop()
root.quit()
sys.exit()
def toggle_num_lock():
"""
Toggle numlock, first switch to window of the windows, then press numlock.
"""
switch_to_wow(r".*魔兽世界.*")
press("numlock")
def toggle_capslock():
"""
Toggle capslock, first switch to window of the windows, then press capslock."""
switch_to_wow(r".*魔兽世界.*")
press("capslock")
def update_tkinter_ui(UI_UPDATE_INTERVAL=200):
"""
Using after method to update the tkinter UI.
- If the color is the same as the current color, only update the text.
- If the color is different from the current color, update the color and text.
- The update_tkinter_ui method will be called every 200 milliseconds by default.
:param UI_UPDATE_INTERVAL: int, the interval in ms to update the UI.
"""
crazy.start()
c, t = get_color_text()
if c == label.cget("bg") and t == label.cget("text"):
pass
if c == label.cget("bg"):
label.configure(text=t)
else:
label.configure(bg=c)
root.after(UI_UPDATE_INTERVAL, update_tkinter_ui)
# pydirectinput.PAUSE for the delay between key presses
pydirectinput.PAUSE = 0.025
if __name__ == '__main__':
SINGLE = "Q"
AOE = "F"
WIDTH = 60
HEIGHT = 60
FONTSIZE = 36
root = tk.Tk()
label = setup_root_properties(root, WIDTH, HEIGHT, FONTSIZE)
update_tkinter_ui()
root.mainloop()
这个GUI是用tkinter
实现的,三个稍微好玩一点的事情。
窗口特性
第一个就是设置一个没有标题栏的窗口,并且把窗口设置为总在最前。
r.wm_attributes("-topmost", True)
r.overrideredirect(True)
尺寸与移动
第二个就是设置窗口的位置,让窗口居中显示,并且可以拖动。
sw = r.winfo_screenwidth()
sh = r.winfo_screenheight()
left = (sw - WIDTH) / 2
top = (sh - HEIGHT) / 2
r.geometry("%dx%d+%d+%d" % (WIDTH, HEIGHT, left, top))
label.bind("<B1-Motion>", move_window)
def move_window(event):
r.geometry("+{0}+{1}".format(event.x_root, event.y_root))
仔细观察这里设置root的位置的代码,可以看到两个数值Width x height
,左上角的坐标是+ left + top
,在tcl/tk中,所有的玩意都是字符串,所以都是采用格式约定的方式来表示。
界面更新
第三个就是更新界面的代码,跟所有的GUI框架一样,tkinter也是单线程的,所以我们不能在主线程中做耗时的操作,所以我们使用after
方法来定时更新界面。
root.after(UI_UPDATE_INTERVAL, update_tkinter_ui)
这个函数在下面还需要手动调用一次,这样就可以实现定时更新界面的效果。
update_tkinter_ui()
这样就在所谓的GUI线程中弄了一个定时事件,每隔200ms更新一次界面,不至于让界面卡死,也不会影响到其他后台操作,比如这里的疯狂按键。
找准目标窗口
import pywinauto
from pydirectinput import position, moveTo
from pywinauto.application import Application as App
wow = None
def _get_wow_window(RE_TITLE=r".*魔兽世界.*"):
"""
Avoid using this function directly, use switch_to_wow instead.
"""
global wow
if wow is not None:
return wow
try:
wow_app = App().connect(title_re=RE_TITLE)
# Simply return the first window
w = wow_app.window()
return w
except pywinauto.findwindows.ElementNotFoundError:
return None
wow = _get_wow_window(r".*魔兽世界.*")
def switch_to_wow(RE_TITLE=r".*魔兽世界.*"):
"""
Switch to the window of the first windows with the title matching RE_TITLE.
Using a global variable wow to store the window object to avoid searching for the window every time.
:param RE_TITLE: str, the regular expression of the title of the window.
"""
global wow
try:
if wow is None:
wow = _get_wow_window(RE_TITLE)
# try to switch to wow if exists
if wow is not None:
x, y = position()
wow.set_focus()
moveTo(x, y)
except pywinauto.findwindows.ElementNotFoundError:
pass
这个部分就是在按键之前找到一个窗口,然后在搞事情。这里使用了pywinauto
这个库,这个库可以用来操作Windows的窗口,比如找到一个窗口,然后把这个窗口放到最前面。
这里采用底层是pywinauto.findwindows.find_elements
,这个函数可以根据窗口的标题来找到一个窗口,然后返回这个窗口的对象。这个对象可以用来操作这个窗口,比如把这个窗口放到最前面。这里我们用的参数是带模式识别的标题字符串。
.*魔兽世界.*
这个字符串是一个正则表达式,表示任意字符0次或多次,然后是魔兽世界
,然后是任意字符0次或多次。这样就可以匹配到魔兽世界
这个字符串。我们用写字板打开一个文件,文件名包含魔兽世界
,然后运行这个函数,就可以找到这个窗口。
此外,用了一个模块的变量wow
来存储这个窗口对象,这样就不用每次都去找这个窗口了。
疯狂小手手
最后是疯狂按键盘的部分,这里可以用pydirectinput
这个库来实现。这个库对于某些用DirectInput的游戏来说,是一个很好的库,可以用来模拟键盘和鼠标的输入。
此外,还要考虑一个问题,就是如何在后台运行这个程序。这里可以使用multiprocessing
这个库来实现。这个库可以用来创建进程,这样就可以在后台运行这个程序。用Thread也可以,但是Thread是在一个进程中的,而且Python的Thread是伪线程,所以还是用进程比较好。
这里用的检测Capslock和Numlock的方法是用win32api
这个库,这个库可以用来操作Windows的API,比如获取键盘的状态。这个函数在我电脑上测试大概是几十微秒的时间,所以放心使用。
值得注意的是pydirectinput.PAUSE
这个变量,这个变量是用来设置按键之间的间隔时间的,这个时间是秒。这个时间是用来模拟人的按键速度的,如果按键太快,可能会被检测到是外挂,所以这个时间不要设置太小。
import multiprocessing as mp
import time
import win32con
from pydirectinput import press
from win32api import GetKeyState as gks
def crazy_press_buttons(start_time=0.020):
"""
Crazy press buttons.
Presses "q" when capslock is on and numlock is off.
Presses "f" when capslock is on and numlock is on.
:param start_time: float, the time to sleep before checking again.
"""
while True:
capslock = gks(win32con.VK_CAPITAL)
if not capslock:
time.sleep(start_time)
continue
num_lock = gks(win32con.VK_NUMLOCK)
if capslock and (not num_lock):
# time.sleep(0.005)
press("q")
continue
if capslock and num_lock:
# time.sleep(0.005)
press("f")
continue
# Start the process to press buttons
# This process will run in the background
# and will be terminated when the program exits.
process = mp.Process(target=crazy_press_buttons, daemon=True)
process_started = False
def start():
"""
Start the process to press buttons.
"""
global process_started, process
if not process_started:
process.start()
process_started = True
def stop():
"""
Stop the process to press buttons.
"""
global process
if process.is_alive():
process.terminate()
运行
在激活虚拟环境之后,运行python main.py
,就可以看到一个小窗口,这个小窗口会显示状态,然后可以通过鼠标操作来控制状态。
总结
- 用Python实现一个奇怪的疯狂按键需求,这个需求是一个奇怪的需求,不是我有这个需求,也不是我认识有这个需求的人。
- 用
tkinter
来实现一个小窗口,这个小窗口可以显示状态,可以通过鼠标操作来控制状态。 - 用
pywinauto
来实现找到一个窗口,然后把这个窗口放到最前面。 - 用
pydirectinput
来实现疯狂按键,这个按键是根据Capslock和Numlock的状态来决定的。 - 用
multiprocessing
来实现在后台运行这个程序,这样就可以在后台运行这个程序,不影响其他的操作。 - 用
win32api
来实现了获取键盘状态,这个函数在我电脑上测试大概是几十微秒的时间,所以放心使用。