Computing listview infotips in the background


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.

Comments (21)
  1. benkaras says:

    "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."

    I am guessing that you’ve also left it as an exersize to fix the memory leak when this occurs.  If the window is destroyed, the WM_APP message is never handled and BackgroundInfoTip gets leaked.

  2. jon says:

    “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.)”

    Um.. where is this documented, just out of interest? (the Unicode statement, anyway).

    The docs say LVM_SETINFOTIP is only available in version 6, but nothing that I’ve seen about version 6 only supporting Unicode in general.

    [It should be documented in MSDN but I wouldn’t be surprised if nobody remember to add it. -Raymond]
  3. kiwiblue says:

    "Um.. where is this documented, just out of interest? (the Unicode statement, anyway)."

    Here?

    http://msdn2.microsoft.com/en-us/library/ms649780.aspx#requirements

  4. A says:

    I guess what Raymond meant to say is “the new LVM_SETINFOTIP message is only available in a Unicode version”. COMCTL32 v6, in general, does support ANSI.

    [None of the new stuff will support ANSI, and even some old stuff doesn’t work. -Raymond]
  5. sergio says:

    That’s why it’s so cool to read Raymond’s blog (and also why I prefer more technical subjects). By following this small article and thread, I’ve learnt:

    1) About some special features in common controls

    2) Much more important: that to use Win XP comctl I have to make my program UNICODE (and no, on the link posted by kiwiblue it’s not written, they just say "ComCtl32.dll, version 6, requires a system for Unicode" which has some other meaning (I don’t know which system they refer, but I’d expect to mean "operating system").

    3) About the existence of "std::nothrow"

    Question: is () in Raymond’s "new BackgroundInfoTip();" and error (I think new A and new A() are different things) or is it some special syntax connected with the fact that it’s "non throwing new"?

  6. mikeb says:

    I’ve noticed that you and Larry Osterman often (always?) use a non-throwing variant of the new operator.

    In order to prevent confusion, you may want to consider using the std::nothrow_t parameter:

    =================

     SomeType *p = new(std::nothrow) SomeType;

    =================

    This would avoid having to comment that fact:

    =================

     // note: uses no-throwing “new”

     BackgroundInfoTip *pbit = new BackgroundInfoTip();

    =================

    Using std::nothrow avoids relying on the not always obvious use of a special .obj linked into the program, a _set_new_handler function, or a standard-breaking operator new function override.

    [Sorry, I always forget that this option exists. It’s just not part of my everyday vocabulary. -Raymond]
  7. kiwiblue says:

    Sergio: even if the "system for Unicode" is ambiguous, the next paragraph is a dead giveaway:

    [quote]

    You should not subclass the updated common controls with an ANSI window procedure.

    [/quote]

  8. bmm6o says:

    Sergio: "I think new A and new A() are different things"

    They are the same.  I prefer the "new A()" variant.  You may be thinking of stack allocation, in which there is a huge difference between "A a;" and "A a();" (use the first, the second is a function declaration).

  9. A says:

    “Much more important: that to use Win XP comctl I have to make my program UNICODE”

    Seriously people, just because Raymond said it doesn’t make it true. Granted, there might be some rare exceptions, but in five years of linking numerous ANSI-based applications against COMCTL32 v6, I’ve never encountered a single compatibility issue that wasn’t due to a bug in my own code.

    In fact, entire frameworks — like Borland’s Visual Component Library — are built around the assumption that COMCTL32 v6 supports ANSI.

    [You’re lucky that your ANSI program never used the EM_GETHANDLE message. -Raymond]
  10. Philip Taylor says:

    "I think new A and new A() are different things"

    My limited understanding of the C++ standard is that it says: If A is a non-POD class type (i.e. it doesn’t act like a plain C struct), then "new A" default-initialises it. Otherwise A is a POD type, and "new A" gives an indeterminate value (e.g. whatever happened to be in that memory before you allocated it). In both cases, "new A()" default-initialises it (which means calling the default constructor for non-POD types, and zero-initialising for POD types).

    So if you did have a POD type ‘T’, then "new T" would leave its contents indeterminate, while "new T()" would zero-initialise it instead.

    In this case, BackgroundInfoTip is non-POD (since it has a constructor and destructor), so there’s no difference. In both versions it calls the default constructor. Since that constructor has an empty member initialisation list, it causes any non-POD members to be default-initialised (but there aren’t any here), and any POD members (which is all it has) will not be initialised so they’ll be random values. If the constructor was "BackgroundInfoTip() : m_sit() {}" instead then it should zero-initialise the m_sit member since that’s a POD type.

    For example, assuming the formatting works alright here:

    struct A { int m; }; // POD

    struct B { ~B(); int m; }; // non-POD

    struct C { C() : m() {}; ~C(); int m; }; // non-POD, default-initialising m

    int main() {

     std::cout

       << (new A  )->m << " "

       << (new A())->m << " "

       << (new B  )->m << " "

       << (new B())->m << " "

       << (new C  )->m << " "

       << (new C())->m << "n";

    }

    MSVC8 outputs "? 0 ? ? 0 0" (where ‘?’ is some random value). ICC9 outputs "? 0 ? 0 0 0". GCC3.4 outputs "0 0 0 0 0 0". So the difference does matter in some cases. (But please point out any mistakes I’ve made!)

  11. Ian Boyd says:

    “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.”

    i was hoping i would finally get to see a decent way to cancel queued user work items, wait on all cancelled work items to finally return, all the while not getting stuck waiting for queued work items to return.

    If concurrency is the wave of the future, operating systems or compilers are going to have to do it all; it’s a nightmare to manage yourself.

    [My rule is one topic per posting. -Raymond]
  12. asdf says:

    The rules are kind of tricky:

    • There are 3 types of initializations: zero, default, and value.
    • new T calls the default-initializer.

    • new T() calls the value-initializer.

    • The compiler generated default ctor for B looks like B::B() {}. B::m remains uninitialized here because it’s a POD.

    • new A doesn’t do anything since it’s a POD.

    • new A() value-initializes A which zero-initializes all fields since it’s a POD.

    • new B default-initializes B which calls the compiler generated default ctor that leaves B::m uninitialized.

    • new B() value-initializes B which zero-initializes all fields since its default ctor is compiler generated as opposed to user-defined.

    • new C default-initializes C, which calls the default ctor.

    • new C() value-initializes C, which calls the default ctor.

    MSVC8’s output is incorrect. GCC looks like it’s allocating memory from a zeroed out chunk, so that’s not really helpful. ICC9’s is the only minimally correct behavior you should depend on (if you ignore the fact that it’s undefined to read from an uninitialized value).

    I’m glad Raymond realized std::nothrow exists but I hope he finally realizes to use static_cast when converting pointers to and from void* even though I’ve said this like 5 times now.

    And thankfully the definition of POD is being loosened up in C++0x (this shouldn’t affect the above) so you can add ctors (except the special ones) to a POD-struct and still have it remain a POD-struct:

    http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2102.html

  13. steveg says:

    <advertisement>re discussion on c++ new: i did 10 years of c++ and am now much happier in the world of c# — i spend more time solving problems, less time fighting obscure c++ rules — YMMV.</advertisement>

  14. mikeb says:

    Hmm… I cannot find any reference in the C++ Standard to "value initialization" (I’m looking at ISO/IEC 14882:1998(E)).  However, in section 5.3.4 (New), it says that, "An object whose initializer is an empty set of parentheses, i.e., (), shall be default-initialized."

    So it seems to me that "new B" and "new B()" should result in the same behavior (ie., that the default constructor for B is called, so B::m is not initialized).  

    So all of the "new" expressions above, except for "new A" result in default initialization.  For a POD type, like A, that results in being zero-initialized.

    new A   – indeterminate value

    new A() – zero-initialize

    new B   – default construct (B::m is uninitialized)

    new B() – default construct (B::m is uninitialized)

    new C   – default construct

    new C() – default construct

    The behavior of MSVC 8 seems to follow these rules.

    I agree with steveg that this kind of obscure crap is a major problem with C++.

  15. Dean Harding says:

    in five years of linking numerous ANSI-based applications against COMCTL32 v6

    Why on earth are you still writing ANSI application in 2006?

  16. asdf says:

    value-initialization was added to TC1, the 2003 revision of the C++ standard. MSVC8 is conformant only if it claims to follow the 1998 standard.

    And for completeness, lets add another example:

    struct D { D() {}; int m; };

    new D; // D::m uninitialized

    new D(); // D::m uninitialized

    static D d; // D::m is zero-initialized in during static initialization

  17. That one Ian guy says:

    Great pedantic comments here.  What the heck does documenting throw/nothrow have to do with listview tooltips?

  18. ghjk says:

    Dean: Maybe because he’s in scientific computing? Most of the libraries I work with on a daily basis speak only ANSI strings, and I mean char*, not even std::string. The conversion overhead is such that it’s just not worth building things to use Unicode until all these libraries get fixed.

  19. Norman Diamond says:

    Thursday, December 14, 2006 10:44 PM by mikeb

    I’ve noticed that you and Larry Osterman

    often (always?) use a non-throwing variant of

    the new operator.

    Yeah, he uses the old new.  (So do I, except for buffers that grow to meet demand where I might use malloc and realloc.)

    Friday, December 15, 2006 7:52 PM by Dean Harding

    > in five years of linking numerous ANSI-based

    > applications against COMCTL32 v6

    >

    Why on earth are you still writing ANSI

    application in 2006?

    You mean 2001, because you quoted A saying A has been linking it for 5 years.  A few theoretically possible reasons might be:  (1) Windows ME still existed in 2001; (2) Machines with 32MB of RAM still run Windows 98 in 2006; (3) Applications which were written in 1999 might still get Windows XP look and feel added to their UI; (4) Some applications which were written for Unix might still get ported to Windows.

  20. old skol says:

    Why on earth are you still writing ANSI application in 2006?

    Why on earth are people still writing C++ applications in 2006?

  21. Norman Diamond says:

    Tuesday, December 19, 2006 9:23 AM by old skol

    Why on earth are people still writing C++

    applications in 2006?

    Here’s one answer:

    http://blogs.msdn.com/oldnewthing/archive/2006/12/18/1317290.aspx

Comments are closed.

Skip to main content