Computing listview infotips in the background

Raymond Chen

When the listview control asks you for an infotip, it sends you then LVN_GETINFOTIP notification, and when you return, the result is displayed as the infotip. But what if computing the infotip takes a long time? You don’t want to stall the UI thread on a long operation, after all. This is where LVM_SETINFOTIP comes in.

If you want to say, “Um, I’m not ready with that infotip yet,” you do two things: First, you return a blank infotip to the listview in response to LVN_GETINFOTIP; this tells the listview not to display anything. Then, when you have the infotip, you send the LVM_SETINFOTIP message to say, “Oh, here’s that infotip you asked for. If you were still wondering.” If the user is still hovering over the item that the infotip was requested for, the infotip will be displayed. Otherwise, the infotip will be thrown away (since the user doesn’t want to see it any more).

Here’s a quick and dirty (and not very good) implementation of this algorithm, just to illustrate the point. Start with the program from last time and make the following changes:

// add to top of file
#define UNICODE
#define _UNICODE
class BackgroundInfoTip {
public:
 BackgroundInfoTip() { ZeroMemory(&m_sit, sizeof(m_sit)); }
 BOOL Start(HWND hwnd, NMLVGETINFOTIP *pit)
 {
  m_hwnd = hwnd;
  m_sit.cbSize = sizeof(m_sit);
  m_sit.dwFlags = 0;
  m_sit.iItem = pit->iItem;
  m_sit.iSubItem = pit->iSubItem;
  if ((pit->dwFlags & LVGIT_UNFOLDED) ||
     (m_pszPrefix = StrDup(pit->pszText)) != NULL) {
   return QueueUserWorkItem(s_Work, this, WT_EXECUTELONGFUNCTION);
  }
  return FALSE;
 }
 ~BackgroundInfoTip() {
  LocalFree(m_pszPrefix);
  CoTaskMemFree(m_sit.pszText);
 }
 static DWORD CALLBACK s_Work(void *lpParameter);
 void Work();
 HWND m_hwnd;
 LVSETINFOTIP m_sit;
 LPTSTR m_pszPrefix;
};
void BackgroundInfoTip::Work()
{
 Sleep(3000); // artificial delay
 TCHAR szInfotip[INFOTIPSIZE];
 if (m_pszPrefix) {
  StringCchCopy(szInfotip, INFOTIPSIZE, m_pszPrefix);
  StringCchCat(szInfotip, INFOTIPSIZE, TEXT("\r\n"));
 } else {
  szInfotip[0] = TEXT('\0');
 }
 StringCchCat(szInfotip, INFOTIPSIZE, TEXT("Here is an infotip"));
 if (SUCCEEDED(SHStrDup(szInfotip, &m_sit.pszText)) &&
     PostMessage(m_hwnd, WM_APP, 0, (LPARAM)this)) {
  // ownership transferred to main window
 } else {
  delete this;
 }
}
DWORD BackgroundInfoTip::s_Work(void *lpParameter)
{
 BackgroundInfoTip *self =
    reinterpret_cast<BackgroundInfoTip*>(lpParameter);
 self->Work();
 return 0;
}
void OnGetInfoTip(HWND hwnd, NMLVGETINFOTIP *pit)
{
 if (!pit->cchTextMax) return;
 // note: uses no-throwing "new"
 BackgroundInfoTip *pbit = new BackgroundInfoTip();
 if (pbit && pbit->Start(hwnd, pit)) {
  pit->pszText[0] = TEXT('\0'); // no tip yet
 } else {
  delete pbit;
 }
}
void FinishInfoTip(BackgroundInfoTip *pbit)
{
 SendMessage(g_hwndChild, LVM_SETINFOTIP, 0, (LPARAM)&pbit->m_sit);
 delete pbit;
}
    case WM_APP: FinishInfoTip((BackgroundInfoTip *)lParam); return 0;

We start by defining UNICODE and _UNICODE because we’re using the Windows XP common controls (version 6), and that version of the common controls supports only Unicode. (Version 5 of the common controls doesn’t support the LVM_SETINFOTIP message.)

Next, let’s skip ahead to the OnGetInfoTip function. When we are asked to produce an infotip, we create an instance of our helper class and get it started. Once we’re convinced that the infotip computation is under way, we return a blank infotip to the listview to tell it, “Don’t display anything yet.”

The helper class BackgroundInfoTip starts by capturing the parameters of the NMLVGETINFOTIP. Again, we pay close attention to the LVGIT_UNFOLDED flag: If it is not set, then we save the text currently in the infotip so we can prepend it to the infotip text. We then toss the item onto the thread pool and wait for the work item to fire.

As before, our infotip computation is artificially simple: It’s just a hard-coded string. In real life you presumably would actually sit down and compute something. I stuck in a Sleep(3000) to create an artificial delay in order to simulate this “computation time”. Once we have our answer, remembering to prefix the original infotip text if the item was folded, we save it in the LVSETINFOTIP structure and post a message back to our main thread to say, “Okay, the infotip is ready.”

On receipt of the WM_APP message (in a proper program, it would have a more meaningful name like WM_INFOTIPREADY), we tell the listview that we have our infotip, in case it was still interested. And since this completes the background infotip calculation, we can delete the helper object.

This is not very good code because it fails to handle some obvious cases: If the user moves to a new listview item, the listview will ask for a new infotip. Our code doesn’t attempt to cancel the previous background infotip; as a result, if the user waves the mouse over the listview, we may end up with a large number of background infotip computations, all but one of which will be discarded. Even worse, all the discarded ones will be ahead of the important one in the work item queue: You’re spending all your time doing something whose result is going to be thrown away, and not executing the work item whose result is actually useful.

The code also doesn’t handle the case where the window is closed while the background work items are still running. Closing the window should cancel the work items or at least tell them that they don’t have a main window to talk to any more.

Adding code to handle all these edge cases would have distracted from the point of this article, so I leave you to make this code more solid as an exercise.

0 comments

Discussion is closed.

Feedback usabilla icon