如何在模型中数据更改时刷新视图?

how to refresh the view when data changes in model?

我正在开发一个简单的树目录浏览器,它基于带有模型 view/controller/implementation 的 qtreeview。我需要使用一些递归搜索子文件夹并提供 qtreeview 的 model/datas 的线程。所有这一切都很好。 但我的问题是当数据更改时视图不会刷新...

我尝试了一些不同的方法,但我对任何解决方案都不满意:

QtGui.QStandardItemModel.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())

从模型的 setData() 发出数据更改应该更新视图,但它对我不起作用。我还没有找到一种查找 qmodelIndex 的优雅方法。我只是递归循环所有数据以找到正确的索引。

from PyQt4 import QtCore, QtGui
from PyQt4.QtGui import *
from PyQt4.QtCore import *

import time
import traceback, sys, os
from glob import glob
from random import randrange
import traceback

DEPTH = 0
threadpool = QThreadPool()

##########################################
###### Example thread function #####
##########################################
def listFolders( parent ):
    global DEPTH
    time.sleep(2)
    if DEPTH>4:
        return {'fileList':[], 'parent':parent}
    else:
        DEPTH+=1
    fileList = []
    for item in range(randrange(1,5)):
        fileList.append('item_'+str(item))

    return {'fileList':fileList, 'parent':parent}



##########################################
###### simple threading #####
##########################################
class WorkerSignals(QObject):
    finished = pyqtSignal()
    error = pyqtSignal(tuple)
    result = pyqtSignal(object)
    progress = pyqtSignal(int)

class Worker(QRunnable):
    def __init__(self, fn, *args, **kwargs):
        super(Worker, self).__init__()
        # Store constructor arguments (re-used for processing)
        self.fn = fn
        self.args = args
        self.kwargs = kwargs
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        try:
            result = self.fn(*self.args, **self.kwargs)
        except:
            traceback.print_exc()
            exctype, value = sys.exc_info()[:2]
            self.signals.error.emit((exctype, value, traceback.format_exc()))
        else:
            self.signals.result.emit(result)  # Return the result of the processing
        finally:
            self.signals.finished.emit()  # Done


##########################################
###### Model for qtreeview #####
##########################################
class SceneGraphModel(QtCore.QAbstractItemModel):
    def __init__(self, root ,parent=None):
        super(SceneGraphModel, self).__init__(parent)
        self._rootNode = root

    def rowCount(self, parent):
        if not parent.isValid():
            parentNode = self._rootNode
        else:
            parentNode = parent.internalPointer()
        return parentNode.childCount()


    def columnCount(self, parent):
        return 1

    def data(self, index, role):

        if not index.isValid():
            return None

        node = index.internalPointer()

        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
            if index.column() == 0:

                return node.name()

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        if index.isValid():
            if role == QtCore.Qt.EditRole:
                node = index.internalPointer()
                node.setName(value)
                return True
        return False

    def headerData(self, section, orientation, role):
        if role == QtCore.Qt.DisplayRole:
            if section == 0:
                return "Scenegraph"

    def flags(self, index):
        return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable


    def parent(self, index):
        node = self.getNode(index)
        parentNode = node.parent()
        if parentNode == self._rootNode:
            return QtCore.QModelIndex()
        if parentNode == None:
            row = 0
        else:
            row = parentNode.row()
        return self.createIndex(row, 0, parentNode)

    def index(self, row, column, parent):
        parentNode = self.getNode(parent)
        childItem = parentNode.child(row)

        if childItem:
            return self.createIndex(row, column, childItem)
        else:
            return QtCore.QModelIndex()


    def getNode(self, index):
        if index.isValid():
            node = index.internalPointer()
            if node:
                return node

        return self._rootNode

##########################################
###### Node class that contain the qtreeview datas #####
##########################################
class Node(object):
    def __init__(self, name, parent=None):
        self._name = name
        self._children = []
        self._parent = parent
        if parent is not None:
            parent.addChild(self)

    def typeInfo(self):
        return "folder"

    def addChild(self, child):
        self._children.append(child)

    def name(self):
        return self._name

    def child(self, row):
        return self._children[row]

    def childCount(self):
        return len(self._children)

    def parent(self):
        return self._parent

    def row(self):
        if self._parent is not None:
            return self._parent._children.index(self)

    def __repr__(self):
        return 'NODE_'+self.name()


##########################################
###### qtreeview containing the threading #####
##########################################
class DirectoryTree(QTreeView):
    def __init__(self):
        super(DirectoryTree, self).__init__()

        #create root node
        self.rootNode   = Node('root')
        #add model to treeview
        self._model = SceneGraphModel(self.rootNode)
        self.setModel(self._model)
        #recurive loop with thread to add more datas
        self.loop( self.rootNode )


    def thread(self, path):
        return listFolders(path)

    def threadResult(self, result ):
        for item in result['fileList']:
            newNode = Node(item,result['parent'])
            self.loop(newNode)


    def loop(self, parent ):
        worker = Worker( self.thread, parent )
        worker.signals.result.connect( self.threadResult )
        threadpool.start(worker)



##########################################
###### window with countdown #####
##########################################

class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.counter = 0
        self.layout = QVBoxLayout()
        self.l = QLabel("Start")
        self.layout.addWidget(self.l)
        w = QWidget()
        w.setLayout(self.layout)
        self.setCentralWidget(w)

        self.treeView = DirectoryTree()
        self.layout.addWidget(self.treeView)

        self.show()

        self.timer = QTimer()
        self.timer.setInterval(1000)
        self.timer.timeout.connect(self.recurring_timer)
        self.timer.start()


        self.setGeometry(0, 0, 650, 550)
        self.setWindowTitle("shot tree")
        self.centerOnScreen()

    def centerOnScreen (self):
        resolution = QtGui.QDesktopWidget().screenGeometry()
        self.move((resolution.width() / 2) - (self.frameSize().width() / 2),
                  (resolution.height() / 2) - (self.frameSize().height() / 2)) 


    def recurring_timer(self):
        self.counter +=1
        self.l.setText("Counter: %d" % self.counter)

        ##### This is a hack to refresh the view
        ##### i want to remove this line 
        ##### and properly emit the changes from the node class to refresh the qtreeview
        self.treeView.expandAll()


app = QApplication([])
window = MainWindow()
app.exec_()

这是我的代码示例。主要 window 中有一个倒计时将执行:self.treeView.expandAll() 每秒强制视图更新,我想找到更好的解决方案...

我找到的相关主题:

PyQt and MVC-pattern

问题与线程无关。对于要通知的视图,模型必须在更改前发出信号 layoutAboutToBeChanged,在更改后发出信号 layoutChanged,但为此节点必须访问模型,因此模型必须作为节点属性.通过该更改,您不再需要 QTimer 来更新视图。

class SceneGraphModel(QtCore.QAbstractItemModel):
    def __init__(self, root, parent=None):
        super(SceneGraphModel, self).__init__(parent)
        self._rootNode = root
        self._rootNode._model = self

    def rowCount(self, parent=QtCore.QModelIndex()):
        if not parent.isValid():
            parentNode = self._rootNode
        else:
            parentNode = parent.internalPointer()
        return parentNode.childCount()

    def columnCount(self, parent=QtCore.QModelIndex()):
        return 1

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if not index.isValid():
            return None

        node = index.internalPointer()

        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
            if index.column() == 0:
                return node.name()

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        if index.isValid():
            if role == QtCore.Qt.EditRole:
                node = index.internalPointer()
                node.setName(value)
                return True
        return False

    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.DisplayRole:
            if section == 0:
                return "Scenegraph"

    def flags(self, index):
        return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable

    def parent(self, index):
        node = self.getNode(index)
        parentNode = node.parent()
        if parentNode == self._rootNode:
            return QtCore.QModelIndex()
        if parentNode is None:
            row = 0
        else:
            row = parentNode.row()
        return self.createIndex(row, 0, parentNode)

    def index(self, row, column, parent=QtCore.QModelIndex()):
        parentNode = self.getNode(parent)
        childItem = parentNode.child(row)
        if childItem:
            return self.createIndex(row, column, childItem)
        else:
            return QtCore.QModelIndex()

    def getNode(self, index):
        if index.isValid():
            node = index.internalPointer()
            if node:
                return node
            print("node", node)
        return self._rootNode


class Node(object):
    def __init__(self, name, parent=None):
        self._name = name
        self._children = []
        self._parent = parent
        self._model = None
        if parent is not None:
            parent.addChild(self)

    def typeInfo(self):
        return "folder"

    def addChild(self, child):
        self._model.layoutAboutToBeChanged.emit()
        self._children.append(child)
        child._model = self._model
        self._model.layoutChanged.emit()

    def name(self):
        return self._name

    def setName(self, name):
        self._name = name

    def child(self, row):
        return self._children[row] if row < len(self._children) else None

    def childCount(self):
        return len(self._children)

    def parent(self):
        return self._parent

    def row(self):
        return 0 if self.parent() is None else self._parent._children.index(self)

    def __repr__(self):
        return 'NODE_' + self.name()