Search code examples
pythonpyqtpyqt4qtreeviewqabstractitemmodel

how to refresh the view when data changes in model?


I am working on a simple tree directory explorer based on a qtreeview with a model view/controller/implementation. I need to use some threads that recursively search the sub-folders and feed the model/datas of the qtreeview. All of this works fine. But my issue is that the view doesnt refresh when the datas change ...

I have tried a few different things, but iam not happy with any of the solutions:

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

Emitting data change from setData() of the model should update the view, but it doesnt work for me. Also i havnt found an elegant way of finding the qmodelIndex. Iam just recusively loop all the datas to find the right Index.

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

This is my code example. there is a count down in the main window that would execute : self.treeView.expandAll() every second to force the view to update, i want to find a better solution ...

Related topics i found:

Refresh view when model data has not changed (Qt/PySide/PyQt)?

PyQt and MVC-pattern


Solution

  • The problem has nothing to do with threads. For the view to be notified the model must emit the signal layoutAboutToBeChanged before the change and layoutChanged after the change, but for this the node must access the model, so the model must be made as a Node attribute. With that change you no longer need a QTimer to update the view.

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