Pyside2: Как заставить рабочий поток порождать окно сообщения в главном потоке GUI и ждать отзывов пользователей? - PullRequest
0 голосов
/ 10 апреля 2020

Я пишу GUI в Python / PySide2.

Я хочу порождать рабочий поток для выполнения некоторых сложных вычислений (функция определена в отдельном модуле) на основе загруженных входных данных. через GUI.

В зависимости от характера / формата данных, я хотел бы создать окно сообщения с предупреждением, предлагающим пользователю либо

  • a ) go назад и загрузите некоторые другие данные, или
  • b) в любом случае выполните вычисления.

Тем временем рабочий поток должен ждать ответа пользователя в окне сообщения, прежде чем продолжить работу с кодом.

Исходя из предложений this вопрос , я получил свой код для работы, как нужно, используя QMutex и QWaitCondition. Приведенный ниже минимальный воспроизводимый пример показывает текущее состояние, в котором я нахожусь. Само собой разумеется, это чрезвычайно хаки:

  • QMutex и QWaitCondition являются глобальными объектами в GUI файл, но должен быть введен в kwargs целевой функции, чтобы заблокировать поток, пока открыто окно сообщения;
  • Я использую QEvent, созданный вручную, чтобы выяснить, пользователь нажал кнопку да или нет (мне нужно что-то с состоянием, которое я мог бы отправить в основной поток и обратно);
  • Я добавил пользовательское исключение, так как в противном случае оно не вызывается должным образом внутри try / исключением блок в методе Worker run (не имею никакого представления об этом);

Я явно что-то здесь упускаю. У кого-нибудь есть идеи о том, как добиться того, чего я хочу? Есть ли очевидный дизайн fl aws или какой-то другой модуль PySide, который я должен использовать?

myapp.py:

import sys

from PySide2.QtCore import (QMutex, QObject, QRunnable, QThreadPool, 
                            QWaitCondition, Signal, Slot)
from PySide2.QtWidgets import (QApplication, QLabel, QMainWindow, 
                               QMessageBox, QPushButton, QVBoxLayout, 
                               QWidget)

from myfunctions import computationally_expensive_function

MUTEX = QMutex()
WAIT_CONDITION = QWaitCondition()


class MyApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.threadpool = QThreadPool()

        self.frame = QWidget(self)
        self.setCentralWidget(self.frame)
        layout = QVBoxLayout()

        self.button = QPushButton(self, text='click me!')
        self.button.clicked.connect(self.start_worker)
        layout.addWidget(self.button)

        self.label = QLabel(self, text='a label')
        layout.addWidget(self.label)
        self.frame.setLayout(layout)

    def start_worker(self):
        worker = Worker(func=computationally_expensive_function)
        worker.signals.warning_callback.connect(self.some_important_waring)
        worker.signals.success.connect(self.worker_finished_gracefully)
        worker.signals.error.connect(self.worker_raised_error)
        self.threadpool.start(worker)

    def some_important_waring(self, warning_tuple):
        warning_event, important_message = warning_tuple
        reply = QMessageBox.question(self, 'WARNING', important_message,
                                     QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
        if reply == QMessageBox.Yes:
            warning_event.accept()
        else:
            warning_event.ignore()
        WAIT_CONDITION.wakeAll()

    def worker_finished_gracefully(self, text):
        self.label.setText(text)

    def worker_raised_error(self, error):
        self.label.setText(error)


class Worker(QRunnable):
    def __init__(self, func, *args, **kwargs):
        super().__init__()
        self.func = func
        self.args = args
        self.kwargs = kwargs
        self.signals = WorkerSignals()
        self.kwargs['warning_callback'] = self.signals.warning_callback
        self.kwargs['mutex'] = MUTEX
        self.kwargs['wait_condition'] = WAIT_CONDITION

    @Slot()
    def run(self):
        try:
            return_value = self.func(*self.args, **self.kwargs)
        except Exception as e:
            error_message = str(e)
            self.signals.error.emit(error_message)
        else:
            self.signals.success.emit(return_value)


class WorkerSignals(QObject):
    warning_callback = Signal(tuple)
    error = Signal(str)
    success = Signal(str)


if __name__ == '__main__':
    app = QApplication()
    gui = MyApp()
    gui.show()
    sys.exit(app.exec_())

myfunctions. py:

import random

from PySide2.QtCore import QEvent


def computationally_expensive_function(*args, **kwargs):
    # some preprocessing is performed here
    warning_callback = kwargs.get('warning_callback')
    mutex = kwargs.get('mutex')
    wait_condition = kwargs.get('wait_condition')

    if random.random() > 0.5:  # simulates issues with the data passed
        mutex.lock()
        warning_event = QEvent(QEvent.WindowActivate)
        important_message = 'This is an important message! Will you continue?'
        warning_callback.emit((warning_event, important_message))
        wait_condition.wait(mutex)
        mutex.unlock()
        if not warning_event.isAccepted():
            raise AbortedByUser('User decided to abort the computation')

    return 'End of computationally expensive function'


class AbortedByUser(Exception):
    """Raised if the user decides to abort the computation"""
...