插件项目所有代码都已经上传至
https://github.com/VanPan/TestOutlookAdding
勿在浮砂筑高台——定位错误
在介绍后面的插件开发技术之前,让我们先来看看已经达到的成果:我们已经创建了第一个项目,并且也已经在Outlook里面运行起来了。
但是一定还是有人想知道,插件到底是如何挂接到Outlook里面去的?如果我们发现插件始终无法出现,到底如何排查问题原因?
让我们先停止向前继续开发的脚步,回过头来看看Windows和Office之间到底是如何协作启动一个Office插件的。
在Outlook 2013中查看插件
如果发现插件始终无法出现,怎么办?
首先,我们先确定Outlook是否已经发现了插件,或者是否将插件禁用了。
我们打开Outlook 2013,并且选择顶部的“文件”标签,首先在“信息”中查看“速度慢且已禁用的加载项”中有没有我们插件的名字。如果有,那就始终启用即可。
如果没有被禁用,再切换到“选项”,在打开的对话框中选择“加载项”,查看其中是否有我们插件的名字。
请注意“非活动应用程序加载项”。如果这栏里面有插件的名字。以我们的例子,我们可以在其中看到“Test Addin For Outlook”,就说明插件已经被Outlook发现,但是在加载过程中出现错误。一般情况下,都是因为x86和x64的版本号无法对应造成的,当然也有启动时出现错误导致无法加载的。对于这种情况,请先简化插件逻辑代码,保证可以启动,再逐步深入查看原因。
在Outlook 2003中查看插件
如果你用的是Outlook 2013,那可以在“工具”菜单栏中点击“信任中心”,在打开的对话框中也能找到类似上图的加载项。
注册表定位
如果这个界面里面都没有插件名称,说明Outlook根本没有发现插件的存在。此时,我们需要进入注册表查看问题的真正原因。
我们用regedit命令打开注册表编辑器,进入以下项HKEY_CURRENT_USER\Software\Microsoft\Office\Outlook\Addins,当然如果你的代码中声明的是LocalMachine,就应该切换到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\Outlook\Addins,这其中需要有我们插件代码中ProgId特性类声明名称的键,在例子中,我们的插件是“TestAddinForOutlook”。
光有这个键值肯定是不够的,我们还需要通过“TestAddinForOutlook”这个名字在注册表中进行查找“项”,我们需要确定注册表中存在项HKEY_LOCAL_MACHINE\SOFTWARE\Classes\TestAddinForOutlook\CLSID。可能根项不一定是HKEY_LOCAL_MACHINE,但是一定是在XXX\SOFTWARE\Classes\TestAddinForOutlook\CLSID中。当然,这个ID就是我们代码中用Guid声明的那个GUID,我们再用这个ID继续查找“项”。就应该查到类似于HKEY_CLASSES_ROOT\CLSID\{AFE67651-951D-4A42-8CAB-E9BF7E219DDF}的多个项,展开以后,我们就能发现真正的奥秘。
原来Outlook就是靠这种方法来查找到插件的安装路径的,简而言之,就是先看注册表中Addin内部的项,再通过项名称找到Class的CLSID,最后用CLSID唯一定位到插件文件路径。
如果以后对插件加载还有什么无法理解的,都可以通过这个方法来查看是否在什么地方出了问题,也可以进行一些调整来确认问题是否已经解决。
插件进阶开发
再回到我们进行插件开发的思路中来,现在我们虽然加载了一个无比简陋的插件,但是它没有产生任何实际效果,我们没有它的任何点击事件,也无法动态改变它的标题,如果遇到一些需要实时调整的情况,那是完全不够用的。好,那我们再把RibbonUI.xml调整一下,改成下面的样子。
<?xml version="1.0" encoding="utf-8" ?> <customUI onLoad="LoadAction" xmlns="http://schemas.microsoft.com/office/2006/01/customui" > <ribbon> <tabs> <tab id="RibbonAddinSampleTabCS35" label="插件标签"> <group id="group1" label="分组名"> <button id="customButton1" size="large" onAction="ButtonAction" getLabel="GetButtonLabel"/> </group> </tab> </tabs> </ribbon> </customUI>
再在项目中引入System.Windows.Forms程序集,把COMEntry类改成如下代码。
using System.Runtime.InteropServices; using System.Windows.Forms; using NetOffice.OutlookApi.Tools; using NetOffice.Tools; namespace TestOutlookAddin { [COMAddin("Test Addin For Outlook", "", 3), CustomUI("TestOutlookAddin.RibbonUI.xml"), RegistryLocation(RegistrySaveLocation.CurrentUser)] [Guid("AFE67651-951D-4A42-8CAB-E9BF7E219DDF"), ProgId("TestAddinForOutlook")] public class COMEntry : COMAddin { private Office.IRibbonUI _ribbon; public void LoadAction(Office.IRibbonUI control) { _ribbon = control; } public string GetButtonLabel(NetOffice.OfficeApi.IRibbonControl control) { return "自定义\n"; } public void ButtonAction(NetOffice.OfficeApi.IRibbonControl control) { MessageBox.Show("Hello World"); } } }
再运行一下,就能看到我们把按钮的Label和点击事件都接入到代码控制里面来了。
LoadAction是为了将整个Ribbon的对象用代码后台对象进行映射,因为Ribbon的控件无法通过 对象变量.属性 赋值来进行修改,如果要刷新UI,需要调用 _ribbon.InvalidateControl("customButton1"),这样控件会重新调用这个Button在xml中定义的各项方法来达到刷新UI的目的,因此我们可能需要在GetButtonLabel中通过一些状态来返回不同的字符串。
需要特意说明的一点,GetButtonLabel返回的字符串里面最后都应该带回车符,因为只有这样Outlook才不会把文字变成两行,否则看起来会非常别扭。原始效果可以查看第一篇教程中的界面截图。
除了这些,我们还能重定义按钮的图标。当然在RibbonUI.xml里面就是GetButtonImage,在代码里面,对应的函数是如下样子
public stdole.IPictureDisp GetButtonImage(NetOffice.OfficeApi.IRibbonControl control) { return PictureConverter.IconToPictureDisp(Properties.Resources.SampleIcon2); }
其中stdole是添加引用,在“程序集”——“扩展”里面可以找到,PictureConverter类代码如下
using System; using System.Drawing; using System.Windows.Forms; namespace TestOutlookAddin { internal class PictureConverter : AxHost { private PictureConverter() : base(String.Empty) { } static public stdole.IPictureDisp ImageToPictureDisp(Image image) { return (stdole.IPictureDisp)GetIPictureDispFromPicture(image); } static public stdole.Picture ImageToPicture(Image image) { return (stdole.Picture)GetIPictureFromPicture(image); } static public stdole.IPictureDisp IconToPictureDisp(Icon icon) { return ImageToPictureDisp(icon.ToBitmap()); } static public stdole.Picture IconToPicture(Icon icon) { return ImageToPicture(icon.ToBitmap()); } static public Image PictureDispToImage(stdole.IPictureDisp picture) { return GetPictureFromIPicture(picture); } } }
其中的Image类通过添加System.Drawing程序集可以获得。
这样,我们就可以得到一个带图标的自定义按钮了。标签和分组的名称都可以通过类似的方法进行修改。
兼容Outlook 2003
我们看起来已经把插件这件事做完了,但是,等一下,Ribbon好像是无法满足所有版本要求的,如果用户使用的是2007或者2003怎么办?
那我们只能向应用程序中注入一个新增的菜单栏或者是工具栏了。再我最后发布的工程中,我们采用的是新增工具栏的方式,因为加入一个菜单栏的方法对用户来说过于干扰,而且我们还有一些随时需要根据情况变化的按钮标签,做在菜单栏上会很突兀。
那么,如何增加工具栏呢?而且顺便一提,如果在代码里面不加任何条件增加工具栏的话,到了2013里面我们又会多一套Ribbon,因为2013是向下兼容旧版本插件的。所以,我们要先判断Office的版本号,根据不同的版本来加载不同的代码。最后,我们的COMEntry类就变成了下面的样子。
using System; using System.Runtime.InteropServices; using System.Windows.Forms; using NetOffice.OfficeApi.Enums; using NetOffice.OutlookApi.Tools; using NetOffice.Tools; using OutLook = NetOffice.OutlookApi; using Office = NetOffice.OfficeApi; namespace TestOutlookAddin { [COMAddin("Test Addin For Outlook", "", 3), CustomUI("TestOutlookAddin.RibbonUI.xml"), RegistryLocation(RegistrySaveLocation.CurrentUser)] [Guid("AFE67651-951D-4A42-8CAB-E9BF7E219DDF"), ProgId("TestAddinForOutlook")] public class COMEntry : COMAddin { NetOffice.OutlookApi.Application _outlookApplication; private NetOffice.OfficeApi.IRibbonUI _ribbon; NetOffice.OfficeApi.CommandBarButton LogonBtn; public COMEntry() { OnStartupComplete += Addin_OnStartupComplete; OnConnection += Addin_OnConnection; OnDisconnection += Addin_OnDisconnection; } private void Addin_OnDisconnection(ext_DisconnectMode RemoveMode, ref Array custom) { try { if (null != _outlookApplication) _outlookApplication.Dispose(); } catch (Exception exception) { // 处理 } } private void Addin_OnConnection(object app, ext_ConnectMode ConnectMode, object AddInInst, ref Array custom) { try { _outlookApplication = new OutLook.Application(null, app); } catch (Exception exception) { // 处理 } } private void Addin_OnStartupComplete(ref Array custom) { if (!_outlookApplication.Version.StartsWith("15.0") && !_outlookApplication.Version.StartsWith("14.0")) { try { SetupGui(); } catch (Exception exception) { // 处理 } } } private void SetupGui() { /* create commandbar */ Office.CommandBar commandBar = _outlookApplication.ActiveExplorer().CommandBars.Add("工具栏名称", MsoBarPosition.msoBarTop, System.Type.Missing, true); commandBar.Visible = true; // add a button to the popup LogonBtn = (Office.CommandBarButton)commandBar.Controls.Add(MsoControlType.msoControlButton, Type.Missing, Type.Missing, Type.Missing, true); LogonBtn.Style = MsoButtonStyle.msoButtonIconAndCaption; LogonBtn.Picture = PictureConverter.IconToPicture(Properties.Resources.SampleIcon2); LogonBtn.Mask = PictureConverter.ImageToPicture(Properties.Resources.sampleicon2Mask); //LogonBtn.ClickEvent += new NetOffice.OfficeApi.CommandBarButton_ClickEventHandler(LoginBtn_ClickEvent); } public void LoadAction(Office.IRibbonUI control) { _ribbon = control; } public string GetButtonLabel(NetOffice.OfficeApi.IRibbonControl control) { return "自定义\n"; } public void ButtonAction(NetOffice.OfficeApi.IRibbonControl control) { MessageBox.Show("Hello World"); } public stdole.IPictureDisp GetButtonImage(NetOffice.OfficeApi.IRibbonControl control) { return PictureConverter.IconToPictureDisp(Properties.Resources.SampleIcon2); } } }
我们又在COMEntry类中添加了很多事件监听,其实主要目的就是在插件加载启动时获得Outlook Application的实例,并且在OnStartupComplete事件发生的时候判断当前Office版本号来进行不同的界面加载。Office的版本号格式是4位数字,类似于15.0.0.xxxx这样的结构,以首位数字表示大版本。15对应Office 2013,14对应Office 2010。
SetupGui函数实现了经典界面的工具栏添加功能,其中注释的部分是监听按钮事件,因为我们保持例子的简单,就不再需要监听处理这个事件了。
经典界面里面的按钮是可以通过对象操作来修改标签等其他信息的,只需要LogonBtn.Caption即可进行重新设置,不需要再去InvalidUI了。
最后需要稍加注意的,是Mask这个属性。这个属性是和Picture配合使用的,主要目的是为了将按钮的图标做到区域透明效果。在经典界面里面,是不支持Icon或者PNG之类的透明图标的,如果你只是设置了一部分轮廓透明的图标作为Button的Picture,那到了显示的时候,透明的效果并不会达成。为了解决这个问题,Office引入了Mask这个方案,其实Mask就是一张黑白图片,和Picture配合,用来表示何处需要显示何处需要透明。
下一篇,我们将会大致了解Outlook的对象模型,相互的属性,嵌入更多的自定义区域,以及一些开发上的技巧。