PyQt5信号和线程.计时器



在Python 3下的PyQt5中,我试图使用QTableView来显示我的域对象的状态,并在域对象更改时保持视图更新。我的域对象应该不需要知道这个视图或PyQt的任何信息,但使用一种观察者模式,每个域对象都有一个回调函数列表,这些函数在对象状态更改时运行,并且从PyQt端我提供回调,这些回调会发出连接到视图的信号。

问题是,我遇到的问题显然与线程有关;简而言之,应该发射的信号似乎确实发射了,而且显然发射到了正确的插槽数量,但插槽没有被调用。

所附的代码给出了一个非常简单的例子。域对象有一个名称和一个整数值。该值可以通过两种方式进行修改:可以立即增加一,也可以在一秒钟内增加一。后一种操作是用CCD_ 2来实现的。

面向模型/视图的GUI显示当前域对象的表,每行一个,并有两个按钮,对应于修改对象值的两种方式。给我带来麻烦的信号是dataChanged,为了进行调试,它连接到视图中的onDataChanged插槽,该插槽在调用时将其参数打印到控制台。

当选择第二行并点击"增加值"时,信号/插槽机制工作正常:在视图中更新值,控制台显示

Emitting dataChanged to 4 receivers...
dataChanged signal detected: (1, 0) / (1, 1)
...done emitting

但是,当单击"在一秒钟内增加值"时,视图不会被更新,直到以其他方式强制更新(例如选择另一行)。此外,控制台输出清楚地表明onDataChanged没有被调用(这当然只是同一问题的另一个症状):

Emitting dataChanged to 4 receivers...
...done emitting

那么,发生了什么事,我该如何解决?我发现有很多人提到线程和PyQt信号机制可能不能很好地协同工作,但我还没有完全理解我正在尝试的东西到底出了什么问题。我看到有人建议使用Qt特定的线程,而不是Pythonthreading模块,但我非常希望我的域数据完全独立于Qt。(此外,即使在这个玩具示例中,从模型/视图端控制1秒延迟也足够容易,但在我的真实应用程序中,这种行为确实属于域对象本身。)

有什么启示或想法吗?

import sys
from threading import Timer
from PyQt5.QtCore import Qt, QAbstractTableModel
from PyQt5.QtWidgets import qApp, QApplication, QWidget, QTableView, QPushButton, QHBoxLayout
class MyDomainObject(object):
'''Class representing something in the real world: a name associated with an integer.
To keep interested parties up to date, each instance has a list of callbacks, functions
which are called with self as argument when the integer is changed.
'''
def __init__(self, name, value):
self.name = name
self.value = value
self.callbacks = []
def add_callback(self, callback):
self.callbacks.append(callback)
def increase_value(self):
self.value += 1
self.notify_observers() 
def increase_value_in_a_second(self):
Timer(1, self.increase_value).start()
def notify_observers(self): 
for cb in self.callbacks:
cb(self)
class MyTableModel(QAbstractTableModel):
'''Model class holding a list of domain objects for showing in a view'''
COLS = ['name', 'value']
def __init__(self, obj_list):
super().__init__()
self.obj_list = obj_list
for row, obj in enumerate(self.obj_list):
obj.add_callback(self.callbackFactory(row))
#########################
# MANDATORY OVERRIDES
def rowCount(self, dummy):
return len(self.obj_list)
def columnCount(self, dummy):
return 2
def data(self, index, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
return getattr(self.obj_list[index.row()], self.COLS[index.column()])
def flags(self, index):
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
# MANDATORY OVERRIDES end
#########################
def callbackFactory(self, row):
'''Factory method making function emitting a dataChanged signal for given row.'''
def _callback(ignored_obj):
model_index_left = self.createIndex(row, 0)
model_index_right = self.createIndex(row, 1)
print('Emitting dataChanged to {} receivers...'.format(self.receivers(self.dataChanged)))
self.dataChanged.emit(model_index_left, model_index_right)
print('...done emitting')
qApp.processEvents()
return _callback
def at(self, ix):
'''For convenience: Return domain object at given index.'''
try:
return self.obj_list[ix]
except IndexError:
return None
class MyTableView(QWidget):
def __init__(self, model):
super().__init__()
self.table = QTableView(self)
self.table.setModel(model)
self.table.model().dataChanged.connect(self.onDataChanged)
# Select whole rows, single selection only:
self.table.setSelectionBehavior(QTableView.SelectRows)
self.table.setSelectionMode(QTableView.SingleSelection)
# Two ways of increasing value: now, or slightly later:
self.increase_button = QPushButton('Increase value', self)
self.increase_button.clicked.connect(self.onIncreaseClicked)
self.increase_later_button = QPushButton('Increase value in a second', self)
self.increase_later_button.clicked.connect(self.onIncreaseLaterClicked)
self.sel_model = self.table.selectionModel()
hbox = QHBoxLayout()
hbox.addWidget(self.table)
hbox.addWidget(self.increase_button)
hbox.addWidget(self.increase_later_button)
self.setLayout(hbox)
self.show()
def onDataChanged(self, top_left_ix, bottom_right_ix):
print('dataChanged signal detected: ({}, {}) / ({}, {})'.format(top_left_ix.row(), top_left_ix.column(),
bottom_right_ix.row(), bottom_right_ix.column()))
def onIncreaseClicked(self):
selected = self.sel_model.selectedIndexes() # either empty, or all have the same row.
if selected:
selected_obj = self.table.model().at(selected[0].row())
selected_obj.increase_value()
def onIncreaseLaterClicked(self):
selected = self.sel_model.selectedIndexes() # either empty, or all have the same row.
if selected:
selected_obj = self.table.model().at(selected[0].row())
selected_obj.increase_value_in_a_second()
if __name__ == '__main__':
app = QApplication(sys.argv)
domain_objects = [MyDomainObject('Foo', 23), MyDomainObject('Bar', 0)]
model = MyTableModel(domain_objects)
view = MyTableView(model)
app.exec_()

一些睡眠和进一步的谷歌搜索可能产生了解决方案:

看起来,虽然这种线程间信号通常是有效的,但PyQt5dataChanged信号尤其有一个怪癖,导致了这种行为:第三个带有QVector<int>C++签名的可选参数,它显然没有在Qt中注册为元类型(注意:我真的不知道我在说什么),开发人员似乎拒绝修复这一问题。

我的解决方案是让模型定义并发出自定义的myDataChanged信号,并将其连接到视图中的一个插槽,然后要求模型发出内置的dataChanged信号,以使引擎盖下的布线进行刷新。在发布的代码中,进行以下更改:

  • 在顶部添加行

    from PyQt5.QtCore import pyqtSignal
    
  • MyTableModel类的开头,添加行

    myDataChanged = pyqtSignal('QModelIndex', 'QModelIndex')
    
  • MyTableModel.callbackFactory中,将dataChanged替换为myDataChanged(一个实际的代码出现,加上注释/字符串中的两个,以保持美观)。

  • MyTableView.__init__中,将dataChanged替换为myDataChanged(一次出现)
  • MyTableView.onDataChanged中,添加行

    self.table.model().dataChanged.emit(top_left_ix, bottom_right_ix)
    

最新更新