GUI主线程中非阻塞嵌套函数调用的设计模式?
当 PyQt GUI 应用程序具有多个调用一个又一个外部子进程的嵌套调用时,我需要一些帮助来识别设计模式或使 PyQt GUI 应用程序非阻塞的技术。伪代码是:
class MainWindow:
def executeInSubprocess(self, theCommand):
subprocess.run(theCommand, check = True)
#program control reaches here only after the command executed by subprocess.run completes execution
def function1(self):
commandToExecute = ['python', '-m', 'someProgram.py', '--CLA_option']
self.executeInSubprocess(commandToExecute)
self.function2()
#some commands to update the GUI widgets
def function2(self):
#Some lines here gather data generated by running function1
self.function3(data)
def function3(self, data):
commandToExecute = ['python', '-m', 'anotherProgram.py', '--CLA_option']
for oneData in data:
newCommand = commandToExecute[:] #copy contents of list
newCommand.append('--someOption')
newCommand.append(oneData)
self.executeInSubprocess(newCommand)
当用户单击 GUI 上的按钮时,将调用 function1
。该按钮保持禁用状态,直到 #some 命令更新 GUI 小部件
行,该按钮重新启用。
问题:
- 如何设计使得
executeInSubprocess
是非阻塞的?我想尝试将subprocess.run
替换为QProcess
,它不会等到外部命令完成,从而不会冻结 GUI。 -
executeInSubprocess
在function3
的for
循环内多次调用,并且依赖于function1
生成的数据。
这些函数并不是太大,因此重构它应该不成问题。我需要的是关于设计这种场景的好方法的知识。我考虑过将对 function1
、function2
和 function3
的引用放入列表中,并让类中的函数一一调用它们,但是那么 function3
中的 executeInSubprocess
会使事情变得复杂。
更新:我提供了一个示例,说明代码可以变得多么复杂和交织。
#!/usr/bin/python
# -*- coding: ascii -*-
from PyQt5.QtWidgets import (QApplication, QMainWindow, QPushButton, QPlainTextEdit, QVBoxLayout, QWidget)
from PyQt5.QtCore import QProcess
from collections import deque
import sys
# Note: The objective was to design a way to execute a function and return control back to the main thread so that the GUI does not freeze.
# But it had to be done in a way that a function could invoke another function, which could invoke another function. Each of these functions
# can have one or more external commands that they need to run. So after invoking each external process, control has to return to the main thread
# and then after running the external process completes, the next command or function needs to run and return control to the main thread and
# so on until all functions and external commands are exhausted. What's given below is a very crude, tangled design that needs to be improved.
class ExternalProcessInvoker:
def __init__(self):
self.externalProcess = None
self.currentFunction = None
self.functionQueue = deque() #a sequence of functions to be executed
self.commandQueue = dict() #{the key is a function reference: the value is a list of commands [['python', 'prog1.py'], ['python', 'prog2.py'], ...]}
self.FIRST_ELEMENT_OF_LIST = 0
self.UTF8 = 'utf8'
def registerTheMessageFunction(self, functionReference):
self.message = functionReference
def startTheProcess(self):
while self.functionQueue:
#print('Elements in queue are: ', self.functionQueue)
theFunctionToExecute = self.functionQueue.popleft()
#---run the function
theFunctionToExecute(theFunctionToExecute) #The function may populate commandQueue when run
self.currentFunction = theFunctionToExecute
self.checkForMoreFunctionPopulatedCommandsToExecute()
def checkForMoreFunctionPopulatedCommandsToExecute(self):
#---check if commandQueue is populated with any external process that needs to be run
if self.currentFunction in self.commandQueue:
if self.commandQueue[self.currentFunction]:#contains some command
command = self.commandQueue[self.currentFunction].popleft()
#command = self.convertToUTF8(command)
if self.externalProcess is None: # No process running.
#---run the command
self.message('Executing ' + str(command)); print('Executing ', command)
self.externalProcess = QProcess()
self.externalProcess.readyReadStandardOutput.connect(self.functionForStdout)
self.externalProcess.readyReadStandardError.connect(self.functionForStderr)
self.externalProcess.stateChanged.connect(self.functionForStateChange)
self.externalProcess.finished.connect(self.processCompleted) # Clean up once complete.
self.externalProcess.start(command[self.FIRST_ELEMENT_OF_LIST], command[self.FIRST_ELEMENT_OF_LIST:]) #the first argument is the first command element (usually 'python'). The second argument is a list of all the remaining elements of the command
print('Process ID: ', self.externalProcess.processId())
else:
self.currentFunction = None
def convertToUTF8(self, command):
return [oneString.encode('utf-8') for oneString in command]
def stopTheProcess(self):
self.message('Attempting to stop the processes')
if self.externalProcess:
self.externalProcess.kill()
self.message('Also cleared ' + len(self.functionQueue) + ' items from execution queue')
self.functionQueue.clear()
def functionForStderr(self):
data = self.externalProcess.readAllStandardError()
stderr = bytes(data).decode(self.UTF8)
self.message(stderr)
def functionForStdout(self):
data = self.externalProcess.readAllStandardOutput()
stdout = bytes(data).decode(self.UTF8)
self.message(stdout)
def functionForStateChange(self, state):
states = {QProcess.NotRunning: 'Not running', QProcess.Starting: 'Starting', QProcess.Running: 'Running'}
state_name = states[state]
self.message(f'State changed: {state_name}')
def processCompleted(self):
self.message('Process finished.')
self.externalProcess = None
self.checkForMoreFunctionPopulatedCommandsToExecute()
def addThisFunctionToProcessingQueue(self, functionReference):
#print('Received request to add this function to processing queue: ', functionReference)
#print('What's already in processing queue: ', self.functionQueue)
self.functionQueue.append(functionReference)
def addThisCommandToProcessingList(self, command, functionReference):
if functionReference not in self.commandQueue:
self.commandQueue[functionReference] = deque()
self.commandQueue[functionReference].append(command)
#print('\nfunc ref: ', functionReference, ' command: ', command)
self.message('Added ' + str(command) + ' for processing')
#print(self.commandQueue)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.proc = ExternalProcessInvoker()
self.proc.registerTheMessageFunction(self.message)
self.executeButton = QPushButton('Execute') #TODO: Disable button until processing completes or processes are killed
self.executeButton.pressed.connect(self.initiateProcessing)
self.killButton = QPushButton('Kill process') #TODO: Disable button until processing is started
self.killButton.pressed.connect(self.killProcessing)
self.textDisplay = QPlainTextEdit()
self.textDisplay.setReadOnly(True)
boxLayout = QVBoxLayout()
boxLayout.addWidget(self.executeButton)
boxLayout.addWidget(self.killButton)
boxLayout.addWidget(self.textDisplay)
theWidget = QWidget()
theWidget.setLayout(boxLayout)
self.setCentralWidget(theWidget)
def message(self, mssg):
self.textDisplay.appendPlainText(mssg)
def initiateProcessing(self):
print('Processing initiated')
#---first populate the functions and commands to execute
self.proc.addThisFunctionToProcessingQueue(self.function1)
self.proc.addThisFunctionToProcessingQueue(self.function2)
self.proc.addThisFunctionToProcessingQueue(self.function3)
self.proc.startTheProcess()
def function1(self, functionReference):
self.message('Executing function1')
command = ['python', 'dummyScript1.py']
self.proc.addThisCommandToProcessingList(command, functionReference)
def function2(self, functionReference):
self.message('Executing function2')
def function3(self, functionReference):
self.message('Executing function3')
maxInvocations = 5
secondsToSleep = 1
for _ in range(maxInvocations):
command = ['python', 'dummyScript2.py', str(secondsToSleep)]
self.proc.addThisCommandToProcessingList(command, functionReference)
secondsToSleep = secondsToSleep + 1
self.message('Processing complete')
print('Completed')
def killProcessing(self):
self.proc.stopTheProcess()
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec_()
虚拟脚本 1:
#!/usr/bin/python
# -*- coding: ascii -*-
import time
import datetime
sec = 3
print(datetime.datetime.now(), " Going to sleep for ", sec, " seconds")
time.sleep(sec)
print(datetime.datetime.now(), " Done sleeping dummy script1")
虚拟脚本 2:
#!/usr/bin/python
# -*- coding: ascii -*-
import time
def gotosleep(sleepTime):
print("Going to sleep for ", sleepTime, " seconds")
time.sleep(sleepTime)
print("Done sleeping for ", sleepTime, " seconds in dummy script2")
if __name__ == "__main__":
FIRST_EXTRA_ARG = 1
sleepTime = sys.argv[FIRST_EXTRA_ARG]
gotosleep(sleepTime)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论