PyQT4 QWidget 在关闭前必须接收两次 'close' 信号
PyQT4 QWidget must receive 'close' signal twice before closing
我正在编写一个 PyQT4 (4.11) 程序,该程序执行一个长时间缓慢的任务,理想情况下需要一个进度条。如果我不使用线程、子classing 一个只包含一个QProgressBar 和一个布局的QWidget,该程序几乎可以完美运行。将这个子 class 实例化为 form
,我可以调用 form.show()
将其显示在屏幕上,然后我的长慢循环可以通过调用 form.progressbar.setValue(progress)
来更新进度。这有两个问题:
如果用户尝试与 window 交互,他们会从 windows manager/OS 桌面进程收到 'not responding' 消息。这是因为事件没有被处理。
因为事件没有被处理,用户不能通过关闭 window.
来取消长的慢循环
所以我尝试在单独的线程中制作长而慢的循环 运行,使用信号更新进度条。我覆盖了我的 QWidget 的 closeEvent,以便它可以取消与硬件设备的交互(全部包装在互斥体中,因此设备通信不会不同步)。同样,这几乎可行。如果我取消,应用程序将退出。如果我让它完成 运行,我必须手动关闭 window(即单击关闭图标或按 alt-f4),即使我正在向 QWidget 发送关闭信号。正如您在下面的代码中看到的那样,有一些复杂情况,因为如果取消应用程序不能立即关闭,因为它必须等待一些硬件清理发生。这是我的代码的最小版本
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,它在线程退出时发出一些信号。 "finished" 或 "terminated" 取决于退出是正常还是异常。这是一些代码
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)
我正在编写一个 PyQT4 (4.11) 程序,该程序执行一个长时间缓慢的任务,理想情况下需要一个进度条。如果我不使用线程、子classing 一个只包含一个QProgressBar 和一个布局的QWidget,该程序几乎可以完美运行。将这个子 class 实例化为 form
,我可以调用 form.show()
将其显示在屏幕上,然后我的长慢循环可以通过调用 form.progressbar.setValue(progress)
来更新进度。这有两个问题:
如果用户尝试与 window 交互,他们会从 windows manager/OS 桌面进程收到 'not responding' 消息。这是因为事件没有被处理。
因为事件没有被处理,用户不能通过关闭 window.
来取消长的慢循环
所以我尝试在单独的线程中制作长而慢的循环 运行,使用信号更新进度条。我覆盖了我的 QWidget 的 closeEvent,以便它可以取消与硬件设备的交互(全部包装在互斥体中,因此设备通信不会不同步)。同样,这几乎可行。如果我取消,应用程序将退出。如果我让它完成 运行,我必须手动关闭 window(即单击关闭图标或按 alt-f4),即使我正在向 QWidget 发送关闭信号。正如您在下面的代码中看到的那样,有一些复杂情况,因为如果取消应用程序不能立即关闭,因为它必须等待一些硬件清理发生。这是我的代码的最小版本
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,它在线程退出时发出一些信号。 "finished" 或 "terminated" 取决于退出是正常还是异常。这是一些代码
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)