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.

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:
- Every node except the root needs a horizontal line leading to its parent.
- Each ancestor that has further siblings needs a vertical line.
- Stop the vertical line when the ancestor is the last child.
- 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.