PyQt5信号和线程.Timer

时间:2017-10-20 00:30:48

标签: python multithreading signals pyqt5

在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_()

1 个答案:

答案 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)