VC++ 外壳扩展编程之windows右键菜单

简介:

这次我想和大家一起讨论一下 Windows 的 Shell 扩展编程,首先在阅读以下内容之前我还是推荐大家看一下《COM技术内幕》这本大作,不过即使您没有有关的基础知识其实也是无所谓的,因为以下讲解是傻瓜式讲解。

开发环境

  • Windows Professional 2000
  • Microsoft Visual C++ 6.0 + ATL3.0

参考文献

  • COM技术内幕
  • ATL应用与开发指南(第二版)

Windows外壳扩展
    Windows外壳扩展的英文名称为:Windows Shell Extension。Windows外壳扩展是一类特殊的COM对象,在这类COM对象中用户可以加入自己的特殊功能,而Windows外壳扩展最终都会 被Windows Explorer所引用。举个最简单的例子,比如 WinRar 应用程序,如果你安装完 WinRar 后,它会在你的右键菜单中加入很多快捷菜单,如 图1.1 所示:

图1.1

而上图却仅仅是外壳扩展编程中一种:"Context Menu Handler"。难道外壳扩展也分类吗?是的,但是不多,并且它们的实现大都一致,总体来说有如下几种分类:

表(一)

处理器类型 何时触发 所做处理
Context menu 处理器 当用户鼠标右击文件或文件夹时触发。但是在Shell V4.71+中,用户在文件夹目录的空白处点击鼠标右键也会触发该事件。 加入上下文菜单项。
Property sheet 处理器 当用户鼠标右击文件,选择文件"属性"菜单弹出文件属性对话框时触发。 加入用户自定义属性页。
Drag and drop 处理器 当用户在文件夹或桌面中用鼠标右键Drag/Drop文件或文件夹时触发。 加入上下文菜单项。
Drop处理器 当某一数据对象被Drag Over/Dropped Into某一文件时触发。 加入任何用户自定义动作。
QueryInfo 处理器(Shell V4.71+) 当用户鼠标滑过某一个文件或某一Shell对象时触发。 加入用户自定义提示信息(ToolTips)。

 

    也许有人会问我实现它们困难吗?答案是:比较简单。实现它是不是必须得去看那些枯燥乏味的ATL模板类,或者生硬死板的 MFC 宏定义呢?答案是否定的。也许以上的问题阻碍了大多数COM初学者的学习欲望,其实我刚接触ATL时多的是迷惘,常常抱怨 ATL 的知识太深奥,MFC的构架太生硬,一般我是不太喜欢用#define来定义程序的全部(请参阅 effective C++)。言归正传,我们再回到今天的话题上来,那么为实现 图1.1 所示功能可以通过哪些途径呢?答案有二,第一:注册表编程。第二:Shell Extension COM编程。通过注册表方式实现其实十分简单,请参阅 COM 组件注册表实现,在这里本文不做重复介绍,再者也不是本文的主题所在。在以下的内容中我会以第一类 Shell 扩展编程---" Context Menu 处理器" 为例来讲解 Handler 的实现过程。

组件功能
    该组件实现的功能为:当用户在Explorer中鼠标右击DLL类型文件时,在弹出的上下文菜单中注册我们自己的菜单项,如图1.2 所示:

图1.2

"Register Component"和"UnRegister Component"菜单项既是我们自己的菜单项。并且这两个菜单项分别完成进程内组件(DLL) 的注册和反注册,菜单项的功能倒很简单,只是简单地执行了 Windows 的 Regsvr32.exe而已,但是我们已经感觉到它给我们带来的实用和方便,难道你不觉得 "Over and Over" 手工输入 "Regsvr32 xxx.dll" 或者 "Regsvr32 /u xxx.dll" 很乏味吗……。

编写组件  

  1. 建立工程: 打开VC++,新建一个"ATL Com AppWizard"模板工程,工程名称为:SimpleExt。

图 1.3

Shell扩展实例均为进程内组件,它们均以动态库的形式存在,所以在接下来的向导中我们用默认设置:"Dynamic Link Library(DLL)",然后点击"完成"。如 下图所示:

图 1.4

此时我们已经拥有了一个没有实现任何功能的进程内 COM 组件,为什么说"没有实现任何功能"呢?那是因为我们没有实现任何接口,再者在我们的DLL中也没有任何可供外部使用的接口。
    如果我们的组件不继承其他外部已有接口,那么这样的COM组件实现起来则非常简单,它和编写普通类代码没有任何不一样的地方,只需要使用 ATL 接口的 Method 和Property 增/删向导即可实现。
    显然我们的组件要 继承 Shell 的扩展接口,并且还得实现所有继承的 Shell 接口,所以我们就不能完全依赖 ATL 的"自动化"了,这里需要我们自己写代码来实现该接口。首先我们通过 AT L向导新增一个简单接口 SimpleShlExt,如下图1.5,图1.6 和 图1.7 所示操作过程:

图 1.5


图 1.6

图 1.7

然后一切默认即可,这样ATL就为我们生成了一个组件框架,我们以下的讨论都基于此框架。

2.添加代码

图1.8 组件类继承关系

图1.8 中红色方框是我们自己要实现的 Shell 扩展接口,它不是向导自动生成代码,需要我们手工输入。

    我们从该框架中可以获得很多好处,首先通过 ATL 的模板类 CcomCoClass 我们就可以省去反复再三的 QueryInterface 接口的实现,而我们只需要绑定组件和接口的映射关系(如下图1.9 所示)以及实现所继承接口的全部虚函数即可,以及组件的注册等它基本上都为我们做好了一切,好处大家就慢慢体会吧......。下面我们首先介绍继承的各接口和其虚成员函数的作用,它们的声明包含在头文件中,首先头文件你必须包含进来:

图1.9 建立组件和接口的映射关系

图1.9 红色方框为 IShellExtInit 和 IContextMenu 接口和组件的接口映射关系,它不是向导自动生成代码,需要我们手工输入。

IShellExtInit接口:IShellExtInit 接口为 Shell 扩展编程必须要实现的接口。该接口主要用来初始化 Shell 扩展处理器(表一所列的处理器),它仅有一个虚成员函数Initialize,用户所有的 Shell 扩展初始化动作都由该函数完成。该函数的原型如下:

 

HRESULT Initialize(
    LPCITEMIDLIST pidlFolder,
LPDATAOBJECT lpdobj,
HKEY hkeyProgID
);

 

    在 Initialize 函数中,我们要做的事情就是获取用户鼠标右键点击的文件名称,但是有可能用户选择了多个文件,这里为了简单起见我们仅获取文件列表中的第一个文件。在这里 我们得补充一点内容:当用户在一个拥有 WS_EX_ACCEPTFILES 风格的窗体中Drag/Drop 文件时这些文件名会以同一种格式存储,而且文件完整路径的获取也都以DragQueryFile API函数来实现。但是 DragQueryFile 需要传入一个 HDROP 句柄,该句柄即为 Drag/Drop 文件名称列表数据句柄(开始存放数据的内存区域首指针)。而 HDROP 句柄的可以通过接口 " DATAOBJECT lpdobj" 的成员函数" GetData" 来获取。以下为获取第一个 Drag/Drop 文件的完整文件路径的具体代码:

 

//数据存储格式
FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };

//数据存储内存句柄(常用于IDataObject和IAdviseSink接口的数据传输操作)
STGMEDIUM stg = { TYMED_HGLOBAL };

if(FAILED(pDataObj->GetData(&fmt, &stg)))
{
     //如果获取数据内存句柄失败则返回E_INVALIDARG,
     //返回E_INVALIDARG则Explorer不会再调用我们的Shell扩展接口
     return E_INVALIDARG;
}

//获取实际数据内存句柄
HDROP hDrop = (HDROP)GlobalLock(stg.hGlobal);
if(NULL==hDrop)
{
     //在COM程序中养成良好的检错习惯是很重要的!!!
     return E_INVALIDARG;
}
//获取用户Drag/Drop的文件数目
int nDropCount = ::DragQueryFile((HDROP)stg.hGlobal, 
0xFFFFFFFF, NULL, 0);

//本示例程序仅获取第一个Drag/Drop文件完整路径
//以下注释代码为获取所有文件完整路径的实现代码:
//for(int i = 0; i < nDropCount; ++i){
     //循环获取每个Drag/Drop文件的完整文件名
     // ::DragQueryFile((HDROP)stg.hGlobal, i, m_pzDropFile, MAX_PATH);
//}

//如果用户Drag/Drop的文件数目不为一个则不予处理
if(1==nDropCount)
{
     //pzDropFile为组件类内部的private变量
     //它用来保存用户Drag/Drop的文件完整文件名
     memset(m_pzDropFile, 0x0, MAX_PATH*sizeof(TCHAR));
     ::DragQueryFile((HDROP)stg.hGlobal, 0, m_pzDropFile, MAX_PATH);
}

//释放内存句柄
::ReleaseStgMedium(&mdmSTG);

 

至此 IShellExtInit 接口已经完全实现,从此我们也可以看出进程内组件编程的一些特点,大体总结如下:"新建自己的接口,然后继承某些接口,最后一一实现这些接口的所有虚成员函数或 加入自己的成员函数,最后就是组件的注册"。   IContextMenu 接口:该接口和 "Context Menu 处理器" 一一对应,说到此我们也顺便说一下 Shell 扩展接口编程中和(表一)中所列处理器各自对应的COM接口:

(表二)

处理器类型 COM接口
Context menu 处理器 IContextMenu
Property sheet 处理器 IShellPropSheetExt
Drag and drop 处理器 IContextMenu
Drop 处理器 IDropTarget
QueryInfo 处理器(Shell V4.71+) IQueryInfo

 

其中 "Drag and drop 处理器" 的除了 COM 接口 IContextMenu 实现外还得需要注册表的特殊注册才可以实现。其中 IContextMenu 接口有三个虚成员函数需要我们的组件来实现,其函数原型分别如下:

 

HRESULT QueryContextMenu(
    HMENU hmenu,
    UINT indexMenu,
    UINT idCmdFirst,
    UINT idCmdLast,
    UINT uFlags
);

 

注:在QueryContextMenu 成员函数中我们可以加入自己的菜单项,插入菜单项其实很简单,我们可以通过 InsertMenu API 函数来实现,如下代码所示:

 

::InsertMenu(hmenu, indexMenu, MF_STRING | MF_BYPOSITION, 
idCmdFirst, IDM_REG_MNU_TXT);

 

QueryContextMenu 的处理过程十分简单,在这里无须多说。

 

HRESULT GetCommandString(
   UINT idCmd,
   UINT uFlags,
   UINT *pwReserved,
    LPSTR pszName,
    UINT cchMax
);

 

注:GetCommandString 成员函数为 Explorer 提供了在状态栏显示菜单命令提示信息的方法。在这个方法中 "LPSTR pszName" 是我们要关注的参数,我们只要根据 "UINT uFlags" 参数来填充 "LPSTR pszName" 参数即可。在这里可能会涉及到 ANSI 和 UNICODE 之间相互转换的知识,不过在这里我要提醒大家的是:在 COM 编程中尽可能使用兼容的 TCHAR 类型,同时对字符操作也尽量不要使用 C 类的 和 等等函数库,因为这样会使您无法通过 "Win32 Release Mindependency " 或其他 UINCode/Release 版本的编译过程。

 

HRESULT InvokeCommand(
    LPCMINVOKECOMMANDINFO pici
);

 

InvokeCommand 函数实现最终菜单项命令的执行。在 "LPCMINVOKECOMMANDINFO pici" 参数中包含了当前用户执行的菜单项ID和其他一些标志信息,如下代码可获取菜单项的ID:

 

//如果 nFlag 不为0则说明 pici->lpVerb 指向一个以''\0''结尾的字符串
int nFlag = HIWORD(pici->lpVerb);

//用户当前点击的菜单项ID 
int nMnuId = LOWORD(lpici->lpVerb);

 

一旦获取了菜单项ID那么我们就可以根据不同的菜单项来执行相应的动作,如图1.2 所示的 "Register Component" 和 "UnRegister Component" 菜单项所对应的 "注册/反注册进程内组件" 动作。

3.组件必要的宏定义部分

其实这一步十分简单,本可以忽略,但是为了把过程讲的更清楚一点我还是列了出来:

 

//声明组件注册所用的注册表REG资源
//其中IDR_SIMPLESHLEXT为注册表资源ID
DECLARE_REGISTRY_RESOURCEID(IDR_SIMPLESHLEXT)
//AddRef和Release成员函数的实现
DECLARE_PROTECT_FINAL_CONSTRUCT()
//组件接口映射部分,该部分映射主要是告诉QueryInterface能返回哪些接口给外部
BEGIN_COM_MAP(CSimpleShlExt)
COM_INTERFACE_ENTRY(ISimpleShlExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit) //IShellExtInit接口
COM_INTERFACE_ENTRY(IContextMenu) //IContextMenu接口
END_COM_MAP()

 

4.组件注册

  • 首先要在系统文件类型".DLL"下注册上下文菜单处理器

    创建注册表项HKCR\dllfile\ShellEx\ContextMenuHandlers\SimpleShlExt,
    并设置其默认值为我们类的GUID值即可。
  • 设置访问许可权(适应于WinNT构架的操作系统)

    如果您不设置该选项则只有 Administrator 权限的用户才可以使用该 Shell 扩展
    在 HKCM\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved 注册表项下创建键值:

    键名为:类的 GUID
    键值为:有关类的描述信息(任意字符串,无特殊要求)。
  • 组件注册的程序内部实现

    DLL文件上下文菜单的实现通过 REG 注册表资源文件实现,描述层次结构如下:
HKCR
{
    NoRemove dllfile
   {
      NoRemove ShellEx
      {
          NoRemove ContextMenuHandlers
          {
    //类的GUID字符串                   
    ForceRemove SimpleShlExt = s''{7C108295-19DE-4093-A9F8-ACC5E031E27A}''
          }
      }
   }
}
  • 访问许可权的注册则在DllRegisterServer DLL输出函数中完成。
    其实现只需要使用注册表的Win32 API函数即可;相应的反注册组件时则应在  DllUnregisterServer中删除相应的注册表键值即可。

    好了,"Context Menu 处理器" 的实现到此完毕,还是老规矩如有问题请直接来信,我期待大家的来信,同时在以后的时间里我会继续讨论Shell扩展编程,并会对我以前的文章大家所存在的疑点做出合适的回答,再次感谢大家的支持和鼓励。

本文转自博客园知识天地的博客,原文链接:VC++ 外壳扩展编程之windows右键菜单 ,如需转载请自行联系原博主。

相关文章
|
16天前
|
C++
C++ 语言异常处理实战:在编程潮流中坚守稳定,开启代码可靠之旅
【8月更文挑战第22天】C++的异常处理机制是确保程序稳定的关键特性。它允许程序在遇到错误时优雅地响应而非直接崩溃。通过`throw`抛出异常,并用`catch`捕获处理,可使程序控制流跳转至错误处理代码。例如,在进行除法运算或文件读取时,若发生除数为零或文件无法打开等错误,则可通过抛出异常并在调用处捕获来妥善处理这些情况。恰当使用异常处理能显著提升程序的健壮性和维护性。
33 2
|
2月前
|
XML Ubuntu Linux
部署08---扩展-Win10配置WSL(Ubuntu)环境,WSL系统是什么意思,是Windows系统上的一个子系统, xml的一大特点是直链系统,直接链接你的CPU,硬盘和内存,如何用 WSL部署
部署08---扩展-Win10配置WSL(Ubuntu)环境,WSL系统是什么意思,是Windows系统上的一个子系统, xml的一大特点是直链系统,直接链接你的CPU,硬盘和内存,如何用 WSL部署
|
16天前
|
算法 C语言 C++
C++语言学习指南:从新手到高手,一文带你领略系统编程的巅峰技艺!
【8月更文挑战第22天】C++由Bjarne Stroustrup于1985年创立,凭借卓越性能与灵活性,在系统编程、游戏开发等领域占据重要地位。它继承了C语言的高效性,并引入面向对象编程,使代码更模块化易管理。C++支持基本语法如变量声明与控制结构;通过`iostream`库实现输入输出;利用类与对象实现面向对象编程;提供模板增强代码复用性;具备异常处理机制确保程序健壮性;C++11引入现代化特性简化编程;标准模板库(STL)支持高效编程;多线程支持利用多核优势。虽然学习曲线陡峭,但掌握后可开启高性能编程大门。随着新标准如C++20的发展,C++持续演进,提供更多开发可能性。
40 0
|
3月前
|
编译器 C++ 开发者
C++一分钟之-C++20新特性:模块化编程
【6月更文挑战第27天】C++20引入模块化编程,缓解`#include`带来的编译时间长和头文件管理难题。模块由接口(`.cppm`)和实现(`.cpp`)组成,使用`import`导入。常见问题包括兼容性、设计不当、暴露私有细节和编译器支持。避免这些问题需分阶段迁移、合理设计、明确接口和关注编译器更新。示例展示了模块定义和使用,提升代码组织和维护性。随着编译器支持加强,模块化将成为C++标准的关键特性。
127 3
|
3天前
|
存储 设计模式 编译器
C++(十三) 类的扩展
本文详细介绍了C++中类的各种扩展特性,包括类成员存储、`sizeof`操作符的应用、类成员函数的存储方式及其背后的`this`指针机制。此外,还探讨了`const`修饰符在成员变量和函数中的作用,以及如何通过`static`关键字实现类中的资源共享。文章还介绍了单例模式的设计思路,并讨论了指向类成员(数据成员和函数成员)的指针的使用方法。最后,还讲解了指向静态成员的指针的相关概念和应用示例。通过这些内容,帮助读者更好地理解和掌握C++面向对象编程的核心概念和技术细节。
|
7天前
|
Rust 安全 C++
系统编程的未来之战:Rust能否撼动C++的王座?
【8月更文挑战第31天】Rust与C++:现代系统编程的新选择。C++长期主导系统编程,但内存安全问题频发。Rust以安全性为核心,通过所有权和生命周期概念避免内存泄漏和野指针等问题。Rust在编译时确保内存安全,简化并发编程,其生态系统虽不及C++成熟,但发展迅速,为现代系统编程提供了新选择。未来有望看到更多Rust驱动的系统级应用。
25 1
|
7天前
|
图形学 C++ C#
Unity插件开发全攻略:从零起步教你用C++扩展游戏功能,解锁Unity新玩法的详细步骤与实战技巧大公开
【8月更文挑战第31天】Unity 是一款功能强大的游戏开发引擎,支持多平台发布并拥有丰富的插件生态系统。本文介绍 Unity 插件开发基础,帮助读者从零开始编写自定义插件以扩展其功能。插件通常用 C++ 编写,通过 Mono C# 运行时调用,需在不同平台上编译。文中详细讲解了开发环境搭建、简单插件编写及在 Unity 中调用的方法,包括创建 C# 封装脚本和处理跨平台问题,助力开发者提升游戏开发效率。
17 0
|
7天前
|
C# Windows 监控
WPF应用跨界成长秘籍:深度揭秘如何与Windows服务完美交互,扩展功能无界限!
【8月更文挑战第31天】WPF(Windows Presentation Foundation)是 .NET 框架下的图形界面技术,具有丰富的界面设计和灵活的客户端功能。在某些场景下,WPF 应用需与 Windows 服务交互以实现后台任务处理、系统监控等功能。本文探讨了两者交互的方法,并通过示例代码展示了如何扩展 WPF 应用的功能。首先介绍了 Windows 服务的基础知识,然后阐述了创建 Windows 服务、设计通信接口及 WPF 客户端调用服务的具体步骤。通过合理的交互设计,WPF 应用可获得更强的后台处理能力和系统级操作权限,提升应用的整体性能。
21 0
|
7天前
|
数据库 Windows
超详细步骤解析:从零开始,手把手教你使用 Visual Studio 打造你的第一个 Windows Forms 应用程序,菜鸟也能轻松上手的编程入门指南来了!
【8月更文挑战第31天】创建你的第一个Windows Forms (WinForms) 应用程序是一个激动人心的过程,尤其适合编程新手。本指南将带你逐步完成一个简单WinForms 应用的开发。首先,在Visual Studio 中创建一个“Windows Forms App (.NET)”项目,命名为“我的第一个WinForms 应用”。接着,在空白窗体中添加一个按钮和一个标签控件,并设置按钮文本为“点击我”。然后,为按钮添加点击事件处理程序`button1_Click`,实现点击按钮后更新标签文本为“你好,你刚刚点击了按钮!”。
22 0
|
1月前
|
编译器 开发工具 C语言
解锁QtCreator跨界神技!Windows下轻松驾驭OpenCV动态库,让你的跨平台开发如虎添翼,秒变视觉编程大师!
【8月更文挑战第4天】QtCreator是一款强大的跨平台IDE,便于创建多平台应用。本教程教你如何在Windows环境下集成OpenCV库至Qt项目。首先,下载匹配MinGW的OpenCV预编译版并解压。接着,在QtCreator中新建或打开项目,并在.pro文件中添加OpenCV的头文件和库文件路径。确保编译器设置正确。随后编写测试代码,例如加载和显示图片,并进行编译运行。完成这些步骤后,你就能在QtCreator中利用OpenCV进行图像处理开发了。
77 6