创建工程目录,创建UI文件,创建Main Window
使用思维导图进行简单的需求整理
由于没怎么用过pyqt,简单排版一下,后续也可以再用Vertical Layout和Horizontal Layout进行排版优化,按设计,三个文本框进行-功能说明,-时间间隔填写,-状态说明的显示,一个按钮可以选择-删除文件,下方的大表格框用来选中并且显示录制的脚本文件名及其属性。
逐一修改控件的objectname,便于代码调用,增加可读性,然后保存ui文件生成界面代码:
生成了UI文件:
代码如下:
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'c:\learning\python\pyqt\mouse_recorder\mouse_recorder.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 400)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.doc_textBrowser = QtWidgets.QTextBrowser(self.centralwidget)
self.doc_textBrowser.setGeometry(QtCore.QRect(40, 10, 391, 61))
self.doc_textBrowser.setObjectName("doc_textBrowser")
self.deleteFile_Btn = QtWidgets.QPushButton(self.centralwidget)
self.deleteFile_Btn.setGeometry(QtCore.QRect(450, 50, 131, 21))
self.deleteFile_Btn.setObjectName("deleteFile_Btn")
self.file_tableWidget = QtWidgets.QTableWidget(self.centralwidget)
self.file_tableWidget.setGeometry(QtCore.QRect(40, 90, 700, 300))
self.file_tableWidget.setObjectName("file_tableWidget")
self.file_tableWidget.setColumnCount(0)
self.file_tableWidget.setRowCount(0)
self.file_tableWidget.horizontalHeader().setCascadingSectionResizes(False)
self.file_tableWidget.verticalHeader().setVisible(False)
self.timeinterval_lineEdit = QtWidgets.QLineEdit(self.centralwidget)
self.timeinterval_lineEdit.setGeometry(QtCore.QRect(460, 20, 113, 20))
self.timeinterval_lineEdit.setObjectName("timeinterval_lineEdit")
self.status_textEdit = QtWidgets.QTextEdit(self.centralwidget)
self.status_textEdit.setGeometry(QtCore.QRect(600, 10, 141, 61))
self.status_textEdit.setObjectName("status_textEdit")
MainWindow.setCentralWidget(self.centralwidget)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "KeyHelper"))
self.doc_textBrowser.setHtml(_translate("MainWindow", "\n"
"\n"
"功能说明:
\n"
"功能1.填入时间间隔(可以是小数),按F8鼠标左键连点当前位置。
\n"
"功能2.按F9开始鼠标键盘录制,再按F9结束录制并保存在scripts目录下。
\n"
"功能3.勾选录制文件,按F10进行录制回放。
\n"
"功能4.默认加载当前目录下scripts目录的脚本(其他目录目前不支持)。
\n"
"功能6.点击“删除录制文件”,可以将当前选中的文件删除。
"))
self.deleteFile_Btn.setText(_translate("MainWindow", "删除录制文件"))
后续我们需要一个文件实现界面类对界面进行操作,另外实现一个功能类进行各类功能监听及实现,简单创建两个py文件:
为了界面和功能逻辑分开,需要简单设计一下多线程:
界面逻辑代码my_window.py:
import sys
import os
import time
import json
from PyQt5.QtCore import Qt, QObject, QThread, pyqtSignal
from PyQt5.QtWidgets import QApplication, QMainWindow, QCheckBox, QTableWidgetItem, QFileDialog
from Ui_mouse_recorder import *
from my_recorder import *
class MyWindow(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent)
self.__cur_checkbox = None
self.__filelist = []
self.__curfilepath = []
self.setupUi(self)
self.inittable()
self.getscripts()
self.timeinterval_lineEdit.setPlaceholderText("请输入点击间隔(秒):")
self.deleteFile_Btn.clicked.connect(self.deletefile)
self.recorder = MyRecorder(self)
self.recorder.refresh.connect(self.refresh_file_list)
self.recorder.minimized.connect(self.minimized_window)
self.recorder.active.connect(self.active_window)
self.recorder.savefile.connect(self.savefile)
self.recorder.showstatus.connect(self.showstatus)
def inittable(self):
# set row and column
self.file_tableWidget.setColumnCount(3)
self.file_tableWidget.setRowCount(1)
self.file_tableWidget.setColumnWidth(0, 50)
self.file_tableWidget.setColumnWidth(1, 325)
self.file_tableWidget.setColumnWidth(2, 325)
# set data title
self.file_tableWidget.horizontalHeader().setStyleSheet("QHeaderView::section{background:skyblue;}")
self.file_tableWidget.setHorizontalHeaderItem(0, QTableWidgetItem("序号"))
self.file_tableWidget.setHorizontalHeaderItem(1, QTableWidgetItem("文件名"))
self.file_tableWidget.setHorizontalHeaderItem(2, QTableWidgetItem("修改时间"))
self.file_tableWidget.horizontalHeader().setStretchLastSection(True)
# setting
self.file_tableWidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
def getscripts(self):
scriptspath = self.get_scripts_path()
if not os.path.exists(scriptspath):
os.makedirs(scriptspath)
if self.__filelist==[]: #file list is [], reinit file list
for file in os.listdir(scriptspath):
if file.endswith(".json"):
self.__filelist.append(file)
row_num = len(self.__filelist)
self.file_tableWidget.setRowCount(row_num)
# set data items
for row in range(row_num):
# first column is checkbox
item_checked = QCheckBox(parent=self.file_tableWidget)
item_checked.setText(str(row + 1))
item_checked.setCheckState(Qt.Unchecked)
item_checked.clicked.connect(self.table_item_clicked)
self.file_tableWidget.setCellWidget(row, 0, item_checked)
# second column is filename
item_name = QTableWidgetItem(self.__filelist[row])
item_name.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
self.file_tableWidget.setItem(row, 1, item_name)
# third column is modification time
filepath = os.path.join(scriptspath, self.__filelist[row])
mtime = os.stat(filepath).st_mtime
mtime_str = time.strftime('%Y_%m_%d %H:%M:%S', time.localtime(mtime))
item_time = QTableWidgetItem(mtime_str)
item_time.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
self.file_tableWidget.setItem(row, 2, item_time)
def refresh_file_list(self, file):
#if file do not exist
if not file in self.__filelist:
self.__filelist.append(file)
self.getscripts()
def deletefile(self):
ix = self.file_tableWidget.indexAt(self.__cur_checkbox.pos())
file = self.__filelist[ix.row()]
scriptpath = self.get_scripts_path()
filepath = scriptpath + file
print("delete file", filepath)
os.remove(filepath)
self.__filelist.remove(file)
self.getscripts()
def get_scripts_path(self):
#get current path
cwdpath = os.getcwd()
print("current path" + cwdpath)
#create or enter scripts path
scriptspath = cwdpath + "\scripts\\"
print("scripts path" + scriptspath)
return scriptspath
# when select one check box,others should be not selected, everytime we only delete/excute one file
def table_item_clicked(self):
check_box = self.sender()
ix = self.file_tableWidget.indexAt(check_box.pos())
num = self.file_tableWidget.rowCount()
print("check box number:", num)
print(ix.row(), ix.column(), check_box.isChecked())
self.__cur_checkbox = check_box
file = self.__filelist[ix.row()]
scriptpath = self.get_scripts_path()
self.__curfilepath = scriptpath + file
if check_box.isChecked() == True:
for i in range(num):
if i != ix.row():
ch = self.file_tableWidget.cellWidget(i,0)
ch.setCheckState(Qt.Unchecked)
def get_current_filepath(self):
return self.__curfilepath
def minimized_window(self):
self.setWindowState(Qt.WindowMinimized)
def active_window(self):
self.setWindowState(Qt.WindowActive)
def savefile(self):
path = self.get_scripts_path()
filepath = QFileDialog.getSaveFileName(self, "保存文件", path, "json(*.json)")
print("save file path", filepath[0])
filename = os.path.basename(filepath[0])
command_list = self.recorder.getcommandlist()
self.tofile(command_list, filepath[0])
self.refresh_file_list(filename)
def tofile(self, commandlist, path):
with open(path, "w") as f:
f.write(json.dumps(commandlist)) #使用json格式写入
def showstatus(self, str):
self.status_textEdit.setText(str)
if __name__ == '__main__':
app = QApplication(sys.argv)
myWin = MyWindow()
myWin.show()
sys.exit(app.exec_())
应用逻辑代码my_recorder.py:
import os, time
import threading
import json
#import pyautogui
from datetime import datetime
from pynput import mouse, keyboard
from pynput.mouse import Button, Controller as MouseController
from pynput.keyboard import Key, Controller as KeyController, KeyCode
from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog
from PyQt5.QtCore import QObject, QThread, pyqtSignal
from Ui_mouse_recorder import *
from my_window import *
RECORD_KEY = Key.f9
PLAY_KEY = Key.f10
CON_CLICK_KEY = Key.f8
command_list=[]
BUTTONS = {
"Button.left": Button.left,
"Button.right": Button.right,
"Button.middle": Button.middle
}
class MyRecorder(QObject):
refresh = pyqtSignal(str)
minimized = pyqtSignal()
active = pyqtSignal()
savefile = pyqtSignal()
showstatus = pyqtSignal(str)
def __init__(self, Window=None):
super().__init__()
self.__window = Window
self.__record_flag = False
self.__conclick_flag = False
self.__play_flag = False
self.__key_thread = keyboard.Listener(on_press=self.on_press, on_release=self.on_release)
self.__key_thread.start()
self.__mouse_thread = mouse.Listener(on_click=self.on_click)
self.__mouse_thread.start()
def on_click(self, x, y, button, pressed):
if self.__record_flag:
timestr= datetime.now().strftime("%Y_%m_%d_%H_%M_%S.%f")[:-3]
print(timestr, x, y, pressed, button)
command_list.append((
"mouse", #opration object
(x, y, pressed, str(button)),
timestr
))
def on_press(self, key):
if key == CON_CLICK_KEY or key == RECORD_KEY or key == PLAY_KEY:
print("press key", key)
else:
if self.__record_flag:
self.record_key(key, True) #True or False for press or release
def on_release(self, key):
print("release key", key)
if key == CON_CLICK_KEY or key == RECORD_KEY or key == PLAY_KEY:
self.handle_key(key)#do nothing when press function keys
else:
if self.__record_flag:
self.record_key(key, False)
def handle_key(self, key):
if key == CON_CLICK_KEY:
self.handle_conclick()
elif key == RECORD_KEY:
self.handle_record()
elif key == PLAY_KEY:
self.handle_play()
else:
print("error key")
def continuous_click(self):
interval = self.__window.timeinterval_lineEdit.text()
print("interval", interval)
if interval == "": #if no numbers, default to 1s
interval = 1
interval = float(interval)
#self.__window.setWindowState(Qt.WindowMinimized)
mouse = MouseController()
while self.__conclick_flag:
print(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
mouse.click(button=Button.left)
print("continuous click key")
time.sleep(interval)
def handle_conclick(self):
global mouse_click_thread
if self.__conclick_flag:
self.__conclick_flag = False
mouse_click_thread.join()
if not mouse_click_thread.is_alive():
mouse_click_thread = None
self.showstatus.emit("当前状态:\n鼠标左键连点结束...")
self.active.emit()
#self.__window.setWindowState(Qt.WindowActive)
else:
self.__conclick_flag = True
#create new threading everytime
#self.__window.setWindowState(Qt.WindowMinimized)
mouse_click_thread = threading.Thread(target=self.continuous_click)
mouse_click_thread.start()
self.showstatus.emit("当前状态:\n鼠标左键连点中...")
self.minimized.emit()
def handle_record(self):
print(self.__record_flag)
if self.__record_flag:
print("stop recording")
self.__record_flag = False
self.showstatus.emit("当前状态:\n鼠标键盘录制结束...")
self.active.emit()
self.savefile.emit()
#self.__window.setWindowState(Qt.WindowActive)
#get current path
#cwdpath = os.getcwd()
#print("current path" + cwdpath)
#create or enter scripts path
#scriptspath = cwdpath + "\scripts\\"
#filename = time.strftime('%Y_%m_%d_%H_%M_%S', time.localtime(time.time())) + "_Record.json"
#path = scriptspath + filename
#print(path)
#self.tofile(command_list, path)
#self.refresh.emit(filename)
else:
#self.__window.setWindowState(Qt.WindowMinimized)
self.showstatus.emit("当前状态:\n鼠标键盘录制中...")
self.minimized.emit()
print("start recording")
self.__record_flag = True
print("flag", self.__record_flag)
def handle_play(self):
global record_play_thread
if self.__play_flag:
self.__play_flag = False
print("play flag", self.__play_flag)
record_play_thread.join()
if record_play_thread.is_alive():
record_play_thread = None
self.showstatus.emit("当前状态:\n鼠标键盘播放结束...")
self.active.emit()
else:
self.__play_flag = True
#self.__window.setWindowState(Qt.WindowMinimized)
record_play_thread = threading.Thread(target=self.do_play)
record_play_thread.start()
self.showstatus.emit("当前状态:\n鼠标键盘播放中...")
self.minimized.emit()
def do_play(self):
filepath = self.__window.get_current_filepath()
print(filepath)
if filepath == []:
return
command_read = []
with open(filepath, encoding='utf-8-sig', errors='ignore') as f:
command_read = json.loads(f.read())
while True:
print("do play")
old_timestamp = 0
timestamp = 0
i = 0
for command in command_read:
print("one command start", i)
print("oldtimestamp", old_timestamp)
timestr = command[2]
print("timestr", timestr)
timestamp = datetime.strptime(timestr, "%Y_%m_%d_%H_%M_%S.%f").timestamp()
print("timestamp", timestamp)
if old_timestamp !=0:
timedelay = timestamp - old_timestamp
print("delay", timedelay)
time.sleep(timedelay)
if not self.__play_flag:
break
old_timestamp = timestamp
if command[0] == "mouse":
mouse = MouseController()
paser = command[1]
x = paser[0]
y = paser[1]
pressed = paser[2]
value = paser[3]
print("mouse play", x, y, value, pressed, timestr)
#pyautogui.moveTo(x,y)
mouse.position = (x,y)
if pressed:
mouse.press(button=BUTTONS[value])
else:
mouse.release(button=BUTTONS[value])
elif command[0] == "keyboard":
keycon= KeyController()
paser = command[1]
pressed = paser[2]
value = paser[3]
if value[:3] == "Key":
key = eval(value, {}, {"Key": keyboard.Key})
else:
key = value
#value = "Key." + value.replace('\'','')
print(value, pressed, timestr)
#key = getattr(KeyCode, value)
if pressed:
keycon.pressed(key)
else:
keycon.release(key)
print("keyboard play", key, pressed)
else:
print("incorrect mode")
i = i + 1
if not self.__play_flag:
break
print("self.__play_flag", self.__play_flag)
if not self.__play_flag:
break
interval = self.__window.timeinterval_lineEdit.text()
print("interval", interval)
if interval == "": #if no numbers, default to 1s
interval = 1
interval = float(interval)
time.sleep(interval)
def record_key(self, key, pressed):
timestr= datetime.now().strftime("%Y_%m_%d_%H_%M_%S.%f")[:-3]
print(timestr, pressed, key)
command_list.append((
"keyboard", #opration object
(-1, -1, pressed, str(key).strip("'")),
timestr
))
def getcommandlist(self):
return command_list
首先安装pyinstaller:
pip install Pyinstaller
进入工程目录执行命令打包主函数文件:
Pyinstaller -F -w my_window.py
最终的可执行exe文件在代码目录的dist目录中,如果没有防火墙或者杀毒软件之类,双击即可使用。
PS:博主也刚开始学习python跟pyqt5,以上实现肯定有所缺陷,如发现问题请多多指正。
参考文章:
https://blog.csdn.net/sinat_33408502/article/details/121903944
https://www.freesion.com/article/2628907460/
代码传到github:
https://github.com/Sampsin/learning.git