C# Office COM 加载项

打印 上一主题 下一主题

主题 1649|帖子 1649|积分 4947

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
Office 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
  1. #if BrandName1
  2. namespace BrandName1 // 品牌1
  3. #else
  4. namespace BrandName2 // 品牌2
  5. #endif
  6. {
  7.     [Obfuscation] // 不可被混淆
  8.     [ComVisible(true)] // COM 组件类可见, 并且类型要设为公开 public
  9.     [Guid("XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")] // CLSID
  10.     // [ProgId("BrandName1.OfficeAddIn")] // Office COM 加载项的 ProgID 须与类全名一致
  11.     public class OfficeAddIn : IDTExtensibility2
  12.     {
  13.         public void OnConnection(
  14.             object application, ext_ConnectMode connectMode,
  15.             object addInInst, ref Array custom)
  16.         {
  17.             MessageBox.Show("OnConnection"); // 注册成功的加载项将会在对应应用启动时弹窗
  18.         }
  19.         // 其他 IDTExtensibility2 的接口方法...
  20.     }
  21. }
复制代码
⚠ 注意: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 
RegAsm.exe 作用就是添加注册表项,克制系统缺失该文件,也为了添加日志输出,可自行写注册表实现
  1. {HKCU|HKLM}\Software\Classes
  2.     ProgID
  3.         ● "" = 'ProgID'
  4.         CLSID
  5.             ● "" = 'CLSID'
  6.     [Wow6432Node\]CLSID\'CLSID'
  7.         ● "" = 'ProgID'
  8.         Implemented Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}
  9.         InprocServer32
  10.             ● "" = "mscoree.dll"
  11.             ● "Assembly" = 'Assembly.FullName'
  12.             ● "Class" = 'ProgID'
  13.             ● "CodeBase" = 'Assembly.CodeBase'
  14.             ● "RuntimeVersion" = 'Environment.Version'
  15.             ● "ThreadingModel" = "Both"
  16.         ProgId
  17.             ● "" = '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 同名子项,并添加三个必要的注册表键值
  1. {HKCU|HKLM}\Software\[Wow6432Node\]Microsoft\Office
  2.     <app>
  3.         AddIns
  4.             'ProgID'
  5.                 ● FriendlyName = "加载项列表中显示的友好名称"
  6.                 ● Description = "加载项列表中显示的描述"
  7.                 ● LoadBehavior = 3 (启动时连接和加载)
复制代码
另外还可以添加 CommandLineSafe = 1, 指示下令行操作安全,大概可以减少弹窗警告
经过这两步注册示例插件后,启动对应的 Office 应用时,就会弹出消息框,验证注册成功了
三、实现接口 IRibbonExtensibility

这个接口用于在 Office 应用的 Ribbon 中添加自定义 UI
添加 COM 引用 Microsoft Office  Object Library 即可, 是 Office 版本号
为提高兼容性,可以安装 Office 2007 获取到 12.0 版本的 COM,并将对应的文件 Office.dll 复制到项目目录中,并修改引用为相对文件,克制在其他未安装 Office 2007 的电脑上无法生成。注意此接口也要被加载项类继承,故不可被肴杂
此接口只有一个 GetCutsomUI 的方法,需要返回 XML 格式的字符串
为了代码可读性,发起使用编写和加载 XML 资源文件的方式
而且在 VS 中编写 XML 添加名称空间后在编写元素属性时将会有候选词列表,十分方便
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui">
  3.   <ribbon>
  4.     <tabs>
  5.       <tab id="TestTab" getLabel="GetLabel">
  6.         <group id="TestGroup" getLabel="GetLabel">
  7.           <button id="TestButton" size="large"
  8.             onAction="OnButtonPressed"
  9.             getLabel="GetLabel"
  10.             getImage="GetImage"/>
  11.         </group>
  12.       </tab>
  13.     </tabs>
  14.   </ribbon>
  15. </customUI>
复制代码
  1. public class OfficeAddIn : IDTExtensibility2, IRibbonExtensibility
  2. {
  3.     public string GetLabel(IRibbonControl control)
  4.     {
  5.         switch(control.ID)
  6.         {
  7.             case "TestTab": return "Test Tab";
  8.             case "TestGroup": return "Test Group";
  9.             case "TestButton": return "Test Button";
  10.         }
  11.         return null;
  12.     }
  13.     public {Bitmap|IPictureDisp} GetImage(IRibbonControl control)
  14.     {
  15.         // 返回控件图像,支持 Bitmap 或 IPictureDisp 类型返回值
  16.     }
  17.     public void OnButtonPressed(IRibbonControl control)
  18.     {
  19.         MessageBox.Show("Test Button Clicked!");
  20.     }
  21. }
复制代码
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 接口,同样可以减少模块引用自行复制代码到本身项目中
  1. [DllImport("oleaut32.dll", ExactSpelling = true, PreserveSig = false)]
  2. static extern IPictureDisp OleCreatePictureIndirect(
  3.     ref PictDesc pictdesc,
  4.     [MarshalAs(UnmanagedType.LPStruct)] Guid iid,
  5.     [MarshalAs(UnmanagedType.Bool)] bool fOwn);
  6. struct PictDesc
  7. {
  8.     public int cbSizeofstruct;
  9.     public int picType;
  10.     public IntPtr hbitmap;
  11.     public IntPtr hpal;
  12.     public int unused;
  13. }
  14. public static IPictureDisp CreatePictureIndirect(Bitmap bitmap)
  15. {
  16.     var picture = new PictDesc
  17.     {
  18.         cbSizeofstruct = Marshal.SizeOf(typeof(PictDesc)),
  19.         picType = 1,
  20.         hbitmap = bitmap.GetHbitmap(Color.Black), // 创建纯透明底色位图
  21.         hpal = IntPtr.Zero,
  22.         unused = 0,
  23.     };
  24.     return OleCreatePictureIndirect(ref picture, typeof(IPictureDisp).GUID, true);
  25. }
复制代码
Bitmap.GetHbitmap有无参和传参Color两个重载,无参重载在内部传参Color.LightGray调用另一重载,这应该和直接返回Bitmap在 WPS 中会有浅灰色填充相关。
需要注意的是,GetHbitmap方法内部不会使用到颜色的 Alpha 值, 创建纯透明背景图像句柄,应该使用Color.Black 255,0,0,0而不是Color.Transparent``0,255,255,255
CustomUI 刷新控件


  • 利用customUI元素的onload回调方法,在 C# 中记载IRibbonUI对象,可调用其Invalidate方法刷新整个 UI,或者调用InvalidateControl(string id)根据 id 刷新指定控件
  1. public class OfficeAddIn : IDTExtensibility2, IRibbonExtensibility
  2. {
  3.     IRibbonUI ribbon;
  4.     public void OnCustomUILoad(IRibbonUI ribbon)
  5.     {
  6.         this.ribbon = ribbon;
  7.     }
  8.    
  9.     internal void Invalidate()
  10.     {
  11.         ribbon?.Invalidate();
  12.     }
  13.    
  14.     internal void InvalidateControl(string id)
  15.     {
  16.         ribbon?.InvalidateControl(id);
  17.     }
  18. }
复制代码

  • dynamicMenu控件invalidateContentOnDrop="true"可在每次展开时重新触发getContent刷新内容
四、Office 互操作能力

需要添加引用对应 Office 应用的互操作库,在 VS 中可以很方便的跳转 MSDN 文档
添加 COM 引用 Microsoft   Object Library 即可
下文演示 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
  1. using Microsoft.Office.Interop.Word;
  2. public class OfficeAddIn : IDTExtensibility2, IRibbonExtensibility
  3. {
  4.     Application app;
  5.     public void OnConnection(
  6.         object application, ext_ConnectMode connectMode,
  7.         object addInInst, ref Array custom)
  8.     {
  9.         if (application is Application)
  10.             app = (Application)Application;
  11.     }
  12.     public void OnButtonPressed(IRibbonControl control)
  13.     {
  14.         app?.ActiveDocument?.ExportAsFixedFormat(fileName, WdExportFormat.wdExportFormatPDF);
  15.     }
  16. }
复制代码
Excel 导出 PDF
  1. using Microsoft.Office.Interop.Excel;
  2. // 工作簿
  3. app?.ActiveWorkbook?.ExportAsFixedFormat(XlFixedFormatType.xlTypePDF, fileName);
  4. // 工作表
  5. (app?.ActiveSheet as Worksheet)?.ExportAsFixedFormat(XlFixedFormatType.xlTypePDF, fileName);
  6. // 图表, WPS 不支持
  7. app.ActiveChart?.ExportAsFixedFormat(XlFixedFormatType.xlTypePDF, fileName);
  8. // 框选区域
  9. var range = app.Selection as Range;
  10. var sheet = range.Worksheet;
  11. var area = sheet.PageSetup.PrintArea;
  12. sheet.PageSetup.PrintArea = range.Address; // 设置打印区域为选定区域
  13. sheet.ExportAsFixedFormat(XlFixedFormatType.xlTypePDF, fileName);
  14. sheet.PageSetup.PrintArea = area; // 还原打印区域
复制代码
PowerPoint 导出 PDF
  1. using Microsoft.Office.Interop.PowerPoint;
  2. app?.ActivePresentation?.ExportAsFixedFormat(fileName, PpFixedFormatType.ppFixedFormatTypePDF);
复制代码
Publisher 导出 PDF
  1. using Microsoft.Office.Interop.Publisher;
  2. app?.ActiveDocument?.ExportAsFixedFormat(PbFixedFormatType.pbFixedFormatTypePDF, fileName);
复制代码
Outlook 导出邮件为 PDF
  1. using Microsoft.Office.Interop.Outlook;
  2. using Microsoft.Office.Interop.Word;
  3. var mailItem = outlook?.ActiveExplorer()?.Selection?.OfType<MailItem>()?.FirstOrDefault();
  4. var inspector = mailItem?.GetInspector;
  5. var document = inspector?.WordEditor as Document;
  6. document?.ExportAsFixedFormat(fileName, WdExportFormat.wdExportFormatPDF);
  7. // GetInspector 会打开一个隐藏窗口,比较吃内存,需要及时关闭
  8. inspector?.Close(OlInspectorClose.olPromptForSave);
复制代码
五、实现 WPS COM 加载项

须在注册 Office COM 加载项基础上(包括添加到 Office 加载项列表),另外添加到 WPS 加载项列表
添加到 WPS 加载项列表

Word 对应 WPS,Excel 对应 ET,PowerPoint 对应 WPP,不区分 64/32位
  1. HKCU\Software\kingsoft\office
  2.     {WPS|ET|WPP}
  3.         AddinsWL
  4.             'ProgID' = ""
复制代码
Office 与 WPS COM 组件对应表

⚠ 注意:WPS 和 Office 官方为了互相兼容,Office、Word、Excel、PowerPoint 相关的 COM 接口使用相同的 CLSID。如果插件需要兼容两者,对应的互操作库文件只需要一组,因程序集内名称空间不同,且用户基本只会安装其中一套,须复制互操作库文件到运行目录,否则无法同时兼容
OfficeWPSMicrosoft Add-In Designer
Extensibility.dllKingsoft Add-In Designer
Interop.AddInDesignerObjects.dllMicrosoft Office  Object Library
Office.dllUpgrage WPS Office  Object Library
Interop.Office.dllMicrosoft Word  Object Library
Microsoft.Office.Interop.Word.dllUpgrade Kingsoft WPS  Object Library
Interop.Word.dllMicrosoft Excel  Object Library
Microsoft.Office.Interop.Excel.dllUpgrage WPS Spreadsheets  Object Library
Interop.Excel.dllMicrosoft PowerPoint  Object Library
Microsoft.Office.Interop.PowerPoint.dllUpgrage WPS Presentation  Object Library
Interop.PowerPoint.dll六、卸载清理注册表

除了清理上文中添加的 COM 组件和加载项的注册表,还可以清理以下相关的注册表:

  • HKCU\Software\Microsoft\Office\\AddinsData插件数据
  • HKCU\Software\Microsoft\Office\\Common\CustomUIValidationCacheCustomUI 校验缓存
  • HKCU\Software\Microsoft\Office\\\Addins版本插件列表
  • HKCU\Software\Microsoft\Office\\\AddInLoadTimes版本加载次数
  • HKCU\Software\Microsoft\Office\\\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等强制方法手动竣事进程,一是强制竣事进程大概会导致下次打开文档时会提示文档保存异常,二是插件可在程序运行中被手动卸载,可以使用卸载当前应用程序域的方式友好解决问题
  1. public void OnDisconnection(ext_DisconnectMode removeMode, ref Array custom)
  2. {
  3.     try
  4.     {
  5.         AppDomain.Unload(AppDomain.CurrentDomain);
  6.     }
  7.     catch (CannotUnloadAppDomainException)
  8.     {
  9.         // ignored
  10.     }
  11. }
复制代码
相关资料

如何使用 Visual C# .NET 生成 Office COM 加载项 - Office
[MS-CUSTOMUI]: CustomUI |Microsoft 学习
COM Add-In 加载失败疑难解答 |Microsoft 学习

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

羊蹓狼

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表