PySide 在工作线程中等待来自主线程的信号
-
14-12-2019 - |
题
我决定在我的一个脚本中添加一个 GUI。该脚本是一个简单的网络抓取工具。我决定使用工作线程,因为下载和解析数据可能需要一段时间。我决定使用 PySide,但我对 Qt 的了解总体上非常有限。
由于脚本应该在遇到验证码时等待用户输入,因此我决定它应该等到 QLineEdit
火灾 returnPressed
然后将其内容发送到工作线程,以便它可以发送它进行验证。这应该比忙于等待按下返回键要好。
等待信号似乎并不像我想象的那么简单,经过一段时间的搜索,我发现了几种类似于 这. 。不过,跨线程发信号和工作线程中的本地事件循环使我的解决方案变得更加复杂。
折腾了好几个小时,还是不行。
应该发生什么:
- 下载数据直到引用验证码并进入循环
- 下载验证码并将其显示给用户,启动
QEventLoop
通过致电self.loop.exec_()
- 出口
QEventLoop
通过致电loop.quit()
在通过连接的工作线程槽中self.line_edit.returnPressed.connect(self.worker.stop_waiting)
在里面main_window
班级 - 验证验证码并在验证失败时循环,否则重试现在应该可下载的最后一个网址,然后继续处理下一个网址
会发生什么:
...往上看...
退出
QEventLoop
不起作用。self.loop.isRunning()
回报False
调用其后exit()
.self.isRunning
回报True
, ,因此该线程似乎并没有在奇怪的情况下死亡。线程仍然停在self.loop.exec_()
线。因此,线程被卡住执行事件循环,即使事件循环告诉我它不再运行。GUI 的响应与工作线程类的插槽的响应相同。我可以看到发送到工作线程的文本、事件循环的状态和线程本身,但在执行上述行之后什么也没有。
代码有点复杂,因此我添加了一些伪代码-python-mix,省略了不重要的内容:
class MainWindow(...):
# couldn't find a way to send the text with the returnPressed signal, so I
# added a helper signal, seems to work though. Doesn't work in the
# constructor, might be a PySide bug?
helper_signal = PySide.QtCore.Signal(str)
def __init__(self):
# ...setup...
self.worker = WorkerThread()
self.line_edit.returnPressed.connect(self.helper_slot)
self.helper_signal.connect(self.worker.stop_waiting)
@PySide.QtCore.Slot()
def helper_slot(self):
self.helper_signal.emit(self.line_edit.text())
class WorkerThread(PySide.QtCore.QThread):
wait_for_input = PySide.QtCore.QEventLoop()
def run(self):
# ...download stuff...
for url in list_of_stuff:
self.results.append(get(url))
@PySide.QtCore.Slot(str)
def stop_waiting(self, text):
self.solution = text
# this definitely gets executed upon pressing return
self.wait_for_input.exit()
# a wrapper for requests.get to handle captcha
def get(self, *args, **kwargs):
result = requests.get(*args, **kwargs)
while result.history: # redirect means captcha
# ...parse and extract captcha...
# ...display captcha to user via not shown signals to main thread...
# wait until stop_waiting stops this event loop and as such the user
# has entered something as a solution
self.wait_for_input.exec_()
# ...this part never get's executed, unless I remove the event
# loop...
post = { # ...whatever data necessary plus solution... }
# send the solution
result = requests.post('http://foo.foo/captcha_url'), data=post)
# no captcha was there, return result
return result
frame = MainWindow()
frame.show()
frame.worker.start()
app.exec_()
解决方案
该槽在创建该槽的线程内执行 QThread
, ,而不是在线程中 QThread
控制。
你需要移动一个 QObject
到线程并将其槽连接到信号,该槽将在线程内执行:
class SignalReceiver(QtCore.QObject):
def __init__(self):
self.eventLoop = QEventLoop(self)
@PySide.QtCore.Slot(str)
def stop_waiting(self, text):
self.text = text
eventLoop.exit()
def wait_for_input(self):
eventLoop.exec()
return self.text
class MainWindow(...):
...
def __init__(self):
...
self.helper_signal.connect(self.worker.signalReceiver.stop_waiting)
class WorkerThread(PySide.QtCore.QThread):
def __init__(self):
self.signalReceiver = SignalReceiver()
# After the following call the slots will be executed in the thread
self.signalReceiver.moveToThread(self)
def get(self, *args, **kwargs):
result = requests.get(*args, **kwargs)
while result.history:
...
self.result = self.signalReceiver.wait_for_input()
其他提示
你所描述的看起来很理想 QWaitCondition
.
简单的例子:
import sys
from PySide import QtCore, QtGui
waitCondition = QtCore.QWaitCondition()
mutex = QtCore.QMutex()
class Main(QtGui.QMainWindow):
def __init__(self, parent=None):
super(Main, self).__init__()
self.text = QtGui.QLineEdit()
self.text.returnPressed.connect(self.wakeup)
self.worker = Worker(self)
self.worker.start()
self.setCentralWidget(self.text)
def wakeup(self):
waitCondition.wakeAll()
class Worker(QtCore.QThread):
def __init__(self, parent=None):
super(Worker, self).__init__(parent)
def run(self):
print "initial stuff"
mutex.lock()
waitCondition.wait(mutex)
mutex.unlock()
print "after returnPressed"
if __name__=="__main__":
app = QtGui.QApplication(sys.argv)
m = Main()
m.show()
sys.exit(app.exec_())
不隶属于 StackOverflow