Van Pan

导航

如何使用.NET开发全版本支持的Outlook插件产品(二)——完善插件

插件项目所有代码都已经上传至

https://github.com/VanPan/TestOutlookAdding

 

勿在浮砂筑高台——定位错误

在介绍后面的插件开发技术之前,让我们先来看看已经达到的成果:我们已经创建了第一个项目,并且也已经在Outlook里面运行起来了。

但是一定还是有人想知道,插件到底是如何挂接到Outlook里面去的?如果我们发现插件始终无法出现,到底如何排查问题原因?

让我们先停止向前继续开发的脚步,回过头来看看Windows和Office之间到底是如何协作启动一个Office插件的。

在Outlook 2013中查看插件

如果发现插件始终无法出现,怎么办?

首先,我们先确定Outlook是否已经发现了插件,或者是否将插件禁用了。

我们打开Outlook 2013,并且选择顶部的“文件”标签,首先在“信息”中查看“速度慢且已禁用的加载项”中有没有我们插件的名字。如果有,那就始终启用即可。

image

如果没有被禁用,再切换到“选项”,在打开的对话框中选择“加载项”,查看其中是否有我们插件的名字。

image

请注意“非活动应用程序加载项”。如果这栏里面有插件的名字。以我们的例子,我们可以在其中看到“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”。

image

光有这个键值肯定是不够的,我们还需要通过“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}的多个项,展开以后,我们就能发现真正的奥秘。

image

原来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的对象模型,相互的属性,嵌入更多的自定义区域,以及一些开发上的技巧。

posted on 2014-03-07 12:46  Van Pan  阅读(2617)  评论(0编辑  收藏  举报