1

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

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
nicosalto
  • 33
  • 1
  • 6

1 Answers1

3

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()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thank you! this is exactly what i was missing. I didnt knew how to use layoutAboutToBeChanged and layoutChanged. Now it all makes sens! – nicosalto Jul 30 '18 at 05:34
  • @nicosalto the model is an element independent of the view, for example a model can be in several views, so I ask you who should notify: the model to the views, or the views to the model? – eyllanesc Jul 30 '18 at 05:35
  • the model should notify the views ;) – nicosalto Jul 30 '18 at 06:10
  • iam actually following this cool youtube tutorial. https://www.youtube.com/watch?v=2sRoLN337cs&index=2&list=PL8B63F2091D787896 – nicosalto Jul 30 '18 at 06:11
  • @nicosalto Before watching those videos I recommend you read the official documentation: http://doc.qt.io/qt-5/model-view-programming.html , On the other hand I recommend you not combine themes if you are a beginner: models and threads a bad combination for a beginner because you will always blame the threads. – eyllanesc Jul 30 '18 at 06:22
  • thanks for the link! yeah no worries, i have experiences with threads and iam not blaming them ;) – nicosalto Jul 30 '18 at 07:32
  • @nicosalto The title indicates this, because if you had known that the problem is not the threads, why put it in the title and the tags ?, in my test eliminate the threads, use a timer and I saw that the problem was not there, so I noticed the implementation of your mode – eyllanesc Jul 30 '18 at 07:34
  • sorry, but you are being a bit unfair. the title is "PyQt, MVC and threads, how to refresh the view when datas changes?" i think that reflect the issue that i was facing. at no point in my description i mention that its coming from the threads. Plus its not in the tags either. – nicosalto Jul 30 '18 at 07:40
  • @nicosalto The title is what is used to filter questions, and in your case screams Threads, on the other hand I change the tags, you can see it in the edition. :) – eyllanesc Jul 30 '18 at 07:43