Delphi实现shell扩展

来源:互联网 发布:骑士对火箭数据 编辑:程序博客网 时间:2024/05/18 02:21


博主的新Blog地址:http://www.brantchen.com

欢迎访问:)


        转贴自网上余昊 的pdf格式,经过自己的整理,放于此共享。本博客转贴文章无意侵犯版权,如有,请先通知,本博客会即刻处理

1. 准备工作

1. 对注册表做一些工作。因为任何外壳扩展都是作为DLL加载到Explorer的进程空间的,如果不做手脚,那么,只要Explorer存在,那么你就无法顺利编译shell程序。建议使用Windows优化大师,选中“启动系统时为桌面和Explorer创建独立的进程”

2. 下载DebugView来调试外壳扩展程序。

3. 一定要处理你能够处理的所有错误。因为,你知道,ExplorerWindows中的重要性,你稍不留神就崩掉的话,恐怕没人敢用你的外壳程序了:)

 

 

 

2. 需求

1. 对任何文件可以进行Copy(Move) to Anywhere。参考软件Nuts & Bolt

2. 对于COM组件库,能够实现Register/Unregister功能。

3. 对于图片文件,能在Context Menu中预览。参考软件PicaView

 

 

 

3. 搭建框架

因为任何外壳扩展都是COM组件,所以,需要建立一个ActiveX Library,以及一个COM Object。另外,外壳扩展需要对Delphi生成的代码进行额外处理才能成为一个外壳扩展COM组件,即从TComObjectFactory派生一个类才行。

 

 

4. 接口支持需求

绝大多数外壳程序需要支持基本的接口:IShellExtInit

另外,对于每一种扩展,我们还需要实现一到两个接口。

对于Context Menu,必须支持的两个接口是:IShellExtInit IContextMenu

如果要支持自绘式菜单,还需要支持的接口:IContextMenu2或者 IContextMenu3

 

 

 

5. 解决继承接口的命名冲突

示例代码:使用语法解决继承接口的命名冲突

TCCContextMenu = class(TComObject, IShellExtInit)

private

FFileList: TStringList;

FGraphic: TGraphic;

protected

{ IShellExtInit接口 }

function IShellExtInit.Initialize = SEInitialize;

function SEInitialize(pidFolder: PItemIDList; lpdobj: IDataObject; hKeyProgID: HKEY): HResult; stdcall;

public

procedure Initialize; override;

destructor Destroy; override;

 

代码分析:

1) 为什么重载了TComObjInitializeDestroy而不是Create
因为TComObj有多个构造函数,但是无论哪个,都会调用Initialize,所以,这里是初始化的最好地方。

 

 

6. 实现InitializeDestroyIShellExtInit.Initialize

InitializeDestroy很简单,可以加入打印的调试信息,便于观察外壳扩展的生命周期;主要是实现IShellExtInit.Initialize

 

IShellExtInit.Initialize的三个参数中,最重要的是系统传递给我们的IDataObject,我们可以从中获得用户选择的文件列表。

示例代码:IShellExtInit.Initialize.可以被任何实现IShellExtInit的类所调用

function TCCContextMenu.SEInitialize(pidlFolder: PItemIDList; lpdobj: IDataObject; hKeyProgID: HKEY): HResult;

begin

 Result := GetFileListFromDataObject(lpdobj, FFileList);

end;

 

function GetFileListFromDataObject(lpdobj: IDataObject; sl: TStringList): HResult;

var

fe: FormatEtc;

sm: StgMedium;

i, iFileCount: Integer;

FileName: array[0..MAX_PATH+1] of char;

begin

assert(lpdobj<>nil);

 assert(sl<>nil);

sl.clear;

 

with fe do

begin

cfFormat := CF_HDROP;

ptd := nil;

dwAspect := DVASPECT_CONTENT;

lindex := -1;

tymed := TYMED_HGLOBAL;

end;

 

with sm do

begin

tymed := TYMED_HGLOBAL;

end;

 

Result := lpdobj.GetData(fe, sm);

if Failed(Result) then Exit;

iFileCount := DragQueryFile(sm.hGlobal, $ffffffff, nil, 0);

if iFileCount<=0 then

begin

ReleaseStgMedium(sm);

Result := E_INVALIDARG;

Exit;

end;

 

for i:=0 to iFileCount-1 do

begin

DragQueryFile(sm.hGlobal, i, FileName, sizeof(FileName));

sl.Add(FileName);

end;

 

ReleaseStgMedium(sm);

Result := S_OK;

end;

 

 

7. 实现对IContextMenu的支持

IContextMenu有三个方法,首先讲菜单弹出前系统调用的方法:QueryContextMenu

 

function QueryContextMenu(Menu: HMENU; indexMenu, idCmdFirst, idCmdLast, uFlags: UINT): HRESULT; stdcall;

Ø        Menu:就是系统开发给你的上下文菜单的句柄,可以用InsertMenu或者InsertMenuItem之类的函数向里面增加菜单

Ø        indexMenu:系统预留给你的菜单项的位置,你应该从这个位置开始加入菜单,但是加入的菜单项个数不要超过idCmdLast-idCmdFirst这个范围

Ø        uFlags:是一些标志位。

Ø        返回值:函数的返回值应该是你加入的菜单个数和其他一些标志的组合。

示例代码:QueryContextMenu

const

 // 菜单类型

 mfString = MF_STRING or MF_BYPOSITION;

 mfOwnerDraw = MF_OWNERDRAW or MF_BYPOSITION;

 mfSeparator = MF_SEPARATOR or MF_BYPOSITION;

 

 // 菜单项

 idCopyAnywhere = 0; // 复制(移动)

 idRegister = 5; //注册ActiveX

 idUnregister = 6; //取消注册ActiveX

 idImagePreview = 10; //预览图片文件

 idMenuRange = 90;

 

//SDK中是使用宏Make_HRESULT实现的,Delphi没有宏的概念,所以这里用函数

function Make_HResult(sev, fac, code: Word): DWord;

begin

 Result := (sev shl 31) or (fac shl 16) or code;

end;

 

function TCCContextMenu.QueryContextMenu(Menu: HMENU; indexMenu, idCmdFirst, idCmdLast, uFlags: UINT): HResult;

var

 Added: UINT;

begin

 if(uFlags and CMF_DEFAULTONLY)=CMF_DEFAULTONLY then

 begin

Result := Make_HResult(SEVERITY_SUCCESS, FACILITY_NULL, 0);

Exit;

 end;

 Added := 0;

 

 // 加入CopyAnywhere菜单项

 InsertMenu(Menu, indexMenu, mfSeparator, 0, nil);

 InsertMenu(Menu, indexMenu, mfString, idCmdFirst+idCopyAnywhere, PChar(sCopyAnywhere));

 InsertMenu(Menu, indexMenu, mfSeparator, 0, nil);

 Inc(Added, 3);

 

 Result := Make_HResult(SEVERITY _SUCCESS, FACILITY_NULL, idMenuRange);

end;

 

 

 

接下来实现第二个函数:InvokeCommand

这是在用户点击菜单时调用,是真正执行动作的地方。

示例代码:InvokeCommand

function TCCContextMenu.InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult;

begin

 Result := E_INVALIDARG;

 if HiWord(Integer(lpici.lpVerb))<>0 then Exit;

 case LoWord(Integer(lpici.lpVerb)) of

idCopyAnywhere:

   DoCopyAnywhere(lpici.hwnd, FFileList);

end;

Result := NOERROR;

end;

 

procedure DoCopyAnywhere(Wnd: HWND; sl: TStringList);

var

 frm: TfrmCopyAnywhere;

begin

 frm := TfrmCopyAnywhere.Create(Application);

 try

frm.AddFiles(sl);

frm.ShowModal;

 finally

frm.Free;

 end;

end;

 

TfrmCopyAnywhere是界面,使用SHFileOperation来执行Copies, moves, renames, or deletes a file system object,据说好用。

 

OK,接下来实现第三个函数,也是这个接口的最后一个函数:GetCommandString

当用户选择菜单项时,在资源管理器的状态栏会显示一些提示信息,这里需要注意Unicode/Ansi的区别。

示例代码:GetCommandString

function TCCContextMenu.GetCommandString(idCmd, uType: UINT; pwReserved: PUINT; pszName: LPSTR; cchMax: UINT): HResult;

var

 strTip: String;

 wstrTip: WideString;

begin

 strTip := ‘‘;

 Result := E_INVALIDARG;

 if (uType and GCS_HELPTEXT)<> GCS_HELPTEXT then Exit;

 case idCmd of

idCopyAnywhere: strTip := sCopyAnywhereTip;

 end;

 if strTip<>‘‘ then

 begin

if (uType and GCS_UNICODE)=0 then //Anse

begin

 lstrcpynA(pszName, PChar(strTip), cchMax);

end

else

begin

 wstrTip := strTip;

 lstrcpynW(PWideChar(pszName), PWideChar(wstrTip), cchMax);

end;

Result := S_OK;

 end;

end;

 

 

8. 实现Context Menu Extension的类工厂

如果没有实现Context Menu Extension的类工厂,那么期待已久的shell扩展还是没法实现:)

这里需要处理很多注册表,幸好Delphi有几个好函数,所以可以省很多功夫。

 

示例代码:实现Context Menu Extension的类工厂

procedure TCCContextMenuFactory.UpdateRegistry(Register: Boolean);

 

procedure DeleteRegValue(const Path, ValueName: String; Root: DWord=HKEY_CLASSES_ROOT);

var

   reg: TRegistry;

begin

   reg := TRegistry.Create;

   with reg do

   begin

try

RootKey := Root;

if OpenKey(Path, False) then

begin

     if ValueExists(ValueName) then DeleteValue(ValueName);

     CloseKey;

end;

finally

   Free;

end;

   end;

end;

 

const

RegPath = ‘*/shellex/ContextMenuHandlers/CCShellExt’;

ApprovedPath = ‘Software/Microsoft/Windows/CurrentVersion/ShellExtensions/Approved’;

 

var

strGUID: String;

begin

 inherited;

 strGUID := GUIDToString(Class_CCContextMenu);

 if Register then

 begin

CreateRegKey(RegPath, ‘‘, strGUID);

CreateRegKey(ApprovedPath, strGUID, ‘CC的外壳扩展’, HKEY_LOCAL_MACHINE);

 end

 else

 begin

DeleteRegKey(RegPath);

DeleteRegValue(ApprovedPath, strGUID, HKEY_LOCAL_MACHINE);

 end;

end;

 

现在,在添加新的全局对象初始化:

示例代码:

initialization

 TCCContextMenuFactory.Create(ComServer, TCCContextMenu, Class_CCContextMenu,   '', '', ciMultiInstance, tmApartment);

 TTypedComObjectFactory.Create(ComServer, TCCContextMenu, Class_CCContextMenu,

   ciMultiInstance, tmApartment);

 

然后,只要在IDE中执行Run->Register ActiveX Server命令,就可以在资源管理器中检阅自己的劳动成果了:)

 

9. 实现ActiveX的注册/反注册功能

我们这里还实现了从菜单对选择的单一exe/ocx文件进行注册的功能。这主要就是载入ActiveX库,然后调用DllRegisterServer或者DllUnregisterServer。这样,需要修改原来实现的接口的代码。

 

同时,这里为注册和反注册菜单加入了两个图标,使用SetMenuItemBitmaps函数实现。

 

先讲一下,如何在Delphi中加入资源:

Ø        准备两个14*14的图像(如果不嫌麻烦的话,可以用GetMenuCheckMarkDimensions确认下是否为这个大小)

Ø        建立一个文本文件,写入:
101 BITMAP
reg.bmp
102 BITMAP
unreg.bmp
然后保存为ExtraRes.rc。(其他名称也行,但是不要和项目中的文件重复)

Ø        IDE中选择菜单Add to Project,选择即可。

 

主要代码如下:

 

示例代码:

实现注册/反注册功能。4个方法:IsActiveLibRegisterActiveLibUnregisterActiveLibReportWin32Error

resourcestring

 sCopyAnywhere = ‘复制到... ‘;

 sCopyAnywhereTip = ‘将选定的文件复制到任何路径下’;

 sRegister = ‘注册...’;

 sRegisterTip = ‘注册ActiveX’;

 sUnregister = ‘取消注册...’;

 sUnregisterTip = ‘取消注册ActiveX’;

 sImagePreview = ‘预览图片文件’;

 sImagePreviewTip = ‘预览图片文件’;

 

function IsActiveLib(const FileName: String): Boolean;

var

 Ext: String;

 hLib: THandle;

begin

 Result := False;

 Ext := UpperCase(ExtractFileExt(FileName));

 if (Ext<>‘.EXE’) and (Ext<>‘.DLL’) and (Ext<>‘.OCX’) then Exit;

 

 hLib := LoadLibrary(PChar(FileName));

 if hLib=0 then Exit;

 if GetProcAddress(hLib, ‘DllRegisterServer’)<>nil then Result := True;

 FreeLibrary(hLib);

end;

 

procedure RegisterActiveLib(Wnd: HWND; const FileName: String);

var

 hLib: THandle;

 fn : TDllRegisterServer;

 hr: HResult;

begin

 hLib := LoadLibrary(PChar(FileName));

 if hLib=0 then

 begin

ReportWin32Error(Wnd, ‘装载文件失败’, GetLastError);

Exit;

 end;

 

 fn := TDllRegisterServer(GetProcAddress(hLib, ‘DllRegisterServer’));

 if not Assigned(fn) then

 begin

MessageBox(Wnd, ‘定位函数入口点DllRegisterServer失败’, ‘错误’, MB_ICONEXCLAMATION);

FreeLibrary(hLib);

Exit;

 end;

 

 hr := fn();

 if Failed(hr) then

 begin

ReportWin32Error(Wnd, ‘注册动态库失败’, hr);

FreeLibrary(hLib);

Exit;

 end;

 

 MessageBox(Wnd, ‘注册成功’, ‘成功, MB_ICONINFORMATION);

FreeLibrary(hLib);

end;

 

procedure UnregisterActiveLib(Wnd: HWND; const FileName: String);

var

 hLib: THandle;

 fn : TDllRegisterServer;

 hr: HResult;

begin

 hLib := LoadLibrary(PChar(FileName));

 if hLib=0 then

 begin

ReportWin32Error(Wnd, ‘装载文件失败’, GetLastError);

Exit;

 end;

 

 fn := TDllUnregisterServer(GetProcAddress(hLib, ‘DllUnregisterServer’));

 if not Assigned(fn) then

 begin

MessageBox(Wnd, ‘定位函数入口点DllUnregisterServer’失败’, ‘错误’, MB_ICONEXCLAMATION);

FreeLibrary(hLib);

Exit;

 end;

 

 hr := fn();

 if Failed(hr) then

 begin

ReportWin32Error(Wnd, ‘取消注册动态库失败’, hr);

FreeLibrary(hLib);

Exit;

 end;

 

 MessageBox(Wnd, ‘取消注册成功’, ‘成功, MB_ICONINFORMATION);

FreeLibrary(hLib);

end;

 

prcedure ReportWin32Error(Wnd: HWND; const Prefix: String; dwError: DWord);

var

 szError: array[0..399] of char;

 str: String;

begin

 FormatMessage(FROMAT_MESSAGE_FROM_SYSTEM, nil, dwError, Make_LangID(LANG_NEUTRAL, SUBLANG_DEFAULT), szError, sizeof(szError), nil);

 str := Format(‘%s:%s’, [Prefix, StrPas(szError)]);

 MessageBox(Wnd, PChar(str), ‘错误’, MB_ICONEXCLAMATION);

end;

 

 

 

10. 加入图像预览功能

IContextMenu虽然能支持普通的菜单项,但是无法处理自绘制的菜单(Owner-Draw)。即使用MF_OWNERDRAW加入菜单也不行,因为自绘制菜单的处理,最终要由Exploer的窗口进行,而IContextMenu没有提供一条截获窗口过程对菜单的处理。微软然后加入了IContextMenu2IContextMenu2,但是IContextMenu2好像还是没有起作用,所以,我们用IContextMenu3来实现。

主要代码如下:

示例代码:IContextMenu3.HandleMenuMsg2

function TCCContextMenu.HandleMenuMsg2(uMsg: UINT; wParam, lParam: Integer; var lpResult: Integer): HResult;

var

 pmis: PMeasureItemStruct;

 pdis: PDrawItemStruct;

begin

 Result := S_OK;

 case uMsg of

WM_MEASUREITEM:

begin

 pmis := PMeasureItemStruct(lParam);

 if not Assigned(FGraphic) then

 begin

   pmis.itemWidth := 120;

   pmis.itemHeight := 120;

   Exit;

 end;

 // 如果图片小于120*120,那么按照实际的显示,否则缩放到120*120

 if (FGraphic.Width<=120) and (FGraphic.Height<=120) then

 begin

   pmis.itemWidth := 140;

   pmis.itemHeight := FGraphic.Height + 40;

 end

 else

 begin

   pmis.itemWidth := 140;

   pmis.itemHeight := 160;

end;

end;

WM_DRAWITEM:

begin

pdis := PDrawItemStruct(lParam);

 DrawGraphic(pdis.hDC, pdis,rcItem, pdis.itemState, FGraphic);

end;

 end;

end;

 

procedure DrawGraphic(adc: HDC; rc: TRect; State: Integer; Graphic: TGraphic);

var

 rcImage, rcText, rcStretch: TRect;

 Canvas: TCanvas;

 nSaveDC: Integer;

 x, y: Integer;

 xScale, yScale, Scale: Double;

 xStretch, yStretch: Integer;

begin

 with rcImage do

 begin

Left := rc.Left + 10;

Right := rc.Right – 10;

Top := rc.Top + 10;

Bottom := rc.Bottom – 30;

 end;

 with rcText do

 begin

Left := rc.Left + 10;

Right := rc.Right – 10;

Top := rc.Top - 20;

Bottom := rc.Bottom;

 end;

 

 Canvas := TCanvas.Create;

 nSaveDC := 0;

 try

nSaveDC := SaveDC(adc);

Canvas.Handle := adc;

with Canvas do

begin

 if not Assigned(Graphic) then

 begin

   Rectangle(rcImage);

   MoveTo(rcImage.Left, rcImage.Top);

   LineTo(rcImage.Right, rcImage.Bottom);

   MoveTo(rcImage. Right, rcImage.Top);

   LineTo(rcImage. Left, rcImage.Bottom);

   DrawText(Canvas.Handle, ‘未知图像’, -1, rcImage, DT_SINGLELINE or DT_CENTER or DT_VECNTER);

 end

 else

 begin

   if (Graphic.Width<rcImage.Right-rcImage.Left) and (Graphic.Height<rcImage.Bottom-rcImage.Top) then

   begin

     x := rcImage.Left + (rcImage.Right - rcImage.Left - Graphic.Width) div 2;

     y := rcImage. Top + (rcImage. Bottom - rcImage. Top - Graphic. Height) div 2;

     Canvas.Draw(x, y, Graphic);

   end

   else

   begin

     xScale := Graphic.Width / (rcImage.Right - rcImage.Left);

     yScale := Graphic.Height / (rcImage.Bottom - rcImage.Top);

     Scale := Max(xScale, yScale);

     xStretch := Trunc(Graphic.Width / Scale);

     yStretch := Trunc(Graphic. Height / Scale);

     x := rcImage.Left + (rcImage.Right - rcImage.Left - xStretch) div 2;

     y := rcImage. Top + (rcImage. Bottom - rcImage. Top - yStretch) div 2;

     rcStretch := Rect(x, y, x+xStretch, y+yStretch);

     Canvas.StretchDraw(rcStretch, Graphic);

   end;

   Windows.FillRect(Canvas.Handle, GetSysColor(COLOR_MENUTEXT));

   SetBkColor(Canvas. Handle, PChar(ImageInfoToStr(Graphic)), -1, rcText, DT_SINGLELINE or DT_CENTER or DT_VCENTER);

 end;

end;

finally

Canvas.Handle :=0;

Canvas.Free;

RestoreDC(adc, nSaveDC);

end;

end;

 

function ImageInfoToStr(Graphic: TGraphic): String;

begin

 Result := Format(‘%d * %d’, [Graphic.Width, Graphic.Height]);

 if Graphic is TIcon then Result := Result + ‘图标’;

 if Graphic is TBitmap then

begin

 case TBitmap(Graphic).PixelFormat of

   pfDevice: Result := Result + ‘DDB’;

   pf1bit: Result := Result + ‘2;

   pf4bit: Result := Result + ‘16;

   pf8bit: Result := Result + ‘256;

pf15bit, pf16bit: Result := Result + ‘16位色;

pf24bit: Result := Result + ‘24位色;

pf32bit: Result := Result + ‘32位色;

 pfCustom: Result := Result + ‘自定义’;

end;

Result := Result + ‘位图’;

end;

 

if Graphic is TMetaFile then

begin

 Result := Result + Format(‘(%d*%d) 元文件’, [TMetaFile(Graphic),MMWidth div 100, TMetaFile(Graphic).MMHeight div 100])

end;

 

if Graphic is TJPEGImage then

begin

 case TJPEGImage(Graphic).PixelFormat of

   jf24Bit: Result := Result + ‘24位色JPEG’;

   jf8Bit: Result := Result + ‘8位色JPEG’;

 end;

end;

end;

 

 

博主的新Blog地址:http://www.brantchen.com

欢迎访问:)