65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET Shell Extensions - 为 Shell 上下文菜单添加子菜单并动态加载上下文菜单

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (3投票s)

2015年10月7日

CPOL

4分钟阅读

viewsIcon

23535

downloadIcon

904

Windows Shell Extension - 使用 Sharpshell 库为 Shell 上下文菜单添加子菜单并动态加载它们。

摘要

在这篇文章中,我试图解释如何使用 .NET 代码通过 Sharpshell 库动态加载 Windows shell 上下文菜单并向其中添加子菜单。

我将通过一个示例进行解释,该示例根据所选项(文件或目录)加载不同的菜单和子菜单,如下图所示。

Snapshot of windows shell extension, whenthe selected item is a directory

当所选项为目录时,Windows 上下文菜单的快照

Snapshot of windows shell extension, whenthe selected item is a file

当所选项为文件时,Windows 上下文菜单的快照

引言

最近,我发现了 Sharpshell 库用于构建 shell 扩展,它非常棒,非常感谢作者 Dave Kare。我在动态加载子菜单时遇到了一些困难,所以写一篇关于它的文章会帮助到别人,这也是我写这篇帖子的原因。

本文将解决什么问题?

截至目前,sharpshell 库存在的问题[Issue1, Issue2]是,库用于构建上下文菜单的 `CreateMenu` 方法只被调用一次,因此动态加载菜单并不直接。

我们通过将 `menu` 项声明为类字段而不是在 `CreateMenu` 方法中定义它来解决这个问题,然后我们从 `CreateMenu` 返回这个 `menu` 项。我们利用另一个 `UpdateMenu` 方法来动态更新上下文菜单,通过依次调用 `CreateMenu`。每次选择一个文件时,都会调用 `CanShowMenu` 方法来检查底层上下文菜单是否应为所选字段显示。我们总是在 `CanShowMenu` 方法中调用 `UpdateMenu` 方法,只要底层上下文菜单的条件为真。

在我们阅读示例的过程中,您会发现它有多么简单。

我们将分两步来看这篇帖子。

  • 第一步:如何动态加载菜单/子菜单
  • 第二步:通过 sharpshell 添加子菜单

背景

如果您不了解 Sharpshell 库,我强烈建议您看看这个很棒的库。在这里,我假设您熟悉使用 Sharpshell 创建上下文菜单、注册和注销 shell 扩展(COM DLLs)。如果您不熟悉,可以从下面的链接快速阅读一下。

代码概述

在我们开始讲解步骤之前,我想让您快速看一下代码,这样您就能对代码有一个整体的了解,并且知道我在解释哪一部分。请快速浏览一下下面的代码。

// <summary>
// The SubMenuExtension is an example shell context menu extension,
// implemented with SharpShell. It loads the menu dynamically
// files.
// </summary>
[ComVisible(true)]
[COMServerAssociation(AssociationType.AllFiles)]
[COMServerAssociation(AssociationType.Directory)]
public class DynamicSubMenuExtension : SharpContextMenu
{
    //  let's create the menu strip.
    private ContextMenuStrip menu = new ContextMenuStrip();

    // <summary>
    // Determines whether the menu item can be shown for the selected item.
    // </summary>
    // <returns>
    //   <c>true</c> if item can be shown for the selected item for this instance.; 
    // otherwise, <c>false</c>.
    // </returns>
    protected override bool CanShowMenu()
    {            
        //  We can show the item only for a single selection.
        if (SelectedItemPaths.Count() == 1)
        {   this.UpdateMenu();
            return true; 
        } 
        else 
        {   return false; 
        }
    }

    // <summary>
    // Creates the context menu. This can be a single menu item or a tree of them.
    // Here we create the menu based on the type of item
    // </summary>
    // <returns>
    // The context menu for the shell context menu.
    // </returns>
    protected override ContextMenuStrip CreateMenu()
    {
        menu.Items.Clear();
        FileAttributes attr = File.GetAttributes(SelectedItemPaths.First());

        // check if the selected item is a directory
        if (attr.HasFlag(FileAttributes.Directory)) 
        {
            this.MenuDirectory();
        }
        else
        {
            this.MenuFiles();
        }

        // return the menu item
        return menu;
    }

    // <summary>
    // Updates the context menu. 
    // </summary>
    private void UpdateMenu()
    {
        // release all resources associated to existing menu
        menu.Dispose();
        menu = CreateMenu();
    }

    // <summary>
    // Creates the context menu when the selected item is a folder.
    // </summary>
    protected void MenuDirectory()
    {
        ToolStripMenuItem MainMenu;
        MainMenu = new ToolStripMenuItem
        {
            Text = "MenuDirectory",
            Image = Properties.Resources.Folder_icon
        };

                ToolStripMenuItem SubMenu1;
                SubMenu1 = new ToolStripMenuItem
                {
                    Text = "DirSubMenu1",
                    Image = Properties.Resources.Folder_icon
                };

                var SubMenu2 = new ToolStripMenuItem
                {
                    Text = "DirSubMenu2",
                    Image = Properties.Resources.Folder_icon
                };
                SubMenu2.DropDownItems.Clear();
                SubMenu2.Click += (sender, args) => ShowItemName();

                        var SubSubMenu1 = new ToolStripMenuItem
                        {
                            Text = "DirSubSubMenu1",
                            Image = Properties.Resources.Folder_icon
                        };
                        SubSubMenu1.Click += (sender, args) => ShowItemName();
        
        // Let's attach the submenus to the main menu
        SubMenu1.DropDownItems.Add(SubSubMenu1);
        MainMenu.DropDownItems.Add(SubMenu1);
        MainMenu.DropDownItems.Add(SubMenu2);

        menu.Items.Clear();
        menu.Items.Add(MainMenu);
    }

    // <summary>
    // Creates the context menu when the selected item is of file type.
    // </summary>
    protected void MenuFiles()
    {
        ToolStripMenuItem MainMenu;
        MainMenu = new ToolStripMenuItem
        {
            Text = "MenuFiles",
            Image = Properties.Resources.file_icon
        };

                ToolStripMenuItem SubMenu3;
                SubMenu3 = new ToolStripMenuItem
                {
                    Text = "FileSubMenu1",
                    Image = Properties.Resources.file_icon
                };

                var SubMenu4 = new ToolStripMenuItem
                {
                    Text = "FileSubMenu2",
                    Image = Properties.Resources.file_icon
                };
                SubMenu4.DropDownItems.Clear();
                SubMenu4.Click += (sender, args) => ShowItemName();

                        var SubSubMenu3 = new ToolStripMenuItem
                        {
                            Text = "FileSubSubMenu1",
                            Image = Properties.Resources.file_icon
                        };
                        SubSubMenu3.Click += (sender, args) => ShowItemName();

        // Let's attach the submenus to the main menu
        SubMenu3.DropDownItems.Add(SubSubMenu3);
        MainMenu.DropDownItems.Add(SubMenu3);
        MainMenu.DropDownItems.Add(SubMenu4);

        menu.Items.Clear();
        menu.Items.Add(MainMenu);
    }

    // <summary>
    // Shows name of selected files.
    // </summary>
    private void ShowItemName()
    {
        //  Builder for the output.
        var builder = new StringBuilder();
        FileAttributes attr = File.GetAttributes(SelectedItemPaths.First());

        //  check if selected item is a directory.
        if (attr.HasFlag(FileAttributes.Directory))
        {
            //  Show folder name.
            builder.AppendLine(string.Format("Selected folder name is {0}", 
            Path.GetFileName(SelectedItemPaths.First())));
        }
        else
        {         
                //  Show the file name.
                builder.AppendLine(string.Format("Selected file is {0}", 
                Path.GetFileName(SelectedItemPaths.First())));            
        }

        //  Show the ouput.
        MessageBox.Show(builder.ToString());
    }
}

步骤

正如我之前提到的,我已尝试将代码分为以下两部分进行解释。

步骤 1:如何动态加载菜单/子菜单

声明菜单项

首先,让我们将菜单项声明为类字段。

//  lets create the menu strip.
private ContextMenuStrip menu = new ContextMenuStrip();

CanShowMenu 方法

底层上下文菜单项将仅对单个选择项显示。

protected override bool CanShowMenu()
{   
    if (SelectedItemPaths.Count() == 1)
    {   this.UpdateMenu();
        return true; 
    } 
    else 
    {   return false; 
    }
}

CreateMenu 方法

在这里,我们根据选择附加不同的菜单和子菜单项。如果是目录,则调用 `MenuDirectory` 方法;如果是文件,则调用 `MenuFiles` 方法。这些方法将为相应的选择项生成上下文菜单项,并将它们附加到主 `menu` 项,该项将作为上下文菜单显示。

protected override ContextMenuStrip CreateMenu()
{
            menu.Items.Clear();
            FileAttributes attr = File.GetAttributes(SelectedItemPaths.First());

            // check if the selected item is a directory
            if (attr.HasFlag(FileAttributes.Directory)) 
            {
                this.MenuDirectory();
            }
            else
            {
                this.MenuFiles();
            }
    return menu;
}  

虽然 `CreateMenu` 方法只被调用一次,但 `UpdateMenu` 方法在每次需要显示上下文菜单时都会被调用,这将使上下文菜单的动态渲染成为可能。

步骤 2:通过 sharpshell 添加子菜单

添加子菜单很简单,我们在 `MenuDirectory` 和 `MenuFiles` 方法中都进行了操作,我们将看到如何为目录菜单添加子菜单,另一个菜单的操作方式类似。

正如在引言部分的第一个图所示,我们在 `MenuDirectory` 菜单下添加了 `DirSubMenu1` 和 `DirSubMenu2` 子菜单,并在 `DirSubMenu1` 下添加了 `SubSubMenu1` 子菜单。

我们创建 `MainMenu` 并将其附加到 `menu` 项。要添加子菜单,每次我们创建一个 `ToolStripMenuItem` 项,定义它的属性,然后将其作为下拉菜单附加到父菜单项。

这是我们实现它的方式。

protected void MenuDirectory()
{
    ToolStripMenuItem MainMenu;
    MainMenu = new ToolStripMenuItem
    {
        Text = "MenuDirectory",
        Image = Properties.Resources.Folder_icon
    };

            ToolStripMenuItem SubMenu1;
            SubMenu1 = new ToolStripMenuItem
            {
                Text = "DirSubMenu1",
                Image = Properties.Resources.Folder_icon
            };

            var SubMenu2 = new ToolStripMenuItem
            {
                Text = "DirSubMenu2",
                Image = Properties.Resources.Folder_icon
            };
            SubMenu2.DropDownItems.Clear();
            SubMenu2.Click += (sender, args) => ShowItemName();

                    var SubSubMenu1 = new ToolStripMenuItem
                    {
                        Text = "DirSubSubMenu1",
                        Image = Properties.Resources.Folder_icon
                    };
                    SubSubMenu1.Click += (sender, args) => ShowItemName();
    
    // Lets attach the submenus to the main menu
    SubMenu1.DropDownItems.Add(SubSubMenu1);
    MainMenu.DropDownItems.Add(SubMenu1);
    MainMenu.DropDownItems.Add(SubMenu2);

    menu.Items.Clear();
    menu.Items.Add(MainMenu);
}

ShowItemName 方法

在这里,我们只显示所选的项。

private void ShowItemName()
{
    var builder = new StringBuilder();
    FileAttributes attr = File.GetAttributes(SelectedItemPaths.First());

    if (attr.HasFlag(FileAttributes.Directory))
    {
        builder.AppendLine(string.Format("Selected folder name is {0}", 
        	Path.GetFileName(SelectedItemPaths.First())));
    }
    else
    {
        builder.AppendLine(string.Format("Selected file is {0}", 
        	Path.GetFileName(SelectedItemPaths.First())));
    }
    MessageBox.Show(builder.ToString());
}

注册

有许多方法可以注册我们构建项目时生成的 COM DLL,正如您在 Dave 的文章中看到的。我通常倾向于使用 Microsoft 的 Regasm 工具。但在开发阶段,我更喜欢使用服务器管理器工具。我们可以测试 DLL 而无需将其注册到系统,这很方便,并且还有一个附加调试器的选项。

如果您计划在项目中部署 sharpshell 服务器,您应该尝试 Server Registration Manager。注册过程非常简单。不过,我在使用 Advanced Installer 时遇到了一些问题。

通过批处理脚本使用 Regasm 进行注册/注销

如果您使用 `regasm` 注册 shell 扩展,创建一个批处理脚本可以使注册/注销过程更容易。

这是一个用于注册 DLL 的小型批处理脚本。您可以在本帖的附件中找到用于注销的脚本。只需确保将批处理脚本放在与 DLL 文件相同的文件夹中。

@ECHO OFF
ECHO ###    Register DLL     ###
REM  --> code from https://sites.google.com/site/eneerge/scripts/batchgotadmin

:: BatchGotAdmin
:-------------------------------------
REM  --> Check for permissions
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"

REM --> If error flag set, we do not have admin.
if '%errorlevel%' NEQ '0' (
    echo Requesting administrative privileges...
    goto UACPrompt
) else ( goto gotAdmin )

:UACPrompt
    echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
    echo UAC.ShellExecute "%~s0", "", "", "runas", 1 >> "%temp%\getadmin.vbs"

    "%temp%\getadmin.vbs"
    exit /B

:gotAdmin
    if exist "%temp%\getadmin.vbs" ( del "%temp%\getadmin.vbs" )
    pushd "%CD%"
    CD /D "%~dp0"
:--------------------------------------

REM  --> Check OS and register accordingly 
:CheckOS
reg Query "HKLM\Hardware\Description\System\CentralProcessor\0" | 
find /i "x86" > NUL && set SysOS=32BIT || set SysOS=64BIT
    
if %SysOS%==32BIT ( 
"%windir%\Microsoft.NET\Framework\v4.0.30319\regasm.exe" /codebase %cd%\DynamicSubMenus.dll 
) else (
"%windir%\Microsoft.NET\Framework64\v4.0.30319\regasm.exe" /codebase %cd%\DynamicSubMenus.dll )

:END

PAUSE
@ECHO ON
© . All rights reserved.