QTreeView Widget in PyQt6

As a Python developer who has worked extensively with GUI applications, I have found the QTreeView widget in PyQt6 to be very useful for displaying hierarchical data. Whether you’re building an application that needs to show file systems, organizational charts, or any nested data structure, QTreeView offers a flexible solution.

In this article, I will walk you through everything you need to know about using QTreeView in PyQt6. Having implemented this widget in numerous projects over the years, I can assure you that mastering QTreeView will significantly enhance your PyQt applications.

Let’s get into the QTreeView widget and explore more..

Set Up a Basic QTreeView

The first step in working with QTreeView is understanding how to set it up with a data model. Unlike simpler widgets, QTreeView follows the Model-View-Controller pattern.

Here’s how to create a basic QTreeView:

import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QTreeView
from PyQt6.QtGui import QStandardItemModel, QStandardItem

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Basic QTreeView Example")
        self.setGeometry(100, 100, 800, 600)

        # Create the tree view
        self.tree_view = QTreeView(self)
        self.setCentralWidget(self.tree_view)

        # Create the model
        self.model = QStandardItemModel()
        self.model.setHorizontalHeaderLabels(['Name', 'Value'])

        # Add some data
        root_item = QStandardItem("States")

        # Add states as children
        california = QStandardItem("California")
        california_pop = QStandardItem("39.5M")
        root_item.appendRow([california, california_pop])

        texas = QStandardItem("Texas")
        texas_pop = QStandardItem("29.0M")
        root_item.appendRow([texas, texas_pop])

        # Add cities to California
        ca_cities = [("Los Angeles", "3.9M"), ("San Francisco", "0.8M")]
        for city, pop in ca_cities:
            city_item = QStandardItem(city)
            pop_item = QStandardItem(pop)
            california.appendRow([city_item, pop_item])

        # Add cities to Texas
        tx_cities = [("Houston", "2.3M"), ("Austin", "0.9M")]
        for city, pop in tx_cities:
            city_item = QStandardItem(city)
            pop_item = QStandardItem(pop)
            texas.appendRow([city_item, pop_item])

        # Add the root item to the model
        self.model.appendRow(root_item)

        # Set the model for the tree view
        self.tree_view.setModel(self.model)
        self.tree_view.expandAll()  # Expand all nodes

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

This example creates a simple hierarchical display of US states and their major cities, along with population data.

Work with Model/View Architecture

Understanding the Model/View architecture is important for effectively using QTreeView.

Use QStandardItemModel

Python QStandardItemModel is the simplest model to use with QTreeView. It’s perfect for static data that you define in your code:

# Creating a model with custom headers
model = QStandardItemModel()
model.setHorizontalHeaderLabels(['Department', 'Employees', 'Budget'])

# Adding data
engineering = QStandardItem("Engineering")
eng_employees = QStandardItem("120")
eng_budget = QStandardItem("$8.5M")
model.appendRow([engineering, eng_employees, eng_budget])

# Adding child items
frontend = QStandardItem("Frontend")
frontend_employees = QStandardItem("45")
frontend_budget = QStandardItem("$3.2M")
engineering.appendRow([frontend, frontend_employees, frontend_budget])

I executed the above example code and added the screenshot below.

PyQt6 QTreeView

Use Custom Models

For more complex scenarios, you might need to create a custom model by subclassing QAbstractItemModel:

from PyQt6.QtCore import QAbstractItemModel, QModelIndex, Qt

class FileSystemModel(QAbstractItemModel):
    def __init__(self, root_path):
        super().__init__()
        self.root_path = root_path
        # Initialize your data structure here

    def index(self, row, column, parent=QModelIndex()):
        # Implement to create indexes for your data
        pass

    def parent(self, index):
        # Return the parent of the model item with the given index
        pass

    def rowCount(self, parent=QModelIndex()):
        # Return the number of rows under the given parent
        pass

    def columnCount(self, parent=QModelIndex()):
        # Return the number of columns for the children of the given parent
        pass

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        # Return the data stored under the given role for the item referred to by the index
        pass

I executed the above example code and added the screenshot below.

QTreeView Widget in PyQt6

Customize QTreeView Appearance

One of QTreeView’s strengths is how customizable it is. Here are some ways to enhance its appearance:

Read Create Alert Dialogs with QMessageBox in PyQt6

Method 1: Style with Stylesheets

You can enhance the visual appearance of a QTreeView in PyQt6 by using stylesheets and enabling row color alternation.

# Apply stylesheet to the tree view
self.tree_view.setStyleSheet("""
    QTreeView {
        background-color: #f5f5f5;
        alternate-background-color: #e9e9e9;
        border: 1px solid #d3d3d3;
    }
    QTreeView::item {
        padding: 5px;
        border-bottom: 1px solid #d3d3d3;
    }
    QTreeView::item:selected {
        background-color: #0078d7;
        color: white;
    }
""")

# Enable alternating row colors
self.tree_view.setAlternatingRowColors(True)

I executed the above example code and added the screenshot below.

Mastering QTreeView in PyQt6

Styling with stylesheets gives your QTreeView a polished, modern look while improving readability.

Check out QFontDialog in PyQt6: Create and Customize Font

Method 2: Custom Delegates

For advanced item-level customization in a QTreeView, you can implement a custom delegate using QStyledItemDelegate.

from PyQt6.QtWidgets import QStyledItemDelegate
from PyQt6.QtGui import QColor, QPainter, QFont

class CustomDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        # Customize how items are painted
        if index.column() == 1 and index.data():  # Population column
            painter.save()

            # Fill background
            if option.state & QStyleOptionViewItem.State_Selected:
                painter.fillRect(option.rect, option.palette.highlight())

            # Set text color
            if int(index.data().replace('M', '')) > 1.0:
                painter.setPen(QColor(0, 128, 0))  # Green for large cities
            else:
                painter.setPen(QColor(0, 0, 0))  # Black for smaller cities

            # Draw text
            painter.drawText(option.rect, Qt.AlignmentFlag.AlignVCenter, index.data())

            painter.restore()
        else:
            super().paint(painter, option, index)

# Apply the delegate
self.tree_view.setItemDelegate(CustomDelegate())

Custom delegates offer full control over how items are rendered, allowing dynamic styling based on data values.

Handle User Interactions

Responding to user interactions makes your QTreeView more interactive and useful.

Method 1: Connect to Signals

You can make your QTreeView interactive by connecting to key signals like selection changes and item expansion/collapse events.

# Connect to selection changed signal
self.tree_view.selectionModel().selectionChanged.connect(self.on_selection_changed)

# Connect to item expanded/collapsed signals
self.tree_view.expanded.connect(self.on_item_expanded)
self.tree_view.collapsed.connect(self.on_item_collapsed)

def on_selection_changed(self, selected, deselected):
    indexes = selected.indexes()
    if indexes:
        item = self.model.itemFromIndex(indexes[0])
        print(f"Selected: {item.text()}")

def on_item_expanded(self, index):
    item = self.model.itemFromIndex(index)
    print(f"Expanded: {item.text()}")

def on_item_collapsed(self, index):
    item = self.model.itemFromIndex(index)
    print(f"Collapsed: {item.text()}")

Method 2: Implement Context Menus

Add right-click context menus to your QTreeView for user-friendly item-specific actions like view, edit, or delete.

from PyQt6.QtWidgets import QMenu

# Enable context menu
self.tree_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.tree_view.customContextMenuRequested.connect(self.show_context_menu)

def show_context_menu(self, position):
    index = self.tree_view.indexAt(position)

    if not index.isValid():
        return

    item = self.model.itemFromIndex(index)

    menu = QMenu()
    view_action = menu.addAction("View Details")
    edit_action = menu.addAction("Edit")
    delete_action = menu.addAction("Delete")

    action = menu.exec(self.tree_view.viewport().mapToGlobal(position))

    if action == view_action:
        print(f"View details for {item.text()}")
    elif action == edit_action:
        print(f"Edit {item.text()}")
    elif action == delete_action:
        print(f"Delete {item.text()}")

Read Work with QColorDialog in PyQt6

Practical Example: US County Data Viewer

This practical example showcases how to use QTreeView and QStandardItemModel to build an interactive US County Data Viewer with hierarchical data, styling, and selection feedback.

import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow, QTreeView, QVBoxLayout, 
                           QWidget, QLabel, QPushButton)
from PyQt6.QtGui import QStandardItemModel, QStandardItem
from PyQt6.QtCore import Qt

class CountyDataViewer(QMainWindow):
    def __init__(self):
        super().__init__()
        
        self.setWindowTitle("US County Data Viewer")
        self.setGeometry(100, 100, 900, 700)
        
        # Create main widget and layout
        main_widget = QWidget()
        layout = QVBoxLayout(main_widget)
        
        # Add header label
        header = QLabel("US States and Counties")
        header.setStyleSheet("font-size: 18pt; font-weight: bold;")
        layout.addWidget(header)
        
        # Create the tree view
        self.tree_view = QTreeView()
        layout.addWidget(self.tree_view)
        
        # Set up the model
        self.model = QStandardItemModel()
        self.model.setHorizontalHeaderLabels(['State/County', 'Population', 'Area (sq mi)'])
        
        # Add sample data
        self.populate_model()
        
        # Set the model and customize the tree view
        self.tree_view.setModel(self.model)
        self.tree_view.setAlternatingRowColors(True)
        self.tree_view.setAnimated(True)
        self.tree_view.setSortingEnabled(True)
        self.tree_view.setColumnWidth(0, 300)
        
        # Add button for expanded/collapsed all
        btn_layout = QVBoxLayout()
        expand_btn = QPushButton("Expand All")
        expand_btn.clicked.connect(self.tree_view.expandAll)
        collapse_btn = QPushButton("Collapse All")
        collapse_btn.clicked.connect(self.tree_view.collapseAll)
        btn_layout.addWidget(expand_btn)
        btn_layout.addWidget(collapse_btn)
        layout.addLayout(btn_layout)
        
        # Connect to selection changed signal
        self.tree_view.selectionModel().selectionChanged.connect(self.on_selection_changed)
        
        # Set up info display
        self.info_label = QLabel("Select a state or county to view details")
        layout.addWidget(self.info_label)
        
        self.setCentralWidget(main_widget)
    
    def populate_model(self):
        # California data
        ca = QStandardItem("California")
        ca_pop = QStandardItem("39.5M")
        ca_area = QStandardItem("163,696")
        self.model.appendRow([ca, ca_pop, ca_area])
        
        # California counties
        counties = [
            ("Los Angeles County", "10.0M", "4,751"),
            ("San Diego County", "3.3M", "4,526"),
            ("Orange County", "3.2M", "948"),
            ("Riverside County", "2.4M", "7,303"),
            ("San Bernardino County", "2.2M", "20,105")
        ]
        
        for name, pop, area in counties:
            county = QStandardItem(name)
            county_pop = QStandardItem(pop)
            county_area = QStandardItem(area)
            ca.appendRow([county, county_pop, county_area])
        
        # Texas data
        tx = QStandardItem("Texas")
        tx_pop = QStandardItem("29.0M")
        tx_area = QStandardItem("268,596")
        self.model.appendRow([tx, tx_pop, tx_area])
        
        # Texas counties
        counties = [
            ("Harris County", "4.7M", "1,778"),
            ("Dallas County", "2.6M", "909"),
            ("Tarrant County", "2.1M", "902"),
            ("Bexar County", "2.0M", "1,256"),
            ("Travis County", "1.3M", "1,023")
        ]
        
        for name, pop, area in counties:
            county = QStandardItem(name)
            county_pop = QStandardItem(pop)
            county_area = QStandardItem(area)
            tx.appendRow([county, county_pop, county_area])
        
        # New York data
        ny = QStandardItem("New York")
        ny_pop = QStandardItem("19.8M")
        ny_area = QStandardItem("54,555")
        self.model.appendRow([ny, ny_pop, ny_area])
        
        # New York counties
        counties = [
            ("Kings County (Brooklyn)", "2.6M", "71"),
            ("Queens County", "2.3M", "109"),
            ("New York County (Manhattan)", "1.6M", "23"),
            ("Suffolk County", "1.5M", "912"),
            ("Bronx County", "1.4M", "42")
        ]
        
        for name, pop, area in counties:
            county = QStandardItem(name)
            county_pop = QStandardItem(pop)
            county_area = QStandardItem(area)
            ny.appendRow([county, county_pop, county_area])
    
    def on_selection_changed(self, selected, deselected):
        indexes = selected.indexes()
        if indexes:
            # Get the first column's index
            index = indexes[0]
            row = index.row()
            
            # Get the parent index
            parent = index.parent()
            
            # Get data from all columns
            name = self.model.data(self.model.index(row, 0, parent))
            population = self.model.data(self.model.index(row, 1, parent))
            area = self.model.data(self.model.index(row, 2, parent))
            
            # Update info display
            if parent.isValid():  # County
                state_name = self.model.data(parent)
                info_text = f"<b>{name}</b> is a county in {state_name}<br>"
                info_text += f"Population: {population}<br>"
                info_text += f"Area: {area} square miles<br>"
                
                # Calculate population density
                pop_num = float(population.replace('M', ''))
                if 'M' in population:
                    pop_num *= 1000000
                area_num = float(area.replace(',', ''))
                density = pop_num / area_num
                
                info_text += f"Population Density: {density:.1f} people per square mile"
            else:  # State
                info_text = f"<b>{name}</b><br>"
                info_text += f"Population: {population}<br>"
                info_text += f"Area: {area} square miles<br>"
                info_text += f"Click on counties to see more details"
            
            self.info_label.setText(info_text)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyle("Fusion")  # For a consistent look across platforms
    window = CountyDataViewer()
    window.show()
    sys.exit(app.exec())

It effectively demonstrates how to organize, display, and interact with complex datasets in a user-friendly PyQt6 interface.

Best Practices for QTreeView

After years of working with QTreeView, I’ve found these practices to be crucial for creating efficient and user-friendly tree views:

  1. Lazy Loading: For large datasets, implement lazy loading to only load child items when a parent is expanded.
  2. Visual Indicators: Use icons or styling to help users understand the data hierarchy.
  3. Responsive UI: Keep the UI responsive by performing heavy data operations in separate threads.
  4. Memory Management: Be mindful of memory usage, especially when working with large datasets.
  5. User Preferences: Save and restore expanded states and selections to improve the user experience.

QTreeView is an incredibly versatile widget that can handle complex hierarchical data structures with ease.

By mastering its features and following best practices, you can create intuitive and useful interfaces that help users navigate even the most complex data sets.

You may read:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.