Building a Searchable Autocomplete Box in Qt with Preview Dropdown

A search widget featuring real-time filtering and a preview dropdown can be implemented by combining a model/view architecture with custom proxy filtering. Below is a breakdown of the approach to create a control that accepts a data set, filters rows on the fly as the user types, and displays matching results in a popup list with hover highlighting.

Core Components

The implementation involves four custom classes:

  • SearchFilterProxy – derives from QSortFilterProxyModel and applies multi-column pattern matching.
  • ResultsTableView – a QTableView subclass that manages mouse tracking for row highlighting and selection.
  • SearchBoxWidget – the main composite widget containing a line edit and a popup for the result list.
  • ResultsItemDelegate – a custom delegate controlling how each row is painted.

Data Flow

A QStandardItemModel holds the raw source data. The proxy model is set up to use this model as its source and then assigned to the table view. When the user types, the proxy evaluates each row against the pattern across all columns. Any row where at least one column matches is passed through. The view updates automatically.

Setting Up the Filter Proxy

The proxy reimplemenst filterAcceptsRow. It iterates over every column of the given row and returns true if the regular expression matches any cell's display text. Sorting can be customized via lessThan if needed.

bool SearchFilterProxy::filterAcceptsRow(int sourceRow,
                                          const QModelIndex &sourceParent) const
{
    QRegExp pattern = filterRegExp();
    if (pattern.isEmpty())
        return true;

    bool accepted = false;
    int cols = sourceModel()->columnCount();
    for (int c = 0; c < cols; ++c) {
        QModelIndex idx = sourceModel()->index(sourceRow, c, sourceParent);
        QString cellText = sourceModel()->data(idx).toString();
        if (pattern.exactMatch(cellText)) {
            accepted = true;
            break;
        }
    }
    return accepted;
}

Managing the Popup

The preview table is placed inside a frameless, tool-tip-like widget. Its visibility is toggled by the line edit's textChanged signal and focus events. A key challenge is keeping the popup aligned when the main window moves. This is handled by installing a native event filter on the parent widget and moving the popup in moveEvent.

void SearchBoxWidget::repositionPopup()
{
    QWidget *popup = d->resultsPopup;
    QPoint anchor = d->headerWidget->mapToGlobal(
        QPoint(0, d->headerWidget->height()));
    popup->move(anchor);

    int availHeight = d->filterProxy->rowCount() * d->resultsView->rowHeight(0);
    const int maxPopupHeight = 300;
    popup->setFixedHeight(qMin(availHeight, maxPopupHeight));
    popup->show();
}

Row Highlighting

To highlight the hovered row, the table view tracks the mouse position. In mouseMoveEvent, the current row index is computed and compared with the previously highlighted row. If they differ, a helper method updates the background coler of all cells in the new row and resets the old one.

void ResultsTableView::applyRowHighlight(int targetRow)
{
    if (targetRow == m_activeRow)
        return;

    SearchFilterProxy *proxy = static_cast<SearchFilterProxy *>(model());
    if (targetRow >= proxy->rowCount())
        return;

    for (int c = 0; c < proxy->columnCount(); ++c) {
        QModelIndex viewIdx = proxy->index(targetRow, c);
        if (!viewIdx.isValid()) continue;

        QStandardItem *srcItem = m_sourceModel->itemFromIndex(
            proxy->mapToSource(viewIdx));
        if (srcItem) {
            srcItem->setBackground(QBrush(QColor(43, 92, 151)));
        }
    }

    if (m_activeRow != -1)
        clearRowHighlight();
    m_activeRow = targetRow;
}

Delegate and View Setup

The custom delegate is assigned to the result table to handle per-item painting. It can also draw additional elements, such as progress bars, by reading model data for a specific column.

The view and the proxy model are wired together as shown:

void SearchBoxWidget::Initialize()
{
    ResultsItemDelegate *delegate = new ResultsItemDelegate(d->resultsView);
    d->resultsView->setItemDelegate(delegate);
    delegate->setView(d->resultsView);  // for communication back to the view

    d->resultsPopup->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::Popup);

    d->filterProxy->setSourceModel(d->sourceModel);
    d->resultsView->setModel(d->filterProxy);
}

The result list appears beneath the text field, updates its height based on the number of filtered rows, and closes when the search box loses focus. By adding a mouse-press handler on the view, a row selection can populate the line edit and hide the popup to complete the search interaction.

Tags: Qt C++ UI QSortFilterProxyModel Custom Widget

Posted on Tue, 25 Aug 2026 16:29:15 +0000 by GravityFX