C# - 如何正确触发使用上下文菜单打开的应用程序

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

我需要在我的应用程序中显示 Open with 的 Windows 本机上下文菜单,我已经可以显示它了。但是,我遇到了一个问题,我无法正确执行 Open with 子菜单中的任何应用程序 (Photos/Paint/...)。

比如我在一张jpg图片上右击鼠标悬停打开,然后选择Paint打开,但是点击Paint后没有任何反应(没有异常,报错)(Task中没有Paint进程经理)。

下面的截图可以准确地揭示我的问题,红色方框内的应用程序无法正常执行(Native native nor third-party applications can be executed)。但是 Search the Microsoft StoreChoose another app 可以很好地工作。

我发现@yberk的post也提到了这个问题,但是他没有找到任何解决方案

看了很多文档和例子,还是没搞清楚问题所在

对了,我的开发环境是.NET Framework 4.7.2 on Windows10 2004 version.

以下是我的代码片段

// 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);
}

覆盖 WindowMessages - 处理用户在上下文菜单上的行为

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);
}

右键单击文件时显示上下文菜单

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 - 触发基于 lpverb 的特定命令(通过 id 位置获取)

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);
}

终于找到原因了。我们需要把 [STAThread] 放在入口点上。

参见 windows 文档 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);
        }
    }
    .
    .
    .
}