C# 開發 Office 和 WPS COM 加載項
一、實現接口 IDTExtensibility2
這是實現 Office COM 加載項最基本的接口
添加 COM 引用 Microsoft Add-In Designer 即可
對應文件 Extensibility.dll 只包含 IDTExtensibility2 接口其中和用到的枚舉 ext_ConnectMode、ext_DisconnectMode,
可以減少模塊引用自行復制代碼到自己項目中,注意 IDTExtensibility2 不可被混淆
? 注意:開發 Office 或 WPS COM 加載項添加 COM 引用時,需要安裝對應的套件才能找到相關的 COM 組件,添加 WPS COM 引用時會受到兩者安裝的先后順序和管理員權限影響,導致無法添加引用,若 VS 報錯無法添加,需要卸載 Office 才能順利添加。但下文會提到僅需引用其中一套 COM 組件即可同時兼容 Office 和 WPS
#if BrandName1
namespace BrandName1 // 品牌1
#else
namespace BrandName2 // 品牌2
#endif
{
[Obfuscation] // 不可被混淆
[ComVisible(true)] // COM 組件類可見, 并且類型要設為公開 public
[Guid("XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")] // CLSID
// [ProgId("BrandName1.OfficeAddIn")] // Office COM 加載項的 ProgID 須與類全名一致
public class OfficeAddIn : IDTExtensibility2
{
public void OnConnection(
object application, ext_ConnectMode connectMode,
object addInInst, ref Array custom)
{
MessageBox.Show("OnConnection"); // 注冊成功的加載項將會在對應應用啟動時彈窗
}
// 其他 IDTExtensibility2 的接口方法...
}
}
? 注意:Office COM 加載項須保證類的 ProgID 與類全名完全匹配, ProgID 特性未設置時默認使用類的全名,故也無需設置;并且類不可被混淆,被其繼承的接口也不可被混淆
ProgID 與產品和對應功能相關,文件關聯也會用到,建議名稱是
. ,故示例中以 BrandName1 為名稱空間,OfficeAddIn 為類名,那么 ProgID 就是 BrandName1.OfficeAddIn。為區分品牌,在品牌條件編譯中使用不同的名稱空間,而不是不同的類名,這樣更符合規范,也更好編寫注冊的代碼
二、注冊 Office COM 加載項
COM CLSID 和 Office 產品的注冊表都有 HKCU、HKLM 和 64、32位的項,為了提高兼容性,可在這些注冊表項下都添加上注冊信息
注冊 COM 組件
C# 注冊 COM 組件一般通過調用 RegAsm.exe 文件來注冊,區分位數和運行時版本
%windir%\Microsoft.NET\Framework[64]_"ver"_\RegAsm.exe MyCOM.dll /codebase [/u]
RegAsm.exe 作用就是添加注冊表項,避免系統缺失該文件,也為了添加日志輸出,可自行寫注冊表實現
{HKCU|HKLM}\Software\Classes
ProgID
● "" = 'ProgID'
CLSID
● "" = 'CLSID'
[Wow6432Node\]CLSID\'CLSID'
● "" = 'ProgID'
Implemented Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}
InprocServer32
● "" = "mscoree.dll"
● "Assembly" = 'Assembly.FullName'
● "Class" = 'ProgID'
● "CodeBase" = 'Assembly.CodeBase'
● "RuntimeVersion" = 'Environment.Version'
● "ThreadingModel" = "Both"
ProgId
● "" = 'ProgID'
? 注意:RegAsm.exe 注冊方式會用到反射,如果不將引用到的程序集文件放到同目錄下,并且系統未注冊 COM 類繼承的接口時,會出錯導致注冊失敗。如果注冊方式和 RegAsm.exe 一樣會用到類型本身,為避免用戶未安裝 COM 組件相關的應用,需要打包引用到的程序集
? 注意:同一個 COM 組件項目創建不同品牌的程序集并注冊時,如果兩個程序集文件名相同、簽名相同、版本相同,則會導致兩者程序集全名相同,導致系統無法區分。區分方式是三者至少有一個不同,最簡單的方式就是條件編譯設置不同版本號
? 注意:為提高兼容性,目標平臺選擇 AnyCPU 即可兼容 64/32 位系統和軟件。作為 COM 組件運行時,是作為被 .NET 虛擬機進程引用的程序集來運行的,只需將目標框架設為 .NET Framework 3.5, 不需要 app.config 文件就可以兼容 3.5 和 4.0,無需編譯多個框架版本。支持 .dll 和 .exe 文件,只要是 .NET 程序集就可以,如果是可將自身注冊為 COM 組件 exe,那在注冊時還是需要 app.config 的
添加到 Office 加載項列表
需要在加載項列表下新建加載項類 ProgID 同名子項,并添加三個必要的注冊表鍵值
{HKCU|HKLM}\Software\[Wow6432Node\]Microsoft\Office
<app>
AddIns
'ProgID'
● FriendlyName = "加載項列表中顯示的友好名稱"
● Description = "加載項列表中顯示的描述"
● LoadBehavior = 3 (啟動時連接和加載)
另外還可以添加 CommandLineSafe = 1, 指示命令行操作安全,可能可以減少彈窗警告
經過這兩步注冊示例插件后,啟動對應的 Office 應用時,就會彈出消息框,驗證注冊成功了
三、實現接口 IRibbonExtensibility
這個接口用于在 Office 應用的 Ribbon 中添加自定義 UI
添加 COM 引用 Microsoft Office
為提高兼容性,可以安裝 Office 2007 獲取到 12.0 版本的 COM,并將對應的文件 Office.dll 復制到項目目錄中,并修改引用為相對文件,避免在其他未安裝 Office 2007 的電腦上無法生成。注意此接口也要被加載項類繼承,故不可被混淆
此接口只有一個 GetCutsomUI 的方法,需要返回 XML 格式的字符串
為了代碼可讀性,建議使用編寫和加載 XML 資源文件的方式
并且在 VS 中編寫 XML 添加名稱空間后在編寫元素屬性時將會有候選詞列表,十分方便
<?xml version="1.0" encoding="utf-8"?>
<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui">
<ribbon>
<tabs>
<tab id="TestTab" getLabel="GetLabel">
<group id="TestGroup" getLabel="GetLabel">
<button id="TestButton" size="large"
onAction="OnButtonPressed"
getLabel="GetLabel"
getImage="GetImage"/>
</group>
</tab>
</tabs>
</ribbon>
</customUI>
public class OfficeAddIn : IDTExtensibility2, IRibbonExtensibility
{
public string GetLabel(IRibbonControl control)
{
switch(control.ID)
{
case "TestTab": return "Test Tab";
case "TestGroup": return "Test Group";
case "TestButton": return "Test Button";
}
return null;
}
public {Bitmap|IPictureDisp} GetImage(IRibbonControl control)
{
// 返回控件圖像,支持 Bitmap 或 IPictureDisp 類型返回值
}
public void OnButtonPressed(IRibbonControl control)
{
MessageBox.Show("Test Button Clicked!");
}
}
CustomUI 注意事項
建議攜帶 XML 聲明部分指定utf-8編碼,否則如果有中文會亂碼
customUI元素中的名稱空間,年月可以用2009/07或2006/01,但 Office 2007 不支持解析前者
控件的文本、圖像、懸浮提示語等,都可使用對應的屬性label、image在 XML 中直接設置。也可以使用對應的回調方法getLabel、getImage。使用回調方法的方式需要在加載項類中聲明同名公開方法,比如 XML 中編寫getLabel="GetLabel",C# 中就須編寫對應的public string GetLabel (IRibbonControl control)方法,類似于 WPF XAML 的事件綁定,支持多個控件使用同一個方法并根據控件的 id 返回合適的值
第 3 條中屬性和回調方法互斥,只允許使用其中一個。另外圖像還可使用內置圖像屬性imageMso ,與image和getImage也是互斥的,比如 imageMso="FileSaveAs"使用內置的另存為圖像
如果使用了 dynamicMenu 控件,其 getContent 方法也需要返回 XML 格式字符串,但與第 1 條不同,不能有 XML 聲明部分,否則解析失敗
大小寫敏感,大小寫錯誤會導致解析失敗
使用透明背景圖像
CustomUI 中控件的getImage方法支持直接返回Bitmap類型,Office 支持透明背景的Bitmap,但 WPS 不支持,會用淺灰色的背景填充。這里可以轉換并返回IPictureDisp類型
另外值得一提的是,Office 在切換深色主題后,黑灰單色圖像還會自動轉換為白色圖像,WPS 沒有這個機制
IPictureDisp 在 COM 組件 OLE Automation 中定義,一般在添加 Microsoft Office
Object Library 引用時會自動添加上,對應文件是 stdole.dll,我們只需要用到 IPictureDisp 接口,同樣可以減少模塊引用自行復制代碼到自己項目中
[DllImport("oleaut32.dll", ExactSpelling = true, PreserveSig = false)]
static extern IPictureDisp OleCreatePictureIndirect(
ref PictDesc pictdesc,
[MarshalAs(UnmanagedType.LPStruct)] Guid iid,
[MarshalAs(UnmanagedType.Bool)] bool fOwn);
struct PictDesc
{
public int cbSizeofstruct;
public int picType;
public IntPtr hbitmap;
public IntPtr hpal;
public int unused;
}
public static IPictureDisp CreatePictureIndirect(Bitmap bitmap)
{
var picture = new PictDesc
{
cbSizeofstruct = Marshal.SizeOf(typeof(PictDesc)),
picType = 1,
hbitmap = bitmap.GetHbitmap(Color.Black), // 創建純透明底色位圖
hpal = IntPtr.Zero,
unused = 0,
};
return OleCreatePictureIndirect(ref picture, typeof(IPictureDisp).GUID, true);
}
Bitmap.GetHbitmap有無參和傳參Color兩個重載,無參重載在內部傳參Color.LightGray調用另一重載,這應該和直接返回Bitmap在 WPS 中會有淺灰色填充相關。
需要注意的是,GetHbitmap方法內部不會使用到顏色的 Alpha 值, 創建純透明背景圖像句柄,應該使用Color.Black255,0,0,0而不是Color.Transparent``0,255,255,255
CustomUI 刷新控件
- 利用
customUI元素的onload回調方法,在 C# 中記錄IRibbonUI對象,可調用其Invalidate方法刷新整個 UI,或者調用InvalidateControl(string id)根據 id 刷新指定控件
public class OfficeAddIn : IDTExtensibility2, IRibbonExtensibility
{
IRibbonUI ribbon;
public void OnCustomUILoad(IRibbonUI ribbon)
{
this.ribbon = ribbon;
}
internal void Invalidate()
{
ribbon?.Invalidate();
}
internal void InvalidateControl(string id)
{
ribbon?.InvalidateControl(id);
}
}
dynamicMenu控件invalidateContentOnDrop="true"可在每次展開時重新觸發getContent刷新內容
四、Office 互操作能力
需要添加引用對應 Office 應用的互操作庫,在 VS 中可以很方便的跳轉 MSDN 文檔
添加 COM 引用 Microsoft
下文演示 Office 導出 PDF 能力,僅作演示,另外需要釋放 COM 對象
ExportAsFixedFormat 方法有很多可選參數,支持設置打印頁數、包含文檔信息、生成書簽等
? 注意:Office 2007(只有 32 位版本)導出 PDF/XPS 會提示未安裝此功能時,需要用到 Office 2010 才有的 EXP_PDF.dll 和 EXP_XPS.dll 文件,復制到 Office 2007 的共享目錄即可
%CommonProgramFiles[(x86)]%\Microsoft Shared\OFFICE12
Word 導出 PDF
using Microsoft.Office.Interop.Word;
public class OfficeAddIn : IDTExtensibility2, IRibbonExtensibility
{
Application app;
public void OnConnection(
object application, ext_ConnectMode connectMode,
object addInInst, ref Array custom)
{
if (application is Application)
app = (Application)Application;
}
public void OnButtonPressed(IRibbonControl control)
{
app?.ActiveDocument?.ExportAsFixedFormat(fileName, WdExportFormat.wdExportFormatPDF);
}
}
Excel 導出 PDF
using Microsoft.Office.Interop.Excel;
// 工作簿
app?.ActiveWorkbook?.ExportAsFixedFormat(XlFixedFormatType.xlTypePDF, fileName);
// 工作表
(app?.ActiveSheet as Worksheet)?.ExportAsFixedFormat(XlFixedFormatType.xlTypePDF, fileName);
// 圖表, WPS 不支持
app.ActiveChart?.ExportAsFixedFormat(XlFixedFormatType.xlTypePDF, fileName);
// 框選區域
var range = app.Selection as Range;
var sheet = range.Worksheet;
var area = sheet.PageSetup.PrintArea;
sheet.PageSetup.PrintArea = range.Address; // 設置打印區域為選定區域
sheet.ExportAsFixedFormat(XlFixedFormatType.xlTypePDF, fileName);
sheet.PageSetup.PrintArea = area; // 還原打印區域
PowerPoint 導出 PDF
using Microsoft.Office.Interop.PowerPoint;
app?.ActivePresentation?.ExportAsFixedFormat(fileName, PpFixedFormatType.ppFixedFormatTypePDF);
Publisher 導出 PDF
using Microsoft.Office.Interop.Publisher;
app?.ActiveDocument?.ExportAsFixedFormat(PbFixedFormatType.pbFixedFormatTypePDF, fileName);
Outlook 導出郵件為 PDF
using Microsoft.Office.Interop.Outlook;
using Microsoft.Office.Interop.Word;
var mailItem = outlook?.ActiveExplorer()?.Selection?.OfType<MailItem>()?.FirstOrDefault();
var inspector = mailItem?.GetInspector;
var document = inspector?.WordEditor as Document;
document?.ExportAsFixedFormat(fileName, WdExportFormat.wdExportFormatPDF);
// GetInspector 會打開一個隱藏窗口,比較吃內存,需要及時關閉
inspector?.Close(OlInspectorClose.olPromptForSave);
五、實現 WPS COM 加載項
須在注冊 Office COM 加載項基礎上(包括添加到 Office 加載項列表),另外添加到 WPS 加載項列表
添加到 WPS 加載項列表
Word 對應 WPS,Excel 對應 ET,PowerPoint 對應 WPP,不區分 64/32位
HKCU\Software\kingsoft\office
{WPS|ET|WPP}
AddinsWL
'ProgID' = ""
Office 與 WPS COM 組件對應表
? 注意:WPS 和 Office 官方為了互相兼容,Office、Word、Excel、PowerPoint 相關的 COM 接口使用相同的 CLSID。如果插件需要兼容兩者,對應的互操作庫文件只需要一組,因程序集內名稱空間不同,且用戶基本只會安裝其中一套,須復制互操作庫文件到運行目錄,否則無法同時兼容
| Office | WPS |
|---|---|
| Microsoft Add-In Designer Extensibility.dll |
Kingsoft Add-In Designer Interop.AddInDesignerObjects.dll |
| Microsoft Office Office.dll |
Upgrage WPS Office Interop.Office.dll |
| Microsoft Word Microsoft.Office.Interop.Word.dll |
Upgrade Kingsoft WPS Interop.Word.dll |
| Microsoft Excel Microsoft.Office.Interop.Excel.dll |
Upgrage WPS Spreadsheets Interop.Excel.dll |
| Microsoft PowerPoint Microsoft.Office.Interop.PowerPoint.dll |
Upgrage WPS Presentation Interop.PowerPoint.dll |
六、卸載清理注冊表
除了清理上文中添加的 COM 組件和加載項的注冊表,還可以清理以下相關的注冊表:
-
HKCU\Software\Microsoft\Office\<app>\AddinsData插件數據 -
HKCU\Software\Microsoft\Office\<ver>\Common\CustomUIValidationCacheCustomUI 校驗緩存 -
HKCU\Software\Microsoft\Office\<ver>\<app>\Addins版本插件列表 -
HKCU\Software\Microsoft\Office\<ver>\<app>\AddInLoadTimes版本加載次數 -
HKCU\Software\Microsoft\Office\<ver>\<app>\Resiliency\NotificationReminderAddinDataOffice 禁用通知
七、其他問題
未加載,加載 COM 加載項時出現運行錯誤
這是一個比較令人頭疼的問題,可能原因有很多,但 Office 沒有報錯日志,導致很難排查問題
微軟官方博客 給出了一些解答,個人也復現了一些情況:
-
部署問題:COM 組件注冊表內容缺失項或鍵值,需要注意 COM 組件與 Office 加載項注冊表的位數
-
運行問題:在 Outlook 中比較明顯,本身就啟動緩慢卡頓,切忌在啟動時調用 Sleep 函數,輕則 Office 直接提示建議禁用插件,重則直接出現未加載的問題
Outlook 退出前操作
Outlook 16.0(其他版本未測試)退出時不會觸發 OnBeginShutdown和OnDisconnection,原因未知,應該是 Outlook 自己的 Bug,故 Outlook 插件不要在這兩個方法中進行退出前操作
經過測試,程序退出時會觸發System.Windows.Forms.Application.ThreadExit,但是不會觸發(來不及?)AppDomain.CurrentDomain.ProcessExit,可以利用前者來進行退出前操作,比如保存配置和釋放資源
Office 應用關閉后進程不結束
出現此問題一般是 COM 對象資源未釋放干凈,但是頻繁使用 Office 互操作很難保證所有 COM 對象都及時正確釋放。為了讓進程正確退出,不可使用Process.Kill等強制方法手動結束進程,一是強制結束進程可能會導致下次打開文檔時會提示文檔保存異常,二是插件可在程序運行中被手動卸載,可以使用卸載當前應用程序域的方式友好解決問題
public void OnDisconnection(ext_DisconnectMode removeMode, ref Array custom)
{
try
{
AppDomain.Unload(AppDomain.CurrentDomain);
}
catch (CannotUnloadAppDomainException)
{
// ignored
}
}
相關資料
如何使用 Visual C# .NET 生成 Office COM 加載項 - Office

浙公網安備 33010602011771號