• The Old New Thing

    How to host an IContextMenu, part 9 - Adding custom commands

    • 5 Comments

    The indexMenu, idCmdFirst and idCmdLast parameters to the IContextMenu::QueryContextMenu method allow you, the host, to control where in the context menu the IContextMenu will insert its commands. To illustrate this, let's put two bonus commands on our context menu, with the boring names "Top" and "Bottom".

    We need to reserve some space in our menu identifiers, so let's carve some space out for our private commands:

    #define SCRATCH_QCM_FIRST 1
    #define SCRATCH_QCM_LAST  0x6FFF
    #define IDM_TOP           0x7000
    #define IDM_BOTTOM        0x7001
    

    We reserved 0x1000 commands for ourselves, allowing the IContextMenu to play with commands 1 through 0x6FFF. (We could have carved our space out of the low end, too, by increasing SCRATCH_QCM_FIRST instead of decreasing SCRATCH_QCM_LAST.)

    Go back to the program we had in part 6 and make these changes:

    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(GetUIObjectOfFile(hwnd, L"C:\\Windows\\clock.avi",
                       IID_IContextMenu, (void**)&pcm))) {
        HMENU hmenu = CreatePopupMenu();
        if (hmenu) {
          if (InsertMenu(hmenu, 0, MF_BYPOSITION,
                         IDM_TOP, TEXT("Top")) &&
              InsertMenu(hmenu, 1, MF_BYPOSITION,
                         IDM_BOTTOM, TEXT("Bottom")) &&
              SUCCEEDED(pcm->QueryContextMenu(hmenu, 1,
                                 SCRATCH_QCM_FIRST, SCRATCH_QCM_LAST,
                                 CMF_NORMAL))) {
            pcm->QueryInterface(IID_IContextMenu2, (void**)&g_pcm2);
            pcm->QueryInterface(IID_IContextMenu3, (void**)&g_pcm3);
            int iCmd = TrackPopupMenuEx(hmenu, TPM_RETURNCMD,
                                        pt.x, pt.y, hwnd, NULL);
            if (g_pcm2) {
              g_pcm2->Release();
              g_pcm2 = NULL;
            }
            if (g_pcm3) {
              g_pcm3->Release();
              g_pcm3 = NULL;
            }
            if (iCmd == IDM_TOP) {
              MessageBox(hwnd, TEXT("Top"), TEXT("Custom"), MB_OK);
            } else if (iCmd == IDM_BOTTOM) {
              MessageBox(hwnd, TEXT("Bottom"), TEXT("Custom"), MB_OK);
            } else if (iCmd > 0) {
              CMINVOKECOMMANDINFOEX info = { 0 };
              info.cbSize = sizeof(info);
              info.fMask = CMIC_MASK_UNICODE | CMIC_MASK_PTINVOKE;
              if (GetKeyState(VK_CONTROL) < 0) {
                info.fMask |= CMIC_MASK_CONTROL_DOWN;
              }
              if (GetKeyState(VK_SHIFT) < 0) {
                info.fMask |= CMIC_MASK_SHIFT_DOWN;
              }
              info.hwnd = hwnd;
              info.lpVerb  = MAKEINTRESOURCEA(iCmd - SCRATCH_QCM_FIRST);
              info.lpVerbW = MAKEINTRESOURCEW(iCmd - SCRATCH_QCM_FIRST);
              info.nShow = SW_SHOWNORMAL;
              info.ptInvoke = pt;
              pcm->InvokeCommand((LPCMINVOKECOMMANDINFO)&info);
            }
          }
          DestroyMenu(hmenu);
        }
        pcm->Release();
      }
    }
    

    [Corrected insertion location for "Bottom" 9:42am.]

    Before calling IContextMenu::QueryContextMenu, we added our own custom commands (with menu identifiers outside the range we offer to IContextMenu::QueryContextMenu so they won't conflict), and then call IContextMenu::QueryContextMenu passing the new reduced range as well as specifying that the insertion position is 1 instead of 0.

    When we pass the context menu to to IContextMenu::QueryContextMenu, the menu looks like this:

    Top
    Bottom

    By passing 1 as the insertion point, we are telling the context menu handler that it should insert its commands at position 1 (pushing out what is currently at positions 1 and onwards).

    Top

    ... new stuff ...
     
    Bottom

    After displaying this enhanced context menu, we check which command the user picked, whether it's one of ours (which we handle directly) or one from the inserted portion of the context menu (which we dispatch to the handler).

  • The Old New Thing

    How to host an IContextMenu, part 8 - Optimizing for the default command

    • 5 Comments

    There is a small improvement that can be made to to the program we wrote last time. It involves taking advantage of the last parameter to the IContextMenu::QueryContextMenu method:

    CMF_DEFAULTONLY
    This flag is set when the user is activating the default action, typically by double-clicking. This flag provides a hint for the shortcut menu extension to add nothing if it does not modify the default item in the menu. A shortcut menu extension or drag-and-drop handler should not add any menu items if this value is specified. A namespace extension should add only the default item (if any).

    As the text from MSDN indicates, this flag is a hint to the IContextMenu implementation that it should worry only about the default command.

    void OnContextMenu(HWND hwnd, HWND hwndContext, UINT xPos, UINT yPos)
    {
      IContextMenu *pcm;
      if (SUCCEEDED(GetUIObjectOfFile(hwnd, L"C:\\Windows\\clock.avi",
                       IID_IContextMenu, (void**)&pcm))) {
        HMENU hmenu = CreatePopupMenu();
        if (hmenu) {
          if (SUCCEEDED(pcm->QueryContextMenu(hmenu, 0,
                                 SCRATCH_QCM_FIRST, SCRATCH_QCM_LAST,
                                 CMF_DEFAULTONLY))) {
            UINT id = GetMenuDefaultItem(hmenu, FALSE, 0);
            if (id != (UINT)-1) {
              CMINVOKECOMMANDINFO info = { 0 };
              info.cbSize = sizeof(info);
              info.hwnd = hwnd;
              info.lpVerb = MAKEINTRESOURCEA(id - SCRATCH_QCM_FIRST);
              pcm->InvokeCommand(&info);
            }
          }
          DestroyMenu(hmenu);
        }
        pcm->Release();
      }
    }
    

    With this change on my machine, the time taken by the call to IContextMenu::QueryContextMenu dropped from 100ms to 50ms. Your mileage may vary. It depends on how many context menu extensions you have and how well they respect the CMF_DEFAULTONLY flag.

    (And this exercise highlights how important it is that people who implement the IContextMenu interface pay attention to the flags. If your context menu handler doesn't respect the CMF_DEFAULTONLY flag, then you're part of the problem.)

  • The Old New Thing

    How to host an IContextMenu, part 7 - Invoking the default verb

    • 3 Comments

    When we last left our hero, we were wondering how to invoke the default verb programmatically. Now that we've learned a lot about how IContextMenu is used in the interactive case, we can use that information to guide us in its use in the noninteractive case.

    The key here is using the HMENU to identify the default menu item and just invoke it directly. Go back to the program from part 1 where we left it and make these changes:

    void OnContextMenu(HWND hwnd, HWND hwndContext, UINT xPos, UINT yPos)
    {
      IContextMenu *pcm;
      if (SUCCEEDED(GetUIObjectOfFile(hwnd, L"C:\\Windows\\clock.avi",
                       IID_IContextMenu, (void**)&pcm))) {
        HMENU hmenu = CreatePopupMenu();
        if (hmenu) {
          if (SUCCEEDED(pcm->QueryContextMenu(hmenu, 0,
                                 SCRATCH_QCM_FIRST, SCRATCH_QCM_LAST,
                                 CMF_NORMAL))) {
            UINT id = GetMenuDefaultItem(hmenu, FALSE, 0);
            if (id != (UINT)-1) {
              CMINVOKECOMMANDINFO info = { 0 };
              info.cbSize = sizeof(info);
              info.hwnd = hwnd;
              info.lpVerb = MAKEINTRESOURCEA(id - SCRATCH_QCM_FIRST);
              pcm->InvokeCommand(&info);
            }
          }
          DestroyMenu(hmenu);
        }
        pcm->Release();
      }
    }
    

    We added the call to GetMenuDefaultItem to obtain the default menu item and then set the verb in the form of a menu identifier offset. (I.e., we subtract the starting point we passed to IContextMenu::QueryContextMenu.)

    This code works but could be better. Next time, we'll make a minuscule tweak that improves the performance.

  • The Old New Thing

    Still more goofy terms of service - restrictions on information disclosure

    • 20 Comments

    Part of the terms of the Continental Airlines OnePass Account Privacy rules seem unusually onerous.

    You are authorized to access OnePass account information solely to obtain information regarding your OnePass account and for no other purpose. You may not delegate or grant any power of attorney or other authorization regarding any such access. Any other use of OnePass account information is strictly prohibited. You may reproduce information regarding your own account for personal use and, in consideration of this authorization, you agree that any copy of such information shall retain all copyright and other proprietary notices contained therein. Redistribution in any way requires the express written consent of Continental Airlines.

    Under these terms, I cannot tell you that I have 6058 OnePass miles in my account. Because that is a use of OnePass account information that is not explicitly granted above. In fact, if I were to order a ticket from Continental via their online purchasing system, I would not be allowed to enter my OnePass account number, since disclosing my account number is not an explicitly authorized activity. (I am permitted only to access my account information, not to disclose it.)

    Furthermore, the second highlighted phrase says that even when I jot down my mileage on a scrap of paper, I have to write down next to it, "Copyright (c) 2004 Continental Airlines, Inc. All rights reserved." Similarly, if I tell my brother how many miles I have, I can't just say, hypothetically, "I have 6058 miles." I have to say, "I have 6058 miles, Copyright 2004 Continental Airlines, Inc. All rights reserved."

    And you wonder why people say this country has too many lawyers.

  • The Old New Thing

    What does boldface on a menu mean?

    • 5 Comments

    On many context menus you will see an item in boldface. For example, if you right-click a text file, you will most likely see "Open" in boldface at the top of the mean. What does the boldface mean?

    The boldface menu item is the default command for that menu. It represents the action that would have occurred if you had double-clicked the item instead of viewing its context menu.

    In the above example, the fact that "Open" is in boldface means that if you had double-clicked the text file instead of right-clicked it, you would have opened the document.

    Programmatically, the default menu item is set via the SetMenuDefaultItem function and can be retrieved with the corresponding GetMenuDefaultItem function

    If you put a default menu item in a submenu, then Windows will invoke the default item in the submenu when you double-clicking the submenu's parent. But if you put a default menu item in a top-level menu (i.e., not on a submenu), then it is your responsibility to invoke the default menu item when the user double-clicks the object that led to the menu. (This last bit should be obvious: It is the code for the object being clicked on which decides what to do on a double-click.)

    We'll see more about default menu commands next time.

  • The Old New Thing

    How to host an IContextMenu, part 6 - Displaying menu help

    • 14 Comments

    One of the subtleties of context menus is showing help in the status bar. Now, the program we've been developing doesn't have a status bar, so we'll fake it by putting the help text in the title bar.

    The key method for this task is IContextMenu::GetCommandString, which allows communication with a context menu handler about the verbs in the menu. We'll have to stash yet another interface in our "instance variables disguised as globals".

    IContextMenu *g_pcm;
    

    (Remember, in a "real program", these would be per-window instance variables, not globals.)

    We also need to update that variable during menu tracking.

          g_pcm = pcm;
          int iCmd = TrackPopupMenuEx(hmenu, TPM_RETURNCMD, pt.x, pt.y, hwnd, NULL);
          g_pcm = NULL;
    

    With that out of the way, we can now provide feedback as the user browses the popup menu.

    [Introduction of g_pcm variable added 29 September.]

    // This code is buggy - see below.
    void OnMenuSelect(HWND hwnd, HMENU hmenu,
                      int item, HMENU hmenuPopup, UINT flags)
    {
      if (g_pcm && item >= SCRATCH_QCM_FIRST &&
          item <= SCRATCH_QCM_LAST) {
        TCHAR szBuf[MAX_PATH];
        if (FAILED(g_pcm->GetCommandString(item - SCRATCH_QCM_FIRST,
                                           GCS_HELPTEXT, NULL,
                                           (LPSTR)szBuf, MAX_PATH))) {
          lstrcpyn(szBuf, TEXT("No help available."), MAX_PATH);
        }
        SetWindowText(hwnd, szBuf);
      }
    }
    

    This function checks whether the menu selection is in the range of items that we allowed the context menu to own. If so, we ask for the help string (or use fallback text if the context menu handler didn't provide a help string) and display it as our window title.

    Finally, we insert this function into our window procedure. We want to update the menu selection status even if the context menu handlers do something with it, so we need to call OnMenuSelect before dispatching to the context menu handlers.

    LRESULT CALLBACK
    WndProc(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
    {
        if (uiMsg == WM_MENUSELECT) {
            HANDLE_WM_MENUSELECT(hwnd, wParam, lParam, OnMenuSelect);
        }
        if (g_pcm3) {
    ...
    

    Wait a second, there was a comment up there that said that the OnMenuSelect function is buggy. Where's the bug?

    Well, technically there is no bug, but if you run this program as-is (and I suggest that you do), you'll find that what you get is rather erratic.

    That's because there are a lot of buggy context menu handlers out there.

    Some context menu handlers don't support Unicode; others don't support Ansi. What's really fun is that instead of returning E_NOTIMPL, they return S_OK but don't actually do anything. Other context menus have buffer overflow problems and write to the buffer beyond the actual size you specified.

    Welcome to the world of application compatibility.

    Let's write a helper function that tries to hide all of these weirdnesses.

    HRESULT IContextMenu_GetCommandString(
        IContextMenu *pcm, UINT_PTR idCmd, UINT uFlags,
        UINT *pwReserved, LPWSTR pszName, UINT cchMax)
    {
      // Callers are expected to be using Unicode.
      if (!(uFlags & GCS_UNICODE)) return E_INVALIDARG;
    
      // Some context menu handlers have off-by-one bugs and will
      // overflow the output buffer. Let's artificially reduce the
      // buffer size so a one-character overflow won't corrupt memory.
      if (cchMax <= 1) return E_FAIL;
      cchMax--;
    
      // First try the Unicode message.  Preset the output buffer
      // with a known value because some handlers return S_OK without
      // doing anything.
      pszName[0] = L'\0';
    
      HRESULT hr = pcm->GetCommandString(idCmd, uFlags, pwReserved,
                                         (LPSTR)pszName, cchMax);
      if (SUCCEEDED(hr) && pszName[0] == L'\0') {
        // Rats, a buggy IContextMenu handler that returned success
        // even though it failed.
        hr = E_NOTIMPL;
      }
    
      if (FAILED(hr)) {
        // try again with ANSI - pad the buffer with one extra character
        // to compensate for context menu handlers that overflow by
        // one character.
        LPSTR pszAnsi = (LPSTR)LocalAlloc(LMEM_FIXED,
                                          (cchMax + 1) * sizeof(CHAR));
        if (pszAnsi) {
          pszAnsi[0] = '\0';
          hr = pcm->GetCommandString(idCmd, uFlags & ~GCS_UNICODE,
                                      pwReserved, pszAnsi, cchMax);
          if (SUCCEEDED(hr) && pszAnsi[0] == '\0') {
            // Rats, a buggy IContextMenu handler that returned success
            // even though it failed.
            hr = E_NOTIMPL;
          }
          if (SUCCEEDED(hr)) {
            if (MultiByteToWideChar(CP_ACP, 0, pszAnsi, -1,
                                    pszName, cchMax) == 0) {
              hr = E_FAIL;
            }
          }
          LocalFree(pszAnsi);
    
        } else {
          hr = E_OUTOFMEMORY;
        }
      }
      return hr;
    }
    

    The shell has lots of strange functions like this.

    [pszAnsi comparison fixed, 29 September.]

    With this helper function, we can fix our help text function.

    void OnMenuSelect(HWND hwnd, HMENU hmenu,
                      int item, HMENU hmenuPopup, UINT flags)
    {
      if (g_pcm && item >= SCRATCH_QCM_FIRST &&
          item <= SCRATCH_QCM_LAST) {
        WCHAR szBuf[MAX_PATH];
        if (FAILED(IContextMenu_GetCommandString(g_pcm,
                                           item - SCRATCH_QCM_FIRST,
                                           GCS_HELPTEXTW, NULL,
                                           szBuf, MAX_PATH))) {
          lstrcpynW(szBuf, L"No help available.", MAX_PATH);
        }
        SetWindowTextW(hwnd, szBuf);
      }
    }
    

    This new version displays help texts for all the context menu handlers that support it, in spite of the attempts of many of those context menu handlers to get it wrong or even create a buffer overflow security vulnerability.

    Okay, that was quite a long digression from part 1 of this series. Let's return to the subject of invoking the default verb next time.

  • The Old New Thing

    How to host an IContextMenu, part 5 - Handling menu messages

    • 2 Comments

    One bug that was called out immediately in our first attempt at displaying the context menu to the user is that the Open With and Send To submenus don't work.

    The reason for this is that these submenus are delay-generated (which explains why they don't contain anything interesting when you expand them) and owner-drawn (which you can't notice yet because of the first problem, but trust me, they are).

    This is where the IContextMenu2::HandleMenuMsg and IContextMenu3::HandleMenuMsg2 methods are used.

    Historical note: IContextMenu2::HandleMenuMessage is on its own interface rather than being merged with the base interface IContextMenu because it was added late in Windows 95 development, so it was considered safer to add a derived interface than to make everybody who had been writing Windows 95 shell extensions go back and rewrite their code. IContextMenu3::HandleMenuMessage2 was added in Internet Explorer 4 (I think) when it became clear that the ability for a context menu extension to override the default message return value was necessary in order to support keyboard accessibility in owner-drawn context menus.

    In a "real program", these two variables would be class members associated with the window, but this is just a sample program, so they are globals. When you write your own programs, don't use global variables here because they will result in mass mayhem once you get a second window, since both of them will try to talk to the interface even though only the window displaying the context menu should be doing so.

    IContextMenu2 *g_pcm2;
    IContextMenu3 *g_pcm3;
    

    These two new variables track the IContextMenu2 and IContextMenu3 interfaces of the active tracked popup menu. We need to initialize and uninitalize them around our call to TrackPopupMenuEx:

          pcm->QueryInterface(IID_IContextMenu2, (void**)&g_pcm2);
          pcm->QueryInterface(IID_IContextMenu3, (void**)&g_pcm3);
          int iCmd = TrackPopupMenuEx(hmenu, TPM_RETURNCMD, pt.x, pt.y, hwnd, NULL);
          if (g_pcm2) {
            g_pcm2->Release();
            g_pcm2 = NULL;
          }
          if (g_pcm3) {
            g_pcm3->Release();
            g_pcm3 = NULL;
          }
    

    And finally we need to invoke the HandleMenuMessage/HandleMenuMessage methods in the window procedure:

    LRESULT CALLBACK
    WndProc(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
    {
        if (g_pcm3) {
            LRESULT lres;
            if (SUCCEEDED(g_pcm3->HandleMenuMsg2(uiMsg, wParam, lParam, &lres))) {
              return lres;
            }
        } else if (g_pcm2) {
            if (SUCCEEDED(g_pcm2->HandleMenuMsg(uiMsg, wParam, lParam))) {
              return 0;
            }
        }
    
        switch (uiMsg) {
        ....
    

    In the window procedure, we ask the context menu whether it wishes to handle the menu message. If so, then we stop and return the desired value (if HandleMenuMsg2) or just zero (if HandleMenuMsg).

    With these changes, run the scratch program again and observe that the Open With and Send To submenus now operate as expected.

    Next time: Getting menu help text.

  • The Old New Thing

    The unrecognized assistants on those do-it-yourself shows

    • 28 Comments

    Some people from my knitting group have been among those assisting in the preparation of today's episode of the knitting show Knitty Gritty on The Do It Yourself Network.

    Stop reading if you don't want the television illusion shattered, or at least cracked a bit.

    Each volunteer was assigned a section of a sweater to knit. The show's host will take those pieces and do some short demonstration with it, perhaps sewing pieces together or illustrating one of the trickier parts.

    After each volunteer made their assigned part, they also had to start working on the same sweater again, but not to finish it. They will be in the background knitting while the host does the feature demonstration.

    What's more, each volunteer has already been told what problem they will be having with the sweater, so that the host can help extricate them from whatever mess they got themselves into. Nevermind that they already made the sweater that the host was using in the demonstration just a few minutes ago. Clearly, they know what they're doing; they did all the knitting!

    If you thought the hosts of these shows actually did the cooking/sewing/knitting/whatever, well now you know better.

  • The Old New Thing

    How to host an IContextMenu, part 4 - Key context

    • 6 Comments

    Another of the bugs you may have noticed in our first attempt at displaying the context menu to the user is that the Delete command doesn't alter its behavior depending on whether you hold the shift key. Recall that holding the shift key changes the behavior of the Delete command, causing it to delete a file immediately instead of moving it to the Recycle Bin. But in our sample program, it always offers to move the file to the Recycle Bin, even if you have the shift key down.

    (You can see the difference in the wording of the dialog and in the icon. If the operation is to move the item into the Recycle Bin, you get a Recycle Bin icon and the text asks you to confirm sending the item to the Recycle Bin. If the operation will delete the item permanently, then you get an icon that shows a file and a folder fading away and the text asks you to confirm deleting the item.)

    To convey this information to the context menu, you need to pass the key states in the CMINVOKECOMMANDINFOEX structure.

              CMINVOKECOMMANDINFOEX info = { 0 };
              info.cbSize = sizeof(info);
              info.fMask = CMIC_MASK_UNICODE | CMIC_MASK_PTINVOKE;
              if (GetKeyState(VK_CONTROL) < 0) {
                info.fMask |= CMIC_MASK_CONTROL_DOWN;
              }
              if (GetKeyState(VK_SHIFT) < 0) {
                info.fMask |= CMIC_MASK_SHIFT_DOWN;
              }
    

    Make this change and observe that the dialogs you get from the Delete option now respect your shift key state.

    Warning: Before playing with this, make sure that you have enabled delete confirmation warnings or you will end up deleting your clock.avi file for real! If you want to play around with the Delete option, you may want to tweak the program so it operates on a file you don't mind losing.

    Exercise: There's another place where key context influences the context menu, namely the convention that holding the shift key while right-clicking enables "extended verbs". These are verbs that are lesser-used and therefore do not appear on the conventional context menu to avoid creating clutter. For homework, incorporate the extended verb convention into the sample program.

    [Sorry today's entries are late. Had problems connecting to the blog server.]

  • The Old New Thing

    Penguins do not fall over!

    • 20 Comments

    Everyone has read the story about penguins falling over backwards while observing low-flying aircraft. The Annals of Improbable Research reports that Dr. Richard Stone has been researching the ecological effects of helicopter overflights on King penguins and his preliminary results show that King penguins do not, in fact, fall over backwards (or forwards or even sideways) when you fly over them. On the other hand, low-flying aircraft do create distress among the penguins; Dr. Stone recommends a minimum overflight altitude of 1000 feet.

    [Typo fixed, 24 September.]

Page 376 of 429 (4,287 items) «374375376377378»