用Python实现奇怪的疯狂按键需求

项目背景

说起来好笑,假设有一个奇怪需求 — 仅仅是假设,不代表我有这个需求,虽然可以想象有人会有这个需求,但是这个人不是我,我也不认识任何这样的人 — 疯狂向某个程序输出按键,比如,一会儿疯狂输入f,一会儿疯狂输入q

如果是两个按键需求,我想要设置一个最简单最通用的开启和关闭办法,那么我就会考虑使用CapslockNumlock.

CapslockNumlock功能
关闭*关闭疯狂按键
打开关闭疯狂输入q
打开打开疯狂输入f

画成一个状态图,就是这样的。

Capslock亮灯
Capslock关灯
疯狂模式
Numlock亮灯
Numlock关灯
疯狂模式2
疯狂模式1

这里,可以看到Capslock是一个总开关,Numlock是一个切换开关。这样,我就可以通过Capslock来控制是否疯狂输出按键,通过Numlock来控制疯狂输出的按键是什么。这么考虑一方面是直觉,另一方面也是因为Capslock在所有键盘上基本上都有,而且很容易用小手指按到。相对而言,Numlock在一些键盘上可能没有,而且也不容易按到。

如果你要输入的键更多,那么你可以考虑使用ScrollLock。同样,我们考虑使用Capslock作为总开关,NumlockScrollLock作为切换开关。

CapslockNumlockScrollLock功能
关闭**关闭疯狂按键
打开关闭关闭疯狂输入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,就可以看到一个小窗口,这个小窗口会显示状态,然后可以通过鼠标操作来控制状态。

总结

  1. 用Python实现一个奇怪的疯狂按键需求,这个需求是一个奇怪的需求,不是我有这个需求,也不是我认识有这个需求的人。
  2. tkinter来实现一个小窗口,这个小窗口可以显示状态,可以通过鼠标操作来控制状态。
  3. pywinauto来实现找到一个窗口,然后把这个窗口放到最前面。
  4. pydirectinput来实现疯狂按键,这个按键是根据Capslock和Numlock的状态来决定的。
  5. multiprocessing来实现在后台运行这个程序,这样就可以在后台运行这个程序,不影响其他的操作。
  6. win32api来实现了获取键盘状态,这个函数在我电脑上测试大概是几十微秒的时间,所以放心使用。
  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大福是小强

除非你钱多烧得慌……

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值