C# 开发 Office 和 WPS COM 加载项
<h2 id="一实现接口idtextensibility2">一、实现接口 IDTExtensibility2</h2><p>这是实现 Office COM 加载项最基本的接口</p>
<p>添加 COM 引用 Microsoft Add-In Designer 即可</p>
<blockquote>
<p>对应文件 Extensibility.dll 只包含 IDTExtensibility2 接口其中和用到的枚举 ext_ConnectMode、ext_DisconnectMode,<br>
可以减少模块引用自行复制代码到自己项目中,注意 IDTExtensibility2 不可被混淆</p>
</blockquote>
<p>⚠ 注意:开发 Office 或 WPS COM 加载项添加 COM 引用时,需要安装对应的套件才能找到相关的 COM 组件,添加 WPS COM 引用时会受到两者安装的先后顺序和管理员权限影响,导致无法添加引用,若 VS 报错无法添加,需要卸载 Office 才能顺利添加。但下文会提到仅需引用其中一套 COM 组件即可同时兼容 Office 和 WPS</p>
<pre><code class="language-csharp">#if BrandName1
namespace BrandName1 // 品牌1
#else
namespace BrandName2 // 品牌2
#endif
{
// 不可被混淆
// COM 组件类可见, 并且类型要设为公开 public
// CLSID
// // Office COM 加载项的 ProgID 须与类全名一致
public class OfficeAddIn : IDTExtensibility2
{
public void OnConnection(
object application, ext_ConnectMode connectMode,
object addInInst, ref Array custom)
{
MessageBox.Show("OnConnection"); // 注册成功的加载项将会在对应应用启动时弹窗
}
// 其他 IDTExtensibility2 的接口方法...
}
}
</code></pre>
<p>⚠ 注意:Office COM 加载项须保证类的 ProgID 与类全名完全匹配, ProgID 特性未设置时默认使用类的全名,故也无需设置;并且类不可被混淆,被其继承的接口也不可被混淆</p>
<blockquote>
<p>ProgID 与产品和对应功能相关,文件关联也会用到,建议名称是 <brandname>.,故示例中以 BrandName1 为名称空间,OfficeAddIn 为类名,那么 ProgID 就是 BrandName1.OfficeAddIn。为区分品牌,在品牌条件编译中使用不同的名称空间,而不是不同的类名,这样更符合规范,也更好编写注册的代码</assocname></brandname></p>
</blockquote>
<h2 id="二注册officecom加载项">二、注册 Office COM 加载项</h2>
<p>COM CLSID 和 Office 产品的注册表都有 HKCU、HKLM 和 64、32位的项,为了提高兼容性,可在这些注册表项下都添加上注册信息</p>
<h3 id="注册com组件">注册 COM 组件</h3>
<p>C# 注册 COM 组件一般通过调用 RegAsm.exe 文件来注册,区分位数和运行时版本</p>
<blockquote>
<p>%windir%\Microsoft.NET\Framework_"ver"_\RegAsm.exe MyCOM.dll /codebase </p>
</blockquote>
<p>RegAsm.exe 作用就是添加注册表项,避免系统缺失该文件,也为了添加日志输出,可自行写注册表实现</p>
<pre><code>{HKCU|HKLM}\Software\Classes
ProgID
● "" = 'ProgID'
CLSID
● "" = 'CLSID'
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'
</code></pre>
<p>⚠ 注意:RegAsm.exe 注册方式会用到反射,如果不将引用到的程序集文件放到同目录下,并且系统未注册 COM 类继承的接口时,会出错导致注册失败。如果注册方式和 RegAsm.exe 一样会用到类型本身,为避免用户未安装 COM 组件相关的应用,需要打包引用到的程序集</p>
<p>⚠ 注意:同一个 COM 组件项目创建不同品牌的程序集并注册时,如果两个程序集文件名相同、签名相同、版本相同,则会导致两者程序集全名相同,导致系统无法区分。区分方式是三者至少有一个不同,最简单的方式就是条件编译设置不同版本号</p>
<p>⚠ 注意:为提高兼容性,目标平台选择 AnyCPU 即可兼容 64/32 位系统和软件。作为 COM 组件运行时,是作为被 .NET 虚拟机进程引用的程序集来运行的,只需将目标框架设为 .NET Framework 3.5, 不需要 app.config 文件就可以兼容 3.5 和 4.0,无需编译多个框架版本。支持 .dll 和 .exe 文件,只要是 .NET 程序集就可以,如果是可将自身注册为 COM 组件 exe,那在注册时还是需要 app.config 的</p>
<h3 id="添加到office加载项列表">添加到 Office 加载项列表</h3>
<p>需要在加载项列表下新建加载项类 ProgID 同名子项,并添加三个必要的注册表键值</p>
<pre><code>{HKCU|HKLM}\Software\Microsoft\Office
<app>
AddIns
'ProgID'
● FriendlyName = "加载项列表中显示的友好名称"
● Description = "加载项列表中显示的描述"
● LoadBehavior = 3 (启动时连接和加载)
</code></pre>
<blockquote>
<p>另外还可以添加 CommandLineSafe = 1, 指示命令行操作安全,可能可以减少弹窗警告</p>
</blockquote>
<p>经过这两步注册示例插件后,启动对应的 Office 应用时,就会弹出消息框,验证注册成功了</p>
<h2 id="三实现接口iribbonextensibility">三、实现接口 IRibbonExtensibility</h2>
<p>这个接口用于在 Office 应用的 Ribbon 中添加自定义 UI</p>
<p>添加 COM 引用 Microsoft Office <ver> Object Library 即可,<ver> 是 Office 版本号</ver></ver></p>
<blockquote>
<p>为提高兼容性,可以安装 Office 2007 获取到 12.0 版本的 COM,并将对应的文件 Office.dll 复制到项目目录中,并修改引用为相对文件,避免在其他未安装 Office 2007 的电脑上无法生成。注意此接口也要被加载项类继承,故不可被混淆</p>
</blockquote>
<p>此接口只有一个 <code>GetCutsomUI</code> 的方法,需要返回 XML 格式的字符串</p>
<p>为了代码可读性,建议使用编写和加载 XML 资源文件的方式</p>
<p>并且在 VS 中编写 XML 添加名称空间后在编写元素属性时将会有候选词列表,十分方便</p>
<pre><code class="language-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>
</code></pre>
<pre><code class="language-csharp">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!");
}
}
</code></pre>
<h3 id="customui注意事项">CustomUI 注意事项</h3>
<p>建议携带 XML 声明部分指定<code>utf-8</code>编码,否则如果有中文会乱码</p>
<p><code>customUI</code>元素中的名称空间,年月可以用<code>2009/07</code>或<code>2006/01</code>,但 Office 2007 不支持解析前者</p>
<p>控件的文本、图像、悬浮提示语等,都可使用对应的属性<code>label</code>、<code>image</code>在 XML 中直接设置。也可以使用对应的回调方法<code>getLabel</code>、<code>getImage</code>。使用回调方法的方式需要在加载项类中声明同名公开方法,比如 XML 中编写<code>getLabel="GetLabel"</code>,C# 中就须编写对应的<code>public string GetLabel (IRibbonControl control)</code>方法,类似于 WPF XAML 的事件绑定,支持多个控件使用同一个方法并根据控件的 id 返回合适的值</p>
<p>第 3 条中属性和回调方法互斥,只允许使用其中一个。另外图像还可使用内置图像属性<code>imageMso</code> ,与<code>image</code>和<code>getImage</code>也是互斥的,比如 <code>imageMso="FileSaveAs"</code>使用内置的另存为图像</p>
<p>如果使用了 <code>dynamicMenu</code> 控件,其 <code>getContent</code> 方法也需要返回 XML 格式字符串,但与第 1 条不同,不能有 XML 声明部分,否则解析失败</p>
<p>大小写敏感,大小写错误会导致解析失败</p>
<h3 id="使用透明背景图像">使用透明背景图像</h3>
<p>CustomUI 中控件的<code>getImage</code>方法支持直接返回<code>Bitmap</code>类型,Office 支持透明背景的<code>Bitmap</code>,但 WPS 不支持,会用浅灰色的背景填充。这里可以转换并返回<code>IPictureDisp</code>类型</p>
<blockquote>
<p>另外值得一提的是,Office 在切换深色主题后,黑灰单色图像还会自动转换为白色图像,WPS 没有这个机制</p>
</blockquote>
<blockquote>
<p>IPictureDisp 在 COM 组件 OLE Automation 中定义,一般在添加 Microsoft Office <ver> Object Library 引用时会自动添加上,对应文件是 stdole.dll,我们只需要用到 IPictureDisp 接口,同样可以减少模块引用自行复制代码到自己项目中</ver></p>
</blockquote>
<pre><code class="language-csharp">
static extern IPictureDisp OleCreatePictureIndirect(
ref PictDesc pictdesc,
Guid iid,
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);
}
</code></pre>
<blockquote>
<p><code>Bitmap.GetHbitmap</code>有无参和传参<code>Color</code>两个重载,无参重载在内部传参<code>Color.LightGray</code>调用另一重载,这应该和直接返回<code>Bitmap</code>在 WPS 中会有浅灰色填充相关。<br>
需要注意的是,<code>GetHbitmap</code>方法内部不会使用到颜色的 Alpha 值, 创建纯透明背景图像句柄,应该使用<code>Color.Black</code> <code>255,0,0,0</code>而不是<code>Color.Transparent``0,255,255,255</code></p>
</blockquote>
<h3 id="customui刷新控件">CustomUI 刷新控件</h3>
<ol>
<li>利用<code>customUI</code>元素的<code>onload</code>回调方法,在 C# 中记录<code>IRibbonUI</code>对象,可调用其<code>Invalidate</code>方法刷新整个 UI,或者调用<code>InvalidateControl(string id)</code>根据 id 刷新指定控件</li>
</ol>
<pre><code class="language-csharp">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);
}
}
</code></pre>
<ol start="2">
<li><code>dynamicMenu</code>控件<code>invalidateContentOnDrop="true"</code>可在每次展开时重新触发<code>getContent</code>刷新内容</li>
</ol>
<h2 id="四office互操作能力">四、Office 互操作能力</h2>
<p>需要添加引用对应 Office 应用的互操作库,在 VS 中可以很方便的跳转 MSDN 文档</p>
<p>添加 COM 引用 Microsoft <ver> Object Library 即可</ver></app></p>
<p>下文演示 Office 导出 PDF 能力,仅作演示,另外需要释放 COM 对象</p>
<p>ExportAsFixedFormat 方法有很多可选参数,支持设置打印页数、包含文档信息、生成书签等</p>
<p>⚠ 注意:Office 2007(只有 32 位版本)导出 PDF/XPS 会提示未安装此功能时,需要用到 Office 2010 才有的 EXP_PDF.dll 和 EXP_XPS.dll 文件,复制到 Office 2007 的共享目录即可</p>
<p><code>%CommonProgramFiles[(x86)]%\Microsoft Shared\OFFICE12</code></p>
<h3 id="word导出pdf">Word 导出 PDF</h3>
<pre><code class="language-csharp">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);
}
}
</code></pre>
<h3 id="excel导出pdf">Excel 导出 PDF</h3>
<pre><code class="language-csharp">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; // 还原打印区域
</code></pre>
<h3 id="powerpoint导出pdf">PowerPoint 导出 PDF</h3>
<pre><code class="language-csharp">using Microsoft.Office.Interop.PowerPoint;
app?.ActivePresentation?.ExportAsFixedFormat(fileName, PpFixedFormatType.ppFixedFormatTypePDF);
</code></pre>
<h3 id="publisher导出pdf">Publisher 导出 PDF</h3>
<pre><code class="language-csharp">using Microsoft.Office.Interop.Publisher;
app?.ActiveDocument?.ExportAsFixedFormat(PbFixedFormatType.pbFixedFormatTypePDF, fileName);
</code></pre>
<h3 id="outlook导出邮件为pdf">Outlook 导出邮件为 PDF</h3>
<pre><code class="language-csharp">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);
</code></pre>
<h2 id="五实现wpscom加载项">五、实现 WPS COM 加载项</h2>
<p>须在注册 Office COM 加载项基础上(包括添加到 Office 加载项列表),另外添加到 WPS 加载项列表</p>
<h3 id="添加到wps加载项列表">添加到 WPS 加载项列表</h3>
<p>Word 对应 WPS,Excel 对应 ET,PowerPoint 对应 WPP,不区分 64/32位</p>
<pre><code>HKCU\Software\kingsoft\office
{WPS|ET|WPP}
AddinsWL
'ProgID' = ""
</code></pre>
<h3 id="office与wpscom组件对应表">Office 与 WPS COM 组件对应表</h3>
<p>⚠ 注意:WPS 和 Office 官方为了互相兼容,Office、Word、Excel、PowerPoint 相关的 COM 接口使用相同的 CLSID。如果插件需要兼容两者,对应的互操作库文件只需要一组,因程序集内名称空间不同,且用户基本只会安装其中一套,须复制互操作库文件到运行目录,否则无法同时兼容</p>
<table>
<thead>
<tr>
<th>Office</th>
<th>WPS</th>
</tr>
</thead>
<tbody>
<tr>
<td>Microsoft Add-In Designer<br>Extensibility.dll</td>
<td>Kingsoft Add-In Designer<br>Interop.AddInDesignerObjects.dll</td>
</tr>
<tr>
<td>Microsoft Office <ver> Object Library<br>Office.dll</ver></td>
<td>Upgrage WPS Office <ver> Object Library<br>Interop.Office.dll</ver></td>
</tr>
<tr>
<td>Microsoft Word <ver> Object Library<br>Microsoft.Office.Interop.Word.dll</ver></td>
<td>Upgrade Kingsoft WPS <ver> Object Library<br>Interop.Word.dll</ver></td>
</tr>
<tr>
<td>Microsoft Excel <ver> Object Library<br>Microsoft.Office.Interop.Excel.dll</ver></td>
<td>Upgrage WPS Spreadsheets <ver> Object Library<br>Interop.Excel.dll</ver></td>
</tr>
<tr>
<td>Microsoft PowerPoint <ver> Object Library<br>Microsoft.Office.Interop.PowerPoint.dll</ver></td>
<td>Upgrage WPS Presentation <ver> Object Library<br>Interop.PowerPoint.dll</ver></td>
</tr>
</tbody>
</table>
<h2 id="六卸载清理注册表">六、卸载清理注册表</h2>
<p>除了清理上文中添加的 COM 组件和加载项的注册表,还可以清理以下相关的注册表:</p>
<ol>
<li>
<p><code>HKCU\Software\Microsoft\Office\<app>\AddinsData</code>插件数据</p>
</li>
<li>
<p><code>HKCU\Software\Microsoft\Office\<ver>\Common\CustomUIValidationCache</code>CustomUI 校验缓存</p>
</li>
<li>
<p><code>HKCU\Software\Microsoft\Office\<ver>\<app>\Addins</code>版本插件列表</p>
</li>
<li>
<p><code>HKCU\Software\Microsoft\Office\<ver>\<app>\AddInLoadTimes</code>版本加载次数</p>
</li>
<li>
<p><code>HKCU\Software\Microsoft\Office\<ver>\<app>\Resiliency\NotificationReminderAddinData</code>Office 禁用通知</p>
</li>
</ol>
<h2 id="七其他问题">七、其他问题</h2>
<h3 id="未加载加载com加载项时出现运行错误">未加载,加载 COM 加载项时出现运行错误</h3>
<p>这是一个比较令人头疼的问题,可能原因有很多,但 Office 没有报错日志,导致很难排查问题</p>
<p>微软官方博客 给出了一些解答,个人也复现了一些情况:</p>
<ul>
<li>
<p>部署问题:COM 组件注册表内容缺失项或键值,需要注意 COM 组件与 Office 加载项注册表的位数</p>
</li>
<li>
<p>运行问题:在 Outlook 中比较明显,本身就启动缓慢卡顿,切忌在启动时调用 Sleep 函数,轻则 Office 直接提示建议禁用插件,重则直接出现未加载的问题</p>
</li>
</ul>
<h3 id="outlook退出前操作">Outlook 退出前操作</h3>
<p>Outlook 16.0(其他版本未测试)退出时不会触发 <code>OnBeginShutdown</code>和<code>OnDisconnection</code>,原因未知,应该是 Outlook 自己的 Bug,故 Outlook 插件不要在这两个方法中进行退出前操作</p>
<p>经过测试,程序退出时会触发<code>System.Windows.Forms.Application.ThreadExit</code>,但是不会触发(来不及?)<code>AppDomain.CurrentDomain.ProcessExit</code>,可以利用前者来进行退出前操作,比如保存配置和释放资源</p>
<h3 id="office应用关闭后进程不结束">Office 应用关闭后进程不结束</h3>
<p>出现此问题一般是 COM 对象资源未释放干净,但是频繁使用 Office 互操作很难保证所有 COM 对象都及时正确释放。为了让进程正确退出,不可使用<code>Process.Kill</code>等强制方法手动结束进程,一是强制结束进程可能会导致下次打开文档时会提示文档保存异常,二是插件可在程序运行中被手动卸载,可以使用卸载当前应用程序域的方式友好解决问题</p>
<pre><code class="language-csharp">public void OnDisconnection(ext_DisconnectMode removeMode, ref Array custom)
{
try
{
AppDomain.Unload(AppDomain.CurrentDomain);
}
catch (CannotUnloadAppDomainException)
{
// ignored
}
}
</code></pre>
<h2 id="相关资料">相关资料</h2>
<p>如何使用 Visual C# .NET 生成 Office COM 加载项 - Office</p>
<p>: CustomUI |Microsoft 学习</p>
<p>COM Add-In 加载失败疑难解答 |Microsoft 学习</p><br><br>
来源:https://www.cnblogs.com/BluePointLilac/p/18802868
頁:
[1]