Date: | December 14, 2006 / year-entry #414 |
Tags: | code |
Orig Link: | https://blogs.msdn.microsoft.com/oldnewthing/20061214-02/?p=28713 |
Comments: | 21 |
Summary: | 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... |
When the listview control asks you for an infotip, it sends you
then
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 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
Next, let's skip ahead to the
The helper class
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
On receipt of the 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)
Comments are closed. |
"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.
“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.
"Um.. where is this documented, just out of interest? (the Unicode statement, anyway)."
Here?
http://msdn2.microsoft.com/en-us/library/ms649780.aspx#requirements
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.
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"?
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.
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]
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).
“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.
"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!)
“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.
The rules are kind of tricky:
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
<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>
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++.
Why on earth are you still writing ANSI application in 2006?
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
Great pedantic comments here. What the heck does documenting throw/nothrow have to do with listview tooltips?
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.
Thursday, December 14, 2006 10:44 PM by mikeb
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
>
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.
Why on earth are people still writing C++ applications in 2006?
Tuesday, December 19, 2006 9:23 AM by old skol
Here’s one answer:
http://blogs.msdn.com/oldnewthing/archive/2006/12/18/1317290.aspx