• The Old New Thing

    How to host an IContextMenu, part 10 - Composite extensions - groundwork

    • 11 Comments

    You might wonder why the IContextMenu interface operates on menu identifier offsets so much rather than with the menu identifiers themselves.

    The reason is to support something which I will call "compositing".

    You may have multiple context menu extensions that you want to combine into one giant context menu extension. The shell does this all over the place. For example, the context menu we have been playing with all this time is really a composite of several individual context menu extensions: the static registry verbs plus all the COM-based extensions like "Send To", "Open With", and anything else that may have been added by a program you installed (like a virus checker).

    So before we can write a compositor, we need to have a second context menu to composite. Here's a quickie that implements two commands, let's call them "Top" and "Next" for lack of anything interesting to do.

    class CTopContextMenu : public IContextMenu
    {
    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);
    
      static HRESULT Create(REFIID riid, void **ppv);
    
    private:
      CTopContextMenu() : m_cRef(1), m_cids(0) { }
    
    private:
      HRESULT ValidateCommand(UINT_PTR idCmd, BOOL fUnicode,
                              UINT *puOffset);
      HRESULT Top(LPCMINVOKECOMMANDINFO lpici);
      HRESULT Next(LPCMINVOKECOMMANDINFO lpici);
    
    private:
      ULONG m_cRef;
      UINT  m_cids;
    };
    

    The class declaration isn't particularly interesting. We are not owner-draw so we don't bother implementing IContextMenu2 or IContextMenu3.

    First, some basic paperwork for getting off the ground.

    HRESULT CTopContextMenu::Create(REFIID riid, void **ppv)
    {
      *ppv = NULL;
      HRESULT hr;
      CTopContextMenu *self = new CTopContextMenu();
      if (self) {
        hr = self->QueryInterface(riid, ppv);
        self->Release();
      } else {
        hr = E_OUTOFMEMORY;
      }
      return hr;
    }
    

    We have two commands. Instead of hard-coding the numbers 0 and 1, let's give them nice names.

    #define TOPCMD_TOP      0
    #define TOPCMD_NEXT     1
    #define TOPCMD_MAX      2
    

    And here's a table that we're going to use to help us manage these two commands.

    const struct COMMANDINFO {
      LPCSTR  pszNameA;
      LPCWSTR pszNameW;
      LPCSTR  pszHelpA;
      LPCWSTR pszHelpW;
    } c_rgciTop[] = {
      { "top",  L"top",
        "The top command",  L"The top command", }, // TOPCMD_TOP
      { "next", L"next",
        "The next command", L"The next command", },// TOPCMD_NEXT
    };
    

    Our TOPCMD_* values conveniently double as indices into the c_rgciTop array.

    Next come the boring parts of a COM object:

    HRESULT CTopContextMenu::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);
      }
    
      *ppv = punk;
      if (punk) {
        punk->AddRef();
        return S_OK;
      } else {
        return E_NOINTERFACE;
      }
    }
    
    ULONG CTopContextMenu::AddRef()
    {
      return ++m_cRef;
    }
    
    ULONG CTopContextMenu::Release()
    {
      ULONG cRef = --m_cRef;
      if (cRef == 0) delete this;
      return cRef;
    }
    

    Finally, we get to something interesting: IContextMenu::QueryContextMenu. Things to watch out for in the code below:

    • Checking whether there is room between idCmdFirst and idCmdLast is complicated by the fact that idCmdLast is endpoint-inclusive, which forces a strange +1. Another reason to prefer endpoint-exclusive ranges.
    • If the CMF_DEFAULTONLY flag is set, then we don't bother adding our menu items since none of our options is the default menu item.
    HRESULT CTopContextMenu::QueryContextMenu(
        HMENU hmenu, UINT indexMenu, UINT idCmdFirst,
        UINT idCmdLast, UINT uFlags)
    {
      m_cids = 0;
    
      if ((int)(idCmdLast - idCmdFirst + 1) >= TOPCMD_MAX &&
        !(uFlags & CMF_DEFAULTONLY)) {
        InsertMenu(hmenu, indexMenu + TOPCMD_TOP, MF_BYPOSITION,
                   idCmdFirst + TOPCMD_TOP, TEXT("Top"));
        InsertMenu(hmenu, indexMenu + TOPCMD_NEXT, MF_BYPOSITION,
                   idCmdFirst + TOPCMD_NEXT, TEXT("Next"));
        m_cids = TOPCMD_MAX;
      }
    
      return MAKE_HRESULT(SEVERITY_SUCCESS, 0, m_cids);
    }
    

    In order to implement the next few methods, we need to have some culture-invariant comparison functions.

    int strcmpiA_invariant(LPCSTR psz1, LPCSTR psz2)
    {
      return CompareStringA(LOCALE_INVARIANT, NORM_IGNORECASE,
                            psz1, -1, psz2, -1) - CSTR_EQUAL;
    }
    
    int strcmpiW_invariant(LPCWSTR psz1, LPCWSTR psz2)
    {
      return CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE,
                            psz1, -1, psz2, -1) - CSTR_EQUAL;
    }
    

    These are like the strcmpi functions except that they use the invariant locale since they will be used to compare canonical strings rather than strings that are meaningful to an end user. (More discussion here in MSDN.)

    Now we have enough to write a helper function which is central to the context menu: Figuring out which command somebody is talking about.

    Commands can be passed to the IContextMenu interface either (a) by ordinal or by name, and either (b) as ANSI or as Unicode. This counts as either three ways or four ways, depending on whether you treat "ANSI as ordinal" and "Unicode as ordinal" as the same thing or not.

    HRESULT CTopContextMenu::ValidateCommand(UINT_PTR idCmd,
                            BOOL fUnicode, UINT *puOffset)
    {
      if (!IS_INTRESOURCE(idCmd)) {
        if (fUnicode) {
          for (idCmd = 0; idCmd < TOPCMD_MAX; idCmd++) {
            if (strcmpiW_invariant((LPCWSTR)idCmd,
                                   c_rgciTop[idCmd].pszNameW) == 0) {
              break;
            }
          }
        } else {
          for (idCmd = 0; idCmd < TOPCMD_MAX; idCmd++) {
            if (strcmpiA_invariant((LPCSTR)idCmd,
                                   c_rgciTop[idCmd].pszNameA) == 0) {
              break;
            }
          }
        }
      }
    
      if (idCmd < m_cids) {
        *puOffset = (UINT)idCmd;
        return S_OK;
      }
    
      return E_INVALIDARG;
    }
    

    This helper function takes a "something" parameter in the form of a UINT_PTR and a flag that indicates whether that "something" is ANSI or Unicode. The function itself checks whether the "something" is a string or an ordinal. If a string, then it converts that string into an ordinal by looking for it in the table of commands in the appropriate character set and using a locale-insensitive comparison. Notice that if the string is not found, then idCmd is left equal to TOPCMD_MAX, which is an invalid value (and therefore is neatly handled by the fall-through).

    After the (possibly failed) conversion to an ordinal, the ordinal is checked for validity; if valid, then the ordinal is returned back for further processing.

    With this helper function the implementation of the other methods of the IContextMenu interface are a lot easier.

    We continue with the IContextMenu::InvokeCommand method:

    HRESULT CTopContextMenu::InvokeCommand(
                                LPCMINVOKECOMMANDINFO lpici) {
    
      CMINVOKECOMMANDINFOEX* lpicix = (CMINVOKECOMMANDINFOEX*)lpici;
      BOOL fUnicode = lpici->cbSize >= sizeof(CMINVOKECOMMANDINFOEX) &&
                      (lpici->fMask & CMIC_MASK_UNICODE);
      UINT idCmd;
      HRESULT hr = ValidateCommand(fUnicode ? (UINT_PTR)lpicix->lpVerbW
                                            : (UINT_PTR)lpici->lpVerb,
                                   fUnicode, &idCmd);
      if (SUCCEEDED(hr)) {
        switch (idCmd) {
        case TOPCMD_TOP: hr = Top(lpici); break;
        case TOPCMD_NEXT: hr = Next(lpici); break;
        default: hr = E_INVALIDARG; break;
        }
      }
      return hr;
    }
    

    Here is a case where the "Are there three cases or four?" question lands squarely on the side of "four". There are two forms of the CMINVOKECOMMANDINFO structure, the base structure (which is ANSI-only) and the extended structure CMINVOKECOMMANDINFOEX which adds Unicode support.

    If the structure is CMINVOKECOMMANDINFOEX and the CMIC_MASK_UNICODE flag is set, then the Unicode fields of the CMINVOKECOMMANDINFOEX structure should be used in preference to the ANSI ones.

    This means that there are indeed four scenarios:

    1. ANSI string in lpVerb member.
    2. Ordinal in lpVerb member.
    3. Unicode string in lpVerbW member.
    4. Ordinal in lpVerbW member.

    After figuring out whether the parameter is ANSI or Unicode, we ask ValidateCommand to do the work of validating the verb and converting it to an ordinal, at which point we use the ordinal in a switch statement to dispatch the actual operation.

    Failing to implement string-based command invocation is an extremely common oversight in context menu implementations. Doing so prevents people from invoking your verbs programmatically.

    "Why should I bother to let people invoke my verbs programmatically?"

    Because if you don't, then people won't be able to write programs like the one we are developing in this series of articles! For example, suppose your context menu extension lets people "Frob" a file. If you don't expose this verb programmability, then it is impossible to write a program that, say, takes all the files modified in the last twenty-four hours and Frobs them.

    (I'm always amused by the people who complain that Explorer doesn't expose enough customizability programmatically, while simultaneously not providing the same degree of programmatic customizability in their own programs.)

    Oh wait, I guess I should implement those two operations. They don't do anything particularly interesting.

    HRESULT CTopContextMenu::Top(LPCMINVOKECOMMANDINFO lpici)
    {
      MessageBox(lpici->hwnd, TEXT("Top"), TEXT("Title"), MB_OK);
      return S_OK;
    }
    
    HRESULT CTopContextMenu::Next(LPCMINVOKECOMMANDINFO lpici)
    {
      MessageBox(lpici->hwnd, TEXT("Next"), TEXT("Title"), MB_OK);
      return S_OK;
    }
    

    The remaining method is IContextMenu::GetCommandString, which is probably the one people most frequently get wrong since the consequences of getting it wrong are not immediately visible to the implementor. It is the people who are trying to access the context menu programmatically who most likely to notice that the method isn't working properly.

    HRESULT CTopContextMenu::GetCommandString(
                                UINT_PTR    idCmd,
                                UINT        uType,
                                UINT      * pwReserved,
                                LPSTR       pszName,
                                UINT        cchMax)
    {
      UINT id;
      HRESULT hr = ValidateCommand(idCmd, uType & GCS_UNICODE, &id);
      if (FAILED(hr)) {
        if (uType == GCS_VALIDATEA || uType == GCS_VALIDATEW) {
          hr = S_FALSE;
        }
        return hr;
      }
    
      switch (uType) {
      case GCS_VERBA:
        lstrcpynA(pszName, c_rgciTop[id].pszNameA, cchMax);
        return S_OK;
    
      case GCS_VERBW:
        lstrcpynW((LPWSTR)pszName, c_rgciTop[id].pszNameW, cchMax);
        return S_OK;
    
      case GCS_HELPTEXTA:
        lstrcpynA(pszName, c_rgciTop[id].pszHelpA, cchMax);
        return S_OK;
    
      case GCS_HELPTEXTW:
        lstrcpynW((LPWSTR)pszName, c_rgciTop[id].pszHelpW, cchMax);
        return S_OK;
    
      case GCS_VALIDATEA:
      case GCS_VALIDATEW:
        return S_OK;    // all they wanted was validation
      }
    
      return E_NOTIMPL;
    }
    

    Here again we use the ValidateCommand method to do the hard work of validating the command, which is passed in the idCmd parameter, with interpretive assistance in the GCS_UNICODE flag of the uType parameter.

    If the command is not valid, then we propagate the error code, except in the GCS_VALIDATE cases, where the documentation says that we should return S_FALSE to indicate that the command is not valid.

    If the command is valid, we return the requested information, which is handled by a simple switch statement.

    Okay, now that we have this context menu, we can even test it out a little bit. Throw out the changes from part 9 and return to the program as it was in part 6, making the following change to the OnContextMenu function:

    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(CTopContextMenu::Create(
                        IID_IContextMenu, (void**)&pcm))) {
        ...
    

    We now obtain our context menu not by calling the GetUIObjectOfFile function but rather by constructing a CTopContextMenu object. Since our CTopContextMenu implements IContextMenu, all the remaining code can be left unchanged.

    When you run this program, observe that even the help text works.

    Ah, one of the powers of operating with interfaces rather than objects: You can swap out the object and the rest of the code doesn't even realize what happened, so long as the interface stays the same.

    Okay, today was a long day spent just laying groundwork, just writing what has to be written. No breakthroughs, no "aha" moments, just typing. Read the method, understand what you have to do, and do it.

    Next time, we're going to see context menu composition, using this context menu as one of the components.

  • The Old New Thing

    The macros for declaring and implementing COM interfaces

    • 16 Comments

    There are two ways of declaring COM interfaces, the hard way and the easy way.

    The easy way is to use an IDL file and let the MIDL compiler generate your COM interface for you. If you let MIDL do the work, then you also get __uuidof support at no extra charge, which is a very nice bonus.

    The hard way is to do it all by hand. If you choose this route, your interface will look something like this:

    #undef  INTERFACE
    #define INTERFACE   ISample2
    
    DECLARE_INTERFACE_(ISample2, ISample)
    {
        BEGIN_INTERFACE
    
        // *** IUnknown methods ***
        STDMETHOD(QueryInterface)(THIS_ REFIID riid, void **ppv) PURE;
        STDMETHOD_(ULONG,AddRef)(THIS) PURE;
        STDMETHOD_(ULONG,Release)(THIS) PURE;
    
        // ** ISample methods ***
        STDMETHOD(Method1)(THIS) PURE;
        STDMETHOD_(int, Method2)(THIS) PURE;
    
        // *** ISample2 methods ***
        STDMETHOD(Method3)(THIS_ int iParameter) PURE;
        STDMETHOD_(int, Method4)(THIS_ int iParameter) PURE;
    
        END_INTERFACE
    };
    

    What are the rules?

    • You must set the INTERFACE macro to the name of the interface being declared. Note that you need to #undef any previous value before you #define the new one.
    • You must use the DECLARE_INTERFACE and DECLARE_INTERFACE_ macros to generate the preliminary bookkeeping for an interface. Use DECLARE_INTERFACE for interfaces that have no base class and DECLARE_INTERFACE_ for interfaces that derive from some other interface. In our example, we derive the ISample2 interface from ISample. Note: In practice, you will never find the plain DECLARE_INTERFACE macro because all interfaces derive from IUnknown if nothing else.
    • You must list all the methods of the base interfaces in exactly the same order that they are listed by that base interface; the methods that you are adding in the new interface must go last.
    • You must use the STDMETHOD or STDMETHOD_ macros to declare the methods. Use STDMETHOD if the return value is HRESULT and STDMETHOD_ if the return value is some other type.
    • If your method has no parameters, then the argument list must be (THIS). Otherwise, you must insert THIS_ immediately after the open-parenthesis of the parameter list.
    • After the parameter list and before the semicolon, you must say PURE.
    • Inside the curly braces, you must say BEGIN_INTERFACE and END_INTERFACE.

    There is a reason for each of these rules. They have to do with being able to use the same header for both C and C++ declarations and with interoperability with different compilers and platforms.

    • You must set the INTERFACE macro because its value is used by the THIS and THIS_ macros later.
    • You must use one of the DECLARE_INTERFACE* macros to ensure that the correct prologue is emitted for both C and C++. For C, a vtable structure is declared, whereas for C++ the compiler handles the vtable automatically; on the other hand, since C++ has inheritance, the macros need to specify the base class so that upcasting will work.
    • You must list the base class methods in exactly the same order as in the original declarations so that the C vtable structure for your derived class matches the structure for the base class for the extent that they overlap. This is required to preserve the COM rule that a derived interface can be used as a base interface.
    • You must use the STDMETHOD and STDMETHOD_ macros to ensure that the correct calling conventions are declared for the function prototypes. For C, the macro creates a function pointer in the vtable; for C++, the macro creates a virtual function.
    • The THIS and THIS_ macros are used so that the C declaration explicitly declares the "this" parameter which in C++ is implied. Different versions are needed depending on the number of parameters so that a spurious trailing comma is not generated in the zero-parameter case.
    • The word PURE ensures that the C++ virtual function is pure, because one of the defining characteristics of COM interfaces is that all methods are pure virtual.
    • The BEGIN_INTERFACE and END_INTERFACE macros emit compiler-specific goo which the compiler vendor provides in order to ensure that the generated interface matches the COM vtable layout rules. Different compilers have historically required different goo, though the need for goo is gradually disappearing over time.

    And you wonder why I called it "the hard way".

    Similar rules apply when you are implementing an interface. Use the STDMETHODIMP and STDMETHODIMP_ macros to declare your implementations so that they get the proper calling convention attached to them. We'll see examples of this next time.

  • The Old New Thing

    Those partisan non-partisan groups

    • 31 Comments

    Just because they say they're non-partisan doesn't mean that they're non-partisan.

    Friday night, I got a phone call from "Victor" at "Washington Counts" who came right out and asked me whom I was going to vote for.

    I asked him to repeat the name of the organization he represents, and he said, "Washington Counts, a non-partisan organization, working in cooperation with Emily's List."

    I couldn't find any information about "Washington Counts", but I did find Emily's List, which is a blatantly partisan group. Their own About Page say that they are "dedicated to... electing pro-choice Democratic women".

    I pointed out to "Victor" that Emily's List is a partisan group. He ignored me and repeated his question, asking me whom I was going to vote for.

    I told him that I was unlikely to be inclined to assist an organization that start out by lying to me.

    And then I heard a click and a recorded voice saying, "This survey was sponsored by Emily's List. On the web at www.emilyslist.org."

    Let's see what happened here. Somebody claimed to be from "Washington Counts", but in fact they were from "Emily's List". [Corrected identification 9:38am.] That person claimed to be representing a non-partisan group, but in fact the group is highly partisan.

    I guess if you're going to lie, you may as well go all-out.

    Of course, this could have been a double-fake-out. Perhaps it was really a pro-life Republican group pretending to be a pro-choice Democratic group?

    These sorts of double-fake-outs are not unheard of. In California, anybody who pays the requisite fee can get a statement printed in the voter's guide. It has been known to occur that somebody who holds one position on an issue submits an incoherent or absurd statement in support of the opposition position, thereby making the opposition look stupid. During the 1996 U.S. presidential election primary season, Candidate X sponsored a telephone survey asking voters "If Candidate Y took <controversial position>, would your opinion of Candidate Y go up, down, or stay the same?" The intent here was to start the rumor that Candidate Y was actually considering taking said controversial position (which would have undermined Candidate Y's traditional support).

    You can never tell where the dirty tricks are coming from in politics.

  • 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.

Page 373 of 426 (4,260 items) «371372373374375»