December, 2006

  • The Old New Thing

    The evolution of version resources - 16-bit version resources

    • 14 Comments

    I return to the extremely sporadic series on resources with a description of the version resource. You don't need to know how version resources are formatted internally; you should just use the version resource manipulation functions GetFileVersionInfo, VerQueryValue, and their friends. I'm providing this information merely for its historical significance.

    Version resources can be viewed as a serialized tree structure. Each node of the tree has a name and associated data (either binary or text), and each node can have zero or more child nodes. The root node is always named VS_VERSION_INFO and is a binary node consisting of a VS_FIXEDFILEINFO structure. Beyond that, you can call your nodes anything you want and give them any kind of data you want. But if you want other people to understand your version information, you'd be best off following the conventions I describe below. Actually, since people seem to prefer diagrams to words, I'll give you a diagram:

    VS_VERSION_INFO VS_FIXEDFILEINFO structure (binary)
    StringFileInfo (no data)
    xxxxyyyy (no data)
    CompanyName string for xxxxyyyy
    FileDescription string for xxxxyyyy
    FileVersion string for xxxxyyyy
    ...
    zzzzwwww (no data)
    CompanyName string for zzzzwwww
    FileDescription string for zzzzwwww
    FileVersion string for zzzzwwww
    ...
    VarFileInfo (no data)
    Translation array of locale/codepage pairs (binary, variable-size)

    The child nodes can appear in any order, and the strings like CompanyName are all optional. VarFileInfo\Translation, however, is mandatory (by convention).

    If you've used VerQueryValue, you know that the binary data stored under VarFileInfo\Translation consists of a variable-length array of locale/codepage pairs, each of which in turn corresponds to a child of the StringfileInfo node. I'm not going to go into what each of the strings means and how the local/codepage pairs turn into child nodes of StringFileInfo; I'll leave you to research that on your own (assuming you don't already know).

    How does this tree get stored into a resource? It's actually quite simple. Each node is stored in a structure which takes the following form (in pseudo-C):

    struct VERSIONNODE {
      WORD  cbNode;
      WORD  cbData;
      CHAR  szName[];
      BYTE  rgbPadding1[]; // DWORD alignment
      BYTE  rgbData[cbData];
      BYTE  rgbPadding2[]; // DWORD alignment
      VERSIONNODE rgvnChildren[];
    };
    

    In words, each version node begins with a 16-bit value describing the size of the nodes in bytes (including its children), followed by a 16-bit value that specifies how many bytes of data (either binary or text) are associated with the node. (If the node contains text data, the count includes the null terminator.) Next comes the null-terminated name of the node and padding bytes to bring us back into DWORD alignment. After the key name (and optional padding) comes the data, again followed by padding bytes to bring us back into DWORD alignment. Finally, after all the node information come its children.

    Since each of the children might themselves have children, you can see how the tree structure "flattens" into this serialized format. To move from one node to its next sibling, you skip ahead by cbNode bytes. To move from a node to its first child, you skip over the key name and associated data.

    Let's take a look at the resources for the 16-bit shell.dll to see how this all fits together.

    0000  E4 01 34 00 56 53 5F 56-45 52 53 49 4F 4E 5F 49  ..4.VS_VERSION_I
    0010  4E 46 4F 00 BD 04 EF FE-00 00 01 00 0A 00 03 00  NFO.............
    0020  67 00 00 00 0A 00 03 00-67 00 00 00 3F 00 00 00  g.......g...?...
    0030  0A 00 00 00 01 00 01 00-02 00 00 00 00 00 00 00  ................
    0040  00 00 00 00 00 00 00 00-78 01 00 00 53 74 72 69  ........x...Stri
    0050  6E 67 46 69 6C 65 49 6E-66 6F 00 00 64 01 00 00  ngFileInfo..d...
    0060  30 34 30 39 30 34 45 34-00 00 00 00 27 00 17 00  040904E4....'...
    0070  43 6F 6D 70 61 6E 79 4E-61 6D 65 00 4D 69 63 72  CompanyName.Micr
    0080  6F 73 6F 66 74 20 43 6F-72 70 6F 72 61 74 69 6F  osoft Corporatio
    0090  6E 00 00 00 2A 00 16 00-46 69 6C 65 44 65 73 63  n...*...FileDesc
    00A0  72 69 70 74 69 6F 6E 00-57 69 6E 64 6F 77 73 20  ription.Windows 
    00B0  53 68 65 6C 6C 20 6C 69-62 72 61 72 79 00 00 00  Shell library...
    00C0  16 00 06 00 46 69 6C 65-56 65 72 73 69 6F 6E 00  ....FileVersion.
    00D0  33 2E 31 30 00 00 00 00-1A 00 06 00 49 6E 74 65  3.10........Inte
    00E0  72 6E 61 6C 4E 61 6D 65-00 00 00 00 53 48 45 4C  rnalName....SHEL
    00F0  4C 00 00 00 3B 00 27 00-4C 65 67 61 6C 43 6F 70  L...;.'.LegalCop
    0100  79 72 69 67 68 74 00 00-43 6F 70 79 72 69 67 68  yright..Copyrigh
    0110  74 20 A9 20 4D 69 63 72-6F 73 6F 66 74 20 43 6F  t . Microsoft Co
    0120  72 70 2E 20 31 39 38 31-2D 31 39 39 36 00 00 00  rp. 1981-1996...
    0130  22 00 0A 00 4F 72 69 67-69 6E 61 6C 46 69 6C 65  "...OriginalFile
    0140  6E 61 6D 65 00 00 00 00-53 48 45 4C 4C 2E 44 4C  name....SHELL.DL
    0150  4C 00 00 00 39 00 29 00-50 72 6F 64 75 63 74 4E  L...9.).ProductN
    0160  61 6D 65 00 4D 69 63 72-6F 73 6F 66 74 AE 20 57  ame.Microsoft. W
    0170  69 6E 64 6F 77 73 28 54-4D 29 20 4F 70 65 72 61  indows(TM) Opera
    0180  74 69 6E 67 20 53 79 73-74 65 6D 00 00 00 00 00  ting System.....
    0190  1A 00 06 00 50 72 6F 64-75 63 74 56 65 72 73 69  ....ProductVersi
    01A0  6F 6E 00 00 33 2E 31 30-00 00 00 00 14 00 04 00  on..3.10........
    01B0  57 4F 57 20 56 65 72 73-69 6F 6E 00 34 2E 30 00  WOW Version.4.0.
    01C0  24 00 00 00 56 61 72 46-69 6C 65 49 6E 66 6F 00  $...VarFileInfo.
    01D0  14 00 04 00 54 72 61 6E-73 6C 61 74 69 6F 6E 00  ....Translation.
    01E0  09 04 E4 04                                      ....
    

    We start with the root node.

    0000  E4 01         // cbNode (node ends at 0x0000 + 0x01E4 = 0x01E4)
    0002  34 00         // cbData = sizeof(VS_FIXEDFILEINFO)
    0004  56 53 5F 56 45 52 53 49 4F 4E 5F 49 4E 46 4F 00
                        // "VS_VERSION_INFO" + null terminator
    

    Notice that the size of the root node equals the size of the entire version resource. This is to be expected, of course, because the version resource is merely a serialization of the resource tree diagram.

    Since the string name (plus null terminator) happens to come out to an exact multiple of four bytes, there is no need for padding between the name and the binary data, which takes the form of a VS_FIXEDFILEINFO:

    0014  BD 04 EF FE   // dwSignature
    0018  00 00 01 00   // dwStrucVersion
    001C  0A 00 03 00   // dwFileVersionMS = 3.10
    0020  67 00 00 00   // dwFileVersionLS = 0.103
    0024  0A 00 03 00   // dwProductVersionMS = 3.10
    0028  67 00 00 00   // dwProductVersionLS = 0.103
    002C  3F 00 00 00   // dwFileFlagsMask
    0030  0A 00 00 00   // dwFileFlags
    0034  01 00 01 00   // dwFileOS = VOS_DOS_WINDOWS16
    0038  02 00 00 00   // dwFileType = VFT_DLL
    003C  00 00 00 00   // dwFileSubtype
    0040  00 00 00 00   // dwFileDateMS
    0044  00 00 00 00   // dwFileDateLS
    

    The structure is also a multiple of 4 bytes in length, so no padding is necessary between the data and the child nodes.

    0048  78 01         // cbNode (node ends at 0x0048 + 0x0178 = 0x01C0)
    004A  00 00         // cbData (no data)
    004C  53 74 72 69 6E 67 46 69 6C 65 49 6E 66 6F 00
                        // "StringFileInfo" + null
    005B  00            // padding to restore alignment
    005C                // no data
    

    The first child is the StringFileInfo. It has no data, so its own children come directly after the name (and padding). And the children of StringFileInfo are the language nodes.

    005C  64 01         // cbNode (node ends at 0x005C + 0x0164 = 0x01C0)
    005E  00 00         // cbData (no data)
    0060  30 34 30 39 30 34 45 34 00
                        // "040904E4" + null terminator
    0069  00 00 00      // padding to restore alignment
    006C                // no data
    

    The children of the language node are the strings. This is where all the goodies can be found.

    006C  27 00         // cbNode (node ends at 0x006C + 0x0027 = 0x0093)
    006E  17 00         // cbData
    0070  43 6F 6D 70 61 6E 79 4E 61 6D 65 00
                        // "CompanyName" + null terminator
    007C                // no padding needed
    007C  4D 69 63 72 6F 73 6F 66 74 20 43 6F
          72 70 6F 72 61 74 69 6F 6E 00
                        // "Microsoft Corporation" + null terminator
    0091  00 00 00      // padding to restore alignment
    

    Notice that the padding bytes are not counted in the cbData. In fact, the padding bytes at the end of the data don't even count towards the cbNode. This is a leaf node since we already reach the end of the node once we store the data. Therefore, the next node in the version resource is a sibling, not a child.

    0094  2A 00         // cbNode (node ends at 0x0094 + 0x002A = 0x00BE)
    0096  16 00         // cbData
    0098  46 69 6C 65 44 65 73 63 72 69 70 74 69 6F 6E 00
                        // "FileDescription" + null terminator
    00A8                // no padding needed
    00A8  57 69 6E 64 6F 77 73 20 53 68 65 6C 6C 20 6C 69
          62 72 61 72 79 00
                        // "Windows Shell library" + null terminator
    00BE  00 00         // padding to restore alignment
    

    All of these nodes have no children since we run out of bytes in cbNode after representing the node's name and data.

    00C0  16 00         // cbNode (node ends at 0x00C0 + 0x0016 = 0x00D6)
    00C2  06 00         // cbData
    00C4  46 69 6C 65 56 65 72 73 69 6F 6E 00
                        // "FileVersion" + null terminator
    00D0  33 2E 31 30 00
                        // "3.10"
    00D5  00 00 00      // padding to restore alignment
    
    00D8  1A 00         // cbNode (node ends at 0x00D8 + 0x001A = 0x00F2)
    00DA  06 00         // cbData
    00DC  49 6E 74 65 72 6E 61 6C 4E 61 6D 65 00
                        // "InternalName" + null terminator
    00E9  00 00 00      // padding to restore alignment
    00EC  53 48 45 4C 4C 00
                        // "SHELL" + null terminator
    00F2  00 00         // padding to restore alignment
    
    00F4  3B 00         // cbNode (node ends at 0x00F4 + 0x003B = 0x12E)
    00F6  27 00
    00F8  4C 65 67 61 6C 43 6F 70 79 72 69 67 68 74 00
                        // "LegalCopyright" + null terminator
    0107  00            // padding to restore alignment
    0108  43 6F 70 79 72 69 67 68 74 20 A9 20 4D 69 63 72
          6F 73 6F 66 74 20 43 6F 72 70 2E 20 31 39 38 31
          2D 31 39 39 36 00
                        // "Copyright © Microsoft Corp. 1981-1996"
                        // + null terminator + another null terminator?
    012F  00            // padding to restore alignment
    
    Wait a second, what's that "another null terminator"? if you count the bytes, you'll see that the cbData for the LegalCopyright node counts not only the terminating null, but another bonus null after it. I suspect that somebody put an extra null terminator in the resource file by mistake:

        VALUE "LegalCopyright", "Copyright\251 Microsoft corp. 1981-1996\0"
    

    For whatever reason, there's an extra null in there.

    0130  22 00         // cbNode (node ends at 0x0130 + 0x0022 = 0x0152)
    0132  0A 00         // cbData
    0134  4F 72 69 67 69 6E 61 6C 46 69 6C 65 6E 61 6D 65 00
                        // "OriginalFilename" + null terminator
    0145  00 00 00      // padding to restore alignment
    0148  53 48 45 4C 4C 2E 44 4C 4C 00
                        // "SHELL.DLL" + null terminator
    0152  00 00         // padding to restore alignment
    
    0154  39 00         // cbNode (node ends at 0x0154 + 0x0039 = 0x018D)
    0156  29 00         // cbData
    0158  50 72 6F 64 75 63 74 4E 61 6D 65 00
                        // "ProductName" + null terminator
    0164  4D 69 63 72 6F 73 6F 66 74 AE 20 57 69 6E 64 6F
          77 73 28 54 4D 29 20 4F 70 65 72 61 74 69 6E 67
          20 53 79 73 74 65 6D 00 00
                        // "Microsoft® Windows(TM) "
                        // "Operating System" + null terminator
                        // + another null terminator?
    018D  00 00 00      // padding to restore alignment
    

    There's another of those extra null terminators. Go figure.

    0190  1A 00         // cbNode (node ends at 0x0190 + 0x001A = 0x01AA)
    0192  06 00         // cbData
    0194  50 72 6F 64 75 63 74 56 65 72 73 69 6F 6E 00
                        // "ProductVersion" + null terminator
    01A3  00            // padding to restore alignment
    01A4  33 2E 31 30 00 00
                        // "3.10" + null terminator
                        // + another null terminator?
    01AA  00 00         // padding to restore alignment
    
    01AC  14 00         // cbNode (node ends at 0x01AC + 0x0014 = 0x01C0)
    01AE  04 00         // cbData
    01B0  57 4F 57 20 56 65 72 73 69 6F 6E 00
                        // "WOW Version"
    01BC                // no padding needed
    01BC  34 2E 30 00   // "4.0" + null terminator
    01C0                // no padding needed
    

    Once we reach offset 0x01C0, we're reached the end of not only the WOW Version node, but also the end of the 040904E4 node and the StringFileInfo node. Therefore, the next node is a child of the root.

    01C0  24 00         // cbNode (node ends at 0x01C0 + 0x0024 = 0x01E4)
    01C2  00 00         // cbData (no data)
    01C4  56 61 72 46 69 6C 65 49 6E 66 6F 00
                        // "VarFileInfo" + null terminator
    01D0                // no padding needed
    01D0                // no data
    

    Since we have not reached the end of the VarFileInfo node, the data that comes next must be a child node.

    01D0  14 00         // cbNode (noed ends at 0x01D0 + 0x0014 = 0x01E4)
    01D4  04 00         // cbData
    01D6  54 72 61 6E 73 6C 61 74 69 6F 6E 00
                        // "Translation" + null terminator
    01E0  09 04 E4 04   // 0x0409 = US English
                        // 0x04E4 = 1252 = Western European
    01E4                // no padding needed
    

    And once we've reached offset 0x01E4, we've reached the end of the Translation node, the VarFileInfo node, and the root node.

    Thus, we have reconstructed the original version resource:

    FILEVERSION    3,10,0,103
    PRODUCTVERSION 3,10,0,103
    FILEFLAGSMASK  VS_FFI_FILEFLAGSMASK
    FILEFLAGS      VS_FF_PRERELEASE | VS_FF_PRIVATEBUILD
    FILEOS         VOS_DOS_WINDOWS16
    FILETYPE       VFT_DLL
    FILESUBTYPE    VFT_UNKNOWN
    BEGIN
     BLOCK "StringFileInfo"
     BEGIN
      BLOCK "040904E4"
      BEGIN
       VALUE "CompanyName", "Microsoft Corporation"
       VALUE "FileDescription", "Windows Shell library"
       VALUE "FileVersion", "3.10"
       VALUE "InternalName", "SHELL"
       VALUE "LegalCopyright", "Copyright\251 Microsoft corp. 1981-1996\0"
       VALUE "OriginalFilename", "SHELL.DLL"
       VALUE "ProductName", "Microsoft\256 Windows(TM) Operating System\0"
       VALUE "ProductVersion", "3.10\0"
       VALUE "WOW Version", "4.0"
      END
     END
     BLOCK "VarFileInfo"
     BEGIN
      VALUE "Translation", 0x0409, 0x04E4
     END
    END
    

    Next time, we'll look at how version resources are represented in 32-bit resources.

  • The Old New Thing

    Pacific Northwest storm recovery continues

    • 16 Comments

    Puget Sound Energy has a service status page where they update how things are going in the power restoration process. The repair crews (some from as far away as Kansas) are working 40-hour shifts with eight hours' rest between shifts. (That article is from a snowstorm a few weeks ago. A manager is quoted as saying. "I've been in this industry 30 years, and I haven't seen anything like this." Well, now he gets to see it twice in one year, and it's not even winter yet!)

    Electricity was restored to my house after only 36 hours. All of a sudden I was popular! (If you can't be popular for who you are, at least be popular for what you have.)

    I know people who have been told that "the lines through the mountain between the main station and the substation are heavily damaged. ... Right now with the current information, Thursday night or Friday morning are the best-case scenarios" for having power restored to their neighborhood. But that explanation makes me wonder. First, how do you repair lines that run through a mountain? And second, how do you damage lines that run through a mountain? (Now, it's entirely likely that by "through" the mountain, they really meant "over the mountain", but it's funnier to think that the lines actually run through the mountain.)

  • The Old New Thing

    I bet somebody is looking to get a really nice bonus for that feature: Attention

    • 73 Comments

    "I bet somebody is looking to get a really nice bonus for that feature."

    A customer was having trouble with one of their features that scans for resources that their program can use, and, well, the details aren't important. What's important is that their feature ran in the Startup group, and as soon as it found a suitable resource, it displayed a balloon tip: "Resource XYZ has been found. Click here to add it to your resource portfolio."

    We interrupted them right there.

    — Why are you doing this?

    "Oh, it's a great feature. That way, when users run our program, they don't have to go looking for the resources they want to operate with. We already found the resources for them."

    — But why are you doing it even when your program isn't running? The user is busy editing a document or working on a spreadsheet or playing a game. The message you're displaying is out of context: You're telling users about a program they aren't even using.

    "Yeah, but this feature is really important to us. It's crucial in order to remain competitive in our market."

    — The message is not urgent. It's a disruption. Why don't you wait until they launch your program to tell them about the resources you found? That way, the information appears in context: They're using your program, and the program tells them about these new resources.

    "We can't do that! That would be annoying!"

  • The Old New Thing

    Some call it context, others call it reference data, but whatever it is, it's yours

    • 24 Comments

    Different functions call it different things. RegisterWaitForSingleObject calls it Context. SetWindowSubclass calls it reference data. EnumWindows calls it lParam. CreateThread just calls it a parameter! But whatever its name is, it means the same thing: It's a value the function doesn't care about. All the function does is hand that value back to you. What the value means is up to you.

    What if you need to pass more context than a single pointer? What if you want to pass, say, two pointers? Then put the two pointers in a structure and pass a pointer to that structure. When you do this, you introduce lifetime issues, so make sure you have a plan for deciding who is responsible for freeing the memory when it is no longer needed.

    "Why isn't this documented in MSDN? Otherwise, people who call CreateThread won't know that the parameter needs to be a pointer to this structure and that the thread procedure needs to free the memory."

    It's not documented in MSDN because MSDN doesn't care. This is entirely a convention within your program. It's your responsibility to ensure that the code that calls CreateThread and the thread procedure agree on what the thread parameter means and how it should be managed.

  • The Old New Thing

    Do not write in-process shell extensions in managed code

    • 42 Comments

    Jesse Kaplan, one of the CLR program managers, explains why you shouldn't write in-process shell extensions in managed code. The short version is that doing so introduces a CLR version dependency which may conflict with the CLR version expected by the host process. Remember that shell extensions are injected into all processes that use the shell namespace, either explicitly by calling SHGetDesktopFolder or implicitly by calling a function like SHBrowseForFolder, ShellExecute, or even GetOpenFileName. Since only one version of the CLR can be loaded per process, it becomes a race to see who gets to load the CLR first and establish the version that the process runs, and everybody else who wanted some other version loses.

    Update 2013: Now that version 4 of the .NET Framework supports in-process side-by-side runtimes, is it now okay to write shell extensions in managed code? The answer is still no.

  • The Old New Thing

    Why do user interface actions tend to occur on the release, not on the press?

    • 39 Comments

    If you pay close attention, you'll notice that most user interface actions tend to occur on the release, not on the press. When you click on a button, the action occurs when the mouse button is released. When you press the Windows key, the Start menu pops up when you release it. When you tap the Alt key, the menu becomes active when you release it. (There are exceptions to this general principle, of course, typing being the most notable one.) Why do most actions wait for the release?

    For one thing, waiting for the completion of a mouse action means that you create the opportunity for the user to cancel it. For example, if you click the mouse while it is over a button (a radio button, push button, or check box), then drag the mouse off the control, the click is cancelled.

    But a more important reason for waiting for the press is to ensure that the press won't get confused with the action itself. For example, suppose you are in mode where objects disappear when the user clicks on them. For example, it might be a customization dialog, with two columns, one showing available objects and another showing objects in use. Clicking on an available object moves it to the list of in-use objects and vice versa. Now, suppose you acted on the click rather than the release. When the mouse button goes down while the mouse is over on an item, you remove it from the list and add it to the opposite list. This moves the items the user clicked on, so that the item beneath the mouse is now some other item that moved into the original item's position. And then the mouse button is released, and you get a WM_LBUTTONUP message for the new item. Now you have two problems: First, the item the user clicked on got a WM_LBUTTONDOWN and no corresponding WM_LBUTTONUP, and second, the new item got a WM_LBUTTONUP with no corresponding WM_LBUTTONDOWN.

    You can also get into a similar situation with the keyboard, though it takes more work. For example, if you display a dialog box while the Alt key is still pressed rather than waiting for the release, the Alt key may autorepeat and end up delivered to the dialog box. This prevents the dialog box from appearing since it's stuck in menu mode that was initiated by the Alt key, and it's is waiting for you to finish your menu operation before it will display itself.

    Now, this type of mismatch situation is not often a problem, but when it does cause a problem, it's typically a pretty nasty one. This is particularly true if you're using some sort of windowless framework that tries to associate mouse and keyboard events with the corresponding windowless objects. When the ups and downs get out of sync, things can get mighty confusing.

    (This entry was posted late because a windstorm knocked out power to the entire Seattle area. My house still doesn't have electricity.)

  • The Old New Thing

    Computing listview infotips in the background

    • 21 Comments

    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.

  • The Old New Thing

    Maybe if it had characters and stuff and different levels, it would be OK

    • 19 Comments

    In a classic series, Electronic Games Monthly plopped modern kids, ages 9 through 12, in front of classic video games: Pong, Donkey Kong, Tetris. And then recorded their reactions. Here, the kids are playing Tetris and appear to be obsessed with stuff blowing up.

    Tim: Which button do I press to make the blocks explode?

    EGM: Sorry, they don't explode.

    Becky: This is boring. Maybe if it had characters and stuff and different levels, it would be OK. If things blew up or something or—

    Sheldon: If there were bombs.

    Becky: Yeah, or special bricks. Like, if a yellow brick touched a red brick it would blow up and you'd have to start over.

    John: Why haven't I won yet? I've paired up so many of the same color.

    EGM: Don't worry about colors.

    John: I just lined up six of the same color. Why didn't they blow up?

    EGM: Nothing blows up.

    Best single line: "I'm sure everyone who made this game is dead by now."

    The article was such a hit that they did it again the following year.

  • The Old New Thing

    Displaying infotips for folded and unfolded listview items

    • 13 Comments

    When displaying infotips for listview items, you have to deal with both the folded and unfolded case. "Folded" is the term used to describe a listview item in large icon mode whose text has been truncated due to length. When the user selects the item, the full text is revealed, a process known as "unfolding".

    Take our scratch program and make the following changes:

    #include <strsafe.h>
    
    BOOL
    OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
    {
      g_hwndChild = CreateWindow(WC_LISTVIEW, NULL,
                                 WS_CHILD | WS_VISIBLE | LVS_ICON,
                                 0, 0, 0, 0,
                                 hwnd, (HMENU)1, g_hinst, 0);
      if (!g_hwndChild) return FALSE;
    
      ListView_SetExtendedListViewStyleEx(g_hwndChild,
                                 LVS_EX_INFOTIP,
                                 LVS_EX_INFOTIP);
    
      LVITEM item;
      item.iItem = 0; // added 9pm
      item.iSubItem = 0;
      item.mask = LVIF_TEXT;
      item.pszText = TEXT("Item with a long name that will be truncated");
      if (ListView_InsertItem(g_hwndChild, &item) < 0)
        return FALSE;
    
      return TRUE;
    }
    
    void OnGetInfoTip(HWND hwnd, NMLVGETINFOTIP *pit)
    {
     if (!pit->cchTextMax) return;
     if (pit->dwFlags & LVGIT_UNFOLDED) {
      pit->pszText[0] = TEXT('\0');
     } else {
      StringCchCat(pit->pszText, pit->cchTextMax, TEXT("\r\n"));
     }
     StringCchCat(pit->pszText, pit->cchTextMax, TEXT("Here is an infotip"));
    }
    
    LRESULT OnNotify(HWND hwnd, int idCtrl, NMHDR *pnm)
    {
     if (idCtrl == 1) {
      switch (pnm->code) {
      case LVN_GETINFOTIP:
       OnGetInfoTip(hwnd, (NMLVGETINFOTIP*)pnm);
       break;
      }
     }
     return 0;
    }
    
      HANDLE_MSG(hwnd, WM_NOTIFY, OnNotify);
    

    We create our listview, enable infotips, and add a single item with a rather long name. When you run the program, observe that the item's text is truncated at two lines if it is not selected, but it expands to full size when you selected it.

    When the listview notifies us that it's time to display the infotip, we check whether the item is folded or unfolded. If it is unfolded, then we set the buffer to an empty string so that our StringCchCat at the end will merely copy the infotip text into the buffer. On the other hand, if the item is folded, then we append a line terminator because we want the infotip to contain the full text of the item, followed by the tip text.

    When you run this program, hover over the item both when it is folded and unfolded, and observe that the folded infotip includes the name of the item. This is a detail of infotips that is called out in the documentation but which many programs fail to observe.

  • The Old New Thing

    Jeanne Martinet teaches you how to survive a party where you don't know anybody

    • 11 Comments

    It's the time of year where you may find yourself attending a party where you know hardly anyone. Jeanne Martinet, author of The Art of Mingling, walks Steve Inskeep through a mock-party with tips on how to join a conversation, and (perhaps more importantly) exit one. Don't just read the article; you need to listen to the story. The mock-party is a hoot! When Susan Stamberg starts going on and on about that cranberry relish recipe...

Page 2 of 4 (32 items) 1234