February, 2011

  • The Old New Thing

    WM_NCHITTEST is for hit-testing, and hit-testing can happen for reasons other than the mouse being over your window

    • 8 Comments

    The WM_NC­HIT­TEST message is sent to your window in order determine what part of the window corresponds to a particular point. The most common reason for this is that the mouse is over your window.

    • The default WM_SET­CURSOR handler uses the result of WM_NC­HIT­TEST to figure out what type of cursor to show. for example, if you return HT­LEFT, then Def­Window­Proc will show the IDC_SIZE­WE cursor.
    • If the user clicks the mouse, the default WM_NC­LBUTTON­DOWN handler uses the result of WM_NC­HIT­TEST to figure out where on the window you clicked. For example, if you return HT­CLOSE, then it will act as if the user clicked on the Close button.

    Although WM_NC­HIT­TEST triggers most often for mouse activity, that is not the only reason why somebody might want to ask, "What part of the window does this point correspond to?"

    • The Window­From­Point function uses WM_NC­HIT­TEST in its quest to figure out which window is under the point you passed in. If you return HT­TRANSPARENT, then it will skip your window and keep looking.
    • Drag/drop operations use the result of WM_NC­HIT­TEST to figure out what part of the window you are dragging over.
    • Accessibility tools use the result of WM_NC­HIT­TEST to help the user understand what's on the screen.
    • Anybody can use the result of WM_NC­HIT­TEST to see how your window is laid out. We used it a few years ago to detect a right-click on the caption button.

    Consider a program that wants to beep when the mouse is over the Close button. This is an artificial example, but you can use your imagination to come up with more realistic ones, like showing a custom mouseover animation or displaying a balloon tip if the document is unsaved. I chose beeping because it requires less code; otherwise, all the details of its implementation would distract from the point of the example.

    Start with the scratch program and make the following changes:

    BOOL g_fInCloseButton = FALSE;
    
    void EnterCloseButton(HWND hwnd)
    {
     if (g_fInCloseButton) return;
     g_fInCloseButton = TRUE;
     MessageBeep(-1); // obviously something more interesting goes here
     TRACKMOUSEEVENT tme = { sizeof(tme), TME_NONCLIENT | TME_LEAVE, hwnd };
     TrackMouseEvent(&tme);
    }
    
    void LeaveCloseButton(HWND hwnd)
    {
     if (g_fInCloseButton) {
      // stop animation, remove balloon, etc.
      g_fInCloseButton = FALSE;
     }
    }
    
    // This code is wrong - see text
    UINT OnNcHitTest(HWND hwnd, int x, int y)
    {
     UINT ht = FORWARD_WM_NCHITTEST(hwnd, x, y, DefWindowProc);
     if (ht == HTCLOSE) {
      EnterCloseButton(hwnd);
     } else {
      LeaveCloseButton(hwnd);
     }
     return ht;
    }
    
    HANDLE_MSG(hwnd, WM_NCHITTEST, OnNcHitTest);
    case WM_NCMOUSELEAVE:
     LeaveCloseButton(hwnd);
     break;
    

    We keep track of whether or not the mouse is in the close button so that we don't double-start the animation or double-cancel it. (For us, this keeps us from beeping when the mouse moves around within the Close button.) When the mouse leaves the close button—either because it moved to another part of the window or because it left the window entirely—we reset the flag.

    When you run this program, it pretty much behaves as intended. But that's because we haven't tried anything interesting yet.

    Merge in the changes from our sample drag/drop program, so now you have a program that both performs drag/drop and which has special Close button behavior.

    Now things get interesting. Run the program and drag out of the client area (triggering the drag/drop behavior) and hover the mouse over the Close button.

    Ow, my ears!

    What happened here?

    When the drag/drop loop is in progress, the mouse is captured to the drag/drop window. Mouse capture means that all mouse messages go to that window (for as long as a mouse button is held down). "I don't care what window you think the mouse is over; it's over me!" Another way of looking at this is that the capture window logically covers the entire screen (for the purpose of determining who gets the mouse message).

    The drag/drop loop wants to know which window is under the drag cursor so it can figure out whose IDropTarget should receive the drag/drop notifications. This WindowFromPoint call triggers a WM_NC­HIT­TEST message, which our program incorrectly interprets as a "the mouse is now in my window". (Since the mouse is captured, the mouse really isn't in your window; it's in the window that has capture because that window is stealing all the mouse input.) It then performs its "The mouse is in the Close button" activities (BEEP). But since the mouse was never in the window to begin with, the Track­Mouse­Event call that requests "let me know when the mouse leaves my window" posts a WM­_NC­MOUSE­LEAVE message immediately. The window then cleans up its "mouse is in the Close button" behaviors, ready for the next cycle.

    And the next cycle begins pretty much as soon as the previous cycle finished, because the mouse is still physically (but not logically) in the Close button.

    Result: Infinite beep loop.

    (The real-life situation that triggered this article was much more complicated than this, involving an animation rather than a beep, but the result was effectively the same: Under the right circumstances, just moving the mouse over the caption resulted in the animation becoming an epileptic-seizure-inducing flicker as the animation continuously started and stopped.)

    As we saw some time ago, the WM_MOUSE­MOVE message is the way to detect that the mouse has entered your window. (Though some people haven't figured this out and continue on their fruitless quest for the WM_MOUSE­ENTER message.)

    In our case, the applicable message is WM_NC­MOUSE­MOVE rather than WM_MOUSE­MOVE, since we are operating on the nonclient area. Therefore, the fix is to move the code that starts the animation from WM_NC­HIT­TEST to WM_NC­MOUSE­MOVE.

    // Delete the old OnNcHitTest function and replace it with this
    void OnNcMouseMove(HWND hwnd, int x, int y, UINT codeHitTest)
    {
     FORWARD_WM_NCMOUSEMOVE(hwnd, x, y, codeHitTest, DefWindowProc);
     if (codeHitTest == HTCLOSE) {
      EnterCloseButton(hwnd);
     } else {
      LeaveCloseButton(hwnd);
     }
     return ht;
    }
    
    // delete HANDLE_MSG(hwnd, WM_NCHITTEST, OnNcHitTest);
    HANDLE_MSG(hwnd, WM_NCMOUSEMOVE, OnNcMouseMove);
    

    Remember, if you want to do something when the mouse enters your window, wait until the mouse actually enters your window. The WM_NC­HIT­TEST message doesn't mean that the mouse is in your window; it just means that somebody is asking, "If the mouse were in your window, what would it be doing?"

  • The Old New Thing

    What is the highest numerical resource ID permitted by Win32?

    • 11 Comments

    A customer asked the following question:

    What is the maximum legal value of a resource identifier? Functions like Load­String take a UINT as the resource ID, which suggests a full 32-bit range, but in practice, most resource IDs appear to be in the 16-bit range. Is there any particular history/precedent for avoiding large numbers as resource IDs? (I have a program that autogenerates string IDs, and having a full 32-bit range gives me some more flexibility in assigning the IDs, but I want to make sure I don't run afoul of any limitations either.)

    Let's answer the literal question first, and then look at the misconceptions behind the question.

    The maximum legal value for an integer resource identifier is 65535. You don't need any special psychic powers for this; it's right there in the MAKE­INT­RESOURCE macro:

    #define MAKEINTRESOURCEA(i) ((LPSTR)((ULONG_PTR)((WORD)(i))))
    #define MAKEINTRESOURCEW(i) ((LPWSTR)((ULONG_PTR)((WORD)(i))))
    #ifdef UNICODE
    #define MAKEINTRESOURCE  MAKEINTRESOURCEW
    #else
    #define MAKEINTRESOURCE  MAKEINTRESOURCEA
    #endif // !UNICODE
    

    The MAKE­INT­RESOURCE macro takes the integer you passed, casts it down to a 16-bit WORD, and then casts the result up to a LPTSTR, effectively generating a pointer whose top bits are all zero (a pointer into the first 64KB of the address space).

    Right off the bat, you can see that integer resources are limited to 16-bit values, because if you pass anything bigger, it'll get truncated by the cast to WORD.

    Why does this limitation exist? Because most resource loading functions overload a single lpName parameter (representing the resource identifier or name) as both an integer (identifier) and a string (name). You can't have the full range of integers and the full range of pointers simultaneously if you want to be able to distinguish the two cases, so you have to choose some rule by which you can tell them apart, and the rule chosen by Win32 is that if the value is in the range 0..0xFFFF, then the value is treated as an integer; otherwise it is treated as a pointer to a string.

    This convention comes from the days of 16-bit Windows, where 32-bit pointers consisted of a 16-bit selector in the high order word and a 16-bit offset in the low order word. The selector 0x0000 is permanently invalid, so that's a natural place to "sneak in" the integers: A "pointer" whose selector is 0x0000 is really an integer smuggled inside a pointers. There was no loss of expressiveness because integers in 16-bit Windows were, well, only 16-bits wide, so the two parameter spaces (strings and integer) neatly meshed with no overlap. (This partitioning of the address space also happily lines up with the convention that in Win32, the first 64KB of address space is permanently invalid.)

    Okay, so that answers the literal question, but there's more going on. Fortunately, the customer provided context: The integer range he's interested in is string identifiers, not resource identifiers.

    String identifiers are not resource identifiers. As we saw earlier, strings are gathered in bundles of 16. The bottom 4 bits of the string identifier specify which string in the bundle contains the string in question, while the remaining bits form the resource identifier of the bundle. We just learned that the resource identifier is a 16-bit value, so string identifiers can go up to 65536 × 16 − 1.

    The customer was pleased with this explanation, contributing the additional insight that "a corollary to string bundling is that it's more efficient to use contiguous ranges of string identifiers (at least gathering them in blocks of 16) rather than sparsely generated ones."

  • The Old New Thing

    What is the difference between a directory and a folder?

    • 37 Comments

    Windows 95 introduced Windows Explorer and along with it the term folder. What is the relationship between folders and directories?

    Some people believe that Windows 95 renamed directories to folders, but it's actually more than that.

    Windows Explorer lets you view folders, which are containers in the shell namespace. Directories are one type of folder, namely, folders which correspond to file system locations. There are other types of folders, such as Control Panel or Network Neighborhood or Printers. These other types of folders represent objects in the shell namespace which do not correspond to files. In common usage, the term virtual folder has been applied to refer to folders which are not directories. In other words, we have this Euler diagram:

    Folders

    Directories
    Virtual folders = Folders − Directories

    In general, code which manipulates the shell namespace should operate on folders and items, not directories and files, so as not to tie themselves to a particular storage medium. For example, code which limits itself to files won't be able to navigate into a Zip file, since the contents of a Zip file are exposed in the form of a virtual folder.

    Update: The Web server "helpfully" closed some tags prematurely. Apparently it doesn't want you to nest tables. Replaced nested table with DIV.

  • The Old New Thing

    Don't mention the war. I mentioned it once, but I think I got away with it all right (Episode 2)

    • 18 Comments

    In preparation for the 2012 Olympic Games in London, the official UK tourism bureau produced tips for dealing with people from other countries.

  • The Old New Thing

    Window message parameters do not come with metaphysical certitude

    • 12 Comments

    The MSDN documentation for window messages describes what each of the parameters means, but just because it means something doesn't mean that it is that something; it merely means it.

    But you knew this already. If you have a window handle, you can send it whatever message you like, with whatever parameters you like, even if those parameters contradict reality. For example, you could write some code that seeks out a target window and sends it a WM_COMMAND with parameters that claim that the message was generated from a keyboard accelerator, when in fact it was generated by code contained within your custom control. But you send the message with the accelerator parameters because your goal is to fool the target program into thinking that it came from a keyboard accelerator. Similarly, if your control wants to simulate a menu click, you should package the parameters in the same way a menu delivers them.

    There are some people, however, who have difficulty wrapping their brains around this concept, that if you are trying to simulate something, then you have to behave as the thing you are trying to simulate and not as your actual self. If the documentation says that "A control sends the message with these parameters" and you're a control but you want to pretend that you're a menu, then you need to send the message the way a menu would. That's why it's called pretending.

    Message parameter documentation is written on the assumption that nobody is pretending anything. What other choice is there? There's no point discussing the possibility that the sender of the message is playing tricks and lying to you because (1) your program should just go along with the ruse and respond to fake menu messages as if they were real menu messages, because (2) there's no way to tell that you're being lied to anyway. To detect lying, you'd have to be able to read into the mindset of the programmer who sent you the message. "Gosh, this code is generating a message and claiming that it was triggered by a menu selection. Is the code is implementing a menu-like window, or is it just trying to trick me?"

  • The Old New Thing

    What happens when you email the people in the I'm a PC commercial?

    • 20 Comments

    In 2008, the first I'm a PC ad aired, opening with Sean Siler doing an impression of John Hodgman portraying a PC, and continuing with montage of people proudly announcing, "I'm a PC!"

    Accompanying the first four people to appear on screen are email addresses. The addresses are live (or at least they were when the campaign launched), and if you write to them, you get an autoresponse.

    I've heard that the most popular of the four email addresses in terms of incoming volume was not Sean Siler's, nor was it the one belonging to Bill Gates. Rather, the most messages arrived from people trying to contact the young woman in the server room.

    And it wasn't even close. We're talking a factor of like 5.[citation needed]

    As it happens, she is a developer on the shell team, and by an astonishing coincidence, her office number at the time was 1337.

    Sadly, that one second of fame took its toll on my colleague, who now deeply wishes for the permanent destruction of that brief clip of her saying "I'm a PC."

    And no, I won't forward your email to her.

  • The Old New Thing

    Any intelligent human being

    • 20 Comments

    The story of The Best reminded me of a classmate from school who was inordinately fond of the phrase any intelligent human being. For example, our classmate would say, "Any intelligent human being would do X" or "It's obvious to any intelligent human being that Y." (I often—but not always—consider myself to be an intelligent human being, yet sometimes the things "any intelligent human being" would do were things I personally wouldn't.) On the other hand, you wouldn't hear our classmate use the phrase in sentences like "I cannot comprehend how any intelligent human being would be fooled by Z."

    Eventually, one of my friends determined that the phrase any intelligent human being was our classmate's replacement for the first person singular pronoun. By "It is obvious to any intelligent human being that Y," our classmate really meant "It is obvious to me that Y."

    Thenceforth, we referred to our classmate as Any Intelligent Human Being (or AIHB for short, and capitalized since it's now a proper noun). Example: "I saw Any Intelligent Human Being in the cafeteria yesterday."

  • The Old New Thing

    How do specify that a shortcut should not be promoted as newly-installed on the Start menu?

    • 32 Comments

    Windows XP employed a number of heuristics to determine which Start menu shortcuts should be promoted when an application is newly-installed. But what if those heuristics end up guessing wrong?

    You can set the System.App­User­Model.Exclude­From­Show­In­New­Install property to VARIANT_TRUE to tell the Start menu, "I am not the primary entry point for the program; I'm a secondary shortcut, like a help file."

    #include <windows.h>
    #include <tchar.h>
    #include <shlobj.h>
    #include <atlbase.h>
    
    // class CCoInitialize incorporated here by reference
    
    int __cdecl _tmain(int argc, TCHAR **argv)
    {
     // error checking elided for expository purposes
     CCoInitialize init;
     CComPtr<IShellLink> spsl;
     spsl.CoCreateInstance(CLSID_ShellLink);
     spsl->SetPath(TEXT("C:\\Program Files\\LitWare\\LWUpdate.exe"));
     PROPVARIANT pvar;
     pvar.vt = VT_BOOL;
     pvar.boolVal = VARIANT_TRUE;
     CComQIPtr<IPropertyStore>(spsl)->SetValue(PKEY_AppUserModel_ExcludeFromShowInNewInstall, pvar);
     CComQIPtr<IPersistFile>(spsl)->Save(L"LitWare Update.lnk", TRUE);
     return 0;
    }
    
  • The Old New Thing

    Psychic debugging: Because of course when something doesn't work, it's because the program was sabotaged from above

    • 25 Comments

    When something stops working, you begin developing theories for why it doesn't work, and normally, you start with simple theories that involve things close to you, and only after you exhaust those possibilities do you expand your scope. Typically, you don't consider that there is a global conspiracy against you, or at least that's not usually your first theory.

    I'm trying to use the XYZ.DLL that comes with your product. I have successfully registered this DLL (as specified in the documentation) by performing a regsvr32 C:\path\to\XYZ.DLL.

    According to the documentation, I should now be able to create a Xyz.Xyz­Widgetizer object, but when I try to do so from C#, I get the exception

    Retrieving the COM class factory for component with CLSID
    {...} failed due to the following error: 80040154.
    

    I tried using the Visual Basic code sample which comes with the documentation, which contains only two lines:

    Dim oXyzWidgetizer
    Set oXyzWidgetizer = WScript.CreateObject("Xyz.XyzWidgetizer")
    

    However, it still fails with the following error:

    Microsoft (R) Windows Script Host Version 5.7
    Copyright (C) Microsoft Corporation. All rights reserved.
    C:\test.vbs(2, 1) WScript.CreateObject: Could not create object
                      named "Xyz.XyzWidgetizer".
    

    Has support for the XyzWidgetizer been silently dropped?

    Let's look at the error message more closely. Error 80040154 is REGDB_E_CLASSNOTREG: The class is not registered. Therefore, whatever regsvr32 did, it didn't register the class.

    My psychic powers tell me that you registered the 32-bit version of XYZ.DLL on a 64-bit machine.

    Registering the 32-bit DLL records the entries into the 32-bit registry (because 32-bit programs run in an emulator), and the 32-bit registry is not consulted when you try to create a COM object from a 64-bit application. Letting 64-bit applications see the registration for 32-bit DLLs doesn't actually accomplish anything because you cannot load a 32-bit DLL into a 64-bit process and vice versa—even if a 64-bit process can figure out what DLL it wants, it won't able to load it.

    It so happens that my psychic powers were correct. How did I know that the person asking the question was running the 32-bit version of XYZ on a 64-bit version of Windows? I didn't, but it was the simplest theory that fit the (extremely limited) data. And it didn't involve a global conspiracy.

  • The Old New Thing

    Why does SHGetKnownFolderPath return E_FAIL for a known folder?

    • 38 Comments

    A customer reported having problems with the SH­Get­Known­Folder­Path function. I've left in the red herrings.

    On Windows 7, I'm trying to retrieve the Internet folder with the following code:

    if (OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken))
    {
     HRESULT hr = SHGetKnownFolderPath(FOLDERID_InternetFolder,
                                  KF_FLAG_DONT_VERIFY, hToken, &pszPath);
     ...
    }
    

    The call always fails with E_FAIL. What am I doing wrong? I tried passing NULL as the token, but that didn't help.

    The reason the call fails has nothing to do with Windows 7 or the token. The call fails because FOLDERID_Internet­Folder is a virtual folder—there is no path in the first place!

    The reason is that the folder you are requesting is a virtual folder (KF_CATEGORY_VIRTUAL). Virtual folders don't exist in the file system, so they don't have a path. SH­Get­Known­Folder­Item should work.

    The customer appears to have misinterpreted this response in a way I wasn't expecting (but which sadly I've seen before):

    I added the KF_CATEGORY_VIRTUAL flag, but I still get the same error back.

    if (OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken))
    {
     HRESULT hr = SHGetKnownFolderPath(FOLDERID_InternetFolder,
                                  KF_FLAG_DONT_VERIFY | KF_CATEGORY_VIRTUAL,
                                  hToken, &pszPath);
     ...
    }
    

    Um, no, that makes no sense at all. KF_CATEGORY_VIRTUAL is a KF_CATEGORY value, but the second parameter to SH­GetKnown­Folder­Path is a KNOWN_FOLDER_FLAG. You can't just combine unrelated values like that. It's like adding 3 grams to 12 meters.

    And second, the KF_CATEGORY_VIRTUAL enumeration isn't something that you pass in to "override" anything. The point is that FOLDERID_Internet­Folder is a virtual folder: It has no path, so if you try to ask for its path, you'll just get an error back because the thing you're looking for simply doesn't exist.

    I never did figure out what this customer was trying to do. Maybe they figured, since they can't download the Internet, they could at least try to do a Find­First­File on it.

Page 2 of 3 (28 items) 123