4

I am fairly new to PyQt, I'm working on a project that contains a QTableView, with one of its columns displaying system paths. I would like to add a QTreeView so users can click the + or > buttons to expand what is underneath the paths.

Here is my basic implementation:

from PyQt4 import QtGui
from PyQt4 import QtCore

class MainWindow(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        self.resize(600,400)
        self.setWindowTitle("My Basic Treeview")

        self.treeview = QtGui.QTreeView(self)

        self.treeview.model = QtGui.QFileSystemModel()
        self.treeview.model.setRootPath('/opt')
        self.treeview.setModel(self.treeview.model)
        self.treeview.setColumnWidth(0, 200)

        self.setCentralWidget(self.treeview)

if __name__ == '__main__':
    import sys
    app = QtGui.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

Although, in the above case, I get all folders but I just want the /opt path and its underneath folders.

import operator
from PyQt4.QtCore import *
from PyQt4.QtGui import *

class MyWindow(QWidget):
    def __init__(self, data_list, header, *args):
        QWidget.__init__(self, *args)
        # setGeometry(x_pos, y_pos, width, height)
        self.setGeometry(300, 200, 570, 450)
        self.setWindowTitle("Click on column title to sort")
        table_model = MyTableModel(self, data_list, header)
        table_view = QTableView()
        table_view.setModel(table_model)
        # set font
        font = QFont("Courier New", 14)
        table_view.setFont(font)
        # set column width to fit contents (set font first!)
        table_view.resizeColumnsToContents()
        # enable sorting
        table_view.setSortingEnabled(True)
        layout = QVBoxLayout(self)
        layout.addWidget(table_view)
        self.setLayout(layout)

class MyTableModel(QAbstractTableModel):
    def __init__(self, parent, mylist, header, *args):
        QAbstractTableModel.__init__(self, parent, *args)
        self.mylist = mylist
        self.header = header

    def rowCount(self, parent):
        return len(self.mylist)

    def columnCount(self, parent):
        return len(self.mylist[0])

    def data(self, index, role):
        if not index.isValid():
            return None
        elif role != Qt.DisplayRole:
            return None
        return self.mylist[index.row()][index.column()]

    def headerData(self, col, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self.header[col]
        return None

# the solvent data ...
header = ['Name', ' Email', ' Status', ' Path']
# use numbers for numeric data to sort properly
data_list = [
('option_A', 'zyro@email.com', 'Not Copied', '/Opt'),
('option_B', 'zyro@email.com', 'Not Copied', '/Users'),
]
app = QApplication([])
win = MyWindow(data_list, header)
win.show()
app.exec_()

Visual example :

enter image description here

Jean-Sébastien
  • 2,649
  • 1
  • 16
  • 21
Ciasto piekarz
  • 7,853
  • 18
  • 101
  • 197

1 Answers1

7

I think your question can be divided in two parts:

  1. how, in a QTreeView, the /opt path and its children can be shown, but without showing its siblings. In other words, how is it possible to show the root directory in a QTreeView ;

  2. how can a QTreeView be added to a QTableView.

1. How to include the root directory in a QTreeView :

The root of a QTreeView is the directory for which the content is shown in the view. It is set when calling the method setRootIndex. According to a post by wysota on Qt Centre:

You can't display the invisibleRootItem because it is a fake item used only to have an equivalent of empty QModelIndex.

A workaround would be to set the root directory to the parent of /opt and filtering out the siblings of /opt with a subclass of a QSortFilterProxyModel. Note that I've also reimplemented the sizeHint method which will be necessary for the resizing of the rows of the QTableView:

from PyQt4 import QtGui, QtCore
import os

class MyQTreeView(QtGui.QTreeView):

    def __init__(self, path, parent=None):
        super(MyQTreeView, self).__init__(parent)

        ppath = os.path.dirname(path) # parent of path
        self.setFrameStyle(0)

        #---- File System Model ----

        sourceModel = QtGui.QFileSystemModel()
        sourceModel.setRootPath(ppath)

        #---- Filter Proxy Model ----

        proxyModel = MyQSortFilterProxyModel(path)
        proxyModel.setSourceModel(sourceModel)

        #---- Filter Proxy Model ----

        self.setModel(proxyModel)
        self.setHeaderHidden(True)
        self.setRootIndex(proxyModel.mapFromSource(sourceModel.index(ppath)))  

        #--- Hide All Header Sections Except First ----

        header = self.header()
        for sec in range(1, header.count()):
            header.setSectionHidden(sec, True)

    def sizeHint(self):
        baseSize = super(MyQTreeView,self).sizeHint()

        #---- get model index of "path" ----
        qindx = self.rootIndex().child(0, 0)

        if self.isExpanded(qindx): # default baseSize height will be used
            pass

        else:  # shrink baseShize height to the height of the row           
            baseSize.setHeight(self.rowHeight(qindx))

        return baseSize


class MyQSortFilterProxyModel(QtGui.QSortFilterProxyModel):    
    def __init__(self, path, parent=None):
        super(MyQSortFilterProxyModel, self).__init__(parent)

        self.path = path

    def filterAcceptsRow(self, row, parent):

        model = self.sourceModel()
        path_dta = model.index(self.path).data()
        ppath_dta = model.index(os.path.dirname(self.path)).data()

        if parent.data() == ppath_dta:
            if parent.child(row, 0).data() == path_dta:                
                return True
            else:
                return False            
        else:
            return True

2. How to add a *QTreeView* to a *QTableView* :

It is possible to add a QTreeView to a QTableView by using a QItemDelegate. The post by Pavel Strakhov greatly helped me for this, since I had never used QTableView in combination with delegates before answering to this question. I always used QTableWidget instead with the setCellWidget method.

Note that I've setup a signal in the MyDelegate class which call the method resizeRowsToContents in the MyTableView class. This way, the height of the rows resize according the the reimplementation of the sizeHint method of the MyQTreeView class.

class MyTableModel(QtCore.QAbstractTableModel):
    def __init__(self, parent, mylist, header, *args):
        super(MyTableModel, self).__init__(parent, *args)
        self.mylist = mylist
        self.header = header

    def rowCount(self, parent=QtCore.QModelIndex()):
        return len(self.mylist)

    def columnCount(self, parent=QtCore.QModelIndex()):
        return len(self.mylist[0])

    def data(self, index, role):
        if not index.isValid():
            return None
        elif role != QtCore.Qt.DisplayRole:
            return None
        return self.mylist[index.row()][index.column()]

    def headerData(self, col, orientation, role):
        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
            return self.header[col]
        return None

class MyDelegate(QtGui.QItemDelegate):

    treeViewHeightChanged = QtCore.pyqtSignal(QtGui.QWidget)

    def createEditor(self, parent, option, index):

        editor = MyQTreeView(index.data(), parent)
        editor.collapsed.connect(self.sizeChanged)
        editor.expanded.connect(self.sizeChanged)

        return editor

    def sizeChanged(self):
        self.treeViewHeightChanged.emit(self.sender())

class MyTableView(QtGui.QTableView):
    def __init__(self, data_list, header, *args):
        super(MyTableView, self).__init__(*args)

        #---- set up model ----

        model = MyTableModel(self, data_list, header)
        self.setModel(model)

        #---- set up delegate in last column ----

        delegate = MyDelegate()

        self.setItemDelegateForColumn(3, delegate)
        for row in range(model.rowCount()):
            self.openPersistentEditor(model.index(row, 3))

        #---- set up font and resize calls ----

        self.setFont(QtGui.QFont("Courier New", 14))
        self.resizeColumnsToContents()
        delegate.treeViewHeightChanged.connect(self.resizeRowsToContents)

3. Basic application :

Here is a basic application based on the code you provided in your OP:

if __name__ == '__main__':

    header = ['Name', ' Email', ' Status', ' Path']
    data_list = [('option_A', 'zyro@email.com', 'Not Copied', '/opt'),
                 ('option_B', 'zyro@email.com', 'Not Copied', '/usr')]

    app = QtGui.QApplication([])
    win = MyTableView(data_list, header)
    win.setGeometry(300, 200, 570, 450)
    win.show()
    app.exec_()

Which results in:

enter image description here

Community
  • 1
  • 1
Jean-Sébastien
  • 2,649
  • 1
  • 16
  • 21
  • Thank you Jean for your efforts are much appreciated, i hope this post is also useful for other learners . – Ciasto piekarz Aug 21 '15 at 17:29
  • @Ciastopiekarz You are quite welcomed. I've learned a lot in the process also, so its a win-win scenario. I'm glad if it can help others as well. – Jean-Sébastien Aug 22 '15 at 13:15