天天看點

如何使用.NET開發全版本支援的Outlook插件産品(二)——完善插件勿在浮砂築高台——定位錯誤插件進階開發相容Outlook 2003

插件項目所有代碼都已經上傳至

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_USERSoftwareMicrosoftOfficeOutlookAddins,當然如果你的代碼中聲明的是LocalMachine,就應該切換到HKEY_LOCAL_MACHINESOFTWAREMicrosoftOfficeOutlookAddins,這其中需要有我們插件代碼中ProgId特性類聲明名稱的鍵,在例子中,我們的插件是“TestAddinForOutlook”。

光有這個鍵值肯定是不夠的,我們還需要通過“TestAddinForOutlook”這個名字在系統資料庫中進行查找“項”,我們需要确定系統資料庫中存在項HKEY_LOCAL_MACHINESOFTWAREClassesTestAddinForOutlookCLSID。可能根項不一定是HKEY_LOCAL_MACHINE,但是一定是在XXXSOFTWAREClassesTestAddinForOutlookCLSID中。當然,這個ID就是我們代碼中用Guid聲明的那個GUID,我們再用這個ID繼續查找“項”。就應該查到類似于HKEY_CLASSES_ROOTCLSID{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 "自定義
";
        }

        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 "自定義
";
        }

        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的對象模型,互相的屬性,嵌入更多的自定義區域,以及一些開發上的技巧。