上下文菜单创建

来源:百度文库 编辑:神马文学网 时间:2024/04/29 00:58:51

Creating ContextMenu Handlers

Creating Context Menu Handlers

When a user right-clicks a Shell object,the Shell displays its context menu. For file system objects there are a numberof standard items, such as Cut and Copy, that are on the menu bydefault. If the object is a file that is a member of a class, additionalitems can be specified in the registry.Finally, the Shell checks the registry to see if the file class is associatedwith any context menu handlers. If it is, the Shell queries the handlerfor additional context menu items.

A context menu handler is a shell extension handlerthat adds commands to an existing context menu. Context menu handlers areassociated with a particular file class and are called any time a context menuis displayed for a member of the class. While you can add items to a file classcontext menu with the registry, the items will be the same for all members ofthe class. By implementing and registering such a handler, you can dynamicallyadd items to an object's context menu, customized for the particular object.

A closely related Shell extension handleris the drag-and-drop handler. It is a context menu handler that addsitems to the context menu that is displayed when a user drags and drops a filewith the right mouse button.

The procedures for implementing andregistering a Shell extension handler are discussed in Creating ShellExtension Handlers. This document focuses on those aspects ofimplementation that are specific to context menu handlers.

The following topics are discussed.

·        How Context Menu HandlersWork

·        Registering a Context Menu Handler

·        Implementing IContextMenu

·        Creating Drag-and-Drop Handlers

Context menu handlers are a type of Shellextension handler. Like all such handlers, they are in-process Component ObjectModel (COM) objects implemented as DLLs. Context menu handlers must export twointerfaces in addition to IUnknown: IShellExtInit andIContextMenu. Optionally,a context menu handler can also export IContextMenu2 andIContextMenu3.These interfaces handle the messaging needed to implement owner-drawn menuitems. For more information on owner-drawn menu items, see the CreatingOwner-Drawn Menu Items section under Using Menus.

The IShellExtInit interface is usedby the Shell to initialize the handler. When the Shell calls IShellExtInit::Initialize,it passes in a data object with the object's name and the pointer to an itemidentifier list (PIDL) of the folder that contains the file. The hKeyProgIDparameter is not used with context menu handlers. The IShellExtInit::Initializemethod must extract the file name from the data object and store the name andthe folder's PIDL for later use. For further details, see Implementing IShellExtInit.

The remainder of the operation takes placethrough the handler's IContextMenu interface. The Shell first calls IContextMenu::QueryContextMenu.It passes in an HMENU handle that the method can use to add items to thecontext menu. If the user selects one of the commands, IContextMenu::GetCommandStringis called to retrieve the Help string that will be displayed on the MicrosoftWindows Explorer status bar. If the user clicks one of the handler's items, theShell calls IContextMenu::InvokeCommand.The handler can then execute the appropriate command.

Registeringa Context Menu Handler

Context menu handlers are associated witheither a file class or a folder. For file classes, the handler is registeredunder the following subkey.

·        HKEY_CLASSES_ROOT

o    Program ID

§  shellex

§  ContextMenuHandlers

Create a subkey under ContextMenuHandlersnamed for the handler, and set the subkey's default value to the string form ofthe handler's class identifier (CLSID) GUID.

You can also associate a context menuhandler with different kinds of folders. Register the handler the same way youwould for a file class, but under the following subkey, where FolderTypeis the name of the type of folder.

·        HKEY_CLASSES_ROOT

o    FolderType

§  shellex

§  ContextMenuHandlers

For a discussion of how to register Shellextension handlers and more information about which folder types you canregister handlers for, see Registering Shell Extension Handlers.

If a file class has a context menuassociated with it, double-clicking an object normally launches the defaultcommand. The handler's IContextMenu::QueryContextMenu method is notcalled. To specify that the handler's IContextMenu::QueryContextMenumethod should be called when an object is double-clicked, create a shellex\MayChangeDefaultMenusubkey under the handler's CLSID key. When an object associated with thehandler is double-clicked, IContextMenu::QueryContextMenu will be calledwith the CMF_DEFAULTONLY flag set in the uFlags parameter.

Note  Setting the MayChangeDefaultMenukey forces the system to load the handler's DLL when an associated item isdouble-clicked. If your handler does not change the default verb, you shouldnot set MayChangeDefaultMenu . Doing so causes the system to load yourDLL unnecessarily. Context menu handlers should set this key only if they mightneed to change the context menu's default verb.

The following example illustrates registryentries that enable a context menu handler for an example .myp file class. Thehandler's CLSID key includes a MayChangeDefaultMenu subkey toguarantee that the handler is called when the user double-clicks a relatedobject.

·        HKEY_CLASSES_ROOT

o     

o    .myp
(Default) = MyProgram.1

o    CLSID

§  {00000000-1111-2222-3333-444444444444}

§  InProcServer32
(Default) = C:\MyDir\MyCommand.dll
ThreadingModel = Apartment

§  shellex

§  MayChangeDefaultMenu

o    MyProgram.1
(Default) = MyProgram Application

§  shellex

§  ContextMenuHandler
MyCommand = {00000000-1111-2222-3333-444444444444}

ImplementingIContextMenu

Most of the operation described in How Context Menu HandlersWork is handled by the IContextMenuinterface. This section discusses how to implement its three methods.

QueryContextMenu Method

The Shell calls IContextMenu::QueryContextMenuto allow the context menu handler to add its menu items to the menu. It passesin the HMENU handle in the hmenu parameter. The indexMenuparameter is set to the index to be used for the first menu item that is to beadded.

Any menu items that are added by thehandler must have identifiers that fall between the idCmdFirst and idCmdLastparameters. Typically, the first command identifier is set to idCmdFirst,which is incremented by one (1) for each additional command. This practiceensures that you don't exceed idCmdLast and maximizes the number ofavailable identifiers in case the Shell calls more than one handler.

An item identifier's command offsetis the difference between the identifier and idCmdFirst. Store theoffset of each item that your handler adds to the context menu because theShell might use it to identify the item if it subsequently calls IContextMenu::GetCommandStringor IContextMenu::InvokeCommand.

You should also assign a verb to eachcommand you add. A verb is a language-independent string that can be usedinstead of the offset to identify the command when IContextMenu::InvokeCommandis called. It is also used by functions such as ShellExecuteEx toexecute context menu commands.

Only three of the flags that can be passedin through the uFlags parameter are relevant to context menu handlers.

CMF_DEFAULTONLY

The user has selected the default command, usually by double-clicking the object. IContextMenu::QueryContextMenu should return control to the Shell without modifying the menu.

CMF_NODEFAULT

No item in the menu should be the default item. The method should add its commands to the menu.

CMF_NORMAL

The context menu will be displayed normally. The method should add its commands to the menu.

Use either InsertMenu or InsertMenuItem toadd menu items to the list. Then return an HRESULT value with theseverity set to SEVERITY_SUCCESS. Set the code value to the offset of thelargest command identifier that was assigned, plus one (1). For example, assumethat idCmdFirst is set to 5 and you add three items to the menu withcommand identifiers of 5, 7, and 8. The return value should beMAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 - 5 + 1).

The following example shows a simpleimplementation of IContextMenu::QueryContextMenu that inserts a singlecommand. The identifier offset for the command is IDM_DISPLAY, which is set tozero. The m_pszVerb and m_pwszVerb variables are privatevariables used to store the associated language-independent verb string in bothUnicode and ANSI formats.

Copy Code

#define IDM_DISPLAY 0

 

STDMETHODIMP CMenuExtension::QueryContextMenu(HMENUhMenu,

                                             UINT indexMenu,

                                             UINT idCmdFirst,

                                              UINT idCmdLast,

                                             UINT uFlags)

{

    HRESULT hr;

       

   if(!(CMF_DEFAULTONLY & uFlags))

    {

       InsertMenu(hMenu,

                  indexMenu,

                  MF_STRING | MF_BYPOSITION,

                  idCmdFirst + IDM_DISPLAY,

                  "&Display File Name");

 

        // TODO:Add error handling to verify HRESULT return values.

              

        hr =StringCbCopyA(m_pszVerb, sizeof(m_pszVerb), "display");

        hr =StringCbCopyW(m_pwszVerb, sizeof(m_pwszVerb), L"display");

 

        returnMAKE_HRESULT(SEVERITY_SUCCESS, 0, USHORT(IDM_DISPLAY + 1));

    }

 

    returnMAKE_HRESULT(SEVERITY_SUCCESS, 0, USHORT(0));

}

GetCommandString Method

If a user highlights one of the items addedby a context menu handler, the handler's IContextMenu::GetCommandStringmethod is called to request a Help text string that will be displayed on theWindows Explorer status bar. This method can also be called to request the verbstring that is assigned to a command. Either ANSI or Unicode verb strings canbe requested.

The idCmd parameter holds theidentifier offset of the command that was defined when IContextMenu::QueryContextMenuwas called. If a Help string is requested, uFlags will be set to either GCS_HELPTEXTA or GCS_HELPTEXTW, depending on whether an ANSI or Unicodestring is desired. Copy the Help string to the pszName buffer, castingit to an LPWSTR for the Unicode case. The verb string is requested bysetting uFlags to either GCS_VERBA or GCS_VERBW. Copy the appropriate string to pszName,just as with the Help string. The GCS_VALIDATEA and GCS_VALIDATEW flags are not used by context menu handlers.

The following example shows a simpleimplementation of IContextMenu::GetCommandString that corresponds to theIContextMenu::QueryContextMenu example given in the QueryContextMenu Methodsection. Since the handler adds only one menu item, there is only one set ofstrings that can be returned. The method simply tests whether idCmd isvalid and, if it is, returns the requested ANSI or Unicode string.

The StringCchCopyNfunction is used to copy the requested string to pszName to ensure thatthe copied string doesn't exceed the size of the buffer given by uMaxNameLen.The ANSI and Unicode versions of the function are used explicitly, because theformat defined when the handler is compiled might not match the requestedformat.

Copy Code

STDMETHODIMP CMenuExtension::GetCommandString(UINTidCommand,

                                              UINTuFlags,

                                             LPUINT lpReserved,

                                             LPSTR pszName,

                                             UINT uMaxNameLen)

{

    HRESULT  hr = E_INVALIDARG;

 

    if(idCommand !=IDM_DISPLAY)

    {

        return hr;

    }

 

    switch(uFlags)

    {

        caseGCS_HELPTEXTA:

            hr =StringCchCopyNA(pszName,

                                lstrlen(pszStr)/sizeof(pszStr(0)),

                                 "DisplayFile Name",

                                 uMaxNameLen);

            break;

 

        caseGCS_HELPTEXTW:

            hr =StringCchCopyNW((LPWSTR)pszName,

                                lstrlen(pszStr)/sizeof(pszStr(0)),

                                 L"Display File Name",

                                 uMaxNameLen);

            break;

 

        caseGCS_VERBA:

            hr =StringCchbCopyNA(pszName,

                                 lstrlen(pszStr)/sizeof(pszStr(0)),

                                 m_pszVerb,

                                  uMaxNameLen);

            break;

 

        caseGCS_VERBW:

            hr =StringCchCopyNW((LPWSTR)pszName,

                                lstrlen(pszStr)/sizeof(pszStr(0)),

                                m_pwszVerb,

                                 uMaxNameLen);

            break;

 

        default:

            hr =S_OK;

            break;

    }

    return hr;

}

InvokeCommandMethod

This method is called when a user clicks amenu item to tell the handler to run the associated command. The piciparameter points to a structure that contains the needed information.

Although pici is declared inShlobj.h as a CMINVOKECOMMANDINFOstructure, in practice it often points to a CMINVOKECOMMANDINFOEXstructure. This structure is an extended version of CMINVOKECOMMANDINFOand has several additional members that allow Unicode strings to be passed.

Check the cbSize member of picito determine which structure was passed in. If it is a CMINVOKECOMMANDINFOEXstructure and the fMask member has the CMIC_MASK_UNICODE flag set, cast pici to CMINVOKECOMMANDINFOEX.This allows your application to use the Unicode information contained in thelast five members of the structure.

The structure's lpVerb or lpVerbWmember is used to identify the command to be executed. There are two ways toidentify commands.

·        The command's verb string

·        The command's identifier offset

To distinguish between these two cases,check the high-order word of lpVerb for the ANSI case or lpVerbWfor the Unicode case. If the high-order word is nonzero, lpVerb or lpVerbWholds a verb string. If the high-order word is zero, the command offset is inthe low-order word of lpVerb.

The following example shows a simpleimplementation of IContextMenu::InvokeCommand that corresponds to the IContextMenu::QueryContextMenuand IContextMenu::GetCommandString samples given in the previoussections. The method first determines which structure is being passed in. Itthen determines whether the command is identified by its offset or verb. If lpVerbor lpVerbW holds a valid verb or offset, the method displays a messagebox.

Copy Code

STDMETHODIMPCShellExtension::InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi)

{

    BOOL fEx =FALSE;

    BOOL fUnicode =FALSE;

 

   if(lpcmi->cbSize == sizeof(CMINVOKECOMMANDINFOEX))

    {

        fEx = TRUE;

       if((lpcmi->fMask & CMIC_MASK_UNICODE))

        {

           fUnicode = TRUE;

        }

    }

 

    if( !fUnicode&& HIWORD(lpcmi->lpVerb))

    {

       if(StrCmpIA(lpcmi->lpVerb, m_pszVerb))

        {

            return E_FAIL;

        }

    }

 

    else if(fUnicode && HIWORD(((CMINVOKECOMMANDINFOEX *) lpcmi)->lpVerbW))

    {

       if(StrCmpIW(((CMINVOKECOMMANDINFOEX *)lpcmi)->lpVerbW, m_pwszVerb))

        {

            returnE_FAIL;

        }

    }

 

    elseif(LOWORD(lpcmi->lpVerb) != IDM_DISPLAY)

    {

        returnE_FAIL;

    }

 

    else

    {

       MessageBox(lpcmi->hwnd,

                  "The File Name",

                  "File Name",

                  MB_OK|MB_ICONINFORMATION);

    }

 

    return S_OK;

}

CreatingDrag-and-Drop Handlers

When a user right-clicks a Shell object todrag an object, a context menu is displayed when the user attempts to drop theobject. The following illustration shows a typical drag-and-drop context menu.

A drag-and-drop handler is a context menuhandler that can add items to this context menu. Drag-and-drop handlers aretypically registered under the following subkey.

·        HKEY_CLASSES_ROOT

o    Directory

§  shellex

§  DragDropHandlers

Add a subkey under the DragDropHandlerssubkey named for the drag-and-drop handler, and set the subkey's default valueto the string form of the handler's CLSID GUID. The following exampleenables the MyDD drag-and-drop handler.

·        HKEY_CLASSES_ROOT

o    Directory

§  shellex

§  DragDropHandlers

§  MyDD
(Default) = {MyDD CLSID GUID}

The basic procedure for implementing adrag-and-drop handler is the same as for conventional context menu handlers. However,context menu handlers normally use only the IDataObject pointer passed to thehandler's IShellExtInit::Initialize method to extract the object's name.A drag-and-drop handler could implement a more sophisticated data handler tomodify the behavior of the dragged object.

Windows外壳扩展编程 www.applevb.com
    在Windows下的一些软件提供了这样的功能:当安装了这些软件之后,当在Windows的Explore中鼠标右键单击文件或者文件夹后,在弹出菜单中就会多出与该软件操作相关的菜单项,点击该项就会激活相应的程序对用户选中的文件进行相应的操作。例如安装了Winzip之后,当用户选中一个文件夹后单击右键,在弹出菜单中就会多出一个Add ToZip和一个 Add To xxx.zip的选项,其中xxx为选中的文件夹的名称。只要单击上面的两个菜单项中的一个,就可以方便的压缩目录了。这样的功能称为Windows外壳扩展(Shell Extensions)
外壳扩展概述

  下面是与外壳扩展相关的三个重要术语:
(1)文件对象(File Object)
   文件对象是外壳中的一项,大家最熟识的文件对象是文件和目录,此外,打印机、控制面板程序、共享网
       络等也都是文件对象。
(2)文件类(File Class)
       文件类是具有某种共同特性的文件对象的集合,比如,扩展名相同的文件属于同一文件类。
(3)处理程序(Handler)
   处理程序是具体实现某个外壳扩展的代码。

  Windows支持七种类型的外壳扩展(称为Handler),它们相应的作用简述如下:
(1)Context menu handlers向特定类型的文件对象增添上下文相关菜单;
(2)Drag-and-drop handlers用来支持当用户对某种类型的文件对象进行拖放操作时的OLE数据传输;
(3)Icon handlers用来向某个文件对象提供一个特有的图标,也可以给某一类文件对象指定图标;
(4)Property sheet handlers给文件对象增添属性页,属性页可以为同一类文件对象所共有,也可以给一个
       文件对象指定特有的属性页;
(5)Copy-hook handlers在文件夹对象或者打印机对象被拷贝、移动、删除和重命名时,就会被系统调用,
       通过为Windows增加Copy-hook handlers,可以允许或者禁止其中的某些操作;
(6)Drop target handlers在一个对象被拖放到另一个对象上时,就会被系统被调用;
(7)Data object handlers在文件被拖放、拷贝或者粘贴时,就会被系统被调用。

  Windows的所有外壳扩展都是基于COM(ComponentObject Model) 组件模型的,外壳是通过接口(Interface)来访问对象的。外壳扩展被设计成32位的进程中服务器程序,并且都是以动态链接库的形式为操作系统提供服务的。因此,如果要对Windows的用户界面进行扩充的话,则具备写COM对象的一些知识是十分必要的。

  写好外壳扩展程序后,必须将它们注册才能生效。所有的外壳扩展都必须在Windows注册表的HKEY_CLASSES_ROOT\CLSID键之下进行注册。在该键下面可以找到许多名字像{

  注册表HKEY_CLASSES_ROOT主键下有几个特殊的子键,如*、Folder、Drive以及Printer。如果把外壳扩展注册在*子键下,那么这个外壳扩展将对Windows中所有类型的文件有效;如果把外壳扩展注册在Folder子键下,则对所有目录有效。

    上面提到的在WindowsExplore中在鼠标右键菜单中添加菜单项(我们成为上下文相关菜单)的操作属于外壳扩展的第一类,即Contextmenu handlers向特定类型的文件对象增添上下文相关菜单。要动态地在上下文相关菜单中增添菜单项,可以通过写Context Menu Handler来实现。
编写Context Menu Handler必须实现IShellExtInit和IContextMenu两个接口。除了IUnknown接口所定义的函数之外,Context Menu Handler还需要用到QueryContextMenu、InvokeCommand和GetCommandString这三个非常重要的成员函数。

  (1)QueryContextMenu函数:每当系统要显示一个文件对象的上下文相关菜单时,它首先要调用该函数。为了在上下文相关菜单中添加菜单
项,我们在该函数中调用InsertMenu函数。

  (2)InvokeCommand函数:当用户选定了某个ContextMenu Handler登记过的菜单项后,该函数将会被调用,系统将会传给该函数一个指向
LPCMINVOKECOMMANDINFO结构的指针。在该函数中要执行与所选菜单项相对应的操作。

  (3)GetCommandString函数:当鼠标指针移到一个上下文相关菜单项上时,在当前窗口的状态条上将会出现与该菜单项相关的帮助信息,此
信息就是系统通过调用该函数获取的。

    下面我通过具体的例程来说明编写一个比较完整的上下文菜单程序,这个程序是一个文件操作程序,当安装并注册了外壳扩展的服务器动态连接库之后,当选择一个或者多个文件并单击鼠标右键后,在右键菜单中就会多出一个“执行文件操作”的上下文菜单,点击菜单就会弹出相应的程序执行文件操作。
    在整个程序的编写中,外壳扩展的服务器动态连接库是有Delphi4.0编写的,而动态连接库调用的文件操作程序是由VB6编写的。下面首先介绍服务器动态连接库的编写:
    服务器动态连接库的工程文件内容如下:

library contextmenu;
    uses
ComServ,
ContextMenuHandler in 'Unit2.pas';
//   contmenu_TLB in 'contmenu_TLB.pas';

exports
   DllGetClassObject,
   DllCanUnloadNow,
   DllRegisterServer,
   DllUnregisterServer;

{$R *.TLB}

{$R *.RES}

begin

end.

    将工程文件保存为contextmenu.dpr。
    服务器动态连接库的单位文件内容如下:   

unit ContextMenuHandler;

interface
   uses Windows,ActiveX,ComObj,ShlObj,Classes;

type
   TContextMenu = class(TComObject,IShellExtInit,IContextMenu)
   private
      FFileName: array[0..MAX_PATH] of Char;
   protected
      function IShellExtInit.Initialize =SEIInitialize; // Avoid compiler warning
      function SEIInitialize(pidlFolder: PItemIDList;lpdobj: IDataObject;
              hKeyProgID: HKEY): HResult; stdcall;
      function QueryContextMenu(Menu: HMENU;indexMenu, idCmdFirst, idCmdLast,
              uFlags: UINT): HResult; stdcall;
      function InvokeCommand(var lpici:TCMInvokeCommandInfo): HResult; stdcall;
      function GetCommandString(idCmd, uType: UINT;pwReserved: PUINT;
              pszName: LPSTR; cchMax: UINT): HResult; stdcall;
end;

const

   Class_ContextMenu: TGUID ='{19741013-C829-11D1-8233-0020AF3E

{全局唯一标识符(GUID)是一个16字节(128为)的值,它唯一地标识一个接口(interface)}
var
   FileList:TStringList;
   Buffer:array[1..1024]of char;

implementation

uses ComServ, SysUtils, ShellApi, Registry,UnitForm;

function TContextMenu.SEIInitialize(pidlFolder:PItemIDList; lpdobj: IDataObject;
   hKeyProgID: HKEY): HResult;
var
   StgMedium: TStgMedium;
   FormatEtc: TFormatEtc;
   FileNumber,i:Integer;
begin
   file://如果lpdobj等于Nil,则本调用失败
   if (lpdobj = nil) then begin
      Result := E_INVALIDARG;
      Exit;
   end;

   file://首先初始化并清空FileList以添加文件
   FileList:=TStringList.Create;
   FileList.Clear;
   file://初始化剪贴版格式文件
   with FormatEtc do begin
      cfFormat := CF_HDROP;
      ptd := nil;
      dwAspect := DVASPECT_CONTENT;
      lindex := -1;
      tymed := TYMED_HGLOBAL;
   end;
   Result := lpdobj.GetData(FormatEtc, StgMedium);
   if Failed(Result) then Exit;

   file://首先查询用户选中的文件的个数
   FileNumber := DragQueryFile(StgMedium.hGlobal,$FFFFFFFF,nil,0);
   file://循环读取,将所有用户选中的文件保存到FileList中
   for i:=0 to FileNumber-1 do begin
      DragQueryFile(StgMedium.hGlobal, i, FFileName,SizeOf(FFileName));
      FileList.Add(FFileName);
      Result := NOERROR;
   end;

   ReleaseStgMedium(StgMedium);
end;

function TContextMenu.QueryContextMenu(Menu: HMENU;indexMenu, idCmdFirst,
   idCmdLast, uFlags: UINT): HResult;
begin
   Result := 0;
   if ((uFlags and $)= CMF_NORMAL) or
      ((uFlags and CMF_EXPLORE) <> 0) then begin
// 往Context Menu中加入一个菜单项
    InsertMenu(Menu, indexMenu, MF_STRING or MF_BYPOSITION,idCmdFirst,
      PChar('执行文件操作'));
// 返回增加菜单项的个数
   Result := 1;
   end;
end;

function TContextMenu.InvokeCommand(var lpici:TCMInvokeCommandInfo): HResult;
var
//   sFile:TFileStream;
   charSavePath:array[0..1023]of char;
   sSaveFile:String;
   i:Integer;
   F: TextFile;
   FirstLine: string;
begin
   // 首先确定该过程是被系统而不是被一个程序所调用
   if (HiWord(Integer(lpici.lpVerb)) <> 0) then
   begin
      Result := E_FAIL;
      Exit;
   end;
   // 确定传递的参数的有效性
   if (LoWord(lpici.lpVerb) <> 0) then begin
      Result := E_INVALIDARG;
      Exit;
   end;

   file://建立一个临时文件保存用户选中的文件名
   GetTempPath(1024,charSavePath);
   sSaveFile:=charSavePath+'chen0001.tmp';

   AssignFile(F,sSaveFile);   { nextfile in Files property }
   ReWrite(F);
   file://将文件名保存到临时文件中
   for i:= 0 to FileList.Count -1 do begin
      FirstLine:=FileList.Strings[i];
      Writeln(F,FirstLine);    { Readthe first line out of the file }
   end;
   CloseFile(F);
   file://调用文件操作程序对用户选中的文件进行操作
  ShellExecute(0,nil,'c:\FileOP.exe',PChar(sSaveFile),charSavePath,SW_NORMAL);

   Result := NOERROR;
end;

function TContextMenu.GetCommandString(idCmd, uType: UINT;pwReserved: PUINT;
         pszName: LPSTR; cchMax: UINT):HRESULT;
begin
   if (idCmd = 0) then begin
   if (uType = GCS_HELPTEXT) then
      {返回该菜单项的帮助信息,此帮助信息将在用户把鼠标移动到该菜单项时出现在状态条上。}
      StrCopy(pszName, PChar('点击该菜单项将执行文件操作'));
      Result := NOERROR;
   end
   else
      Result := E_INVALIDARG;
end;

type
   TContextMenuFactory = class(TComObjectFactory)
   public
   procedure UpdateRegistry(Register: Boolean); override;
end;

procedure TContextMenuFactory.UpdateRegistry(Register:Boolean);
var
   ClassID: string;
begin
   if Register then begin
      inherited UpdateRegistry(Register);
      ClassID := GUIDToString(Class_ContextMenu);
      CreateRegKey('*\shellex', '', '');
      CreateRegKey('*\shellex\ContextMenuHandlers','', '');
     CreateRegKey('*\shellex\ContextMenuHandlers\OpenWithWordPad', '', ClassID);

    file://如果操作系统为WindowsNT的话
      if (Win32Platform = VER_PLATFORM_WIN32_NT) then
      with TRegistry.Create do
      try
         RootKey := HKEY_LOCAL_MACHINE;
        OpenKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions', True);
         OpenKey('Approved', True);
         WriteString(ClassID, 'ContextMenu Shell Extension');
      finally
         Free;
      end;
   end
   else begin
     DeleteRegKey('*\shellex\ContextMenuHandlers\FileOpreation');
      DeleteRegKey('*\shellex\ContextMenuHandlers');
//      DeleteRegKey('*\shellex');
      inherited UpdateRegistry(Register);
   end;
end;

initialization
TContextMenuFactory.Create(ComServer, TContextMenu, Class_ContextMenu,
   '', 'Context Menu Shell Extension', ciMultiInstance,tmApartment);

end.

    将该单位文件保存为unit2.pas,文件同contextmenu.dpr位于同一个目录下。
    打开Delphi,选菜单中的 file | open project 打开contextmenu.dpr文件,然后选 Project | build contextmenu菜单项编译连接程序,如果编译成功的话,会建立一个contextmenu.dll的动态连接库文件,这个文件就是服务器动态连接库。

    下面来建立文件操作程序。打开VB,建立一个新的工程文件,在Form1中加入一个ListBox控件和三个CommandButton控件,将ListBox的MultiSelect属性设置为2。然后在Form1的代码窗口中加入以下代码:
Option Explicit

Private Type BrowseInfo
     hwndOwner As Long
     pIDLRoot As Long
     pszDisplayName As Long
     lpszTitle As Long
     ulFlags As Long
     lpfnCallback As Long
     lParam As Long
     iImage As Long
End Type
Private Type SHFILEOPSTRUCT
    hwnd As Long
    wFunc As Long       '对文件的操作指令
    pFrom As String     '源文件或路径
    pTo As String       '目的文件或路径
    fFlags As Integer   '操作标志
    fAnyOperationsAborted As Long
    hNameMappings As Long
    lpszProgressTitle As String
End Type

Const FO_COPY = &H2
Const FO_DELETE = &H3
Const FO_MOVE = &H1
Const FO_RENAME = &H4
Const FOF_ALLOWUNDO = &H40
Const BIF_RETURNONLYFSDIRS = 1
Const MAX_PATH = 260

Private Declare Function ShellAbout Lib"shell32.dll" Alias _
        "ShellAboutA" (ByVal hwndAs Long, ByVal szApp As _
        String, ByVal szOtherStuff AsString, ByVal hIcon As Long) _
        As Long
Private Declare Sub CoTaskMemFree Lib "ole32.dll" (ByVal hMem AsLong)
Private Declare Function lstrcat Lib "kernel32" Alias _
        "lstrcatA" (ByVallpString1 As String, ByVal lpString2 _
        As String) As Long
Private Declare Function SHBrowseForFolder Lib "shell32" (lpbi _
        As BrowseInfo) As Long
Private Declare Function SHGetPathFromIDList Lib "shell32" _
        (ByVal pidList As Long, ByVallpBuffer As String) As Long
Private Declare Function SHFileOperation Lib "shell32" _
        (lpFileOp As SHFILEOPSTRUCT) As Long
Private Declare Function GetWindowsDirectory _
        Lib "kernel32" Alias"GetWindowsDirectoryA" _
        (ByVal lpBuffer As String, ByValnSize As _
        Long) As Long

Dim DirString As String
Dim sFile As String

Sub UpdateList()
    'UpdateList函数检查列表框中的文件是否存在,如果不存在,就将其
    '从文件列表中删除
    Dim bEndList As Boolean
    Dim i As Integer
   
    bEndList = True
    i = 0
    While bEndList
        '检查文件是否存在,如果不存在就删除
        If Dir$(List1.List(i)) ="" Then
           List1.RemoveItem (i)
        Else    '如果文件存在就转移到下一个列表项
            i = i + 1
            If i >List1.ListCount - 1 Then
               bEndList = False
            End If
        End If
    Wend
    Command1.Enabled = False
    Command2.Enabled = False
    Command3.Enabled = False
End Sub

Function BrowseForFolder(hwndOwner As Long, sPrompt AsString) As String
     Dim iNull As Integer
     Dim lpIDList As Long
     Dim lResult As Long
     Dim sPath As String
     Dim udtBI As BrowseInfo

    '初试化udtBI结构
     With udtBI
        .hwndOwner = hwndOwner
        .lpszTitle = lstrcat(sPrompt,"")
        .ulFlags = BIF_RETURNONLYFSDIRS
     End With
   
    '弹出文件夹查看窗口
     lpIDList = SHBrowseForFolder(udtBI)
    
     If lpIDList Then
        sPath = String$(MAX_PATH, 0)
        lResult =SHGetPathFromIDList(lpIDList, sPath)
        Call CoTaskMemFree(lpIDList)
        iNull = InStr(sPath, vbNullChar)
        If iNull Then sPath = Left$(sPath,iNull - 1)
     End If

     BrowseForFolder = sPath
End Function

Private Sub Command1_Click()    '执行文件拷贝操作
    Dim sPath As String
    Dim tCopy As SHFILEOPSTRUCT
    Dim i As Integer
   
    '选择拷贝到的文件夹
    sPath = BrowseForFolder(Form1.hwnd, "选择拷贝到的文件夹")
    If sPath <> "" Then
        With tCopy
            .hwnd =Form1.hwnd
            .lpszProgressTitle= "正在拷贝"
            .pTo = sPath
            .fFlags =FOF_ALLOWUNDO
            .wFunc =FO_COPY
        End With
        For i = 0 To List1.ListCount - 1
            IfList1.Selected(i) Then   '如果文件被选中则拷贝文件
               tCopy.pFrom = List1.List(i)
               SHFileOperation tCopy
            End If
        Next i
        UpdateList
    End If
    Kill sFile
End Sub

Private Sub Command2_Click()    '执行文件移动操作
    Dim sPath As String
    Dim tCopy As SHFILEOPSTRUCT
    Dim i As Integer
   
    '选择移动到的文件夹
    sPath = BrowseForFolder(Form1.hwnd, "选择转移到的文件夹")
    If sPath <> "" Then
        With tCopy
            .hwnd =Form1.hwnd
           .lpszProgressTitle = "正在移动"
            .pTo = sPath
            .fFlags =FOF_ALLOWUNDO
            .wFunc =FO_MOVE
        End With
        For i = 0 To List1.ListCount - 1
            IfList1.Selected(i) Then   '如果文件被选中则拷贝文件
               tCopy.pFrom = List1.List(i)
               SHFileOperation tCopy
            End If
        Next i
        UpdateList
    End If
    Kill sFile
End Sub

Private Sub Command3_Click()    '执行文件删除操作
    Dim sPath As String
    Dim tCopy As SHFILEOPSTRUCT
    Dim i As Integer
   
    With tCopy
        .hwnd = Form1.hwnd
        .lpszProgressTitle = "正在删除"
        .pTo = sPath
        .fFlags = FOF_ALLOWUNDO
        .wFunc = FO_DELETE
    End With
    For i = 0 To List1.ListCount - 1
        If List1.Selected(i) Then
            tCopy.pFrom= List1.List(i)
           SHFileOperation tCopy
        End If
    Next i
    UpdateList
    Kill sFile
End Sub

Private Sub Form_Load()
    Dim hFileHandle As Long
    Dim TextLine As String
   
    Command1.Caption = "拷贝"
    Command2.Caption = "移动"
    Command3.Caption = "删除"
    Command1.Enabled = False
    Command2.Enabled = False
    Command3.Enabled = False
   
    'sFile接受由Windows外壳扩展库contextmenu.dll传递过来的文件参数
    sFile = Command$
    hFileHandle = FreeFile
    Open sFile For Input As hFileHandle
    Do While Not EOF(hFileHandle)
        Line Input #1, TextLine
        If Dir$(TextLine) <>"" Then
           List1.AddItem TextLine
        End If
    Loop
    Close hFileHandle
End Sub

Private Sub Form_Unload(Cancel As Integer)
    If Dir$(sFile) <> "" Then
        Kill sFile
    End If
End Sub

Private Sub List1_Click()
    If Not Command1.Enabled Then
        Command1.Enabled = True
        Command2.Enabled = True
        Command3.Enabled = True
    End If
End Sub
    保存文件并将工程文件编译为FileOP.exe文件,将文件拷贝到C盘根目录下。然后注册contextmenu.dll,注册的方法是,在DOS窗口中进入Windows\system子目录,输入 Regsvr32 x:\xxxxx\contextmenu.dll 。其中x:\xxxxx\为Contextmenu.dll文件所在的驱动器和目录。如果注册成功,系统会弹出对话框,显示 DllRegisterServer in ..\xxx\contextmenu.dll Success 提示注册成功。
    注册成功后,再选择文件并单击右键,就会发现在弹出菜单中多了一个“执行文件操作”的菜单项,点击该项,系统就会调用FileOP.exe执行文件操作,在窗口的列表框中会出现用户选择的文件名,点击相应的文件并点击“拷贝”、“移动”或“删除”按钮就可以对列表框中的选中的文件进行相应的操作。

 

 

 

由 ATL 想起的外壳扩展编程(一)


作者/李晓飞


下载源代码


    好久没有给VC知识库发稿了,实在不好意思,由于前段时间实在太忙所以一直没有时间闲下心来写点东西,期间也有不少朋友给我来信讨论问题,我很感谢大家对我的支持,我欢迎大家继续来信,共同交流,共同进步!这次我想和大家一起讨论一下 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"和"UnRegisterComponent"菜单项既是我们自己的菜单项。并且这两个菜单项分别完成进程内组件(DLL)的注册和反注册,菜单项的功能倒很简单,只是简单地执行了 Windows 的 Regsvr32.exe而已,但是我们已经感觉到它给我们带来的实用和方便,难道你不觉得 "Over and Over" 手工输入"Regsvr32 xxx.dll" 或者 "Regsvr32 /uxxx.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 扩展初始化动作都由该函数完成。该函数的原型如下:
3.  HRESULT Initialize(
4.        LPCITEMIDLIST pidlFolder,
5.    LPDATAOBJECT lpdobj,
6.    HKEY hkeyProgID
 );

    在 Initialize 函数中,我们要做的事情就是获取用户鼠标右键点击的文件名称,但是有可能用户选择了多个文件,这里为了简单起见我们仅获取文件列表中的第一个文件。在这里我们得补充一点内容:当用户在一个拥有 WS_EX_ACCEPTFILES 风格的窗体中Drag/Drop 文件时这些文件名会以同一种格式存储,而且文件完整路径的获取也都以DragQueryFile API函数来实现。但是 DragQueryFile 需要传入一个 HDROP 句柄,该句柄即为 Drag/Drop 文件名称列表数据句柄(开始存放数据的内存区域首指针)。而 HDROP 句柄的可以通过接口 " DATAOBJECTlpdobj" 的成员函数"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 类的 等等函数库,因为这样会使您无法通过 "Win32Release 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" 菜单项所对应的 "注册/反注册进程内组件" 动作。

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

    其实这一步十分简单,本可以忽略,但是为了把过程讲的更清楚一点我还是列了出来:
8.  //声明组件注册所用的注册表REG资源
9.  //其中IDR_SIMPLESHLEXT为注册表资源ID
10.DECLARE_REGISTRY_RESOURCEID(IDR_SIMPLESHLEXT)
11.//AddRef和Release成员函数的实现
12.DECLARE_PROTECT_FINAL_CONSTRUCT()
13.//组件接口映射部分,该部分映射主要是告诉QueryInterface能返回哪些接口给外部
14.BEGIN_COM_MAP(CSimpleShlExt)
15. COM_INTERFACE_ENTRY(ISimpleShlExt)
16. COM_INTERFACE_ENTRY(IDispatch)
17. COM_INTERFACE_ENTRY(IShellExtInit)  //IShellExtInit接口
18. COM_INTERFACE_ENTRY(IContextMenu)  //IContextMenu接口
END_COM_MAP()
  1. 组件注册
    • 首先要在系统文件类型".DLL"下注册上下文菜单处理器

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

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

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

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

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