2015-09-14 4 views
0

Я пишу программу PyQT4 (4.11), которая выполняет длинную медленную задачу, которая идеально нуждается в индикаторе выполнения. Программа работает почти идеально, если я не использую потоки, подклассифицируя QWidget, который содержит только QProgressBar и макет. Создавая этот подкласс таким образом, как form, я могу позвонить form.show(), чтобы поместить его на экран, а затем мой длинный медленный цикл может обновить ход, вызвав form.progressbar.setValue(progress). Есть две проблемы:PyQT4 QWidget должен получить сигнал «закрыть» дважды перед закрытием

  1. , если пользователь пытается взаимодействовать с окном, они получают «не отвечает» сообщения из процесса рабочего стола оконного менеджера/OS. Это происходит потому, что события не обрабатываются.

  2. поскольку события не обрабатываются, пользователь не может отменить длинный медленный цикл, закрыв окно.

Поэтому я попытался выполнить длинный медленный цикл в отдельном потоке, используя сигнал для обновления индикатора выполнения. Я перепробовал closeEvent моего QWidget, чтобы он мог отменить взаимодействие с аппаратным устройством (все обернуты мьютексами, чтобы связь устройства не выходила из синхронизации). Опять же, это почти работает. Если я отменил, приложение завершает работу. Если я оставлю его для завершения до завершения, я должен закрыть окно вручную (например, щелкнуть значок закрытия или нажать 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)  
+0

Сигналы не совпадают с событиями. Вы не можете отправить событие с помощью сигнала. Но в любом случае, если вы хотите закрыть окно, почему бы просто не называть 'form.close()'? – ekhumoro

ответ

0

Оказывается, секрет в том, чтобы использовать 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) 
Смежные вопросы