• The Old New Thing

    Implementing higher-order clicks

    • 35 Comments

    Another question people ask is "How do I do triple-click or higher?" Once you see the algorithm for double-clicks, extending it to higher order clicks should be fairly natural. The first thing you probably should do is to remove the CS_DBLCLKS style from your class because you want to do multiple-click management manually.

    Next, you can simply reimplement the same algorithm that the window manager uses, but take it to a higher order than just two. Let's do that. Start with a clean scratch program and add the following:

    int g_cClicks = 0;
    RECT g_rcClick;
    DWORD g_tmLastClick;
    
    void OnLButtonDown(HWND hwnd, BOOL fDoubleClick,
                       int x, int y, UINT keyFlags)
    {
      POINT pt = { x, y };
      DWORD tmClick = GetMessageTime();
    
      if (!PtInRect(&g_rcClick, pt) ||
          tmClick - g_tmLastClick > GetDoubleClickTime()) {
        g_cClicks = 0;
      }
      g_cClicks++;
    
      g_tmLastClick = tmClick;
      SetRect(&g_rcClick, x, y, x, y);
      InflateRect(&g_rcClick,
                  GetSystemMetrics(SM_CXDOUBLECLK) / 2,
                  GetSystemMetrics(SM_CYDOUBLECLK) / 2);
    
      TCHAR sz[20];
      wnsprintf(sz, 20, TEXT("%d"), g_cClicks);
      SetWindowText(hwnd, sz);
    }
    
    void ResetClicks()
    {
      g_cClicks = 0;
      SetWindowText(hwnd, TEXT("Scratch"));
    }
    
    void OnActivate(HWND hwnd, UINT state, HWND, BOOL)
    {
      ResetClicks();
    }
    
    void OnRButtonDown(HWND hwnd, BOOL fDoubleClick,
                       int x, int y, UINT keyFlags)
    {
      ResetClicks();
    }
    
        HANDLE_MSG(hwnd, WM_LBUTTONDOWN, OnLButtonDown);
        HANDLE_MSG(hwnd, WM_ACTIVATE, OnActivate);
    

    [Boundary test for double-click time corrected 10:36am.]

    (Our scratch program doesn't use the CS_DBLCLKS style, so we didn't need to remove it - it wasn't there to begin with.)

    The basic idea here is simple: When a click occurs, we see if it is in the "double-click zone" and has occurred within the double-click time. If not, then we reset the consecutive click count.

    (Note that the SM_CXDOUBLECLK and SM_CYDOUBLECLK values are the width of the entire rectangle, so we cut it in half when inflating so that the rectangle extends halfway in either direction. Yes, this means that a pixel is lost if the double-click width is odd, but Windows has been careful always to set the value to an even number.)

    Next, we record the coordinates and time of the current click so the next click can compare against it.

    Finally, we react to the click by putting the consecutive click number in the title bar.

    There are some subtleties in this code. First, notice that setting g_cClicks to zero forces the next click to be treated as the first click in a series, for regardless of whether it matches the other criteria, all that will happen is that the click count increments to 1.

    Next, notice that the way we test whether the clicks occurred within the double click time was done in a manner that is not sensitive to timer tick rollover. If we had written

          tmClick > g_tmLastClick + GetDoubleClickTime()) {
    

    then we would fail to detect multiple clicks properly near the timer tick rollover. (Make sure you understand this.)

    Third, notice that we reset the click count when the window gains or loses activation. That way, if the user clicks, then switches away, then switches back, and then clicks again, that is not treated as a double-click. We do the same if the user clicks the right mouse button in between. (You may notice that few programs bother with quite this much subtlety.)

    Exercise: Suppose your program isn't interested in anything beyond triple-clicks. How would you change this program in a manner consistent with the way the window manager stops at double-clicks?

  • The Old New Thing

    Logical consequences of the way Windows converts single-clicks into double-clicks

    • 39 Comments

    First, I'm going to refer you to the MSDN documentation on mouse clicks, since that's the starting point. I'm going to assume that you know the mechanics of how single-clicks are converted to double-clicks.

    Okay, now that you've read it, let's talk about some logical consequences of that article and what it means for the way you design your user interface.

    First, some people design their double-click action to be something unrelated to the single-click action. They want to know if they can suppress the initial WM_LBUTTONDOWN of the double-click sequence.

    Of course, you realize that that would require clairevoyance. When the mouse button goes down for the first time, the window manager doesn't know whether another click will come or not. (Heck, often the user doesn't know either!) So it spits out a WM_LBUTTONDOWN and waits for more.

    Now suppose you're a program that nevertheless wants to continue with the dubious design of having the double-click action be unrelated to the single-click action. What do you do?

    Well, one thing you could do is to do nothing on receipt of the WM_LBUTTONDOWN message aside from set a timer to fire in GetDoubleClickTime() milliseconds. [Corrected 10am.] If you get a WM_LBUTTONDBLCLK message within that time, then it was a double-click after all. If you don't, then it must have been a single-click, so you can do your single-click action (although a bit late).

    This "wait a tick" technique is also necessary if you don't have a double-click action, but the second click causes trouble in conjunction with the first click. Why is this necessary? Because many users double-click everything. Here are some examples of where the "delayed action to avoid the second click" can be seen:

    • The context menu that appears for taskbar notification icons. If the context menu appeared immediately upon the first click, then the second click would dismiss the context menu, leaving the user confused. "I clicked and something happened and then it went away." (Users don't say "I double-clicked"; they just say that they clicked. Double-click is the only thing they know how to do, so they just call it "click". For the same reason you don't say "I drove my blue car" if you have only one car.)
    • If Explorer is in one-click mode, it waits to see if there is a second click, and if so, it ignores it. Otherwise, when people double-click, they launch two copies of the program. Furthermore, if you suppress the second click but don't wait a tick, then the program they launched gets stuck behind the Explorer window, since the user clicked on Explorer after launching the program.
    • The XP style Start button ignores the second click. Otherwise, when people double-click the Start button, the first click would open the Start menu and the second click would dismiss it! (This is sometimes known as "debouncing".)

    Let's demonstrate how you might implement click delay. Start with the scratch program and add the following:

    void CALLBACK DelayedSingleClick(HWND hwnd, UINT,
                                     UINT_PTR id, DWORD)
    {
        KillTimer(hwnd, id);
        MessageBeep(MB_OK);
    }
    
    void OnLButtonDown(HWND hwnd, BOOL fDoubleClick,
                       int x, int y, UINT keyFlags)
    {
        if (fDoubleClick) {
            KillTimer(hwnd, 1);
            MessageBeep(MB_ICONASTERISK);
        } else {
            SetTimer(hwnd, 1, GetDoubleClickTime(),
                     DelayedSingleClick);
        }
    }
    
        HANDLE_MSG(hwnd, WM_LBUTTONDOWN, OnLButtonDown);
        HANDLE_MSG(hwnd, WM_LBUTTONDBLCLK, OnLButtonDown);
    

    Also, since we're messing with double clicks, we should turn them on:

        wc.style = CS_DBLCLKS;
    

    When you run this program, click and double-click in the client area. Notice that the program doesn't react to the single click until after your double-click timeout has elapsed, because it's waiting to see if you are going to continue to click a second time (and therefore double-click instead of single-click).

    Next time, we'll look at clicks beyond two.

  • The Old New Thing

    Little facts you didn't know about volcanoes

    • 9 Comments

    Molten rock makes volcano glow red, reports CNN.

    Amazing the things you can learn from the news. Last year we learned that concrete is stronger once it has hardened.

  • The Old New Thing

    The procedure entry point SHCreateThreadRef could not be located...

    • 86 Comments

    Some people smarter than me have been working on this problem, and here's what they figured out. First, I'll just reprint their analysis (so that people with the problem and are searching for the solution can find it), and then we can discuss it.

    Update (18 October 2005): The official version of this document is now available. Please use that version instead of following the steps below, which were based on a preliminary version of the instructions.

    If you receive an error message: "Explorer.EXE - Entry Point Not Found - The procedure entry point SHCreateThreadRef could not be located in the dynamic link library SHLWAPI.dll", this information will help resolve the issue.

    This appears to have been caused by the following sequence of events:

    1. You installed Windows XP Service Pack 2
    2. The installation of Service Pack 2 failed due to a computer crash during the installation which resulted in the automatic Service Pack Recovery process. On next boot, you should have received an error message telling you that the install failed, and that you need to go to the control panel and uninstall SP2 and then try re-installing it. This message may have been dismissed accidentally or by another individual using your computer. In any event, the Service Pack recovery process was not completed by uninstalling the service pack from the Add/Remove Programs control panel, and the system is consequently in a partially installed state which is not stable.
    3. You then installed the latest security update for Windows XP, MS04-038, KB834707. Because your system is still partially SP2, the SP2 version of this fix was downloaded and installed by Windows Update or Automatic Updates. However, the operating system files on the system are the original versions due to the SP Recovery process. This results in mismatched files causing this error.

    To recover the system, carefully perform the following steps:

    1. Boot normally and attempt to log in to your desktop. At this point you should get the error message listed above.
    2. Press Control+Alt+Delete at the same time to start the Task Manager. (If you are using classic logon, click the "Task Manager" button.) You may get additional error messages but Task Manager will eventually start.
    3. On the menu bar, select File and then New Task (Run).
    4. Type in control appwiz.cpl into the new task box and hit OK. You may get additional errors that can be ignored.
    5. The Add/Remove Control Panel should now be running. You can close Task Manager.
    6. Near the bottom of the list, find the entry titled "Windows XP Hotfix – KB834707".
    7. Click on it and click the "Remove" button. It will take some time to complete. Once the "Finish" button is visible, click on it and reboot your system. If you get messages about additional software or hotfixes installed, you can safely ignore them.

    Do NOT stop now! Your system is still in the "failed SP2 install" state. You MUST complete the SP2 uninstall, and then re-install SP2.

    1. Start the system and log in.
    2. Click on Start and then Control Panel.
    3. Click on the Add/Remove programs item.
    4. Near the bottom of the list, find the entry titled "Windows XP Service Pack 2".
    5. Click on it and remove Service Pack 2. You may get a warning about software you have installed after SP2. Make a note of it as you may need to reinstall some of them after the uninstall operation.
    6. After Service Pack 2 has been successfully removed, you should visit http://www.microsoft.com/sp2install for instructions on installing Service Pack 2. You can get SP2 from http://windowsupdate.microsoft.com.
    7. After Service Pack 2 has been successfully re-installed, you should re-visit Windows Update to get the proper version of the latest critical security updates.

    FAQ:

    Q: I don't believe I am in the "partially installed SP2" state. Is there any way to check that?

    A: After step 7, your system should be able to log in. There are several ways to check.

    1. Open the file c:\windows\svcpack.log, and scroll to the very bottom of the file. About 10 lines from the end, you should see:
      0.xxx: Executing script \SystemRoot\sprecovr.txt
      0.xxx: In cleanup mode. System will not be rebooted.
      
      If you have these lines in svcpack.log, and you did not uninstall Service Pack 2 in Add/Remove Programs, you definitely have a machine in this partially installed state.
    2. Click on the Start button, then Run, and type winver, then click OK. If the version is "Version 5.1 (Build 2600.xpsp_sp2_rtm.040803-2158: Service Pack 2" then you have the correct SP2 install. If, however, it has a number that is less than 040803 after the xpsp2, such as "Build 2600.xpsp2.030422-1633 : Service Pack 2" then you definitely have a machine in the partially installed state. [Corrected typo in version numbering, 11am.]

    Notice that the default answer to every dialog box is "Cancel". The Service Pack setup program detected a problem and gave instructions on how to fix it, and yet people just ignored the message.

    The result of not fixing the problem is that you are left with a machine that is neither Service Pack 1 nor Service Pack 2 but instead is a Frankenstein monster of some files from Service Pack 1 and some files from Service Pack 2. This is hardly a good situation. Half the files are mismatched with the other half.

    There was enough of Service Pack 2 on your machine that Windows Update downloaded the Service Pack 2 version of the security patch and tried to install it. This made a bad situation worse.

    What's the moral of the story?

    First, that users ignore all error messages. Even error messages that tell how to fix the error! Second, that ignoring some error messages often leads to worse error messages. And third, that once you get into this situation, you're off in uncharted territory. And there are only dragons there.

    (These are the types of problems that are nearly impossible to debug: It will never happen in the test lab, because the test lab people know that when an error message tells you, "The install failed; you need to do XYZ to fix it," you should do it! All you can work from are descriptions from people who are having the problem, and a lot of creativity to try to guess "What happened that they aren't telling me?")

  • The Old New Thing

    Dispatching items collected from the suggestion box

    • 62 Comments

    Okay, I got around to digging through the suggestion box, and today I'm going to dispatch the items that don't require much thought but seemed worthy of reply to some degree. You won't learn much of anything today.

    • Topics I try to avoid

      "I would like to hear your opinion of the programming languages you have used."
      "What, in your opinion, is the strengths and weaknesses of the difference programming languages included in Visual Studio?"
      "What things do you wish had been added to Win32 that never were?"
      "What one thing do you believe should never have been added to Win32?"

      I try to avoid pure opinion pieces on technical matters. (Especially if they aren't funny.)

      "How is Microsoft working to eliminate the CRT-based 96dpi default setting in future versions of Windows?"
      "How has the command line evolved and where is it heading?"
      "How might Win32 evolve in the next version of Windows?"

      I try to avoid speculating about the future.

      "Do Microsofties keep their checkouts on their own hard drive, or do you have a home directory on a server?"
      "Does anyone ever get fired for making a mistake that costs everyone a lot of time and money?"

      I don't feel I have the authority to discuss this.

    • Topics that are outside my realm of expertise

      "How should I store a user's credentials securely?"
      "Sometimes when we query the SID of a computer we get back a blank SID."

      You want to ask a security person, like say Michael Howard.

      "Terminal Services sessions won't span more than 1 monitor. Any workarounds or suggestions?"

      This a question for the Terminal Services team.

      "Why is it so hard to make Windows Messenger go away?"

      Try asking the Windows Messenger team.

      "Why doesn't the Office/Visual Studio team release the code for their neato UI's."
      "How do I paste pictures into a Word document?"
      "How do I position page numbers in Word?"
      "Why does the VBA ROUND function differ from Excel?"

      I am not the Office or Visual Studio team.

      "So, I'm wondering who to go to talk about COM marshalling stuff."
      "What settings is Windows saving when it says it's saving your settings at shutdown/logoff?"
      "Why are mutexes internally known in Windows NT as "mutants"?"
      "Is there any way to shutdown USB device using keyboard instead of clicking on notification icon?"
      "Can you comment on the IXmlSerializable interface..."
      "A question about the windows networking system..."
      "Is there any good reason Windows hardly ever finds the CD drive, or whatever directory you've already specified 17 times has the drivers?"
      "How about when you make some very minor change in TCP/IP settings and Windows reinstalls about 34 .dll files that are already in System32?"
      "Why does my 200GB drive show up as 131GB?"
      "How do I install Message Queueing?"
      "How do I fix the error 'Visual Basic module is corrupt'?"
      "Why does Internet Explorer lose my status bar?"

      I have no idea. I have never worked on logon/logoff or COM marshalling or USB hardware or the CLR or networking or Plug and Play or backup... (Though for the status bar question, I have a wild guess which is likely to be completely wrong. If the last window you close is a pop-up ad with no status bar, then maybe it is that "no status bar" that is the one that ends up being saved as your preference. Just a wild guess.)

      "Why can't I customize X?"

      Every customization comes at a cost. The line has to be drawn somewhere.

    • Topics not of general interest (usually "please debug my computer")

      "Why does running MS Money make these shortcuts slow down to a crawl?"

      Sounds like something you should take up with the MS Money folks.

      "What's with that taskbar bug where you click the button for one window and another pops up right before it does?"

      I've never see this.

      "Why can't I rename My Computer?"

      "My Computer" renaming works for me.

      "In my program, I get an error resource not found."
      "I'm having a problem with backing up my computer."
      "I want to grep date or timestamp of files in UNIX through shell script where date format or timestamp is like 'Jul 9' or 'Jul 12'. In this where day is of single digit the space between month and day is 2 characters, but where day is of 2 digits the space is of 1 character."
      "I have a form that needs a date field to populate upon opening the file. Then the date will be used to generate a number for Tracking purposes. Then when the file is saved and e-mailed to the next person the date and number must remain fixed. What is the best way to set this up?"

      These questions aren't of general interest. One of them isn't even about Windows, and I don't understand that last one, but please, don't explain.

      "Help me get my iPod working."
      "Please translate this into German for me."
      "Please forward me some information as well as a demo and some samples of Blogs." (from a Fortune 500 company no less)
      "How do I take a screenshot of my Powerbook G4?"

      I am at a loss as to how I should respond to these requests.

    • Short answers

      "At one point with tab controls it was documented to let you set the color of the tab. Why was it removed?"

      You could do that? News to me.

      "Why do you make it so hard to properly create a replacement shell?"

      It's not that anybody makes it hard; rather that nobody goes out of their way to make it easy.

      "Why is explorer.exe monolithic? Why wasn't there a desktop.exe, taskbar.exe, etc?"

      Processes are expensive.

      "If a program has a dialog box open, why does Alt+Tab show the dialog box title, but Task Manager shows the application title?"

      Because Alt+Tab is for switching among windows. But Task Manager is for switching among applications.

      "What is the history of the "X" button?"

      Nothing earth-shattering. Studies showed that people didn't know how to exit programs, so some people sat down and tried to work out some universal symbol that would clearly indicate "Close".

      "Why can't you change the bitmap on the Start menu?"

      It's branding.

      "Why is it so hard to write a program that does gross things to Explorer?"

      Allowing other programs do gross things to Explorer wasn't on its list of design goals.

    Other entries require more thought. Each non-code entry takes me a half hour or so, more if I have to do research (if somebody asks me a question outside my area of expertise and I feel like hunting for the answer), and the code entries require an hour and a half or more. Some of them take days. I think I spent three weeks on the dialog box template series, and about as much on the context menus. (And there are plenty of other series in various stages of completion.)

    I don't get paid to do this. It's just a hobby. It's frustrating when people order you to spend more time on your hobby. And then when you decide it's too much and start closing comments to try to get some of your life back, people call you a coward and assume that you stopped for some nefarious reason. (Like when they say, "Look, Raymond closed comments as soon as people started asking embarrassing questions." But the comment timestamps prove otherwise. I closed comments weeks or months later.)

  • The Old New Thing

    People lie on surveys and focus groups, often unwittingly

    • 81 Comments

    Philip Su's discussion of early versions of Microsoft Money triggered a topic that had been sitting in the back of my mind for a while: That people lie on surveys and focus groups, often unwittingly. I can think of three types of lies offhand. (I'm not counting malicious lying; that is, intentional lying for the purpose of undermining the results of the survey or focus group.)

    First, people lie about the reasons why they do things.

    The majority of consumers who buy computers claim that personal finance management is one of the top three reasons they are purchasing a PC. They've been claiming this for more than a decade. But only somewhere around 2% of consumers end up using a personal finance manager.

    This is one of those unconscious lies. People claim that they want a computer to do their personal finances, to organize their recipes, to mail-merge their Christmas card labels.

    They are lying.

    Those are the things people wish they would use their computer for. That's before the reality hits them of how much work it is to track every expenditure, transcribe every recipe, type in every address. In reality, they end up using the computer to play video games, surf the web, and email jokes to each other.

    Just because people say they would do something doesn't mean they will. That leads to the second class of focus group lying: The polite lie, also known as "say what the sponsor wants to hear".

    The following story is true, but the names have been changed.

    A company conducted focus groups for their Product X, which had as its main competitor Product Q. They asked people who were using Product Q, "Why do you use Product Q instead of Product X?" The respondents gave their reasons: "Because Product Q has feature F," "Because Product Q performs G faster," "Because Product Q lets me do activity H." They added, "If Product X did all that and was cheaper, we'd switch to it."

    Armed with this valuable insight, the company expended time, effort, and money in adding feature F to Product X, making Product X do G faster, and adding the ability to do activity H. They lowered the price and sat back and waited for the customers to beat a path to their door.

    But the customers didn't come.

    Why not?

    Because the customers were lying. In reality, they had no intention of switching from Product Q to Product X at all. They grew up with Product Q, they were used to the way Product Q worked, they simply liked Product Q. Product Q had what in the hot bubble-days was called "mindshare", but what in older days was called "brand loyalty" or just "inertia".

    When asked to justify why they preferred Product Q, the people in the focus group couldn't say, "I don't know; I just like it." That would be perceived as an "unhelpful" answer, and besides it would be subconsciously admitting that they were being manipulated by Product Q's marketing! Instead, they made up reasons to justify their preference to themselves and consequently to the sponsor of the focus group.

    Result: Company wastes tremendous effort on the wrong thing.

    (Closely related to this is the phenomenon of saying—and even believing—"I'd pay ten bucks for that!" Yet when the opportunity arises to buy it for $10, you decline. I do this myself.)

    The third example of lying that occurred to me is the one where you don't even realize that you are contradicting yourself. My favorite example of this was a poll on the subject of congestion charging on highways in the United States. The idea behind congestion charging is to create a toll road and vary the cost of driving on the road depending on how heavy traffic is. Respondents were asked two questions:

    1. "If congestion charging were implemented in your area, do you think it would reduce traffic congestion?"
    2. "If congestion charging were implemented in your area, would you be less likely to drive during peak traffic hours?"

    Surprisingly, most people answered "No" to the first question and "Yes" to the second. But if you stop and think about it, if people avoid driving during peak traffic hours, then congestion would be reduced because there are fewer cars on the road. An answer of "Yes" to the second question logically implies an answer of "Yes" to the first question.

    (One may be able to explain this by arguing that, "Well, sure congestion charging would be effective for influencing my driving behavior, but I don't see how it would affect enough other people to make it worthwhile. I'm special." Sort of how most people rate themselves as above-average drivers.)

    What I believe happened was that people reacted by saying to themselves, "I am opposed to congestion charging," and concluding, "Therefore, I must do what I can to prevent it from happening." Proclaiming on surveys that it would never work is one way of accomplishing this.

    When I shared my brilliant theories with some of my colleagues, one of them, a program manager on the Office team, added his own observation (which I have edited slightly):

    A variation of two of the above observations that often shows up in the usability lab:

    A user has spent an hour battling with the software. At some point the user's expectation of how the software should behave (the "user model") diverged from the actual behavior. Consequently, the user couldn't predict what will happen next and is therefore having a horrible time making any progress on the task. (Usually, this is the fault of the software design unintentionally misleading the user—which is why we test things.) After many painful attempts, the user finally succeeds, gets hints, or is flat-out told how the feature works. Often, the user stares mutely at the monitor for five seconds, then says: "I suppose that makes sense."

    It's an odd combination of people wanting to give a helpful answer with people wanting to feel special. In this case, the user wants to say something nice about the software that any outside observer could clearly tell was broken. Additionally, the users (subconsciously) don't want to admit that they were wrong and don't understand the software.

    Usability participants also have a tendency to say "I'm being stupid" when those of us on the other side of the one-way glass are screaming "No you're not, the software is broken!" That's an interesting contrast—in some cases, pleading ignorance is a defense. In other cases, pleading mastery is. At the end of the day, you must ignore what the user said and base any conclusions on what they did.

    I'm sure there are other ways people subconsciously lie on surveys and focus groups, but those are the ones that came to mind.

    [Insignificant typos fixed, October 13.]

  • The Old New Thing

    What's the atom returned by RegisterClass useful for?

    • 8 Comments

    The RegisterClass and RegisterClassEx functions return an ATOM. What is that ATOM good for?

    The names of all registered window classes is kept in an atom table internal to USER32. The value returned by the class registration functions is that atom. You can also retrieve the atom for a window class by asking a window of that class for its class atom via GetClassWord(hwnd, GCW_ATOM).

    The atom can be converted to an integer atom via the MAKEINTATOM macro, which then can be used by functions that accept class names in the form of strings or atoms. The most common case is the lpClassName parameter to the CreateWindow macro and the CreateWindowEx function. Less commonly, you can also use it as the lpClassName parameter for the GetClassInfo and GetClassInfoEx functions. (Though why you would do this I can't figure out. In order to have the atom to pass to GetClassInfo in the first place, you must have registered the class (since that's what returns the atom), in which case why are you asking for information about a class that you registered?)

    To convert a class name to a class atom, you can create a dummy window of that class and then do the aforementioned GetClassWord(hwnd, GCW_ATOM). Or you can take advantage of the fact that the return value from the GetClassInfoEx function is the atom for the class, cast to a BOOL. This lets you do the conversion without having to create a dummy window. (Beware, however, that GetClassInfoEx's return value is not the atom on Windows 95-derived operating systems.)

    But what good is the atom?

    Not much, really. Sure, it saves you from having to pass a string to functions like CreateWindow, but all it did was replace a string with with an integer you now have to save in a global variable for later use. What used to be a string that you could hard-code is now an atom that you have to keep track of. Unclear that you actually won anything there.

    I guess you could use it to check quickly whether a window belongs to a particular class. You get the atom for that class (via GetClassInfo, say) and then get the atom for the window and compare them. But you can't cache the class atom since the class might get unregistered and then re-registered (which will give it a new atom number). And you can't prefetch the class atom since the class may not yet be registered at the point you prefetch it. (And as noted above, you can't cache the prefetched value anyway.) So this case is pretty much a non-starter anyway; you may as well use the GetClassName function and compare the resulting class name against the class you're looking for.

    In other words, window class atoms are an anachronism. Like replacement dialog box classes, it's one of those generalities of the Win32 API that never really got off the ground, but which must be carried forward for backwards compatibility.

    But at least now you know what they are.

    [Typos fixed October 12.]

  • The Old New Thing

    Why is there a separate GetSystemDirectory function?

    • 18 Comments

    If the system directory is always %windir%\SYSTEM32, why is there a special function to get it?

    Because it wasn't always that.

    For 16-bit programs on Windows NT, the system directory is %windir%\SYSTEM. That's also the name of the system directory for Windows 95-based systems and all the 16-bit versions of Windows.

    But even in the 16-bit world, if it was always %windir%\SYSTEM, why have a function for it?

    Because even in the 16-bit world, it wasn't always %windir%\SYSTEM.

    Back in the old days, you could run Windows directly over the network. All the system files were kept on the network server, and only the user's files were kept on the local machine. What's more, every single computer on the network used the same system directory on the server. There was only one copy of USER.EXE, for example, which everybody shared.

    Under this network-based Windows configuration, the system directory was a directory on a server somewhere (\\server\share\somewhere) and the Windows directory was a directory on the local machine (C:\WINDOWS). Clients did not have write permission into the shared system directory, but they did have permission to write into the Windows directory.

    That's why GetSystemDirectory is a separate function.

  • The Old New Thing

    Cooking for engineers

    • 22 Comments

    All geeks who enjoy cooking (and who have cable television) worship Alton Brown's program Good Eats. It's the cooking show for engineers.

    And recently I discovered a cooking blog that is designed for engineers, named, not surprisingly, Cooking for Engineers. Marvel at the elegance and beauty of the recipe diagrams. I have no idea whether the dishes are any good, but the recipes themselves are works of art.

    (And don't miss the analysis of orange juice shelf life.)

    Side remark: I found it somewhat odd that many Slashdotters responded to my spam graph of a few days ago with remarks like "This guy needs to get a hobby." I guess they don't consider cooking, knitting, or studying German, Swedish, or Mandarin to be valid hobbies.

  • The Old New Thing

    How to host an IContextMenu, part 11 - Composite extensions - composition

    • 6 Comments

    Okay, now that we have two context menu handlers we want to compose (namely, the "real" one from the shell namespace and a "fake" one that contains bonus commands we want to add), we can use merge them together by means of a composite context menu handler.

    The kernel of the composite context menu is to multiplex multiple context menus onto a single context menu handler, using the menu identifer offsets to route the commands.

    Everything else is just typing.

    class CCompositeContextMenu : public IContextMenu3
    {
    public:
      // *** IUnknown ***
      STDMETHODIMP QueryInterface(REFIID riid, void **ppv);
      STDMETHODIMP_(ULONG) AddRef();
      STDMETHODIMP_(ULONG) Release();
    
      // *** IContextMenu ***
      STDMETHODIMP QueryContextMenu(HMENU hmenu,
                              UINT indexMenu, UINT idCmdFirst,
                              UINT idCmdLast, UINT uFlags);
      STDMETHODIMP InvokeCommand(
                              LPCMINVOKECOMMANDINFO lpici);
      STDMETHODIMP GetCommandString(
                              UINT_PTR    idCmd,
                              UINT        uType,
                              UINT      * pwReserved,
                              LPSTR       pszName,
                              UINT        cchMax);
    
      // *** IContextMenu2 ***
      STDMETHODIMP HandleMenuMsg(
                              UINT uMsg,
                              WPARAM wParam,
                              LPARAM lParam);
    
      // *** IContextMenu3 ***
      STDMETHODIMP HandleMenuMsg2(
                              UINT uMsg,
                              WPARAM wParam,
                              LPARAM lParam,
                              LRESULT* plResult);
    
      // Constructor
      static HRESULT Create(IContextMenu **rgpcm, UINT cpcm,
                            REFIID riid, void **ppv);
    
    private:
    
      HRESULT Initialize(IContextMenu **rgpcm, UINT cpcm);
      CCompositeContextMenu() : m_cRef(1), m_rgcmi(NULL), m_ccmi(0) { }
      ~CCompositeContextMenu();
    
      struct CONTEXTMENUINFO {
        IContextMenu *pcm;
        UINT cids;
      };
    
      HRESULT ReduceOrdinal(UINT_PTR *pidCmd, CONTEXTMENUINFO **ppcmi);
    
    private:
      ULONG m_cRef;
      CONTEXTMENUINFO *m_rgcmi;
      UINT m_ccmi;
    };
    

    The local structure CONTEXTMENUINFO contains information about each of the context menus that are part of our composite. We need to have the context menu pointer itself, as well as the number of menu identifiers consumed by that context menu by its IContextMenu::QueryContextMenu handler. We'll see why as we implement this class.

    HRESULT CCompositeContextMenu::Initialize(
        IContextMenu **rgpcm, UINT cpcm)
    {
      m_rgcmi = new CONTEXTMENUINFO[cpcm];
      if (!m_rgcmi) {
        return E_OUTOFMEMORY;
      }
    
      m_ccmi = cpcm;
      for (UINT icmi = 0; icmi < m_ccmi; icmi++) {
        CONTEXTMENUINFO *pcmi = &m_rgcmi[icmi];
        pcmi->pcm = rgpcm[icmi];
        pcmi->pcm->AddRef();
        pcmi->cids = 0;
      }
    
      return S_OK;
    }
    

    Since a C++ constructor cannot fail, there are various conventions for how one handles failure during construction. One convention, which I use here, is to put the bulk of the work in an Initialize method, which can return an appropriate error code if the initialization fails.

    (Note that here I am assuming a non-throwing new operator.)

    Our initialization function allocates a bunch of CONTEXTMENUINFO structures and copies the IContextMenu pointers (and AddRefs them) for safekeeping. (Note that the m_ccmi member is not set until after we know that the memory allocation succeeded.)

    The destructor therefore undoes these operations.

    CCompositeContextMenu::~CCompositeContextMenu()
    {
      for (UINT icmi = 0; icmi < m_ccmi; icmi++) {
        m_rgcmi[icmi].pcm->Release();
      }
      delete[] m_rgcmi;
    }
    

    (If you don't understand the significance of the [], here's a refresher.)

    The Create pattern you saw last time, so this shouldn't be too surprising.

    HRESULT CCompositeContextMenu::Create(IContextMenu **rgpcm, UINT cpcm,
                                          REFIID riid, void **ppv)
    {
      *ppv = NULL;
    
      HRESULT hr;
      CCompositeContextMenu *self = new CCompositeContextMenu();
      if (self) {
        if (SUCCEEDED(hr = self->Initialize(rgpcm, cpcm)) &&
            SUCCEEDED(hr = self->QueryInterface(riid, ppv))) {
          // success
        }
        self->Release();
      } else {
        hr = E_OUTOFMEMORY;
      }
      return hr;
    }
    

    And then the standard COM bookkeeping.

    HRESULT CCompositeContextMenu::QueryInterface(REFIID riid, void **ppv)
    {
      IUnknown *punk = NULL;
      if (riid == IID_IUnknown) {
        punk = static_cast<IUnknown*>(this);
      } else if (riid == IID_IContextMenu) {
        punk = static_cast<IContextMenu*>(this);
      } else if (riid == IID_IContextMenu2) {
        punk = static_cast<IContextMenu2*>(this);
      } else if (riid == IID_IContextMenu3) {
        punk = static_cast<IContextMenu3*>(this);
      }
    
      *ppv = punk;
      if (punk) {
        punk->AddRef();
        return S_OK;
      } else {
        return E_NOINTERFACE;
      }
    }
    
    ULONG CCompositeContextMenu::AddRef()
    {
      return ++m_cRef;
    }
    
    ULONG CCompositeContextMenu::Release()
    {
      ULONG cRef = --m_cRef;
      if (cRef == 0) delete this;
      return cRef;
    }
    

    Now we reach our first interesting method: IContextMenu::QueryContextMenu:

    HRESULT CCompositeContextMenu::QueryContextMenu(
        HMENU hmenu, UINT indexMenu, UINT idCmdFirst,
        UINT idCmdLast, UINT uFlags)
    {
      UINT idCmdFirstOrig = idCmdFirst;
      UINT cids = 0;
    
      for (UINT icmi = 0; icmi < m_ccmi; icmi++) {
        CONTEXTMENUINFO *pcmi = &m_rgcmi[icmi];
        HRESULT hr = pcmi->pcm->QueryContextMenu(hmenu,
                        indexMenu, idCmdFirst, idCmdLast, uFlags);
        if (SUCCEEDED(hr)) {
          pcmi->cids = (USHORT)hr;
          cids += pcmi->cids;
          idCmdFirst += pcmi->cids;
        }
      }
    
      return MAKE_HRESULT(SEVERITY_SUCCESS, 0, cids);
    }
    

    We ask each contained context menu in turn to add its commands to the context menu. Here is where you see one of the reasons for the return value of the IContextMenu::QueryContextMenu method. By telling tells the container how many menu identifiers you used, the container knows how many are left for others. The container then returns the total number of menu identifiers consumed by all of the context menus.

    Another reason for the return value of the IContextMenu::QueryContextMenu method is seen in the next helper method:

    HRESULT CCompositeContextMenu::ReduceOrdinal(
        UINT_PTR *pidCmd, CONTEXTMENUINFO **ppcmi)
    {
      for (UINT icmi = 0; icmi < m_ccmi; icmi++) {
        CONTEXTMENUINFO *pcmi = &m_rgcmi[icmi];
        if (*pidCmd < pcmi->cids) {
          *ppcmi = pcmi;
          return S_OK;
        }
        *pidCmd -= pcmi->cids;
      }
      return E_INVALIDARG;
    }
    

    This method takes a menu offset and figures out which of the contained context menus it belongs to, using the return value from IContextMenu::QueryContextMenu to decide how to divide up the identifier space. The pidCmd parameter is in/out. On entry, it's the menu offset for the composite context menu; on exit, it's the menu offset for the contained context menu that is returned via the ppcmi parameter.

    The IContextMenu::InvokeCommand is probably the most complicated, since it needs to support the four different ways of dispatching the command.

    HRESULT CCompositeContextMenu::InvokeCommand(
                                LPCMINVOKECOMMANDINFO lpici) {
    
      CMINVOKECOMMANDINFOEX* lpicix =
                    reinterpret_cast<CMINVOKECOMMANDINFOEX*>(lpici);
      BOOL fUnicode = lpici->cbSize >= sizeof(CMINVOKECOMMANDINFOEX) &&
                      (lpici->fMask & CMIC_MASK_UNICODE);
      UINT_PTR idCmd = fUnicode ? reinterpret_cast<UINT_PTR>(lpicix->lpVerbW)
                                : reinterpret_cast<UINT_PTR>(lpici->lpVerb);
    
      if (!IS_INTRESOURCE(idCmd)) {
        for (UINT icmi = 0; icmi < m_ccmi; icmi++) {
          HRESULT hr = m_rgcmi->pcm->InvokeCommand(lpici);
          if (SUCCEEDED(hr)) {
            return hr;
          }
        }
        return E_INVALIDARG;
      }
    
      CONTEXTMENUINFO *pcmi;
      HRESULT hr = ReduceOrdinal(&idCmd, &pcmi);
      if (FAILED(hr)) {
          return hr;
      }
    
      LPCWSTR pszVerbWFake;
      LPCWSTR *ppszVerbW = fUnicode ? &lpicix->lpVerbW : &pszVerbWFake;
      LPCSTR pszVerbOrig = lpici->lpVerb;
      LPCWSTR pszVerbWOrig = *ppszVerbW;
    
      lpici->lpVerb = reinterpret_cast<LPCSTR>(idCmd);
      *ppszVerbW = reinterpret_cast<LPCWSTR>(idCmd);
    
      hr = pcmi->pcm->InvokeCommand(lpici);
    
      lpici->lpVerb = pszVerbOrig;
      *ppszVerbW = pszVerbWOrig;
    
      return hr;
    }
    

    After some preliminary munging to find the command identifier, we dispatch the invocation in three steps.

    First, if the command is being dispatched as a string, then this is the easiest case. We loop through all the contained context menus asking each one if it recognizes the command. Once one does, we are done. And if nobody does, then we shrug and say we don't know either.

    Second, if the command being dispatched is an ordinal, we ask ReduceOrdinal to figure out which contained context menu handler it belongs to.

    Third, we rewrite the CMINVOKECOMMANDINFO structure so it is suitable for use by the contained context menu handler. This means changing the lpVerb member and possibly the lpVerbW member to contain the new menu offset relative to the contained context menu handler rather than being relative to the container. This is complicated slightly by the fact that the Unicode verb lpVerbW might not exist. We hide that behind a pszVerbWFake local variable which stands in if there is no genuine lpVerbW.

    Okay, now that you see the basic idea behind distributing the method calls to the appropriate contained context menu, the rest should be comparatively easy.

    HRESULT CCompositeContextMenu::GetCommandString(
                                UINT_PTR    idCmd,
                                UINT        uType,
                                UINT      * pwReserved,
                                LPSTR       pszName,
                                UINT        cchMax)
    {
      HRESULT hr;
      if (!IS_INTRESOURCE(idCmd)) {
        for (UINT icmi = 0; icmi < m_ccmi; icmi++) {
          hr = m_rgcmi[icmi].pcm->GetCommandString(idCmd,
                            uType, pwReserved, pszName, cchMax);
          if (hr == S_OK) {
            return hr;
          }
        }
        if (uType == GCS_VALIDATEA || uType == GCS_VALIDATEW) {
          return S_FALSE;
        }
        return E_INVALIDARG;
      }
    
      CONTEXTMENUINFO *pcmi;
      if (FAILED(hr = ReduceOrdinal(&idCmd, &pcmi))) {
        return hr;
      }
    
      return pcmi->pcm->GetCommandString(idCmd, uType,
                            pwReserved, pszName, cchMax);
    }
    

    The GetCommandString method follows the same three-step pattern as InvokeCommand.

    First, dispatch any string-based commands by calling each contained context menu handler until somebody accepts it. If nobody does, then reject the command. (Note the special handling of GCS_VALIDATE, which expects S_FALSE rather than an error code.)

    Second, if the command is specified by ordinal, ask ReduceOrdinal to figure out which contained context menu handler it belongs to.

    Third, pass the reduced command to the applicable contained context menu handler.

    The last methods are made easier by a little helper function:

    HRESULT IContextMenu_HandleMenuMsg2(
                IContextMenu *pcm, UINT uMsg, WPARAM wParam,
                LPARAM lParam, LRESULT* plResult)
    {
      IContextMenu2 *pcm2;
      IContextMenu3 *pcm3;
      HRESULT hr;
      if (SUCCEEDED(hr = pcm->QueryInterface(
                        IID_IContextMenu3, (void**)&pcm3))) {
        hr = pcm3->HandleMenuMsg2(uMsg, wParam, lParam, plResult);
        pcm3->Release();
      } else if (SUCCEEDED(hr = pcm->QueryInterface(
                        IID_IContextMenu2, (void**)&pcm2))) {
        if (plResult) *plResult = 0;
        hr = pcm2->HandleMenuMsg(uMsg, wParam, lParam);
        pcm2->Release();
      }
      return hr;
    }
    

    This helper function takes an IContextMenu interface pointer and tries to invoke IContextMenu3::HandleMenuMsg2; if that fails, then it tries IContextMenu2::HandleMenuMsg; and if that also fails, then it gives up.

    With this helper function, the last two methods are a piece of cake.

    HRESULT CCompositeContextMenu::HandleMenuMsg(
                UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
      LRESULT lres;   // thrown away
      return HandleMenuMsg2(uMsg, wParam, lParam, &lres);
    }
    

    The IContextMenu2::HandleMenuMsg method is just a forwarder to the IContextMenu3::HandleMenuMsg2 method:

    HRESULT CCompositeContextMenu::HandleMenuMsg2(
                UINT uMsg, WPARAM wParam, LPARAM lParam,
                LRESULT* plResult)
    {
      for (UINT icmi = 0; icmi < m_ccmi; icmi++) {
        HRESULT hr;
        if (SUCCEEDED(hr = IContextMenu_HandleMenuMsg2(
                        m_rgcmi[icmi].pcm, uMsg, wParam, lParam,
                        plResult))) {
          return hr;
        }
      }
      return E_NOTIMPL;
    }
    

    And the IContextMenu3::HandleMenuMsg2 method merely walks through the list of context menu handlers, asking each one whether it wishes to handle the command, stopping when one finally does.

    Armed with this composite menu class, we can show it off in our sample program by compositing the "real" context menu with our CTopContextMenu, thereby showing how you can combine multiple context menus into one big context menu.

    HRESULT GetCompositeContextMenuForFile(HWND hwnd,
                LPCWSTR pszPath, REFIID riid, void **ppv)
    {
      *ppv = NULL;
      HRESULT hr;
    
      IContextMenu *rgpcm[2] = { 0 };
      if (SUCCEEDED(hr = GetUIObjectOfFile(hwnd, pszPath,
                            IID_IContextMenu, (void**)&rgpcm[0])) &&
          SUCCEEDED(hr = CTopContextMenu::Create(
                            IID_IContextMenu, (void**)&rgpcm[1])) &&
          SUCCEEDED(hr = CCompositeContextMenu::Create(rgpcm, 2, riid, ppv))) {
          // yay
      }
      if (rgpcm[0]) rgpcm[0]->Release();
      if (rgpcm[1]) rgpcm[1]->Release();
    
      return hr;
    }
    

    This function builds the composite by creating the two contained context menu handlers, then creating a composite context menu that contains both of them. We can use this function by making the same one-line tweak to the OnContextMenu function that we tweaked last time:

    void OnContextMenu(HWND hwnd, HWND hwndContext, int xPos, int yPos)
    {
      POINT pt = { xPos, yPos };
      if (pt.x == -1 && pt.y == -1) {
        pt.x = pt.y = 0;
        ClientToScreen(hwnd, &pt);
      }
    
      IContextMenu *pcm;
      if (SUCCEEDED(GetCompositeContextMenuForFile(
                        hwnd, L"C:\\Windows\\clock.avi",
                        IID_IContextMenu, (void**)&pcm))) {
        ...
    

    Notice that with this composite context menu, the menu help text that we update in our window title tracks across both the original file context menu and our "Top" context menu. Commands from either half are also invoked successfully.

    The value of this approach over the method from part 9 is that you no longer have to coordinate the customization of the context menu between two pieces of code. Under the previous technique, you had to make sure that the code that updated the menu help text was in sync with the code that added the custom commands.

    Under the new method, all the customizations are kept in one place (in the "Top" context menu which is inside the composite context menu), so that the window procedure doesn't need to know what customizations have taken place. This becomes more valuable if there are multiple points at which context menus are displayed, some uncustomized, others customized in different ways. Centralizing the knowledge of the customizations simplifies the design.

    Okay, I think that's enough on context menus for now. I hope you've gotten a better understanding of how they work, how you can exploit them, and most importantly, how you can perform meta-operations on them with techniques like composition.

    There are still some other things you can do with context menus, but I'm going to leave you to experiment with them on your own. For example, you can use the IContextMenu::GetCommandString method to walk the menu and obtain a language-independent command mame for each item. This is handy if you want to, say, remove the "delete" option: You can look for the command whose language-independent name is "delete". This name does not change when the user changes languages; it will always be in English.

    As we've noticed before, you need to be aware that many context menu handlers don't implement the IContextMenu::GetCommandString method fully, so there will likely be commands that you simply cannot get a name for. Them's the breaks.

    [Editing errors corrected, 11am.]

Page 371 of 425 (4,250 items) «369370371372373»