前言

实在太懒了,按键精灵还老是报错。
在CSDN的基础上写了一个Python版。
具体明天详写。
具体实现请见:Github

最终成果:
40593cfc38e2cf787c0de8674511c78.png

正文

1. 概述

就,反正最近在玩《数码宝贝物语:网络侦探骇客追忆》,本身也是个数码迷,玩的也很开心,但是唯一的缺点就是,这游戏有点刷子,而本身也是个懒怪,不怎么爱刷,于是乎就下了按键精灵2014,但是这玩意儿疯狂报错,电脑杀软又疯了似地一直删,那没办法,心一狠,写了个按键精灵python版,反正这下有源码了,也不至于报错。但其实还有很多的bug,但是勉强能用。

2. 源码解说

源码解说将基于v0.1版,也就是mouse_keyboard_simulator_sean_current_version.ipynb的这篇juypter notebook的版本。

2.1 import引入包

import json
import threading
import time
import tkinter

from pynput import keyboard, mouse
from pynput.keyboard import Controller as KeyBoardController, KeyCode
from pynput.mouse import Button, Controller as MouseController

不多说,如果没有pynput的,pip install pynput就好。

2.2 Json格式

# json template
def keyboard_action_template():
    return {
        "name": "keyboard",
        "event": "default",
        "vk": "default"
    }

def mouse_action_template():
    return {
        "name": "mouse",
        "event": "default",
        "target": "default",
        "action": "default",
        "location": {
            "x": "0",
            "y": "0"
        }
    }

原CSDN博客用了Json格式,这个地方看起来,hmm,好像确实很好用的样子,于是也用了Json。:D,取其精华,去其糟粕!

2.3 Command adapter

def command_adapter(action):
    # global variables
    global can_start_listening 
    global can_start_executing
    global execute_time_keyboard
    global execute_time_mouse
    
    # command list
    custom_thread_list = []
    print(can_start_listening)
    
    if can_start_listening or can_start_executing:
        
        if action == 'listen':
            custom_thread_list.append(
                {
                    'obj_thread': ListenController(),
                    'obj_ui': startListenerBtn,
                    'action': 'listen'
                }
            )
            if isRecordMouse.get():
                custom_thread_list.append(
                    {
                        'obj_thread': MouseActionListener(),
                        'obj_ui': None
                    }
                )
            if isRecordKeyboard.get():
                custom_thread_list.append(
                    {
                        'obj_thread': KeyboardActionListener(),
                        'obj_ui': None
                    }
                )
            count_down = listenCountDown.get()

        elif action == 'execute':
            custom_thread_list.append(
                {
                    'obj_thread': ExecuteController(),
                    'obj_ui': startExecuteBtn,
                    'action': 'execute'
                }
            )
            if isReplayMouse.get():
                custom_thread_list.append(
                    {
                        'obj_thread': MouseActionExecute(),
                        'obj_ui': None
                    }
                )
            if isReplayKeyboard.get():
                custom_thread_list.append(
                    {
                        'obj_thread': KeyboardActionExecute(),
                        'obj_ui': None
                    }
                )
            count_down = executeCountDown.get()
            execute_time_keyboard = playCount.get()
            execute_time_mouse = playCount.get()

        can_start_listening = False
        can_start_executing = False
        UIUpdateCutDownExecute(count_down, custom_thread_list).start()

这个地方其实写的很复杂,具体就是前端按下按钮,会传递一个action参数回来,

  1. 如果为listen则为recording 录制
  2. 如果为execute 则为replaying播放

其次每个command里面,都会包含3个Json消息:

  1. 第一条Json为记录Controller和UI按钮的变量
  2. 第二条为Mouse鼠标,为鼠标的监听/播放
  3. 第三条为Keyboard键盘,为键盘的监听/播放

下面的can_start_listeningcan_start_executing分别用于控制,在录制和播放的时候,能不能再次触发这个行为,类似一个”锁“的机制。

2.4 Update UI

class UIUpdateCutDownExecute(threading.Thread):
    def __init__(self, count_down, custom_thread_list):
        super().__init__()
        self.count_down = count_down
        self.custom_thread_list = custom_thread_list

    def run(self):
        while self.count_down > 0:
            startListenerBtn['state'] = 'disabled'
            startExecuteBtn['state'] = 'disabled'
            
            for custom_thread in self.custom_thread_list:
                if custom_thread['obj_ui'] is not None:
                    custom_thread['obj_ui']['text'] = str(self.count_down)
                    self.count_down = self.count_down - 1
            time.sleep(1)
        else:
            for custom_thread in self.custom_thread_list:
                if custom_thread['obj_ui'] is not None:
                    if custom_thread['action'] == 'listen':
                        custom_thread['obj_ui']['text'] = str('Recording, "ESC" to stop.')
                if custom_thread['obj_thread'] is not None:
                    custom_thread['obj_thread'].start()

这个class其实就是单纯用来更新UI按钮的,如果按钮被按下

  1. count_down > 0 则说明还在倒数
  2. count_down <= 0 说明倒数结束,进入录制/播放阶段

2.5 Listening

class KeyboardActionListener(threading.Thread):
    
    def __init__(self, file_name='keyboard.action'):
        super().__init__()
        self.file_name = file_name

    def run(self):
        with open(self.file_name, 'w', encoding='utf-8') as file:
            # press keyboard
            def on_press(key):
                template = keyboard_action_template()
                template['event'] = 'press'
                try:
                    template['vk'] = key.vk
                except AttributeError:
                    template['vk'] = key.value.vk
                finally:
                    file.writelines(json.dumps(template) + "\n")
                    file.flush()

            # release keyboard
            def on_release(key):
                global can_start_listening
                global can_start_executing
                global stop_listen

                if key == keyboard.Key.esc:
                    # Stop by pressing "ESC"
                    stop_listen= True
                    keyboardListener.stop()
                    return False

                if not stop_listen:
                    template = keyboard_action_template()
                    template['event'] = 'release'
                    try:
                        template['vk'] = key.vk
                    except AttributeError:
                        template['vk'] = key.value.vk
                    finally:
                        file.writelines(json.dumps(template) + "\n")
                        file.flush()
            
            with keyboard.Listener(on_press=on_press, on_release=on_release) as keyboardListener:
                keyboardListener.join()   

class MouseActionListener(threading.Thread):

    def __init__(self, file_name='mouse.action'):
        super().__init__()
        self.file_name = file_name

    def run(self):
        with open(self.file_name, 'w', encoding='utf-8') as file:
            # move mouse
            def on_move(x, y):
                global stop_listen
                if stop_listen:
                    mouseListener.stop()
                template = mouse_action_template()
                template['event'] = 'move'
                template['location']['x'] = x
                template['location']['y'] = y
                file.writelines(json.dumps(template) + "\n")
                file.flush()

            # click mouse
            def on_click(x, y, button, pressed):
                global stop_listen
                if stop_listen:
                    mouseListener.stop()
                template = mouse_action_template()
                template['event'] = 'click'
                template['target'] = button.name
                template['action'] = pressed
                template['location']['x'] = x
                template['location']['y'] = y
                file.writelines(json.dumps(template) + "\n")
                file.flush()

            # scroll mouse
            def on_scroll(x, y, x_axis, y_axis):
                global stop_listen
                if stop_listen:
                    mouseListener.stop()
                template = mouse_action_template()
                template['event'] = 'scroll'
                template['location']['x'] = x_axis
                template['location']['y'] = y_axis
                file.writelines(json.dumps(template) + "\n")
                file.flush()

            with mouse.Listener(on_move=on_move, on_click=on_click, on_scroll=on_scroll) as mouseListener:
                mouseListener.join()

这里其实就是实现Listening录制的过程,其中会将鼠标/键盘的录制,分别写入当下目录的mouse.action/keyboard.action文件中。

2.6 Executing

class KeyboardActionExecute(threading.Thread):

    def __init__(self, file_name='keyboard.action'):
        super().__init__()
        self.file_name = file_name

    def run(self):
        global can_start_listening
        global can_start_executing
        global execute_time_keyboard
        global stop_execute_keyboard
        while execute_time_keyboard >= 0:
            if stop_execute_keyboard:
                can_start_listening = True
                can_start_executing = True
                startExecuteBtn['text'] = 'Start replaying'
                startListenerBtn['state'] = 'normal'
                startExecuteBtn['state'] = 'normal'
                return
            
            startExecuteBtn['text'] = str('Remaining %d #, "ESC" to stop.' %(execute_time_keyboard))
            with open(self.file_name, 'r', encoding='utf-8') as file:
                keyboard_exec = KeyBoardController()
                line = file.readline()
                while line:
                    obj = json.loads(line)
                    if obj['name'] == 'keyboard':
                        if obj['event'] == 'press':
                            keyboard_exec.press(KeyCode.from_vk(obj['vk']))
                            time.sleep(0.01)
                        elif obj['event'] == 'release':
                            keyboard_exec.release(KeyCode.from_vk(obj['vk']))
                            time.sleep(0.01)
                    line = file.readline()
            execute_time_keyboard = execute_time_keyboard - 1
            if execute_time_keyboard == 0:
                stop_execute_keyboard = True


class MouseActionExecute(threading.Thread):

    def __init__(self, file_name='mouse.action'):
        super().__init__()
        self.file_name = file_name

    def run(self):
        global can_start_listening
        global can_start_executing
        global execute_time_mouse
        global stop_execute_mouse
        while execute_time_mouse >= 0:
            if stop_execute_mouse:
                can_start_listening = True
                can_start_executing = True
                startExecuteBtn['text'] = 'Start replaying'
                startListenerBtn['state'] = 'normal'
                startExecuteBtn['state'] = 'normal'
                return
            
            with open(self.file_name, 'r', encoding='utf-8') as file:
                mouse_exec = MouseController()
                line = file.readline()
                while line:
                    obj = json.loads(line)
                    if obj['name'] == 'mouse':
                        if obj['event'] == 'move':
                            mouse_exec.position = (obj['location']['x'], obj['location']['y'])
                            time.sleep(0.01)
                        elif obj['event'] == 'click':
                            if obj['action']:
                                if obj['target'] == 'left':
                                    mouse_exec.press(Button.left)
                                else:
                                    mouse_exec.press(Button.right)
                            else:
                                if obj['target'] == 'left':
                                    mouse_exec.release(Button.left)
                                else:
                                    mouse_exec.release(Button.right)
                            time.sleep(0.01)
                        elif obj['event'] == 'scroll':
                            mouse_exec.scroll(obj['location']['x'], obj['location']['y'])
                            time.sleep(0.01)
                    line = file.readline()
            execute_time_mouse = execute_time_mouse - 1
            if execute_time_mouse == 0:
                stop_execute_mouse = True

这里就是播放过程,播放当前目录下的mouse.action/keyboard.action文件。

2.7 Controller

class ListenController(threading.Thread):
    
    def __init__(self):
        super().__init__()

    def run(self):
        global stop_listen
        stop_listen = False
        
        def on_release(key):
            global can_start_listening 
            global can_start_executing
            global stop_listen
            
            if key == keyboard.Key.esc:
                stop_listen = True
                can_start_listening = True
                can_start_executing = True
                startListenerBtn['text'] = 'Start recording'
                startListenerBtn['state'] = 'normal'
                startExecuteBtn['state'] = 'normal'
                keyboardListener.stop()

        with keyboard.Listener(on_release=on_release) as keyboardListener:
            keyboardListener.join()

class ExecuteController(threading.Thread):
    
    def __init__(self):
        super().__init__()

    def run(self):
        global stop_execute_keyboard
        global stop_execute_mouse
        stop_execute_keyboard = False
        stop_execute_mouse = False
        
        def on_release(key):
            global can_start_listening 
            global can_start_executing
            global stop_execute_keyboard
            
            if key == keyboard.Key.esc:
                stop_execute_keyboard = True
                stop_execute_mouse = True
                can_start_listening = True
                can_start_executing = True
                startExecuteBtn['text'] = 'Start replaying'
                startListenerBtn['state'] = 'normal'
                startExecuteBtn['state'] = 'normal'
                keyboardListener.stop()

        with keyboard.Listener(on_release=on_release) as keyboardListener:
            keyboardListener.join()

这里具体想实现一个Controller的Design Pattern,可惜因为Mouse和Keyboard用的都是thread,拿到他们具体的thread里面的listener和executer有点麻烦(可以说根本不会,对Python这边的thread不是很熟悉)所以导致了,在实现按下”ESC“就停止的这个功能的时候,会这么的复杂。也因为自己想要mouse和keyboard分开录制实现这一功能,导致如果只录制/播放鼠标的话,那么键盘如果不参与的话,也获得不了”ESC“这个键,造成了很多麻烦。怎么说都是底层没有设计好,如果要refactoring的话可能需要大概。所以大概长时间都会停留在version0.1这个版本了。/(ㄒoㄒ)/~~

2.8 GUI

if __name__ == '__main__':
    
    can_start_listening = True
    can_start_executing = True
    execute_time_keyboard = 0
    
    root = tkinter.Tk()
    root.title('Quick Macro - Sean Zou')
    root.geometry('400x270')
    root.resizable(0,0)

    # recording
    # time to record
    listenerStartLabel = tkinter.Label(root, text='Record countdown')
    listenerStartLabel.place(x=100, y=10, width=120, height=20)
    
    listenCountDown = tkinter.IntVar()
    listenCountDown.set(3)
    
    listenerStartEdit = tkinter.Entry(root, textvariable=listenCountDown)
    listenerStartEdit.place(x=220, y=10, width=60, height=20)
    
    listenerTipLabel = tkinter.Label(root, text='s')
    listenerTipLabel.place(x=280, y=10, width=20, height=20)

    # start recording
    startListenerBtn = tkinter.Button(root, text="Start recording", command=lambda: command_adapter('listen'))
    startListenerBtn.place(x=100, y=45, width=200, height=30)

    # replaying
    # time to replay
    executeEndLabel = tkinter.Label(root, text='Replay countdown')
    executeEndLabel.place(x=100, y=85, width=120, height=20)
    
    executeCountDown = tkinter.IntVar()
    executeCountDown.set(3)
    
    executeEndEdit = tkinter.Entry(root, textvariable=executeCountDown)
    executeEndEdit.place(x=220, y=85, width=60, height=20)
    
    
    executeTipLabel = tkinter.Label(root, text='s')
    executeTipLabel.place(x=280, y=85, width=20, height=20)

    # times for replaying
    playCountLabel = tkinter.Label(root, text='Repeat Times')
    playCountLabel.place(x=100, y=115, width=120, height=20)
    
    playCount = tkinter.IntVar()
    playCount.set(1)
    
    playCountEdit = tkinter.Entry(root, textvariable=playCount)
    playCountEdit.place(x=220, y=115, width=60, height=20)

    playCountTipLabel = tkinter.Label(root, text='#')
    playCountTipLabel.place(x=280, y=115, width=20, height=20)

    # start replaying
    startExecuteBtn = tkinter.Button(root, text="Start replaying", command=lambda: command_adapter('execute'))
    startExecuteBtn.place(x=100, y=145, width=200, height=30)
    
    # if record mouse
    isRecordMouse = tkinter.BooleanVar()
    isRecordMouse.set(False)
    
    recordMouseCheckbox = tkinter.Checkbutton(root, text='record mouse', variable=isRecordMouse)
    recordMouseCheckbox.place(x=80, y=200, width=120, height=20)
    
    # if record keyboard
    isRecordKeyboard = tkinter.BooleanVar()
    isRecordKeyboard.set(True)
    
    recordKeyboardCheckbox = tkinter.Checkbutton(root, text='record keyborad', variable=isRecordKeyboard)
    recordKeyboardCheckbox.place(x=200, y=200, width=120, height=20)
    
    # if replay mouse
    isReplayMouse = tkinter.BooleanVar()
    isReplayMouse.set(False)
    
    replayMouseCheckbox = tkinter.Checkbutton(root, text='replay mouse', variable=isReplayMouse)
    replayMouseCheckbox.place(x=80, y=225, width=120, height=20)
    
    # if replay keyboard
    isReplayKeyboard = tkinter.BooleanVar()
    isReplayKeyboard.set(True)
    
    replayKeyboardCheckbox = tkinter.Checkbutton(root, text='replay keyboard', variable=isReplayKeyboard)
    replayKeyboardCheckbox.place(x=200, y=225, width=120, height=20)
    
    # run
    root.mainloop()

具体的GUI实现,没有什么特别的,用的就是python的tkinter。

2.9 部分Global变量

可以看见我们这里用了一些global变量,以下做一些说明:

  1. global can_start_listening: 是否允许开始新的录制
  2. global can_start_executing: 是否允许开始新的播放
  3. global execute_time_keyboard: keyboard的执行次数,由于没有Controller外部控制,而keyboard和mouse又是thread,所以只能对他们进行分别的execute_time,但这个地方其实可以变为局部变量,因为没有对其进行操作
  4. global execute_time_mouse: 同上,可以变为局部变量
  5. global stop_execute_keyboard: 因为也是由于Controller没有设计好的关系,keyboard和mouse 的结束时间不会一样,所以如果是同一个stop_execute的话,很有可能键盘已经早早结束了,但是鼠标本来不应该结束,但是倍键盘的global stop_execute带结束了
  6. global stop_execute_mouse: 同上,防止被键盘提前带结束
  7. global stop_listen: 停止录制

总结

由于底层架构没有写好,refactoring可能需要大改。
但是又太懒了,不怎么想大改,目前能用。
所以可能长时间都会停留在version0.1的这个版本了。
(●'◡'●)

参考

[1] [620]使用Python实现一个按键精灵

Q.E.D.


立志做一个有趣的碳水化合物