Fork me on GitHub

使用PyQt5,pyinstaller开发跨平台小工具

使用 PyQt5 进行 GUI 开发,并通过 PyInstaller 打包为可执行文件

一、PyQt5 GUI 开发示例

1. 安装 PyQt5

首先,安装 PyQt5 :

1
2
3
4
5
bash


复制编辑
pip install pyqt5

2. 创建简单的 PyQt5 界面(这一部分建议chatgpt来写)

假设我们开发一个简单的工资计算器,用户输入基本工资、绩效奖金后,点击按钮计算总工资。

(1)使用 Qt Designer 设计 UI

PyQt5 提供了一个可视化 UI 设计工具 Qt Designer,可以使用以下命令打开它(Windows):

1
2
3
4
5
bash


复制编辑
pyqt5-tools designer

在 Qt Designer 里:

  1. 创建一个 QWidget 窗口。
  2. 添加 QLineEdit 用于输入基本工资和绩效奖金。
  3. 添加 QPushButton 按钮用于计算工资。
  4. 添加 QLabel 显示计算结果。
  5. 保存 UI 文件,如 salary_calculator.ui

(2)转换 .ui 文件为 Python 代码

使用 pyuic5 将 UI 文件转换为 Python 代码:

1
2
3
4
5
bash


复制编辑
pyuic5 -o ui_salary.py salary_calculator.ui

(3)编写主逻辑代码

创建 main.py,并编写工资计算逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
python


复制编辑
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from ui_salary import Ui_MainWindow # 导入转换后的 UI 代码

class SalaryCalculator(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
self.calculate_button.clicked.connect(self.calculate_salary)

def calculate_salary(self):
try:
base_salary = float(self.base_salary_input.text())
bonus = float(self.bonus_input.text())
total_salary = base_salary + bonus
self.result_label.setText(f"总工资: {total_salary:.2f} 元")
except ValueError:
self.result_label.setText("请输入有效数字!")

if __name__ == "__main__":
app = QApplication(sys.argv)
window = SalaryCalculator()
window.show()
sys.exit(app.exec_())

二、使用 PyInstaller 进行打包

1. 安装 PyInstaller

1
2
3
4
bash

复制编辑
pip install pyinstaller

2. 生成可执行文件

在终端运行以下命令,将 main.py 打包成 Windows 可执行文件:

1
2
3
4
5
6
bash
# 指定配置文件settings.json
# win上打包:
pyinstaller --onefile --add-data=settings.json;. -w main.py
# mac上打包:
pyinstaller --onefile --add-data=settings.json:. -w main.py

打包成功后,Windows 用户会在 dist 目录下找到 main.exe,macOS 用户会得到 .app 应用包。

注意:打包要在目标平台进行:Windows 上打包 Windows 应用,macOS 上打包 macOS 应用。

三、在win7上运行

如果在win10上打包,因为pyinstaller默认会打包win10的api,故在win7上运行时会报错:计算机丢失 api-ms-win-core-path-l1-1-0.dll等。
所以必须使用pyinstaller在win7上打包,打包后的exe才能在win7上运行。
win10机器安装win7虚拟机并完成pyinstaller打包步骤:

  1. 安装vmware(17.0.2之后版本个人使用免费),假如win10在1909版本之前的本系统,安装vmware前需要同时处理Hyper-V和Virtualization-Based Security的兼容性问题(可使用下文脚本)。
  2. 安装win7(MSDN,i tell you)
  3. 安装python3.8.10(最后支持win7的python)
    + 可能报错缺少service pack,安装KB976932KB2533623 补丁
    + 参考:教程
  4. 假如要用cryptography,请使用38.0.4版本

三、解决单一实例限制

用户多次点击 PyInstaller 打包生成的 .exe 文件时,程序会启动多个实例(多开),而你希望无论点击多少次,都只运行一个应用界面(单一实例)。这在 GUI 应用程序中是常见需求,称为单一实例限制。

问题分析
PyInstaller 打包的 .exe 行为:每次点击 .exe 文件,PyInstaller 默认会启动一个新的进程,导致多个独立的应用实例运行。
PyQt5 的特点:PyQt5 本身不会自动限制单一实例,需要手动实现进程间通信(IPC)或锁机制来确保只有一个实例运行。
你的代码:当前的 main.py 没有实现单一实例检查,因此每次点击 .exe 都会启动新的进程和窗口。

解决办法:使用本地 socket 进行 IPC(更灵活)

通过本地 socket 实现进程间通信(IPC)。当新实例启动时,它会尝试连接到一个已知的 socket 端口。如果连接成功,说明已有实例运行,新实例可以发送消息激活已有窗口并退出

以下是一个基于 QLocalSocket 和 QLocalServer 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox
from PyQt5.QtNetwork import QLocalServer, QLocalSocket

class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Single Instance App")
self.setGeometry(100, 100, 400, 300)
self.server = None
self.start_server()

def start_server(self):
"""Start local server to enforce single instance."""
self.server = QLocalServer()
server_name = "SingleInstanceApp"
QLocalServer.removeServer(server_name) # Remove stale server
if not self.server.listen(server_name):
print(f"Server failed to start: {self.server.errorString()}")
return
self.server.newConnection.connect(self.handle_new_connection)

def handle_new_connection(self):
"""Handle new client connection and activate window."""
socket = self.server.nextPendingConnection()
if socket:
socket.readyRead.connect(lambda: self.activate_window(socket))
socket.disconnected.connect(socket.deleteLater)

def activate_window(self, socket):
"""Activate the current window."""
self.raise_()
self.activateWindow()
socket.disconnectFromServer()

def closeEvent(self, event):
"""Clean up server on window close."""
if self.server:
self.server.close()
event.accept()

if __name__ == "__main__":
server_name = "SingleInstanceApp"
socket = QLocalSocket()

# Try to connect to existing server
socket.connectToServer(server_name)
if socket.waitForConnected(500):
# Existing instance found, activate it and exit
socket.write(b"activate")
socket.waitForBytesWritten(500)
socket.disconnectFromServer()
QMessageBox.warning(None, "Application Running", "The application is already running!")
sys.exit(1)

# No existing instance, start new one
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)

window = MainWindow()
window.show()
sys.exit(app.exec_())

说明

  • QLocalServer 和 QLocalSocket:PyQt5 提供的跨平台 IPC 机制,用于进程间通信。
  • 工作原理:
    • 第一个实例启动时,创建并监听一个本地 socket(PensionAppInstance)。
    • 新实例启动时,尝试连接到该 socket。如果连接成功,说明已有实例运行,新实例发送“activate”消息并退出。
    • 已有实例收到消息后,通过 raise_() 和 activateWindow() 将窗口置顶并激活。
  • 优势:不仅防止多开,还能激活已有窗口,提升用户体验。
  • 清理:在窗口关闭时通过 closeEvent 清理服务器,确保下次启动时不会冲突。
  • PyInstaller 兼容性:此方法适用于 PyInstaller 打包的程序,无需额外依赖。

PS

关闭 Virtualization-Based Security:以下代码写成bat文件,管理员权限运行即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@echo off

dism /Online /Disable-Feature:microsoft-hyper-v-all /NoRestart
dism /Online /Disable-Feature:IsolatedUserMode /NoRestart
dism /Online /Disable-Feature:Microsoft-Hyper-V-Hypervisor /NoRestart
dism /Online /Disable-Feature:Microsoft-Hyper-V-Online /NoRestart
dism /Online /Disable-Feature:HypervisorPlatform /NoRestart

REM ===========================================

mountvol X: /s
copy %WINDIR%\System32\SecConfig.efi X:\EFI\Microsoft\Boot\SecConfig.efi /Y
bcdedit /create {0cb3b571-2f2e-4343-a879-d86a476d7215} /d "DebugTool" /application osloader
bcdedit /set {0cb3b571-2f2e-4343-a879-d86a476d7215} path "\EFI\Microsoft\Boot\SecConfig.efi"
bcdedit /set {bootmgr} bootsequence {0cb3b571-2f2e-4343-a879-d86a476d7215}
bcdedit /set {0cb3b571-2f2e-4343-a879-d86a476d7215} loadoptions DISABLE-LSA-ISO,DISABLE-VBS
bcdedit /set {0cb3b571-2f2e-4343-a879-d86a476d7215} device partition=X:
mountvol X: /d
bcdedit /set hypervisorlaunchtype off

echo.
echo.
echo =======================================================
echo 当前操作已完成,接下来请关闭此窗口并重启电脑,然后根据屏幕提示完成剩下操作。
pause > nul
echo.
-------------The End-------------