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) 来更新进度。这有两个问题:

  1. 如果用户尝试与 window 交互,他们会从 windows manager/OS 桌面进程收到 'not responding' 消息。这是因为事件没有被处理。

  2. 因为事件没有被处理,用户不能通过关闭 window.

  3. 来取消长的慢循环

所以我尝试在单独的线程中制作长而慢的循环 运行,使用信号更新进度条。我覆盖了我的 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)