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






4.33/5 (3投票s)
Windows Shell Extension - 使用 Sharpshell 库为 Shell 上下文菜单添加子菜单并动态加载它们。
摘要
在这篇文章中,我试图解释如何使用 .NET 代码通过 Sharpshell 库动态加载 Windows shell 上下文菜单并向其中添加子菜单。
我将通过一个示例进行解释,该示例根据所选项(文件或目录)加载不同的菜单和子菜单,如下图所示。
引言
最近,我发现了 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