Python PyQT4 QWidget必须在关闭前接收两次“关闭”信号
我正在编写一个PyQT4 4.11程序,它执行一个长而慢的任务,理想情况下需要一个进度条。如果我不使用线程,这个程序几乎可以完美地工作,对一个只包含QProgressBar和布局的QWidget进行子分类。将这个子类实例化为表单,我可以调用form.show将它放到屏幕上,然后我的长循环可以通过调用form.progressbar.setValueprogress更新进度。这有两个问题: 如果用户试图与窗口交互,他们会从windows manager/OS桌面进程收到“无响应”消息。这是因为没有处理事件 因为没有处理事件,所以用户不能通过关闭窗口来取消长的慢循环 因此,我尝试在一个单独的线程中运行长而慢的循环,使用一个信号来更新进度条。我重写了我的QWidget的closeEvent,这样它就可以取消与硬件设备的交互,所有这些都封装在互斥锁中,这样设备通信就不会失去同步。同样,这几乎奏效。如果我取消,应用程序将退出。如果我让它运行到完成,我必须手动关闭窗口,即单击关闭图标或按alt-f4,即使我正在向QWidget发送关闭信号。正如您在下面的代码中所看到的,存在一些复杂情况,因为如果取消,应用程序无法立即关闭,因为它必须等待硬件清理。这是我的代码的最低版本Python PyQT4 QWidget必须在关闭前接收两次“关闭”信号,python,multithreading,pyqt4,Python,Multithreading,Pyqt4,我正在编写一个PyQT4 4.11程序,它执行一个长而慢的任务,理想情况下需要一个进度条。如果我不使用线程,这个程序几乎可以完美地工作,对一个只包含QProgressBar和布局的QWidget进行子分类。将这个子类实例化为表单,我可以调用form.show将它放到屏幕上,然后我的长循环可以通过调用form.progressbar.setValueprogress更新进度。这有两个问题: 如果用户试图与窗口交互,他们会从windows manager/OS桌面进程收到“无响应”消息。这是因为没有
import sys
import os
import time
from PyQt4 import QtCore, QtGui
class Ui_ProgressBarDialog(QtGui.QWidget):
def __init__(self, on_close=None):
QtGui.QWidget.__init__(self)
self.setupUi(self)
self.center()
#on_close is a function that is called to cancel
#the long slow loop
self.on_close = on_close
def center(self):
qr = self.frameGeometry()
cp = QtGui.QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
def setupUi(self, ProgressBarDialog):
ProgressBarDialog.setObjectName(_fromUtf8("ProgressBarDialog"))
ProgressBarDialog.resize(400, 33)
self.verticalLayout = QtGui.QVBoxLayout(ProgressBarDialog)
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
self.progressBar = QtGui.QProgressBar(ProgressBarDialog)
self.progressBar.setProperty("value", 0)
self.progressBar.setObjectName(_fromUtf8("progressBar"))
self.verticalLayout.addWidget(self.progressBar)
self.retranslateUi(ProgressBarDialog)
#This allows the long slow loop to update the progress bar
QtCore.QObject.connect(
self,
QtCore.SIGNAL("updateProgress"),
self.progressBar.setValue
)
#Catch the close event so we can interrupt the long slow loop
QtCore.QObject.connect(
self,
QtCore.SIGNAL("closeDialog"),
self.closeEvent
)
#Repaint the window when the progress bar's value changes
QtCore.QObject.connect(
self.progressBar,
QtCore.SIGNAL("valueChanged(int)"),
self.repaint
)
QtCore.QMetaObject.connectSlotsByName(ProgressBarDialog)
def retranslateUi(self, ProgressBarDialog):
ProgressBarDialog.setWindowTitle("Please Wait")
def closeEvent(self, event, force=False):
if self.on_close is not None and not force:
self.on_close()
app = QtGui.QApplication(sys.argv)
filename = str(QtGui.QFileDialog.getSaveFileName(
None,
"Save as",
os.getcwd(),
"Data files: (*.dat)"
))
loop_mutex = thread.allocate_lock()
cancelled = False
can_quit = False
result = None
def cancel_download():
global cancelled
if can_quit:
return
if QtGui.QMessageBox.question(
None,
'Cancel Download',
"Are you sure you want to cancel the download?",
QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
QtGui.QMessageBox.No) == QtGui.QMessageBox.Yes:
with loop_mutex:
selected_device.cancelDownload()
cancelled = True
while not can_quit:
time.sleep(0.25)
form = ProgressBarDialog.Ui_ProgressBarDialog(cancel_download)
form.setWindowTitle("Please Wait")
form.progressBar.setMaximum(1000)
form.progressBar.setValue(0)
form.show()
def long_slow_loop(mutex, filename):
global can_quit, result
progress = 0
temp_binary_file = open(filename, "wb")
#The iterator below does the actual work of interacting with a
#hardware device, so I'm locking around the "for" line. I must
#not fetch more data from the device while a cancel command is
#in progress, and vice versa
mutex.acquire()
for data in ComplexIteratorThatInteractsWithHardwareDevice():
mutex.release()
temp_binary_file.write(datablock)
progress += 1
form.emit(QtCore.SIGNAL("updateProgress"), progress)
mutex.acquire()
if cancelled:
break
mutex.release()
result = not cancelled
temp_binary_file.close()
if cancelled:
os.unlink(filename)
#having set can_quit to True the following emission SHOULD
#cause the app to exit by closing the last top level window
can_quit = True
form.emit(QtCore.SIGNAL("closeDialog"), QtGui.QCloseEvent(), True)
thread.start_new_thread(do_dump, (loop_mutex, filename))
app.exec_()
if result == True:
QtGui.QMessageBox.information(None, 'Success', "Save to disk successful", QtGui.QMessageBox.Ok)
事实证明,秘密在于使用一个QThread,当线程退出时它会发出一些信号。完成或终止取决于退出是正常还是异常。这里有一些代码
import sys
import os
import time
from PyQt4 import QtCore, QtGui
class Ui_ProgressBarDialog(QtGui.QWidget):
def __init__(self, process, parent = None):
QtGui.QWidget.__init__(self, parent)
self.thread = process
self.setupUi(self)
self.center()
self.thread.start()
def center(self):
qr = self.frameGeometry()
cp = QtGui.QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
def setupUi(self, ProgressBarDialog):
ProgressBarDialog.setObjectName("ProgressBarDialog")
ProgressBarDialog.resize(400, 33)
self.verticalLayout = QtGui.QVBoxLayout(ProgressBarDialog)
self.verticalLayout.setObjectName("verticalLayout")
self.progressBar = QtGui.QProgressBar(ProgressBarDialog)
self.progressBar.setProperty("value", 0)
self.progressBar.setObjectName("progressBar")
self.verticalLayout.addWidget(self.progressBar)
self.retranslateUi(ProgressBarDialog)
#Close when the thread finishes (normally)
QtCore.QObject.connect(
self.thread,
QtCore.SIGNAL("finished()"),
self.close
)
#Close when the thread is terminated (exception, cancelled etc)
QtCore.QObject.connect(
self.thread,
QtCore.SIGNAL("terminated()"),
self.close
)
#Let the thread update the progress bar position
QtCore.QObject.connect(
self.thread,
QtCore.SIGNAL("updateProgress"),
self.progressBar.setValue
)
#Repaint when the progress bar value changes
QtCore.QObject.connect(
self.progressBar,
QtCore.SIGNAL("valueChanged(int)"),
ProgressBarDialog.repaint
)
QtCore.QMetaObject.connectSlotsByName(ProgressBarDialog)
def retranslateUi(self, ProgressBarDialog):
ProgressBarDialog.setWindowTitle("Please Wait")
def closeEvent(self, event):
if self.thread.exit_status == self.thread.RUNNING:
if QtGui.QMessageBox.question(None, 'Cancel Download', "Are you sure you want to cancel the download?", QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) == QtGui.QMessageBox.Yes:
if self.thread.exit_status == self.thread.RUNNING:
self.thread.exiting = True
while self.thread.exiting:
time.sleep(0.25)
elif self.thread.exit_status == self.thread.SUCCESS:
self.thread.exit_status = self.thread.CANCELLED
else:
if self.thread.exit_status == self.thread.RUNNING:
event.ignore()
app = QtGui.QApplication(sys.argv)
filename = str(QtGui.QFileDialog.getSaveFileName(
None,
"Save as",
os.getcwd(),
"Data files: (*.dat)"
))
class DeviceDataStreamHandler(QtCore.QThread):
RUNNING = 1
SUCCESS = 0
FAILURE = -1
CANCELLED = -2
class CancelledError(Exception):
pass
def __init__(self, parent = None, **kwargs):
QtCore.QThread.__init__(self, parent)
self.exiting = False
self.exit_status = DeviceDataStreamHandler.RUNNING
self.device = device
self.filename = filename
def run(self):
progress = 0
try:
temp_binary_file = open(self.filename, "wb")
#getDataStream is an interator returing strings
for data in self.device.getDataStream():
temp_binary_file.write(data)
progress += 1
self.emit(QtCore.SIGNAL("updateProgress"), progress)
if self.exiting:
raise DeviceDataStreamHandler.CancelledError()
self.exit_status = DeviceDataStreamHandler.SUCCESS
except DeviceDataStreamHandler.CancelledError:
self.exit_status = DeviceDataStreamHandler.CANCELLED
self.device.cancelDownload()
except Exception as E:
self.exit_status = DeviceDataStreamHandler.FAILURE
self.error_details = str(E)
finally:
temp_binary_file.close()
if self.exit_status == DeviceDataStreamHandler.CANCELLED:
os.unlink(filename)
self.exiting = False
class HardwareDeviceObject(object):
def __init__(self):
#initialises comms with a hardware device
self.event_count = 0
self.capture = False
def startCapture(self):
self.capture = True
def cancelDownload():
self.capture = False
def read(self):
#returns a string sent from the device
time.sleep(1)
self.event_count += 1
return self.event_count
def getDataStream(self):
class DataStreamIterator(object):
def __init__(self, ds, max_capture_count = 100):
self.ds = ds
self.capture_count = 0
self.max_capture_count = max_capture_count
def __iter__(self):
return self
def next(self):
#return string received from device
if self.ds.capture and self.capture_count < self.max_capture_count:
self.capture_count += 1
return self.ds.read()
else:
raise StopIteration()
self.startCapture()
return DataStreamIterator(self)
capture = DeviceDataStreamHandler(device = HardwareDeviceObject(), filename = filename)
form = ProgressBarDialog.Ui_ProgressBarDialog(capture)
form.setWindowTitle("Dumping sessions")
form.progressBar.setMaximum(100) #expect 100 outputs from the device
form.progressBar.setValue(0)
form.show()
app.exec_()
if capture.exit_status == DeviceDataStreamHandler.SUCCESS:
QtGui.QMessageBox.information(None, 'Success', "Save to disk successful", QtGui.QMessageBox.Ok)
elif capture.exit_status == DeviceDataStreamHandler.FAILURE:
QtGui.QMessageBox.critical(None, 'Error interacting with device', "{}".format(capture.error_details), QtGui.QMessageBox.Ok)
信号与事件不同。无法使用信号发送事件。但是在任何情况下,如果您想关闭窗口,为什么不直接调用form.close呢?