Custom-Drawing Branch Lines in a Qt Tree-Table View

When a product owner demanded a tree widget whose row heights could be resized at run-time, the usual style-sheet tricks quickly fell apart: the branch icons became blurry and the dotted connector lines vanished. The only viable escape hatch was to take full control of the painting pipeline.

Visual Result

The screenshot below shows the final look: every node still has the native highlight, yet the connecting lines are crisp at any row height and the expand / collapse indicators remain pixel-perfect.

Tree with custom branch lines

Implementation Walk-through

1. Overridable Hooks in QTreeView

The framework already exposes the necessary virtuals:

Virtual method Responsibility
drawBranches() Paints the expand/collapse indicator and the connector lines.
drawRow() Paints a single row (background, selection, text, …).
drawTree() Paints the entire viewport (rarely overridden).
indexRowSizeHint() Supplies the default height for a row.
rowHeight() Returns the exact height of a row.

We only need the first two; rest remain untouched so the default delegate still works.

2. Adding Grid Lines

Since QTreeView does not draw vertical borders, we inject them in drawRow():

void TreeTable::drawRow(QPainter *p,
                       const QStyleOptionViewItem &opt,
                       const QModelIndex &idx) const
{
    QTreeView::drawRow(p, opt, idx);          // keep native look

    p->save();
    p->setPen(QPen(m_gridColor, m_lineWidth));
    p->drawRect(opt.rect);                    // simple cell border
    p->restore();
}

3. Painting the Branch Lines

All magic happens in drawBranches(). The rules are:

  1. Every node except the root needs a horizontal line leading to its parent.
  2. Each ancestor that has further siblings needs a vertical line.
  3. Stop the vertical line when the ancestor is the last child.
  4. Repeat rule 3 recursively for deeper levels.
void TreeTable::drawBranches(QPainter *p,
                             const QRect &area,
                             const QModelIndex &idx) const
{
    const Node *node   = static_cast<Node*>(idx.internalPointer());
    const int  level   = node->level();
    const int  indent  = indentation();
    const bool open    = isExpanded(idx);
    const bool hasKids = node->childCount() > 0;
    const bool hasNext = node->hasNextSibling();

    p->save();
    p->setPen(m_branchPen);

    // horizontal connector
    int x = area.left() + indent * (level - 1) + indent / 2;
    int y = area.center().y();
    p->drawLine(x, y, x + indent / 2, y);

    // vertical connectors for all ancestor levels
    const Node *cur = node;
    for (int l = level - 1; l >= 0; --l) {
        x -= indent;
        if (cur->parent() && cur->parent()->hasNextSibling())
            p->drawLine(x, area.top(), x, area.bottom());
        cur = cur->parent();
    }

    // expand / collapse icon
    if (hasKids) {
        QPixmap pm = open ? m_pixOpen : m_pixClose;
        QRect iconRect(0, 0, pm.width(), pm.height());
        iconRect.moveCenter(area.center());
        p->drawPixmap(iconRect, pm);
    }

    p->restore();
}

4. Synchronising the Row Header

The widget is actually a QTreeView on the left and a QTableView on the right sharing the same model. When a branch is collapsed or expanded we must hide or show the corresponding rows in the vertical header.

void TreeTable::onExpanded(const QModelIndex &idx)
{
    applyRowVisibility(idx, /*visible=*/true);
    m_headerView->updateGeometry();
}

void TreeTable::onCollapsed(const QModelIndex &idx)
{
    applyRowVisibility(idx, /*visible=*/false);
    m_headerView->updateGeometry();
}

void TreeTable::applyRowVisibility(const QModelIndex &idx, bool visible)
{
    const Node *n = static_cast<Node*>(idx.internalPointer());
    for (Node *child : n->children()) {
        int row = child->row();
        m_headerView->setRowHidden(row, !visible);
        if (!visible || isExpanded(child->index()))
            applyRowVisibility(child->index(), visible);
    }
}

Calling updateGeometry() afterwards ensures the header repaints immediately.

Take-away

By overriding only two virtual methods we obtained a tree-table that supports user-resizable row heights without sacrificing the classic dotted branch lines or crisp icons. The same technique can be extended to draw custom backgrounds, animated open/close indicators, or per-level colours.

Tags: Qt QTreeView custom painting branch lines row height

Posted on Sat, 18 Jul 2026 17:06:21 +0000 by sava