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', '[email protected]', 'Not Copied', '/Opt'),
('option_B', '[email protected]', 'Not Copied', '/Users'),
]
app = QApplication([])
win = MyWindow(data_list, header)
win.show()
app.exec_()
Visual example :

I think your question can be divided in two parts:
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 ;
how can a QTreeView be added to a QTableView.
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
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)
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', '[email protected]', 'Not Copied', '/opt'),
                 ('option_B', '[email protected]', 'Not Copied', '/usr')]
    app = QtGui.QApplication([])
    win = MyTableView(data_list, header)
    win.setGeometry(300, 200, 570, 450)
    win.show()
    app.exec_()
Which results in:
 
    
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With