Custom Stock Table with Color-Coded Rows and Column-Specific Sorting

Core Requirements

  • Colorize row text based on numeric value direction (positive = green, negative = red)
  • Disable sorting on specific columns (e.g., symbol, name)
  • Render custom ascending/descending icons in headers
  • Ensure text and icons do not overlap in narrow columns
  • Suport per-column text alignment (left, center, right)
  • Handle percentage values with special parsing for sorting

Custom Header for Selective Sorting

To prevent sorting on designated columns, we subclass QHeaderView and override mouseReleaseEvent. Instead of disabling sorting globally, we intercept clicks on restricted columns before the sort logic triggers.


class StockHeaderView : public QHeaderView
{
    Q_OBJECT

public:
    explicit StockHeaderView(Qt::Orientation orientation, QWidget* parent = nullptr);

    void setSortEnabled(int column, bool enabled);

protected:
    void mouseReleaseEvent(QMouseEvent* event) override;

private:
    QMap<int, bool> m_sortEnabled;
};

void StockHeaderView::mouseReleaseEvent(QMouseEvent* event)
{
    int column = logicalIndexAt(event->pos().x());
    if (m_sortEnabled.contains(column) && !m_sortEnabled[column])
        return; // Block sorting on this column

    QHeaderView::mouseReleaseEvent(event);
}

Custom Header Painting with Icon Overlay

Overiding paintSection allows us to draw custom sort indicators without interfering with Qt’s default rendering. We first paint the base header, then overlay the arrow if the colum is sorted.


void StockHeaderView::paintSection(QPainter* painter, const QRect& rect, int logicalIndex) const
{
    // Fill background to ensure consistent color under text
    painter->fillRect(rect.adjusted(-1, 0, -1, -1), QColor("#212121"));

    QRect adjustedRect = rect;
    if (sortIndicatorSection() == logicalIndex)
    {
        // Shift text area left to avoid overlapping with icon
        adjustedRect.adjust(0, 0, -20, 0);

        // Draw sort arrow
        QRect iconRect = rect;
        iconRect.setLeft(rect.right() - 16);
        iconRect.setWidth(14);
        iconRect.setHeight(10);
        iconRect.moveTop((rect.height() - iconRect.height()) / 2);

        QPixmap icon = sortIndicatorOrder() == Qt::AscendingOrder
            ? QPixmap(":/icons/arrow_down")
            : QPixmap(":/icons/arrow_up");
        painter->drawPixmap(iconRect, icon);
    }

    // Delegate base header painting
    QHeaderView::paintSection(painter, adjustedRect, logicalIndex);

    // Draw subtle border
    painter->setPen(QColor("#2B2B2B"));
    painter->drawRect(rect.adjusted(-1, 0, -1, -1));
}

Text Alignment Control

Per-column alignment is managed via a custom model that overrides headerData() to return stored alignment flags. The same alignment is applied to both header and data cells.


QVariant StockModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (orientation == Qt::Horizontal && role == Qt::TextAlignmentRole)
    {
        auto it = m_columnAlignments.find(section);
        if (it != m_columnAlignments.end())
            return it.value();
        return Qt::AlignCenter;
    }

    return QStandardItemModel::headerData(section, orientation, role);
}

// Usage:
model->setColumnAlignment(0, Qt::AlignRight | Qt::AlignVCenter);
model->setColumnAlignment(1, Qt::AlignRight | Qt::AlignVCenter);
model->setColumnAlignment(3, Qt::AlignLeft | Qt::AlignVCenter);

Intelligent Sorting with Value Types

The filtering model distinguishes between numeric types: integers, percentages, and strings. Percentage values are parsed by stripping the '%' symbol and converting to double for comparison.


bool StockFilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
{
    switch (m_sortType)
    {
    case SortType::Integer:
    {
        double l = left.data().toDouble();
        double r = right.data().toDouble();
        return l < r;
    }

    case SortType::Percentage:
    {
        QString lStr = left.data(Qt::UserRole + 2).toString();
        QString rStr = right.data(Qt::UserRole + 2).toString();
        double l = lStr.remove('%').toDouble();
        double r = rStr.remove('%').toDouble();
        return l < r;
    }

    default:
        return left.data().toString() < right.data().toString();
    }
}

Dynamic Text Coloring in Cells

Row text color is determined at the model level using the Qt::ForegroundRole. Positive values trigger green, negative values trigger red.


QVariant StockModel::data(const QModelIndex& index, int role) const
{
    if (role == Qt::ForegroundRole)
    {
        QVariant value = QStandardItemModel::data(index, Qt::DisplayRole);
        if (value.type() == QVariant::Double || value.type() == QVariant::Int)
        {
            double num = value.toDouble();
            return num >= 0 ? QColor("#4CAF50") : QColor("#F44336");
        }
    }

    return QStandardItemModel::data(index, role);
}

Styling and Consistency

Application-wide styling ensures visual cohesion:


tableView->setStyleSheet(
    "QTableView { background-color: #333333; gridline-color: #444; }"
    "QHeaderView { background-color: #212121; color: #cccccc; }"
    "QHeaderView::section { padding: 6px; border: none; }"
);

By decoupling sorting logic from the default header behavior, overriding painting for precise visual control, and leveraging role-based data styling, we achieve a professional, responsive financial table that behaves like a native trading terminal.

Tags: Qt QTableView QHeaderView QStandardItemModel custom-table

Posted on Thu, 14 May 2026 13:59:51 +0000 by flashpipe