前言
实在太懒了,按键精灵还老是报错。
在CSDN的基础上写了一个Python版。
具体明天详写。
具体实现请见:Github
最终成果:
正文
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
参数回来,
- 如果为
listen
则为recording 录制 - 如果为
execute
则为replaying播放
其次每个command里面,都会包含3个Json消息:
- 第一条Json为记录Controller和UI按钮的变量
- 第二条为Mouse鼠标,为鼠标的监听/播放
- 第三条为Keyboard键盘,为键盘的监听/播放
下面的can_start_listening
和can_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按钮的,如果按钮被按下
count_down > 0
则说明还在倒数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变量,以下做一些说明:
- global can_start_listening: 是否允许开始新的录制
- global can_start_executing: 是否允许开始新的播放
- global execute_time_keyboard: keyboard的执行次数,由于没有Controller外部控制,而keyboard和mouse又是thread,所以只能对他们进行分别的execute_time,但这个地方其实可以变为局部变量,因为没有对其进行操作
- global execute_time_mouse: 同上,可以变为局部变量
- global stop_execute_keyboard: 因为也是由于Controller没有设计好的关系,keyboard和mouse 的结束时间不会一样,所以如果是同一个stop_execute的话,很有可能键盘已经早早结束了,但是鼠标本来不应该结束,但是倍键盘的global stop_execute带结束了
- global stop_execute_mouse: 同上,防止被键盘提前带结束
- global stop_listen: 停止录制
总结
由于底层架构没有写好,refactoring可能需要大改。
但是又太懒了,不怎么想大改,目前能用。
所以可能长时间都会停留在version0.1的这个版本了。
(●'◡'●)
参考
Q.E.D.