Displaying the dictionary, part 2: Using text callbacks

As we noted last time, adding items to the listview takes an absurd amount of time. Today, we'll make a failed attempt at improving this because it lets me illustrate a listview technique and it lays the groundwork for the real fix next time.

Instead of creating the items in their entirety, let's set their text to LPSTR_TEXTCALLBACK. This is a placeholder value which indicates "I'm not going to tell you what the string is. If you need it, call me back."

class RootWindow : public Window
 LRESULT OnCreate();
 LRESULT OnNotify(NMHDR* pnm);
 void OnGetDispInfo(NMLVDISPINFO* pnmv);

LRESULT RootWindow::OnCreate()
 // item.pszText = const_cast<LPWSTR>(de.m_pszTrad);
   // item.pszText = const_cast<LPWSTR>(de.m_pszPinyin);
   item.pszText = LPSTR_TEXTCALLBACK;
   // item.pszText = const_cast<LPWSTR>(de.m_pszEnglish);
   item.pszText = LPSTR_TEXTCALLBACK;

LRESULT RootWindow::OnNotify(NMHDR *pnm)
 switch (pnm->code) {
 return 0;

void RootWindow::OnGetDispInfo(NMLVDISPINFO* pnmv)
 if (pnmv->item.iItem < 0 || // typo fixed 11am
     pnmv->item.iItem >= Length()) {
  return;         // requesting invalid item

 if (pnmv->item.mask & LVIF_TEXT) {
  const DictionaryEntry& de = Item(pnmv->item.iItem);
  LPCWSTR pszResult = L"";
  switch (pnmv->item.iSubItem) {
   case COL_TRAD:    pszResult = de.m_pszTrad;    break;
   case COL_PINYIN:  pszResult = de.m_pszPinyin;  break;
   case COL_ENGLISH: pszResult = de.m_pszEnglish; break;
  pnmv->item.pszText = const_cast<LPWSTR>(pszResult);

 if (pnmv->item.mask & LVIF_IMAGE) {
  pnmv->item.iImage = -1;

 if (pnmv->item.mask & LVIF_STATE) {
     pnmv->item.state = 0;


LRESULT RootWindow::HandleMessage(
                          UINT uMsg, WPARAM wParam, LPARAM lParam)
  case WM_NOTIFY:
   return OnNotify(reinterpret_cast<NMHDR*>(lParam));

Instead of setting the strings when we create the listview items, we set their texts to LPSTR_TEXTCALLBACK. When the listview needs the text, it sends us a LVN_GETDISPINFO notification, which we handle by returning the data that the listview requested.

Sidebar: In our case, obtaining the missing data is very fast. If it were slow, we could have optimized the function further by adding the line

 pnmv->item.mask |= LVIF_DI_SETITEM;

to the end. This tells the listview, "Please cache these results and don't ask me for them again." That way, we do the slow computation only once.

After making these changes (though not the LVIF_DI_SETITEM change; that was just a sidebar), notice that the it didn't really help much. On my machine, the startup time dropped from eleven to ten seconds, but ten seconds is still way too long. This optimization turns out to have been a washout.

(Note also that our program is now relying heavily on the fact that a vector is a fast random-access data structure.)

We'll do better next time.

Comments (12)
  1. gnobal says:

    A little typo in:

    "if (pnmv->item.iItem <; 0 ||"

  2. macbirdie says:

    I’m pretty new to Win32 programming (a little bit late but what the heck) and I’m surprised with amounts of possibilities of making Windows GUI software blazingly fast with a little bit of work and knowledge. What I’m not surprised with is that there aren’t many software developers who can or want to take advantage of these nice toys – I think it’s worth it anyway.

    I’d love to see some examples of all those callbacks list views, tree views etc. used The Truly Right Way (TM). Any good sites maybe? Thanks in advance!

    And thank you Raymond for your really great, informative blog.

  3. sriram says:

    We are still looping through all ~20,000 entries, and instead of inserting the actual text, just inserting item.pszText = LPSTR_TEXTCALLBACK, which is why we don’t see a significant speed-up I assume? I would have thought this might even be slower than the first method, with the added overhead of callbacks etc…

    I’m curious as to whether the source of the delay is overhead due to sending 20,000 messages, or whether the slowdown is caused by adding an item to the listview.

  4. Janus says:

    snram: The delay is probably a bit of both, but I imagine most of the delay is from adding the items.

  5. Hari says:

    This is not a virtual list yet ; when virtual list view is used, there is no need to insert any items, and it will be lightning fast. I guess thats what RC is upto.

  6. Kevin Idzi says:

    I’ve worked with this a lot at my prior company. simply adding the item kills performance. And removing the items is just as bad. The way that I hacked it, and this was using VB6, so it was a bit uglier.. was to add items as needed.

    I ended up faking out the scrollbar so that I’d add a few pages at a time to the listview with their data, and then I’d adjust the scrollbar so that the scrollbar sizing was correct. Then you adjust if the user clicks pagedown, down, etc. and when they click on the scrollbar. Then if a different dataset is bound to the listview, I would prune the ‘extra’ list items if there were too many, or else I’d leave it as is and grow the listview as the data is paged down.

    If a user clicks on a column to sort, then I go ahead and load all of the data as normal (it’s automatically cached) and do the sorting.

  7. B. Y. says:

    Looks like it starts to free from the 2nd most recently allocated block:



    HEADER* phdr = m_phdrCur;

    while (phdr) {

    HEADER hdr = *phdr;

    VirtualFree(hdr.m_phdrPrev, hdr.m_cb, MEM_RELEASE);

    phdr = hdr.m_phdrPrev;



    how about:



    while (m_phdrCur) {

    HEADER*prev = phdr->m_phdrPrev;

    VirtualFree(m_phdrCur, m_phdrCur->m_cb, MEM_RELEASE);




  8. Probably LVS_OWNERDATA style is needed. You set number of items first but do not insert them. This should improve performance.

  9. Anonymous says:

    Off-topic: "We’ll do better next time." seems to be the mantra of Windows in particular and of (all) software makers in general :) Sorry for being a dirty bastard, I just couldn’t hold it.

  10. Owner-data listviews let you take over data management from the listview.

  11. mfink says:


    I used to call

    ::SendMessage(m_hwndLV, WM_SETREDRAW, (WPARAM)FALSE, 0);

    before inserting all the items, and the same call but with TRUE as wParam parameter, followed by a call

    ::RedrawWindow(m_hwndLV, NULL, NULL, RDW_INVALIDATE);

    to redraw the window. Inserting items then dropped from ~10 seconds to about 1 second. But if that would have used, it would make not that much difference, would it? Or are there any problems using WM_SETREDRAW?

  12. Kevin Idzi says:

    It is true that the ownerdata is the best, but when using VB, to keep it simple and pluggable, sometimes it is best to use the listview as-is and just ‘attach’ the functionality to an existing listview, instead of having to actually create it.

    As far as the redraw function, that does not fix the loading speed issue, and it causes horrible refreshing in some cases. The listview does a much ‘better’ job at refreshing itself by using the dirty areas, you just have to work with it to make it be efficient.

Comments are closed.