在Python 3下的PyQt5中,我尝试使用QTableView
来显示域对象的状态,并在域对象发生更改时保持视图更新。我的域对象不需要知道任何有关此视图或PyQt的信息,但是使用一种观察者模式,每个域对象都有一个回调函数列表,这些函数在对象状态发生变化时运行,并且从PyQt端开始我提供回调,发出连接到视图的信号。
问题是,我遇到的问题显然与线程有关;简而言之,似乎应该触发的信号确实被触发,显然是正确的插槽数量,但是没有调用插槽。
附加的代码给出了一个极少的例子。域对象具有名称和整数值。可以通过两种方式修改此值:它可以立即增加一个,也可以一秒钟增加一个。后一个操作是使用threading.Timer
实现的。
面向模型/视图的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特定的线程而不是Python threading
模块,但我非常强烈希望我的域数据完全独立于Qt。 (另外,即使在这个玩具示例中,从模型/视图侧控制一秒延迟也很容易,在我的真实应用中,这种行为真正属于域对象本身。)
任何启示或想法?
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_()
答案 0 :(得分:0)
appears虽然这样的线程信令通常起作用,但PyQt5 dataChanged
信号特别有一个怪癖,它给出了这种行为:第三个带有QVector<int>
C ++签名的可选参数,显然没有在Qt中注册为元类型(警告:我真的不知道我在说什么),开发人员似乎有declined to fix。
我的解决方案是让模型定义并发出自定义myDataChanged
信号,并将其连接到视图中的插槽,然后要求模型发出内置的-in 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)