使用
Delphi
開發(fā)Office
Word插件
??? 在Office 2000中提供了基于COM的插件開發(fā)框架,這使得我們可以利用Delphi來擴(kuò)展Office的功能。
???
在Delphi
3,4中編寫基于COM的插件,我們需要自己創(chuàng)建COM接口的封裝類,更糟糕的是要想支持事件的話還需要使用連接點(diǎn)(connection
points)對象來實(shí)現(xiàn)事件回調(diào),這是非常麻煩的。但在Delphi 5中這一切就變得非常輕松了,Delphi
5的類型庫引入工具提供了/L+的開關(guān),可以自動(dòng)為我們生成封裝好的OLE Server。這下子再也沒有什么好抱怨的了。
Office 2000 插件框架
??? 在Microsoft'的網(wǎng)站上,知識(shí)庫文章(Knowledge Base article Q230689)中有一篇:Office 2000 COM Add-In Written in Visual C++ 。文章中提供了一個(gè)例子(http://support.microsoft.com/download/support/mslfiles/ COMADDIN.EXE)。這篇文章詳細(xì)地描述了插件框架中的COM接口。仔細(xì)研究一下C++代碼就可以了解如何編寫Office 2000插件。
??? Office 2000插件其實(shí)就是一個(gè)實(shí)現(xiàn)了IDTExtensibility2接口的自動(dòng)化對象。IDTExtensibility2 接口相當(dāng)簡單,插件需要實(shí)現(xiàn)接口定義的全部5個(gè)函數(shù):
??? OnConnection:當(dāng)應(yīng)用程序連接到插件時(shí)會(huì)調(diào)用這個(gè)函數(shù)。插件在函數(shù)中接收下列初始化信息——應(yīng)用程序?qū)ο竽P瓦M(jìn)入點(diǎn)的指針,連接模式(是手工加入還是通過命令行載入), 應(yīng)用程序的對象模型指針和用戶自定義的信息。
??? OnDisconnection:當(dāng)應(yīng)用程序斷開插件時(shí)被調(diào)用,插件應(yīng)該在這里清除先前分配的資源,刪除它添加到應(yīng)用程序的界面元素。
??? OnStartupComplete:這個(gè)函數(shù)是當(dāng)應(yīng)用程序自動(dòng)啟動(dòng)插件時(shí)被調(diào)用的。調(diào)用時(shí),其他的插件都已經(jīng)被加載到了內(nèi)存,這時(shí)可以同其他插件進(jìn)行通信。這個(gè)函數(shù)還適合添加用戶界面元素。
??? OnBeginShutdown:當(dāng)應(yīng)用程序準(zhǔn)備關(guān)閉并將要斷開插件時(shí)會(huì)被調(diào)用,這時(shí)插件應(yīng)該停止接收用戶輸入。
??? OnAddInsUpdate:當(dāng)注冊的插件列表被改變后會(huì)被調(diào)用。如果我們的插件不依賴于其他插件,這個(gè)函數(shù)可以為空。
接口、類型庫和常數(shù)
???
創(chuàng)建插件前,我們需要引入COM對象的接口類型庫。這里使用Delphi 5帶的TlibImp.exe
(Delphi5\Bin目錄下)來引入類型庫。新版的TlibImp.exe支持新的/L+開關(guān),可以自動(dòng)創(chuàng)建一個(gè)OLE
Server的Delphi封裝。IDTExtensibility2接口是在MSADDNDR.DLL文件中聲明的,位于\Program
Files\Common Files\Designer\ 目錄下。調(diào)用TLIBIMP\L+\Program Files\Common
Files\ Designer\MSADDNDR.DLL會(huì)生成AddInDesignerObjects_TLB.pas 和
AddInDesignerObjects_TLB.dcr兩個(gè)文件。在項(xiàng)目的uses部分加上對上面文件的引用以便使用接口。clause of
our project to gain access to the
interface.使用時(shí)注意:TLIBIMP重命名接口為_IDTExtensibility2。
???
本文中將使用Word
2000作為例子,如果想編寫Outlook、Excel或其他Office程序的插件需要引入相應(yīng)特定的類型庫。比如Word的類型庫是定義在\
Program Files\Microsoft
Office\Office\MSWORD9.OLB文件中。類似的,Excel、Access和OutLook類型庫分別定義在EXCEL9.OLB、
MSACC9.OLB和MSOUTL9.OLB文件中。引入的接口生成在Office_TLB.pas和Word_TLB.pas單元中。
??? 注意:Office 2000的插件無法工作在Office 97的應(yīng)用程序中。
最簡單的插件
?? ?現(xiàn)在讓我們來實(shí)現(xiàn)一個(gè)最簡單的插件,它只實(shí)現(xiàn)了IDTExtensibility2接口而沒有實(shí)現(xiàn)任何比較有意義的功能,但對于演示如何實(shí)現(xiàn)插件是一個(gè)很好的開始。
???
插件可以以進(jìn)程內(nèi)或進(jìn)程外COM服務(wù)器的形式實(shí)現(xiàn),在本文中,我們創(chuàng)建的是進(jìn)程內(nèi)COM服務(wù)器。在Delphi中,選擇菜單File |
New命令,然后創(chuàng)建一個(gè)ActiveX Library,保存生成的文件,再創(chuàng)建一個(gè)自動(dòng)化對象(Automation
Object),類名定義為AddIn,把實(shí)現(xiàn)單元保存為AddInMain.pas。在AddInMain.pas單元的uses部分添加對
AddinDesignerObjects_TLB,Word_TLB和Office_TLB單元的引用。最后添加
IDTExtensibility2 接口到類定義部分定義類要實(shí)現(xiàn)的接口。類定義如下:
??? type
??? ??TAddIn = class(TAutoObject, IAddIn, IDTExtensibility2)
??? ...
??? 在類聲明的protected部分,添加IDTExtensibility2 接口聲明的方法定義,代碼示意如下:
? ??// IDTExtensibility2 methods
??? procedure OnConnection(const Application: IDispatch;
??? ??ConnectMode: ext_ConnectMode; const AddInInst: IDispatch;
??? var custom: PSafeArray); safecall;
??? procedure OnDisconnection(RemoveMode: ext_DisconnectMode;
??? var custom: PSafeArray); safecall;
??? procedure OnAddInsUpdate(var custom: PSafeArray); safecall;
??? procedure OnStartupComplete(var custom: PSafeArray);? safecall;
??? procedure OnBeginShutdown(var custom: PSafeArray); safecall;
??? 使用快捷鍵[Ctrl][Shift][C]來完成類定義,并添加方法的實(shí)現(xiàn)部分的框架到單元中。為了測試插件,可添加下面代碼到OnConnection方法中:
??? ShowMessage('連接到' + WordApp.Name);
??? 添加下面代碼到OnDisconnection方法的實(shí)現(xiàn)部分:
??? ShowMessage('斷開插件');
??? 這樣就完成了一個(gè)最簡單的插件了,接下來就是編譯并注冊插件到Word中去。
注冊O(shè)ffice插件
???
同其他COM對象一樣,一個(gè)Office插件必須在系統(tǒng)中注冊后才能使用。在Delphi中選擇Run | Register ActiveX
Server菜單命令,就可以注冊我們剛才創(chuàng)建的插件。除了標(biāo)準(zhǔn)的COM注冊,還需要進(jìn)行Office 相關(guān)的注冊,這需要在注冊表中創(chuàng)建一個(gè)新的鍵值:
??? HKEY_CURRENT_USER\Software\Microsoft\Office\
??? ??<AppName>\Addins\<AddInProgID>
??? <AppName>就是插件宿主應(yīng)用程序的名字(這里是Word),<AddInProgID>是自動(dòng)化對象的名字(這里是DIWordAddIn.AddIn,ActiveX library和類名的組合)。
??? HKEY_CURRENT_USER \ Software \ Microsoft \ Office \ Word \ Addins\DIWord AddIn.AddIn
???
我們還需要在這個(gè)鍵值下創(chuàng)建幾個(gè)值:一個(gè)DWORD類型的名為LoadBehavior的值決定插件是如何加載及被應(yīng)用程序調(diào)用的。在本文中我們設(shè)定它為
3–相當(dāng)于1和2的結(jié)合就是應(yīng)用程序連接插件并在啟動(dòng)時(shí)自動(dòng)加載。值的意義列在表1.2中,各種值可以相互組合。
表1.2
值
|
意??? 義
|
$0
|
? 斷開,不加載
|
$1
|
? 連接,加載
|
$2
|
? 自動(dòng)啟動(dòng)加載
|
$8
|
? 只有當(dāng)用戶請求時(shí)才加載
|
$16
|
? 只在下次程序啟動(dòng)時(shí)加載一次
|
??? 還有一些其他的值可以出現(xiàn)在注冊表鍵值下,比如定義出現(xiàn)在應(yīng)用程序COM管理器對話框中的名字,以及設(shè)定是否可以從命令行激活插件。
Office 2000用戶界面
??? Office應(yīng)用程序共享一組通用的用戶界面元素對象、菜單條、工具條通用控件(比如工具條按鈕和組合編輯框)以及Office小助手。
???
此前引入的Word類型庫就包括了這些通用對象的類型庫,但是Delphi引入時(shí)并沒有像通常那樣建立一個(gè)封裝好的OLE
Server,我們不得不手工創(chuàng)建一個(gè)Office公開的CommandBarButton對象的Delphi封裝。這個(gè)對象對應(yīng)于Office應(yīng)用程序
的一個(gè)簡單的菜單項(xiàng)或工具條按鈕。
???
對大多數(shù)的Microsoft的應(yīng)用程序來說,Application對象代表對象模型的切入點(diǎn)。Office
Application類提供了對CommandBars屬性的引用。CommandBar對象包括工具條、浮動(dòng)工具條和菜單。Office對象模型允許
我們創(chuàng)建或更新已有的CommandBars對象。Office_TLB.pas單元包含了ICommandBar接口,它可以被用來修改
CommandBar對象。
???
CommandBar有一個(gè)Controls集合屬性,對應(yīng)于一組CommandBarControl控件。CommandBarControl控件對應(yīng)
于放置在工具條上的控件,比如一個(gè)CommandBarButton對應(yīng)一個(gè)簡單的工具條按鈕(或菜單項(xiàng)),CommandBarCombo控件對應(yīng)組合
編輯框,CommandBarPopup對應(yīng)于下拉菜單和CommandBarActiveX對應(yīng)于ActiveX控制。
???
在Office_TLB.pas單元中,除了ICommandBarButton接口,還有一個(gè)ICommandBarButtonEvents接口用來
提供對工具條上控件的事件支持。事件的支持通常是通過連接點(diǎn)、事件接收連接到可連接對象來實(shí)現(xiàn)。但這比較麻煩,我們還可以通過更簡單的辦法來實(shí)現(xiàn)事件支
持。檢查一下Delphi在word_tlb.pas單元?jiǎng)?chuàng)建的TWordApplication的實(shí)現(xiàn)代碼可以發(fā)現(xiàn)Delphi封裝了每一個(gè)可連接對
象,自動(dòng)實(shí)現(xiàn)了事件接收機(jī)制。這個(gè)單元可以作為一個(gè)范本用來創(chuàng)建自定義的對接口對象的封裝。
???
BtnSvr.pas單元包含了一個(gè)手工創(chuàng)建的Delphi封裝。除了按標(biāo)準(zhǔn)的Delphi屬性方式實(shí)現(xiàn)了CommandBarButton對象的屬性
外,還實(shí)現(xiàn)了InitServerData、InvokeEvent、Connect、ConnectTo和Disconnect方法。可以注意到這部分
完全是模仿TWordApplication實(shí)現(xiàn)部分編寫的CommandBarButton事件實(shí)現(xiàn)。主要就是在InitServerData方法中定
義服務(wù)器數(shù)據(jù)。根據(jù)Office_TLB.pas中不同的接口GUID,定義一個(gè)CommandBarButton接口的內(nèi)部的接口Fintf,設(shè)定
InvokeEvent方法來激活基于定義在事件接口部分的DispID的Delphi事件支持。最后,Connect、ConnectTo和
Disconnect方法設(shè)定Fintf給需要的接口并接收相應(yīng)的事件。
??? 定義在BtnSvr.pas單元中的Delphi封裝類命名為TButtonServer。它需要從TOleServer對象繼承以便支持事件處理。
同應(yīng)用程序連接
???
有了工具條按鈕封裝類后,接下來要聲明一個(gè)TWordApplication域來保存對Word
Application對象的引用。此外還需要為新的工具條定義一個(gè)接口指針以及兩個(gè)域使用新的TButtonServer類來保存我們要?jiǎng)?chuàng)建的新的工具
條按鈕和菜單項(xiàng)。
??? 在插件類的private部分添加:
??? FWordApp : TWordApplication;
??? DICommandBar : CommandBar;
??? DIBtn : TButtonServer;
??? DIMenu : TButtonServer;
??? 在OnConnection方法中,保存應(yīng)用程序指針:
??? var
??? ??WA : Word_TLB._Application;
??? begin
??? ??FWordApp := TWordApplication.Create(nil);
??? ??WA := Application as Word_TLB._Application;
??? ??WordApp.ConnectTo(WA);
??? ??…………………………..
???
TWordApplication是Delphi 5中帶的Server組件,ConnectTo
方法是用來連接插件和Word提供的接口。因?yàn)門WordApplication
把接口事件映射成了Delphi事件,我們可以直接使用標(biāo)準(zhǔn)的Delphi語法來設(shè)定事件處理過程。示意如下:
??? WordApp.OnEventX := EventXHandler;
??? 比如我們?nèi)绻朐赪ord的選區(qū)發(fā)生改變時(shí)實(shí)現(xiàn)某項(xiàng)功能,就可以設(shè)定OnWindowSelectionChange 事件。
插件如何創(chuàng)建新的工具條、按鈕和菜單
??? 在創(chuàng)建新的工具條和按鈕前,需要為按鈕的OnClick過程先創(chuàng)建事件處理函數(shù),下面就是簡單的處理函數(shù)例子:
??? procedure TAddIn.TestClick(const Ctrl: OleVariant;
??? ??var CancelDefault: OleVariant);
??? begin
????? ShowMessage('有人點(diǎn)我了!');
??? ??CancelDefault := True;
??? end;
???
CancelDefault參數(shù)用來設(shè)定是否替代缺省的菜單或工具條按鈕的處理過程。這里不需要設(shè)定這個(gè)參數(shù),因?yàn)槲覀儗⒃诓寮袆?chuàng)建一個(gè)新的按鈕。插件
注冊為在程序啟動(dòng)時(shí)被加載,所以O(shè)nStartupComplete方法一定會(huì)被調(diào)用,用這個(gè)方法創(chuàng)建用戶界面元素是比較合適的。這里定義BtnIntf
為CommandBarControl接口類型(要?jiǎng)?chuàng)建的CommandBarButton的父類接口)。接下來的任務(wù)是確定自定義的工具條是否已經(jīng)被創(chuàng)
建了
??? DICommandBar := nil;
??? for i := 1 to WordApp.CommandBars.Count do
??? ??if (WordApp.CommandBars.Item[i].Name ='Delphi') then
??? ????DICommandBar := WordApp.CommandBars.Item[i];
??? ????// 確定是否已經(jīng)注冊了命令條
??? ??if (not Assigned(DICommandBar)) then begin
??? ????DICommandBar:=
WordApp.CommandBars.Add('Delphi',EmptyParam,EmptyParam,EmptyParam);
??? ????DICommandBar.Set_Protection(msoBarNoCustomize)
??? end;
???
先給工具條起一個(gè)唯一的名字“Delphi”,然后在啟動(dòng)時(shí)檢查工具條是否已經(jīng)被創(chuàng)建了。如果是的話就把它賦值給DICommandBar,否則調(diào)用
Word的CommandBars屬性的Add方法創(chuàng)建一個(gè)新的工具條。接著給工具條添加msoBarNoCustomize的保護(hù),這可以防止用戶添加
或刪除工具條上的按鈕。這時(shí)DICommandBar指向一個(gè)有效的工具條,我們可以從接口的Controls集合中獲得工具條按鈕接口指針。如果工具條
上沒有控件,就創(chuàng)建一個(gè)新的按鈕。
??? if (DICommandBar.Controls.Count > 0) then
?? ???BtnIntf := DICommandBar.Controls.Item[1]
?? ??else
??? ??BtnIntf := DICommandBar.Controls.Add(msoControlButton,
?? ???EmptyParam, EmptyParam, EmptyParam, EmptyParam);
??? 注意:集合中第一項(xiàng)是以1為底的,而不像Delphi中那樣通常以0為底。現(xiàn)在我們獲得了需要的工具條按鈕接口,然后要?jiǎng)?chuàng)建一個(gè)基于按鈕接口的TButtonServer 類封裝。
??? ?DIBtn := TButtonServer.Create(nil);
??? ?DIBtn.ConnectTo(BtnIntf as _CommandBarButton);
??? ?DIBtn.Caption := 'Delphi Test';
??? ?DIBtn.Style := msoButtonCaption;
??? ?DIBtn.Visible := True;
??? ?DIBtn.OnClick := TestClick;
??? 這里使用ConnectTo 方法連接按鈕的事件并設(shè)定先前創(chuàng)建的OnClick事件處理過程。最后,要確認(rèn)使工具條可見。
??? DICommandBar.Set_Visible(True);
???
TLIBIMP程序創(chuàng)建了一個(gè)只讀的而非可讀寫的工具條Visible 屬性,但可以使用Set_Visible
方法來設(shè)定顯示屬性(不能生成可讀寫的屬性可能是TLIBIMP的bug)。添加新的菜單項(xiàng)類似于前面,首先創(chuàng)建菜單的OnClick事件處理函數(shù),下面
這個(gè)過程遍歷被選文本的第一段,并設(shè)定其邊框樣式:
??? procedure TAddIn.MenuClick(const Ctrl: OleVariant;
??? ??var CancelDefault: OleVariant);
??? var
??? ??Sel : Word_TLB.Selection;
??? ??Par : Word_TLB.Paragraph;
??? begin
??? ??Sel := WordApp.ActiveWindow.Selection;
????? if (Sel.Type_ in [wdSelectionNormal,
??? ????wdSelectionIP]) then begin
??????? Par := Sel.Paragraphs.Item(1);
????? if (Par.Borders.OutsideLineStyle < wdLineStyleInset) then
??? ????Par.Borders.OutsideLineStyle := 1 + Par.Borders.OutsideLineStyle
????? else
??? ????Par.Borders.OutsideLineStyle := wdLineStyleNone;
??? ??end;
??? end;
??? 在OnStartupComplete方法中,添加下面的代碼來獲得工具菜單的接口指針,查找自定義的的菜單項(xiàng),如果沒有就創(chuàng)建新的,然后設(shè)定它的OnClick事件:
??? ToolsBar := WordApp.CommandBars['Tools'];
??? MenuIntf := ToolsBar.FindControl(EmptyParam, EmptyParam,
??? ??'DIMenu', EmptyParam, EmptyParam);
??? if (not Assigned(MenuIntf)) then
??? ??MenuIntf := ToolsBar.Controls.Add(msoControlButton,
????? EmptyParam, EmptyParam, EmptyParam, EmptyParam);
?? ???DIMenu := TButtonServer.Create(nil);
?? ???DIMenu.ConnectTo(MenuIntf as _CommandBarButton);
?? ???DIMenu.Caption := 'Delp&hi Menu';
?? ???DIMenu.ShortcutText := '';
圖1.34
|
??? ??DIMenu.Tag := 'DIMenu';
??? ??DIMenu.Visible := True;
??? ??DIMenu.OnClick := MenuClick;
??? CommandBar接口的FindControl方法使用唯一的標(biāo)識(shí)來查找菜單項(xiàng),如果找到了控件就賦值給 MenuIntf,如果沒有找到就創(chuàng)建一個(gè)新的菜單項(xiàng)。圖1.34顯示了自定義的工具條。
清理資源
??? 注意應(yīng)該在OnBeginShutdown 方法中清理用戶界面元素:
? ????if (Assigned(DIBtn)) then
??? ??begin
??? ????DIBtn.Free;
????? ??DIBtn := nil;
??? ??end;
??? ??if (Assigned(DIMenu)) then
?? ???begin
??? ????DIMenu.Free;
??? ????DIMenu := nil;
??? ??end;
??? ??if (Assigned(DICommandBar)) then begin
??? ????DICommandBar.Delete;
??? ????DICommandBar := nil;
??? ??end;
???
因?yàn)椴寮目蚣苁峭ㄓ玫模覀兛梢詫⑼瑯拥腛LE Server
DLL用于多個(gè)應(yīng)用程序,方法就是確定將激活插件的應(yīng)用程序,并使用合適的對象模型。最簡單的判斷方法是在OnConnection中把應(yīng)用程序的
IDispatch的接口指針賦值給一個(gè)OleVariant變量,然后使用相應(yīng)的Name 屬性來確定相應(yīng)的程序:
??? var
??? ??AppVar : OleVariant;
??? begin
??? ??AppVar := Application;
????? if (AppVar.Name = 'Outlook') then
????? begin
??? ????...
????? end
????? else if (AppVar.Name = 'Microsoft Word') then
????? begin
??????? ...
??? ??end else ...
??? 最后,要想獲得關(guān)于Office開發(fā)和Office 2000插件創(chuàng)建更詳細(xì)的資料,可以查閱microsoft.public.officedev新聞組上的信息。