Let's talk about displaying large datasets in wxWidgets. If you've ever tried adding tens of thousands of items to a wxListCtrl, you know it can get painfully slow. The control creates an internal object for every single row, regardless of whether it's visible on screen.

There's a better way: virtual mode. This is a companion post to my List Controls video tutorial, which also covers sorting and selection handling in detail:

The Regular Approach

Let's start with the standard way to populate a wxListCtrl. We loop through our data and insert each item one by one:

for (int i = 0; i < items.size(); i++)
{
    plainListView->InsertItem(i, items[i].date);
    plainListView->SetItem(i, 1, wxString::Format("%.2f", items[i].low));
    plainListView->SetItem(i, 2, wxString::Format("%.2f", items[i].high));
    // ... more columns
}

This works fine for a few hundred rows. But each InsertItem call creates an internal object, and the control keeps all of them in memory. With large datasets, the population loop itself becomes noticeable (we're talking several seconds for 100K rows) and the memory overhead adds up.

Switching to Virtual Mode

The idea behind virtual mode is simple: the list control doesn't store any data itself. Instead, it calls our OnGetItemText method whenever it needs to display a cell. We tell it how many rows exist, and it only ever asks for the ones currently visible on screen.

Let's build a minimal virtual list control. First, we need a simple data structure:

struct ItemData
{
    int id;
    std::string name;
    std::string description;
};

Now, our virtual list control class. We inherit from wxListCtrl and pass the wxLC_VIRTUAL flag in the constructor. This is what tells the control not to store data internally:

class VirtualListControl : public wxListCtrl
{
public:
    VirtualListControl(wxWindow *parent)
        : wxListCtrl(parent, wxID_ANY, wxDefaultPosition,
                     wxDefaultSize, wxLC_REPORT | wxLC_VIRTUAL)
    {
        AppendColumn("ID", wxLIST_FORMAT_LEFT, 80);
        AppendColumn("Name", wxLIST_FORMAT_LEFT, 120);
        AppendColumn("Description", wxLIST_FORMAT_LEFT, 400);
    }

The key part is the OnGetItemText override. The control calls this method whenever it needs the text for a given cell. We simply look up the data in our vector using the index and return the appropriate field based on the column:

    wxString OnGetItemText(long index, long column) const override
    {
        const auto &item = items[index];

        switch (column)
        {
        case 0: return wxString::Format("%d", item.id);
        case 1: return item.name;
        case 2: return item.description;
        default: return "";
        }
    }

We also need a helper method to update the control after changing the data. SetItemCount tells the control how many rows exist, and Refresh triggers a repaint so it calls OnGetItemText for the visible rows:

    void RefreshAfterUpdate()
    {
        SetItemCount(items.size());
        Refresh();
    }

    std::vector<ItemData> items;
};

Using Our Virtual List

Populating the virtual list is refreshingly simple compared to the regular approach. We just assign our data and call RefreshAfterUpdate:

virtualList->items = myData;
virtualList->RefreshAfterUpdate();

No insertion loop, no SetItem calls. With 100K rows, the difference is dramatic – the virtual list populates almost instantly, while the regular list can take several seconds. If you want to see this comparison in action, check out the video tutorial where we measure the time with wxStopWatch.

When to Use Virtual Mode

Virtual mode is a great fit when our dataset has more than a few hundred rows, or when the data already lives in our own data structure (a vector, a database result set, etc.) and we don't want to duplicate everything into the control's internal storage.

The trade-off is that sorting and selection tracking require a bit more work on our side, since the control doesn't own the data. The video tutorial covers both of these, including how to maintain the user's selection after re-sorting (which can get tricky).

You can find the complete source code on GitHub. The video description has commit hashes for each stage of the tutorial, so you can check out the code at any point.