Skip to content

Commit c0ecea0

Browse files
authored
Merge pull request #1 from muziing/subprocess_dialog
Subprocess Dialog
2 parents b7bf56d + eb68927 commit c0ecea0

4 files changed

Lines changed: 197 additions & 54 deletions

File tree

src/py2exe_gui/Core/packaging.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from PySide6 import QtCore
44
from PySide6.QtCore import QObject
55

6-
from .subprocess_tool import QSubProcessTool
6+
from .subprocess_tool import SubProcessTool
77

88

99
class Packaging(QObject):
@@ -23,9 +23,9 @@ def __init__(self, parent: Optional[QObject] = None):
2323
]
2424
self.args_dict: dict = dict.fromkeys(pyinstaller_args, "")
2525
self._args: List[str] = []
26-
self.subprocess: Optional[QSubProcessTool] = None
26+
self.subprocess: SubProcessTool = SubProcessTool(self, program="pyinstaller")
2727

28-
def get_pyinstaller_args(self, arg: tuple[str, str]) -> None:
28+
def set_pyinstaller_args(self, arg: tuple[str, str]) -> None:
2929
"""
3030
解析传递来的PyInstaller运行参数,并添加至命令参数字典 \n
3131
:param arg: 运行参数
@@ -34,9 +34,9 @@ def get_pyinstaller_args(self, arg: tuple[str, str]) -> None:
3434
arg_key, arg_value = arg
3535
if arg_key in self.args_dict.keys():
3636
self.args_dict[arg_key] = arg_value
37-
self.set_pyinstaller_args()
37+
self._add_pyinstaller_args()
3838

39-
def set_pyinstaller_args(self) -> None:
39+
def _add_pyinstaller_args(self) -> None:
4040
"""
4141
将命令参数字典中的参数按顺序添加到命令参数列表中 \n
4242
:return: None
@@ -67,7 +67,6 @@ def run_packaging_process(self) -> None:
6767
:return: None
6868
"""
6969

70-
if self.subprocess is None: # 确保只在首次调用时实例化一个QSubProcess对象
71-
self.subprocess = QSubProcessTool()
72-
self.subprocess.output.connect(lambda val: print(val)) # 测试用
73-
self.subprocess.start_process("pyinstaller", self._args)
70+
# self._subprocess.output.connect(lambda val: print(val)) # 测试用
71+
self.subprocess.set_arguments(self._args)
72+
self.subprocess.start_process()
Lines changed: 111 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,140 @@
1+
import os.path
12
from sys import getdefaultencoding
23
from typing import Optional, Sequence
34

45
from PySide6 import QtCore
56

67

7-
class QSubProcessTool(QtCore.QObject):
8-
"""辅助使用QProcess创建并管理子进程的工具类"""
8+
class SubProcessTool(QtCore.QObject):
9+
"""辅助QProcess使用的工具类,将所有直接对子进程进行的操作都封装在此类中"""
910

10-
# 自定义信号,参数为 (output_type: QSubProcessTool.output_type, output_text: str)
11+
# 自定义信号,参数为 tuple[output_type: SubProcessTool.output_type, output_text: str]
1112
output = QtCore.Signal(tuple)
1213

1314
# output_types
14-
STATE = 0
15-
FINISHED = 1
15+
STATE = 1
1616
STDOUT = 2
1717
STDERR = 3
18+
FINISHED = 4
19+
20+
def __init__(
21+
self,
22+
parent: Optional[QtCore.QObject] = None,
23+
*,
24+
program: str = "",
25+
arguments: Sequence[str] = (),
26+
working_directory: str = "./",
27+
) -> None:
28+
"""
29+
:param parent: 父对象
30+
:param program: 待运行的子进程
31+
:param arguments: 运行参数
32+
:param working_directory: 子进程工作目录
33+
:return: None
34+
"""
35+
36+
super(SubProcessTool, self).__init__(parent)
37+
38+
self.program: str = program
39+
self._arguments: Sequence[str] = arguments
40+
self._working_directory: str = working_directory
41+
self._process: Optional[QtCore.QProcess] = None
42+
self.exit_code: int = 0
43+
self.exit_status: QtCore.QProcess.ExitStatus = QtCore.QProcess.NormalExit
44+
45+
def start_process(
46+
self,
47+
*,
48+
mode: QtCore.QIODeviceBase.OpenMode = QtCore.QIODeviceBase.OpenModeFlag.ReadWrite, # type: ignore
49+
) -> None:
50+
"""
51+
创建并启动子进程 \n
52+
:param mode: 设备打开的模式
53+
:return: None
54+
"""
55+
56+
if self._process is None: # 防止在子进程运行结束前重复启动
57+
self._process = QtCore.QProcess(self)
58+
59+
self._process.stateChanged.connect(self._handle_state) # type: ignore
60+
self._process.readyReadStandardOutput.connect(self._handle_stdout) # type: ignore
61+
self._process.readyReadStandardError.connect(self._handle_stderr) # type: ignore
62+
self._process.started.connect(self._process_started) # type: ignore
63+
self._process.finished.connect(self._process_finished) # type: ignore
64+
self._process.errorOccurred.connect(self._handle_error) # type: ignore
65+
66+
self._process.setWorkingDirectory(self._working_directory)
67+
self._process.start(self.program, self._arguments, mode)
68+
self._process.waitForStarted(1000) # 阻塞,直到启动了子进程或超时(单位为毫秒)
1869

19-
def __init__(self, parent: Optional[QtCore.QObject] = None) -> None:
20-
super(QSubProcessTool, self).__init__(parent)
21-
self.process: Optional[QtCore.QProcess] = None
70+
def abort_process(self, timeout: int = 10000) -> bool:
71+
"""
72+
终止子进程 \n
73+
:param timeout: 超时时间,单位为毫秒
74+
:return: 子进程是否完成
75+
"""
76+
77+
if self._process:
78+
self._process.terminate()
79+
finished = self._process.waitForFinished(timeout) # 阻塞,直到进程终止或超时
80+
if not finished:
81+
self._process.kill() # 超时后杀死子进程
82+
return finished
83+
else:
84+
return True
2285

23-
def start_process(self, program: str, arguments: Sequence[str]) -> None:
86+
def set_arguments(self, arguments: Sequence[str]) -> None:
2487
"""
25-
使用给定参数启动指定子进程 \n
26-
:param program: 子进程命令
27-
:param arguments: 子进程参数
88+
设置子进程参数 \n
89+
:param arguments: 参数列表
2890
:return: None
2991
"""
3092

31-
if self.process is None: # 防止在子进程运行结束前重复启动
32-
self.process = QtCore.QProcess()
93+
self._arguments = arguments
3394

34-
self.process.stateChanged.connect(self._handle_state) # type: ignore
35-
self.process.readyReadStandardOutput.connect(self._handle_stdout) # type: ignore
36-
self.process.readyReadStandardError.connect(self._handle_stderr) # type: ignore
37-
self.process.started.connect(self._process_started) # type: ignore
38-
self.process.finished.connect(self._process_finished) # type: ignore
95+
def set_working_dir(self, work_dir: str) -> bool:
96+
"""
97+
设置子进程工作目录 \n
98+
:param work_dir: 工作目录
99+
:return: 是否设置成功
100+
"""
39101

40-
self.process.start(program, arguments)
102+
if os.path.isdir(work_dir):
103+
self._working_directory = work_dir
104+
return True
105+
else:
106+
return False
41107

42108
def _process_started(self) -> None:
43109
"""
44-
处理子进程的槽 \n
110+
处理子进程开始的槽 \n
45111
:return: None
46112
"""
113+
47114
pass
48115

49-
def _process_finished(self) -> None:
116+
def _process_finished(self, code: int, status: QtCore.QProcess.ExitStatus) -> None:
50117
"""
51-
处理子进程的槽 \n
118+
处理子进程结束的槽 \n
119+
:param code: 退出码
120+
:param status: 退出状态
52121
:return: None
53122
"""
54123

55-
self.output.emit((self.FINISHED, "Subprocess finished."))
56-
self.process = None
124+
self.exit_code = code
125+
self.exit_status = status
126+
self.output.emit((self.FINISHED, str(code)))
127+
self._process = None
57128

58129
def _handle_stdout(self) -> None:
59130
"""
60131
处理标准输出的槽 \n
61132
:return: None
62133
"""
63134

64-
if self.process:
65-
data = self.process.readAllStandardOutput()
66-
stdout = bytes(data).decode(getdefaultencoding())
135+
if self._process:
136+
data = self._process.readAllStandardOutput()
137+
stdout = bytes(data).decode(getdefaultencoding()) # 将QByteArray转为str
67138
self.output.emit((self.STDOUT, stdout))
68139

69140
def _handle_stderr(self) -> None:
@@ -72,9 +143,9 @@ def _handle_stderr(self) -> None:
72143
:return: None
73144
"""
74145

75-
if self.process:
76-
data = self.process.readAllStandardError()
77-
stderr = bytes(data).decode(getdefaultencoding())
146+
if self._process:
147+
data = self._process.readAllStandardError()
148+
stderr = bytes(data).decode(getdefaultencoding()) # 将QByteArray转为str
78149
self.output.emit((self.STDERR, stderr))
79150

80151
def _handle_state(self, state: QtCore.QProcess.ProcessState) -> None:
@@ -91,3 +162,12 @@ def _handle_state(self, state: QtCore.QProcess.ProcessState) -> None:
91162
}
92163
state_name = states[state]
93164
self.output.emit((self.STATE, state_name))
165+
166+
def _handle_error(self, error: QtCore.QProcess.ProcessError) -> None:
167+
"""
168+
处理子进程错误 \n
169+
:param error: 子进程错误
170+
:return: None
171+
"""
172+
173+
pass

src/py2exe_gui/Widgets/dialog_widgets.py

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
from PySide6.QtCore import Qt
2-
from PySide6.QtWidgets import QFileDialog, QMessageBox, QWidget
2+
from PySide6.QtWidgets import (
3+
QDialog,
4+
QFileDialog,
5+
QLabel,
6+
QMessageBox,
7+
QTextBrowser,
8+
QVBoxLayout,
9+
QWidget,
10+
)
311

4-
"""
5-
由于各种对话框的设置代码较繁琐且独立,故单独在本模块中配置
6-
"""
12+
from py2exe_gui.Core.subprocess_tool import SubProcessTool
713

814

915
class ScriptFileDlg(QFileDialog):
1016
"""用于获取入口脚本文件的对话框"""
1117

1218
def __init__(self, parent: QWidget = None) -> None:
13-
super().__init__(parent)
19+
super(ScriptFileDlg, self).__init__(parent)
1420
self._setup()
1521

1622
def _setup(self) -> None:
@@ -56,7 +62,7 @@ class AboutMessage(QMessageBox):
5662
"""用于显示关于信息的对话框"""
5763

5864
def __init__(self, parent: QWidget = None) -> None:
59-
super(AboutMessage, self).__init__(parent=parent)
65+
super(AboutMessage, self).__init__(parent)
6066
self._about_text: str = ""
6167
self._setup()
6268

@@ -86,6 +92,46 @@ def about_text(self) -> str:
8692
return self._about_text
8793

8894

95+
class SubProcessDlg(QDialog):
96+
"""用于显示子进程信息的对话框"""
97+
98+
def __init__(self, parent: QWidget = None) -> None:
99+
super(SubProcessDlg, self).__init__(parent)
100+
self.info_label = QLabel(self)
101+
self.browser = QTextBrowser(self)
102+
self._setup()
103+
104+
def _setup(self) -> None:
105+
"""
106+
配置子进程信息对话框 \n
107+
"""
108+
109+
self.setWindowTitle("PyInstaller")
110+
self.setModal(True)
111+
112+
layout = QVBoxLayout()
113+
layout.addWidget(self.info_label)
114+
layout.addWidget(self.browser)
115+
self.setLayout(layout)
116+
117+
def handle_output(self, subprocess_output: tuple[int, str]) -> None:
118+
"""
119+
处理子进程的输出 \n
120+
:param subprocess_output: 子进程输出
121+
:return: None
122+
"""
123+
124+
output_type, output_text = subprocess_output
125+
if output_type == SubProcessTool.STDOUT:
126+
self.browser.append(output_text)
127+
elif output_type == SubProcessTool.STDERR:
128+
self.browser.append(output_text)
129+
elif output_type == SubProcessTool.FINISHED:
130+
self.info_label.setText("打包完成!")
131+
elif output_type == SubProcessTool.STATE:
132+
self.info_label.setText(output_text)
133+
134+
89135
if __name__ == "__main__":
90136
import sys
91137

@@ -95,6 +141,7 @@ def about_text(self) -> str:
95141
# window = ScriptFileDlg()
96142
# window = IconFileDlg()
97143
# window.fileSelected.connect(lambda f: print(f)) # type: ignore
98-
window = AboutMessage()
144+
# window = AboutMessage()
145+
window = SubProcessDlg()
99146
window.open()
100147
sys.exit(app.exec())

src/py2exe_gui/__main__.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import sys
22

3+
from PySide6 import QtGui
34
from PySide6.QtWidgets import QApplication
45

5-
from .Core.packaging import Packaging
6-
from .Widgets.main_window import MainWindow
6+
from py2exe_gui.Core.packaging import Packaging
7+
from py2exe_gui.Widgets.dialog_widgets import SubProcessDlg
8+
from py2exe_gui.Widgets.main_window import MainWindow
79

810

911
class MainApp(MainWindow):
@@ -12,21 +14,36 @@ class MainApp(MainWindow):
1214
def __init__(self, *args, **kwargs) -> None:
1315
super(MainApp, self).__init__(*args, **kwargs)
1416

15-
self.packager = Packaging()
17+
self.packager = Packaging(self)
1618

1719
self.packager.args_settled.connect(
1820
lambda val: self.center_widget.pyinstaller_args_browser.enrich_args_text(
1921
val
2022
)
2123
)
24+
self.center_widget.option_selected.connect(self.packager.set_pyinstaller_args)
2225

23-
self.center_widget.option_selected.connect(self.packager.get_pyinstaller_args)
24-
self.center_widget.run_packaging_btn.clicked.connect(
25-
self.packager.run_packaging_process
26-
)
26+
self.subprocess_dlg = SubProcessDlg(self)
27+
28+
def run_packaging():
29+
self.packager.run_packaging_process()
30+
self.subprocess_dlg.show()
31+
32+
self.center_widget.run_packaging_btn.clicked.connect(run_packaging)
33+
self.packager.subprocess.output.connect(self.subprocess_dlg.handle_output)
2734

2835
self.status_bar.showMessage("就绪")
2936

37+
def closeEvent(self, event: QtGui.QCloseEvent) -> None:
38+
"""
39+
重写关闭事件,进行收尾清理 \n
40+
:param event: 关闭事件
41+
:return: None
42+
"""
43+
44+
self.packager.subprocess.abort_process()
45+
super(MainApp, self).closeEvent(event)
46+
3047

3148
app = QApplication(sys.argv)
3249
window = MainApp()

0 commit comments

Comments
 (0)