Перенаправление stdout и stderr в PyQt4 QTextEdit из вторичного потока
Переполнение стека. Я снова прихожу к вам во время острой нужды, шатко балансируя на грани безумия. Этот вопрос - как видно из заголовка - представляет собой объединение нескольких других вопросов, на которые я видел ответы здесь.
У меня есть приложение PyQt, и я хочу без промедления перенаправить потоки stdout и stderr в QTextEdit, который находится в моем GUI.
Первоначально я нашел следующий ответ о переполнении стека: /questions/43304081/pyside-i-python-logirovanie/43304083#43304083
Это работает отлично, но с одной оговоркой: если stdout или stderr обновляются несколько раз, пока ЦП обрабатывает относительно более длинный метод, все обновления отображаются одновременно, когда основной поток возвращается в цикл приложения. К сожалению, у меня есть несколько методов, выполнение которых занимает до 20 секунд (связанных с сетью), и поэтому приложение перестает отвечать на запросы - и QTextEdit не обновляется - пока они не будут завершены.
Чтобы решить эту проблему, я делегировал всю обработку графического интерфейса в основной поток, и я порождаю второй поток для обработки более длительных сетевых операций, используя pyqtSignals, чтобы уведомить основной поток о том, когда работа завершается и проходит обратные результаты. Сразу же, когда я начал тестировать код, написанный таким образом, интерпретатор python начал аварийно завершать работу.
Вот где это очень раздражает: Python падает, потому что - используя класс из включенной ссылки выше - я назначил потоки sys.stdout/err виджету QTextEdit; Виджеты PyQt не могут быть изменены из любого потока, кроме потока приложения, и поскольку обновления stdout и stderr поступают из созданного мной дополнительного рабочего потока, они нарушают это правило. Я закомментировал раздел кода, куда я перенаправляю выходные потоки, и, конечно же, программа работает без ошибок.
Это возвращает меня к исходной точке и оставляет меня в запутанной ситуации; Предполагая, что я продолжаю обрабатывать связанные с графическим интерфейсом операции в главном потоке и иметь дело с вычислениями и более длительными операциями во вторичном потоке (что, как я понял, является наилучшим способом предотвратить блокировку приложения, когда пользователь вызывает события), как я могу это сделать? перенаправить Stdout и Stderr из обоих потоков в виджет QTextEdit? Класс в приведенной выше ссылке отлично работает для основного потока, но убивает python - по причине, описанной выше - когда обновления приходят из второго потока.
1 ответ
Во-первых, +1 за понимание того, наскольконебезопасно множество примеров переполнения стека!
Решение состоит в том, чтобы использовать потокобезопасный объект (например, Python Queue.Queue
) опосредовать передачу информации. Я приложил пример кода ниже, который перенаправляет stdout
к питону Queue
, это Queue
читается QThread
, который отправляет содержимое в основной поток через механизм сигналов / слотов Qt (передача сигналов безопасна для потоков). Затем основной поток записывает текст в текстовое редактирование.
Надеюсь, что это ясно, не стесняйтесь задавать вопросы, если это не так!
РЕДАКТИРОВАТЬ: обратите внимание, что приведенный пример кода не очень хорошо очищает QThreads, поэтому при выходе вы получите предупреждение. Я оставлю это вам, чтобы расширить ваш вариант использования и очистить поток (ы)
import sys
from Queue import Queue
from PyQt4.QtCore import *
from PyQt4.QtGui import *
# The new Stream Object which replaces the default stream associated with sys.stdout
# This object just puts data in a queue!
class WriteStream(object):
def __init__(self,queue):
self.queue = queue
def write(self, text):
self.queue.put(text)
# A QObject (to be run in a QThread) which sits waiting for data to come through a Queue.Queue().
# It blocks until data is available, and one it has got something from the queue, it sends
# it to the "MainThread" by emitting a Qt Signal
class MyReceiver(QObject):
mysignal = pyqtSignal(str)
def __init__(self,queue,*args,**kwargs):
QObject.__init__(self,*args,**kwargs)
self.queue = queue
@pyqtSlot()
def run(self):
while True:
text = self.queue.get()
self.mysignal.emit(text)
# An example QObject (to be run in a QThread) which outputs information with print
class LongRunningThing(QObject):
@pyqtSlot()
def run(self):
for i in range(1000):
print i
# An Example application QWidget containing the textedit to redirect stdout to
class MyApp(QWidget):
def __init__(self,*args,**kwargs):
QWidget.__init__(self,*args,**kwargs)
self.layout = QVBoxLayout(self)
self.textedit = QTextEdit()
self.button = QPushButton('start long running thread')
self.button.clicked.connect(self.start_thread)
self.layout.addWidget(self.textedit)
self.layout.addWidget(self.button)
@pyqtSlot(str)
def append_text(self,text):
self.textedit.moveCursor(QTextCursor.End)
self.textedit.insertPlainText( text )
@pyqtSlot()
def start_thread(self):
self.thread = QThread()
self.long_running_thing = LongRunningThing()
self.long_running_thing.moveToThread(self.thread)
self.thread.started.connect(self.long_running_thing.run)
self.thread.start()
# Create Queue and redirect sys.stdout to this queue
queue = Queue()
sys.stdout = WriteStream(queue)
# Create QApplication and QWidget
qapp = QApplication(sys.argv)
app = MyApp()
app.show()
# Create thread that will listen on the other end of the queue, and send the text to the textedit in our application
thread = QThread()
my_receiver = MyReceiver(queue)
my_receiver.mysignal.connect(app.append_text)
my_receiver.moveToThread(thread)
thread.started.connect(my_receiver.run)
thread.start()
qapp.exec_()