サブクラス化で機能拡張を行う
動機
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); } }