使用VS Code Python和PYQT5实现简单的按键精灵

  1. 创建工程

创建工程目录,创建UI文件,创建Main Window

  1. 需求整理

使用思维导图进行简单的需求整理

  1. 简单设计UI

由于没怎么用过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", "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"
"p, li { white-space: pre-wrap; }\n"
"</style></head><body style=\" font-family:\'SimSun\'; font-size:9pt; font-weight:400; font-style:normal;\">\n"
"<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">功能说明:</p>\n"
"<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">功能1.填入时间间隔(可以是小数),按F8鼠标左键连点当前位置。</p>\n"
"<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">功能2.按F9开始鼠标键盘录制,再按F9结束录制并保存在scripts目录下。</p>\n"
"<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">功能3.勾选录制文件,按F10进行录制回放。</p>\n"
"<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">功能4.默认加载当前目录下scripts目录的脚本(其他目录目前不支持)。</p>\n"
"<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">功能6.点击“删除录制文件”,可以将当前选中的文件删除。</p></body></html>"))
        self.deleteFile_Btn.setText(_translate("MainWindow", "删除录制文件"))

后续我们需要一个文件实现界面类对界面进行操作,另外实现一个功能类进行各类功能监听及实现,简单创建两个py文件:

  1. 线程设计

为了界面和功能逻辑分开,需要简单设计一下多线程:

  1. 代码实现

界面逻辑代码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
  1. 打包python代码成为exe文件

首先安装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

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用Jedi可以实现Python的智能代码补全、函数跳转、代码重构等功能,结合PyQt5可以实现一个简单Python IDE。下面是一个简单的示例: ```python import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QTextEdit from PyQt5.QtGui import QTextCursor import jedi class MainWindow(QMainWindow): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.text_edit = QTextEdit(self) self.text_edit.cursorPositionChanged.connect(self.on_cursor_position_changed) self.setCentralWidget(self.text_edit) self.jedi_script = jedi.Script('', 1, 1, '') def on_cursor_position_changed(self): cursor = self.text_edit.textCursor() line_number = cursor.blockNumber() + 1 column_number = cursor.columnNumber() source_code = self.text_edit.toPlainText() self.jedi_script = jedi.Script(source_code, line_number, column_number, '') completions = self.jedi_script.completions() if completions: self.show_completions(completions) else: self.text_edit.clear() def show_completions(self, completions): cursor = self.text_edit.textCursor() cursor.movePosition(QTextCursor.StartOfWord) cursor.movePosition(QTextCursor.EndOfWord, QTextCursor.KeepAnchor) word_under_cursor = cursor.selectedText() completion_list = [c.name for c in completions if c.name.startswith(word_under_cursor)] self.text_edit.clear() self.text_edit.insertPlainText('\n'.join(completion_list)) if __name__ == '__main__': app = QApplication(sys.argv) main_window = MainWindow() main_window.show() sys.exit(app.exec_()) ``` 这个例子中,我们使用QTextEdit作为文本编辑器,利用jedi实现了在光标所在位置的代码自动补全功能。当光标位置改变时,我们先利用QTextEdit的textCursor方法获取光标位置,然后获取所在行、列的信息和整个代码,再使用jedi的Script类生成一个脚本对象,最后调用completions方法获取自动补全列表。我们将自动补全列表显示在QTextEdit中,方便用户选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值