サブクラス化で機能拡張を行う

動機

Excel上で、セルの選択範囲が変化した際には、SheetSelectionChangeイベントが発生し、以下のようなハンドラを設定していれば、セルの選択範囲を認識することができます。

private void Application_SheetSelectionChange(object Sh, Excel.Range Target)
{
    this.Label.Text = "";
    foreach (Excel.Range current in Target)
    {
        this.Label.Text +=
            "[" + current.Row.ToString() + ", " + current.Column.ToString() + "]\n";
    }
}

しかし、セル→画像、画像→セルのように、異なるオブジェクトを選択した場合には、固有のイベントが発生しないため、選択されたオブジェクトを認識するには一工夫必要になります。
ここでは、既存のクラスを、機能拡張したサブクラスで乗っ取り、新しいイベントを定義することによって、上記の問題を解決します。

既存クラスの調査

まず、実際にメッセージを処理している部分を特定するために、Spy++による解析を行います。
図中にある照準をドラッグし、解析する範囲を選択します。ここでは、シート上のイベントが知りたいので、シートの外枠を選択します。OKクリック後のダイアログで同期をクリックすると、ウインドウの階層構造の中から、該当するウインドウが選択されます。
選択されたウインドウ名を右クリックし、メッセージを選択すると、新しいウインドウが開きます。ここには、該当するウインドウ上で発生したメッセージが表示されます。
全てのメッセージを表示すると、WM_MOUSEMOVEなどで埋まってしまうため、取り除いていきます。
これにより、セル選択時にはWM_CAPTURECHANGED、画像選択時にはWM_PAINT、ウインドウ切り替え時にはWM_SETFOCUSが発生していることが分かります。

サブクラスの定義

既存のウインドウをサブクラス化する際には、NativeWindowクラスを継承した新しいクラスを作成し、WndProcメソッドをオーバーライドすることで、既存のメッセージループに処理を追加することが可能です。以下に実装を示します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Excel = Microsoft.Office.Interop.Excel;
using Office = Microsoft.Office.Core;
using System.Runtime.InteropServices;

namespace ExcelAddinWithNativeWindow
{
    public class NativeExcelControl : NativeWindow, IDisposable
    {
        public event EventHandler<EventArgs> SelectChanged = null;

        public NativeExcelControl(IntPtr hTarget)
        {
            if (hTarget == IntPtr.Zero)
            {
                throw new System.ApplicationException();
            }
            else
            {
                this.AssignHandle(hTarget);
            }
        }

        ~NativeExcelControl()
        {
            this.Dispose();
        }

        public void Dispose()
        {
            this.ReleaseHandle();
        }

        protected override void WndProc(ref Message m)
        {
            const int WM_SETFOCUS = 0x0007;
            const int WM_PAINT = 0x000F;
            const int WM_CAPTURECHANGED = 0x0215;
            if (m.Msg == WM_CAPTURECHANGED || m.Msg == WM_PAINT || m.Msg == WM_SETFOCUS)
            {
                if (Globals.ThisAddIn.Application.Selection != prevSelected)
                {
                    this.OnSelectChanged();
                }
            }

            base.WndProc(ref m);
        }

        protected virtual void OnSelectChanged()
        {
            if (this.SelectChanged != null)
            {
                prevSelected = Globals.ThisAddIn.Application.Selection;
                this.SelectChanged(this, EventArgs.Empty);
            }
        }

        private object prevSelected = null;
    }
}

this.AssignHandle(hTarget)で元のウインドウを乗っ取り、this.ReleaseHandle()で解放するようになっています。
WM_CAPTURECHANGED、WM_PAINT、WM_SETFOCUSの3つを横取りしていますが、これらのイベントが発生しても、選択したオブジェクトに変更がない場合がありますので、前回の値を記憶し、異なっている場合にのみ、イベントを発生させます。

ウインドウの乗っ取り

実際にウインドウを乗っ取るには、別のコードが必要になります。まず、実際に乗っ取る対象を探すコードを示します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Excel = Microsoft.Office.Interop.Excel;
using Office = Microsoft.Office.Core;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace ExcelAddinWithNativeWindow
{
    static class NativeExcelControlHelper
    {
        // Win32 APIのインポート
        [DllImport("USER32.dll")]
        public static extern IntPtr FindWindowEx(
            IntPtr hWndParent, IntPtr hWndChildAfter,
            string strClass, string strWindows);

        static public List<IntPtr> FindTarget()
        {
            List<IntPtr> result = new List<IntPtr>();
            IntPtr hExcelDesk = IntPtr.Zero;
            IntPtr hExcelPrev = IntPtr.Zero;
            IntPtr hExcelBook = IntPtr.Zero;
            hExcelDesk = FindWindowEx((IntPtr)Globals.ThisAddIn.Application.Hwnd,
                IntPtr.Zero, "XLDESK", null);
            if (hExcelDesk != IntPtr.Zero)
            {
                while (true)
                {
                    hExcelBook = FindWindowEx(hExcelDesk, hExcelPrev, "EXCEL7", null);
                    if (hExcelBook != IntPtr.Zero)
                    {
                        result.Add(hExcelBook);
                        hExcelPrev = hExcelBook;
                    }
                    else
                    {
                        break;
                    }
                }
            }
            return result;
        }
    }
}

XLDESKというクラスの下にある、EXCEL7というクラスをすべて列挙し、リストで返しています。このEXCEL7というクラスが乗っ取りの対象となります(Spy++の図の内容を思い出してください)。

ウインドウの乗っ取り2

次に、実際にウインドウの乗っ取りを行っているコードを示します。

private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
    this.Application.WorkbookActivate +=
        new Excel.AppEvents_WorkbookActivateEventHandler(
            Application_WorkbookActivate);
    NativeExcelControl ctrl = new NativeExcelControl(
        NativeExcelControlHelper.FindTarget()[0]);
    ctrl.SelectChanged += new EventHandler<EventArgs>(
        this.NativeExcelControl_SelectChanged);
    ecs.Add(ctrl);

    Microsoft.Office.Tools.CustomTaskPane customTaskPane =
        this.CustomTaskPanes.Add(usercontrol1, "アクションペイン");
    customTaskPane.DockPosition = 
        Microsoft.Office.Core.MsoCTPDockPosition.msoCTPDockPositionRight;
    customTaskPane.Visible = true;
    this.Label.Text = "";
    this.Label2.Text = "";
}

最初の3行は後回しにして、実際にNativeExcelControlクラスが使用されている部分に着目します。まず、乗っ取り対象の0番目をNativeExcelControlクラスのコンストラクタに渡します。これでサブクラス化が完了します。
その後、サブクラス化によって新しく定義されたSelectChangedイベントにイベントハンドラを追加します。
イベントハンドラの実装は以下となります。

private void NativeExcelControl_SelectChanged(object sender, EventArgs e)
{
    this.Label2.Text = "";
    if (this.Application.Selection is Excel.Range)
    {
        this.Label2.Text = "This is a cell.";
    }
    else if (this.Application.Selection is Excel.Picture)
    {
        this.Label2.Text = "This is a picture.";
    }
}

ブックの追加

あるブックを編集している際に、別のブックを開くケースがあるかと思います。この場合、乗っ取る必要があるウインドウが2つに増えます(乗っ取り対象をリストで返しているのはこのためです)。ここでは、新しいウインドウが生成されたことを、WorkbookActivateイベントを使って検出します。
イベントハンドラの実装は以下となります。

void Application_WorkbookActivate(Excel.Workbook Wb)
{
    foreach (NativeExcelControl ctrl in ecs)
    {
        ctrl.SelectChanged -= this.NativeExcelControl_SelectChanged;
        ctrl.Dispose();
    }
    ecs.Clear();
    foreach (IntPtr ptr in NativeExcelControlHelper.FindTarget())
    {
        NativeExcelControl ctrl = new NativeExcelControl(ptr);
        ctrl.SelectChanged += new EventHandler<EventArgs>(
            this.NativeExcelControl_SelectChanged);
        ecs.Add(ctrl);
    }
}