1

I would like to render each item in a QTreeView differently based on a number of attributes stored in a database and based on whether the item is a folder or a file. However, I don't understand how the QTreeView or QFileSystemModel communicate with the delegate. Whenever an item must be drawn, including during initialization, I'd expect to provide the delegate with all the parameters it requires and then use a series of if statements within the delegate to set how the particular item is drawn. I've only found the .setItemDelegate method and don't know when or how the delegate is actually called or how it loops through all the items in the model. Below is an example based on material online. There are two problems:

  1. I placed code in comments that I was unable to get working. Once I understand how the delegate can receive information from the QTreeView (or calling class), I believe I can do the rest.

  2. I was unable to get this subclass of the QTreeView to display the folder and file icons.

Code:

import sys
from PySide.QtCore import *
from PySide.QtGui import *

class fileSystemDelegate(QItemDelegate):
    def __init__(self, parent=None):
        QItemDelegate.__init__(self, parent)        #shouldn't this insure the icons are drawn?

    def paint(self, painter, option, index):
        painter.save()

        # set background
        painter.setPen(QPen(Qt.NoPen))
        if option.state & QStyle.State_Selected:   #DURING DRAW LOOP: idx = self.currentIndex(); if self.fileSystemModel.isDir(idx): PAINT RED
            painter.setBrush(QBrush(Qt.red))
        else:
            painter.setBrush(QBrush(Qt.white))     #ELSE PAINT WHITE
        painter.drawRect(option.rect)

        # draw item
        painter.setPen(QPen(Qt.black))
        text = index.data(Qt.DisplayRole)
        painter.drawText(option.rect, Qt.AlignLeft, text)   #there is no painter.drawIcon?

        painter.restore()

class fileSystemBrowser(QTreeView):
    def __init__(self, parent=None):
        super().__init__(parent)

        delegate = fileSystemDelegate()
        self.setItemDelegate(delegate)                  # how to provide delegate with additional info about the item to be drawn ?

        self.fileSystemModel = QFileSystemModel()
        self.fileSystemModel.setRootPath(QDir.currentPath())
        self.setModel(self.fileSystemModel)

if __name__ == '__main__':

    app = QApplication(sys.argv)
    window = fileSystemBrowser()
    window.show()
    sys.exit(app.exec_())

EDIT 1:

I've added an example "database" in the form of a dictionary and changed the approach to rely on the data method rather than the delegate. I would expect this code to perform the dictionary lookup whenever information is displayed in the tree and therefore print to the terminal when the user enters C:\Program Files\Internet Explorer\ on a Microsoft Windows computer. However, it just displays the directory without printing anything to the terminal. I'd like to know:

  1. How do I get if statements in the data method to trigger for every item in the display as they are being drawn?

  2. How can I display an icon after the default icon is displayed, on the same row?

Code:

import sys
from PySide.QtCore import *
from PySide.QtGui import *

database = {'C:\Program Files\Internet Explorer\ExtExport.exe':(1,3), 'C:\Program Files\Internet Explorer\iexplore.exe':(0,0)}

class fileSystemBrowser(QTreeView):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.fileSystemModel = QFileSystemModel()
        self.fileSystemModel.setRootPath(QDir.currentPath())
        self.setModel(self.fileSystemModel)

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            path = self.fileSystemModel.filePath(index)
            if  self.fileSystemModel.isDir(index):
                if database.get(path) != None:
                    if database[path][0] > 0:
                        print("Acting on custom data 0.") # add another icon after the regular folder icon

                    if database[path][1] > 0:
                        print("Acting on custom data 1.") # add another (different) icon after the regular folder or previous icon

if __name__ == '__main__':

    app = QApplication(sys.argv)
    window = fileSystemBrowser()
    window.show()
    sys.exit(app.exec_())

EDIT 2:

Subclassing the model definitely did make a difference. Now the script appears to be calling my new data method on every item. Unfortunately, the data method doesn't work yet so the result is a treeview without icons or text. Sometimes I receive the error: "QFileSystemWatcher: failed to add paths: C:/PerfLogs". Based on examples online, I've commented where I think my errors may be, but I cannot yet get this to work. What am I doing wrong?

import sys
from PySide.QtCore import *
from PySide.QtGui import *

database = {'C:\Program Files\Internet Explorer\ExtExport.exe':(1,3), 'C:\Program Files\Internet Explorer\iexplore.exe':(0,0)}

class newFileModel(QFileSystemModel):

    def __init__(self, parent=None):
        QFileSystemModel.__init__(self, parent)
        #self.elements = [[Do I need this? What should go here?]]

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            path = self.filePath(index)
            if  self.isDir(index):
                if database.get(path) != None:
                    if database[path][0] > 0:
                        print("Acting on custom data 0.") # I can add code here for different color text, etc.

                    if database[path][1] > 0:
                        print("Acting on custom data 1.") # I'll add code later
        #return self.data(index, role) # Do I need this about here?


class fileSystemBrowser(QTreeView):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.fileSystemModel = newFileModel()
        self.fileSystemModel.setRootPath(QDir.currentPath())
        self.setModel(self.fileSystemModel)


if __name__ == '__main__':

    app = QApplication(sys.argv)
    window = fileSystemBrowser()
    window.show()
    sys.exit(app.exec_())
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
davideps
  • 541
  • 3
  • 13
  • 1
    Depending on exactly what you want to do, an item-delegate may be overkill. Are you sure you want to draw absolutely *everything* yourself? That could end up being a *lot* of work. What specific properties do you want to change? Subclassing the file-model and reimplementing the `data` method looks much easier to implement. – ekhumoro Oct 19 '17 at 17:38
  • I'd like to change the background color, text color, text font, and possibly the icons in the view based on parameters that would (eventually) be available to my fileSystemBrowser class instance. Is [this](https://stackoverflow.com/questions/27587035/qfilesystemmodel-custom-icons) an example of subclassing the file-model and overriding the data method? If so, I, unfortunately, do not understand it. I'm familiar with graphical systems like ggplot2 for R. With Qt delegates, I'm looking for and not finding anything familiar. – davideps Oct 19 '17 at 19:58
  • Yes, that answer has the basics of it. It looks like all the properties you are interested in can be modified that way. I can provide a pyside demo if you'd like. – ekhumoro Oct 19 '17 at 20:03
  • I've edited my question to include example data and a simple data method override. If the index in the data method is the index in the model and is updated as each item is drawn to the screen, then my method should have written to the terminal. It did not. I'd appreciate any guidance you can offer. – davideps Oct 20 '17 at 06:02
  • 1
    You need to subclass the model, not the treeview. I don't see any easy way to add multiple icons, other than creating extra columns for them. The idea of inserting additonal icons after the file-icon seems weird to me - it would surely produce a very muddled and ugly looking interface. Extra columns would be much more user-friendly, and a lot easier to implement. – ekhumoro Oct 20 '17 at 20:03
  • OK. I'll worry about different icons later, right now I just want to understand how to get the new data method working. I've changed my code. Please see "EDIT 2". – davideps Oct 22 '17 at 07:30
  • You changed the question in each edition that I can not understand it today, you could explain to me what it is that you want today to be able to help you. – eyllanesc Oct 22 '17 at 09:24
  • @eyllanesc, I am still trying to write a new data method that will display directories and files differently depending on records in a database. But there are several steps to do this. The first step in my mind is to have the file model display normally but for the print statements to trigger when I open the directory with the files in the "database". That way I know the data method is being called and has access to the "database". I will then change the print statements to modify the way those files are displayed. – davideps Oct 22 '17 at 09:59
  • 1
    the function must return the data that is being requested and in your case it does not return anything, if you only want to know if it is calling, I recommend `return QFileSystemModel.data(self, index, role)` at the end of the function – eyllanesc Oct 22 '17 at 10:06
  • What does that (1, 0) and (0, 0) mean in database? – eyllanesc Oct 22 '17 at 10:08
  • Your suggestion permits the file tree to be displayed normally. Thank you. However, when I open the Internet Explorer folder where the two files in the database are located, the print statements do not trigger. I'll edit this question so that the data method attempts to set display settings. I'll comment the database too. Imagine that each file entry in the database has zero or more major comments (the first number) and zero or more minor comments (the second number). Each file (or folder) should be displayed differently depending on the number and type of comments. – davideps Oct 22 '17 at 10:21
  • In Windows the paths with spaces often cause problems, I would recommend printing the variable path and verifying that it has the same format – eyllanesc Oct 22 '17 at 11:21
  • 1
    In my case I have tried it in Linux with paths without spaces and if it is printed when I enter the paths placed in database – eyllanesc Oct 22 '17 at 11:22
  • Thank you for checking. I've tried all the combinations that usually work with file paths on Windows and nothing has worked yet. But, I believe you are correct. – davideps Oct 22 '17 at 13:38
  • @davideps. I think you may need to normalise the file-paths so you are always comparing the same string. I have added a demo that should deal with this issue (only tested on linux, though). – ekhumoro Oct 22 '17 at 13:53
  • @ eyllanesc, there were (at least) two problems with my code. The path should be written as `'C:/Program Files/Internet Explorer/ExtExport.exe'` and also it will only trigger if when it is within a test for files, not directories: `if not self.isDir(index):` – davideps Oct 22 '17 at 13:55

1 Answers1

1

Here is a basic demo that shows how to add an extra column with icons and other formatting. Note that an attempt is made to normalise the file-paths so that comparisons and dictionary look-ups should be more reliable:

import sys
from PySide.QtCore import *
from PySide.QtGui import *

database = {
    QFileInfo('C:\Program Files\Internet Explorer\ExtExport.exe').absoluteFilePath(): (1, 3),
    QFileInfo('C:\Program Files\Internet Explorer\iexplore.exe').absoluteFilePath(): (0, 0),
    }

class FileSystemModel(QFileSystemModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        style = qApp.style()
        self.icons = [
            style.standardIcon(QStyle.SP_MessageBoxInformation),
            style.standardIcon(QStyle.SP_MessageBoxWarning),
            ]

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

    def data(self, index, role=Qt.DisplayRole):
        extra = False
        if index.isValid():
            extra = index.column() == self.columnCount(index.parent()) - 1
            info = self.fileInfo(index)
            path = info.absoluteFilePath()
            if path in database:
                major, minor = database[path]
                print('found:', (major, minor), path)
                if extra:
                    if role == Qt.DecorationRole:
                        if major > 0:
                            return self.icons[0]
                        else:
                            return self.icons[1]
                    elif role == Qt.DisplayRole:
                        return '%s/%s' % (major, minor)
                    elif role == Qt.ForegroundRole:
                        if minor > 2:
                            return QColor('red')
        if not extra:
            return super().data(index, role)

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if (orientation == Qt.Horizontal and
            role == Qt.DisplayRole and
            section == self.columnCount() - 1):
            return 'Extra'
        return super().headerData(section, orientation, role)

class FileSystemBrowser(QTreeView):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.fileSystemModel = FileSystemModel()
        self.fileSystemModel.setRootPath(QDir.currentPath())
        self.setModel(self.fileSystemModel)
        self.header().moveSection(self.fileSystemModel.columnCount() - 1, 1)

if __name__ == '__main__':

    app = QApplication(sys.argv)
    window = FileSystemBrowser()
    window.show()
    sys.exit(app.exec_())

EDIT:

The roles used in the data method are all documented under the ItemDataRole enum, and are introduced as follows:

Each item in the model has a set of data elements associated with it, each with its own role. The roles are used by the view to indicate to the model which type of data it needs. Custom models should return data in these types.

For the extra column that has been added, it is necessary to supply everything, because it is a virtual column that is not part of the underlying model. But for the other columns, we can just call the base-class implementation to get the default values (of course, if desired, we could also return custom values for these columns to modify the existing behaviour).

ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • this works flawlessly. It will take me a day or more to study how you accomplished it. There are a lot of new (to me) methods. – davideps Oct 22 '17 at 14:01
  • @davideps. Glad you found it helpful. Feel free to ask if there's anything that puzzles you about the methods I used. – ekhumoro Oct 22 '17 at 14:03
  • The doc on `QFileInfo` is clear and it looks very useful. Thanks for introducing it. With `style = qApp.style()` you get a copy of the app's "style object" and then you set two icons to adapt to that style? Is the trailing comma here `SP_MessageBoxWarning),` just to allow the bracket to be on the next line? – davideps Oct 23 '17 at 06:42
  • From Python tutorials, I learned to always use placeholders with print and would have written `print('found:', (major, minor), path)` as `print('found: (%d,%d), %s' % (major, minor, path))`. I didn't realize your syntax was valid. – davideps Oct 23 '17 at 06:53
  • Your `if path in database:` is far more intuitive than my `if database.get(path) != None:`. I didn't realize `in` would target the keys. – davideps Oct 23 '17 at 06:57
  • I can't follow why you need to add one and then later subtract one from `columnCount`. Is this an issue of indexes starting at zero or one? – davideps Oct 23 '17 at 06:59
  • Unfortunately, from `if extra:` to `class FileSystemBrowser(QTreeView):` I'm at a complete loss. You test for different roles--but where are those roles set? Is a role a parameter of a data item, a phase of drawing to the screen, something else? Would it be possible for you to add a few comments? – davideps Oct 23 '17 at 07:14
  • (1) I used the built-in icons from the `style()` so that the example works without having to provide any icon files. You can of course use whatever icons you like. The default trailing comma is a common idiom in python: it makes code more easliy maintainable, because you can rearrange the items in a `list`, `dict`, etc, without ever having to worry about fixing up the commas afterwards. It's very easy to introduce subtle bugs without this. For example, in a list of strings, a missing comma can result in adjacent strings being concatenated. – ekhumoro Oct 23 '17 at 13:21
  • (2) For a debugging `print`, I'm usually too lazy to type out all that formatting stuff. (3) The `get` method has its uses, but `in` is faster and more readable. (4) Yes - the count will always be one greater than the highest index, because of zero-based counting. – ekhumoro Oct 23 '17 at 13:25
  • (5) I have added some further explanation to my answer. – ekhumoro Oct 23 '17 at 13:47
  • I had read over the ItemDataRole already and now did so again. There is still something I don't understand. In this line of your code `if role == Qt.DecorationRole:`, the role is set by the view as it draws data items to the screen? It continually makes requests from the model? The view says, "Hey! I'm working on the data item with index 6 right now, is there a Qt.DecorationRole for that item? How about a ToolTipRole? OK, now the data item with index 7..." – davideps Oct 23 '17 at 14:45
  • @davideps. You can think of the roles as the standardised keys of a mapping. This allows the view to request data from the model without knowing anything about its implementation (and *vice versa*). So the roles are part of the model/view interface specification - they are contractual guarantees that allow the components to negotiate with each other whilst remaining completely independent. – ekhumoro Oct 23 '17 at 16:09