Search code examples
c#windowswinapicontextmenuwindows-shell

C# - How to trigger the App in open with context menu properly


I need to display the Windows native context menu of Open with in my application and I already can show it. However, I encountered a problem is I can't execute any Apps (Photos/Paint/...) in Open with submenu properly.

For example, I press right click on a jpg image and hover cursor to open with, then choose Paint to open it, but nothing happen (no exeception, error) after clicking Paint (There is no Paint process in Task Manager).

The screenshot below can reveals my problem precisely, Apps in red block can't be execute properly (Neither native nor third-party applications can be executed). But Search the Microsoft Store and Choose another app can work well.

enter image description here

I found that @yberk 's post also mentioned this problem, but he didn't find any solution

I have read lots of documents and examples, but still can't figure out the problem.

By the way, my development environment is .NET Framework 4.7.2 on Windows10 2004 version.

The following is my code snippet

// My entry point. Right click on the tofu.png
static void Main(string[] args)
{
        
    FileInfo[] files = new FileInfo[1];
    files[0] = new FileInfo(@"k:\qqq\tofu.png");
    ShellContextMenu scm = new ShellContextMenu();
    scm.ShowContextMenu(files, Cursor.Position);
}

Overwrite WindowMessages - Handle user's behavior on the context menu

protected override void WndProc(ref Message m)
{
    #region IContextMenu

    if (_oContextMenu != null &&
        m.Msg == (int)WM.MENUSELECT &&
        ((int)ShellHelper.HiWord(m.WParam) & (int)MFT.SEPARATOR) == 0 &&
        ((int)ShellHelper.HiWord(m.WParam) & (int)MFT.POPUP) == 0)
    {
        string info = string.Empty;

        if (ShellHelper.LoWord(m.WParam) == (int)CMD_CUSTOM.ExpandCollapse)
            info = "Expands or collapses the current selected item";
        else
        {
            info = ""
        }
    }
    #endregion

    #region IContextMenu2
    if (_oContextMenu2 != null &&
        (m.Msg == (int)WM.INITMENUPOPUP ||
            m.Msg == (int)WM.MEASUREITEM ||
            m.Msg == (int)WM.DRAWITEM))
    {
        if (_oContextMenu2.HandleMenuMsg((uint)m.Msg, m.WParam, m.LParam) == S_OK)
            return;
    }
    #endregion

    #region IContextMenu3
    if (_oContextMenu3 != null &&
        m.Msg == (int)WM.MENUCHAR)
    {
        if (_oContextMenu3.HandleMenuMsg2((uint)m.Msg, m.WParam, m.LParam, IntPtr.Zero) == S_OK)
            return;
    }
    #endregion

    base.WndProc(ref m);
}

Show the context menu while right clicking on a file

private void ShowContextMenu(Point pointScreen)
{
    IntPtr pMenu = IntPtr.Zero,
        iContextMenuPtr = IntPtr.Zero,
        iContextMenuPtr2 = IntPtr.Zero,
        iContextMenuPtr3 = IntPtr.Zero;

    try
    {
        // Gets the interfaces to the context menu (IContextMenu)
        if (false == GetContextMenuInterfaces(_oParentFolder, _arrPIDLs, out iContextMenuPtr))
        {
            ReleaseAll();
            return;
        }
        // Create main context menu instance            
        pMenu = CreatePopupMenu();

        // Get all items of context menu
        int nResult = _oContextMenu.QueryContextMenu(
            pMenu,
            0,
            CMD_FIRST,
            CMD_LAST,
            CMF.EXPLORE |
            CMF.NORMAL |
            ((Control.ModifierKeys & Keys.Shift) != 0 ? CMF.EXTENDEDVERBS : 0));

        Marshal.QueryInterface(iContextMenuPtr, ref IID_IContextMenu2, out iContextMenuPtr2);
        Marshal.QueryInterface(iContextMenuPtr, ref IID_IContextMenu3, out iContextMenuPtr3);


        _oContextMenu2 = (IContextMenu2)Marshal.GetTypedObjectForIUnknown(iContextMenuPtr2, typeof(IContextMenu2));
        _oContextMenu3 = (IContextMenu3)Marshal.GetTypedObjectForIUnknown(iContextMenuPtr3, typeof(IContextMenu3));

        // wait for the user to select an item and will return the id of the selected item.
        uint nSelected = TrackPopupMenuEx(
            pMenu,
            TPM.RETURNCMD,
            pointScreen.X,
            pointScreen.Y,
            this.Handle,
            IntPtr.Zero);

        if (nSelected != 0)
        {
            InvokeCommand(_oContextMenu, nSelected, _strParentFolder, pointScreen);
        }
    }
    catch
    {
        throw;
    }
    finally
    {
        ReleaseAll();
    }
}

InvokeCommand - trigger specific command based on lpverb (get that by id position)

private void InvokeCommand(IContextMenu oContextMenu, uint nCmd, string strFolder, Point pointInvoke)
{
    CMINVOKECOMMANDINFOEX invoke = new CMINVOKECOMMANDINFOEX();
    invoke.cbSize = cbInvokeCommand;
    invoke.lpVerb = (IntPtr)(nCmd - CMD_FIRST);
    invoke.lpDirectory = strFolder;
    invoke.lpVerbW = (IntPtr)(nCmd - CMD_FIRST);
    invoke.lpDirectoryW = strFolder;
    invoke.fMask = CMIC.UNICODE | CMIC.PTINVOKE |
        ((Control.ModifierKeys & Keys.Control) != 0 ? CMIC.CONTROL_DOWN : 0) |
        ((Control.ModifierKeys & Keys.Shift) != 0 ? CMIC.SHIFT_DOWN : 0);
    invoke.ptInvoke = new POINT(pointInvoke.X, pointInvoke.Y);
    invoke.nShow = SW.SHOWNORMAL;

    oContextMenu.InvokeCommand(ref invoke);
}

Solution

  • Finally, I found the reason. We need put the [STAThread] on the entry point.

    See windows document STAThread

    This attribute must be present on the entry point of any application that uses Windows Forms; if it is omitted, the Windows components might not work correctly. If the attribute is not present, the application uses the multithreaded apartment model, which is not supported for Windows Forms.

    namespace test
    {
        class Program
        {
            [STAThread]
            static void Main(string[] args)
            {
                FileInfo[] files = new FileInfo[1];            
                files[0] = new FileInfo(@"K:\qqq\tofu.png");
                ShellContextMenu scm = new ShellContextMenu();
                scm.ShowContextMenu(files, Cursor.Position);
            }
        }
        .
        .
        .
    }