在Excel DNA中通过NativeWindow子类化监控Excel编辑模式失效问题
当用户双击单元格、直接输入内容、按下F2键或点击编辑栏时,Excel会进入Edit Mode(编辑模式)。激活编辑模式时,Excel会打开一个类名为「EXCEL6」的特定窗口。尝试通过自行编写的ExcelEditModeMonitor类子类化该「EXCEL6」窗口来编程检测编辑模式,但重写的WndProc方法似乎从未被调用。
测试代码
public sealed class ExcelDnaAddIn : IExcelAddIn { ExcelEditModeMonitor _editModeMonitor; public void AutoOpen() { _editModeMonitor = new ExcelEditModeMonitor(ExcelDnaUtil.Application); _editModeMonitor.EditModeActivate += OnExcelEditModeActivate; _editModeMonitor.EditModeDeactivate += OnExcelEditModeDeactivate; } public void AutoClose() { _editModeMonitor.Dispose(); } private void OnExcelEditModeActivate(object sender, EventArgs e) { System.Diagnostics.Debug.WriteLine("Editor Active!"); } private void OnExcelEditModeDeactivate(object sender, EventArgs e) { System.Diagnostics.Debug.WriteLine("Editor Inactive!"); } }
参考类代码
public sealed class ExcelEditModeMonitor : NativeWindow, IDisposable { public enum ExcelEditWindowState { InActive = 0, Active = 1 } private class Win32 { public enum WM { WM_WINDOWPOSCHANGED = 0x0047, WM_STYLECHANGED = 0x007D, WM_SETFOCUS = 0x0007, WM_KILLFOCUS = 0x0008 } [DllImport("user32.dll", CharSet = CharSet.Auto)] static public extern IntPtr FindWindowEx(IntPtr hWnd, IntPtr hChild, string strClassName, string strName); [DllImport("user32.dll", CharSet = CharSet.Auto)] static public extern bool IsWindowVisible(IntPtr hWnd); [DllImport("user32.dll", CharSet = CharSet.Auto)] static public extern bool IsWindowEnabled(IntPtr hWnd); } private bool _inCellDirectEditEnabled = false; private ExcelEditWindowState _editWindowState = ExcelEditWindowState.InActive; private event EventHandler _editModeActivate; private event EventHandler _editModeDeactivate; private long _disposedCount = 0; public event EventHandler EditModeActivate { add { _editModeActivate += value; } remove { _editModeActivate -= value; } } public event EventHandler EditModeDeactivate { add { _editModeDeactivate += value; } remove { _editModeDeactivate -= value; } } public ExcelEditWindowState EditWindowState => _editWindowState; public ExcelEditModeMonitor(dynamic excelApp) { if (excelApp == null) throw new ArgumentNullException(nameof(excelApp)); IntPtr editorWindowHwnd = IntPtr.Zero; _inCellDirectEditEnabled = excelApp.EditDirectlyInCell; if (!_inCellDirectEditEnabled) { editorWindowHwnd = Win32.FindWindowEx(new IntPtr(excelApp.Hwnd), IntPtr.Zero, "EXCEL<", String.Empty); if (editorWindowHwnd != IntPtr.Zero) { SetupWindowMonitoring(editorWindowHwnd); return; } } else { editorWindowHwnd = FindEditorHandle(excelApp.Hwnd, "XLDESK", "EXCEL6"); if (editorWindowHwnd != IntPtr.Zero) { SetupWindowMonitoring(editorWindowHwnd); return; } } throw new Exception("Excel editor window not found!"); } private IntPtr FindEditorHandle(int excelAppHwnd, string mainWindowName, string editorWindowName) { IntPtr workbookContainer = Win32.FindWindowEx(new IntPtr(excelAppHwnd), IntPtr.Zero, mainWindowName, String.Empty); IntPtr editWindow = IntPtr.Zero; if (workbookContainer != IntPtr.Zero) { editWindow = Win32.FindWindowEx(workbookContainer, IntPtr.Zero, editorWindowName, String.Empty); } return editWindow; } private void SetupWindowMonitoring(IntPtr editorWindow) { AssignHandle(editorWindow); CheckWindowState(); } public void Dispose() { Dispose(true); } private void Dispose(bool disposing) { if (Interlocked.Read(ref _disposedCount) > 0) return; if (disposing) // Clean up managed state { EditModeActivate -= _editModeActivate; EditModeDeactivate -= _editModeDeactivate; _editModeActivate = null; _editModeDeactivate = null; GC.SuppressFinalize(this); } ReleaseHandle(); Interlocked.Increment(ref _disposedCount); } ~ExcelEditModeMonitor() { Dispose(false); } /// <summary> /// Observe the window state changes, visibility, enabled state, etc. /// </summary> protected override void WndProc(ref Message m) { switch (m.Msg) { // win32 message if style of the targetwindow has changed by any Win32 API call // WS_VISIBLE flag or any other case (int)Win32.WM.WM_STYLECHANGED: CheckWindowState(); break; // win32 message if position of the targetwindow has changed by SetWindowPos // API call SWP_SHOWWINDOW for instance case (int)Win32.WM.WM_WINDOWPOSCHANGED: CheckWindowState(); break; case (int)Win32.WM.WM_SETFOCUS: CheckWindowState(true); break; case (int)Win32.WM.WM_KILLFOCUS: CheckWindowState(false); break; } base.WndProc(ref m); } /// <summary> /// Check style/pos state change. /// <param name="isEnabled"> /// This parameter only takes effect if _inCellDirectEditEnabled == true for /// the excel application. /// </param> /// </summary> private void CheckWindowState(bool isEnabled = false) { try { if (!_inCellDirectEditEnabled && isEnabled) { if (_editWindowState == ExcelEditWindowState.InActive) { _editWindowState = ExcelEditWindowState.Active; RaiseExcelEditWindowStateChangedEvent(_editModeActivate, new object[] { this, EventArgs.Empty }); } } else if (!_inCellDirectEditEnabled && !isEnabled) { if (_editWindowState == ExcelEditWindowState.Active) { _editWindowState = ExcelEditWindowState.InActive; RaiseExcelEditWindowStateChangedEvent(_editModeDeactivate, new object[] { this, EventArgs.Empty }); } } else if (Win32.IsWindowVisible(this.Handle) && Win32.IsWindowEnabled(this.Handle)) { if (_editWindowState == ExcelEditWindowState.InActive) { _editWindowState = ExcelEditWindowState.Active; RaiseExcelEditWindowStateChangedEvent(_editModeActivate, new object[] { this, EventArgs.Empty }); } } else { if (_editWindowState == ExcelEditWindowState.Active) { _editWindowState = ExcelEditWindowState.InActive; RaiseExcelEditWindowStateChangedEvent(_editModeDeactivate, new object[] { this, EventArgs.Empty }); } } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.ToString()); } } private static void RaiseExcelEditWindowStateChangedEvent(Delegate eventHandler, object[] args) { if (eventHandler == null) return; Delegate[] delegates = eventHandler.GetInvocationList(); if (delegates == null) return; foreach (var del in delegates) { try { del.DynamicInvoke(args); } catch { } } } }
问题原因分析
窗口句柄获取时机错误:
ExcelEditModeMonitor在AddIn启动的AutoOpen方法中初始化,此时Excel的「EXCEL6」编辑窗口可能尚未创建。只有用户第一次进入编辑模式时,该窗口才会被实例化,提前获取的句柄无效,后续实际编辑窗口是新创建的,导致子类化的窗口并非实际使用的窗口,无法接收消息。Excel窗口层级结构变化:不同Excel版本(32位/64位、Office 365与传统版)的窗口层级存在差异,
FindEditorHandle方法中XLDESK->EXCEL6的路径可能不准确,获取到的并非实际编辑窗口的句柄。NativeWindow绑定失效:
NativeWindow.AssignHandle仅能捕获绑定窗口的后续消息,若Excel在编辑模式切换时销毁并重建「EXCEL6」窗口,之前绑定的句柄会失效,新窗口的消息无法被WndProc接收,而Excel编辑窗口确实存在销毁重建的行为。监听消息类型不全:Excel对编辑窗口的消息有内部处理逻辑,部分状态变化对应的消息不在当前监听列表中,比如进入编辑模式时可能发送
WM_ACTIVATE而非WM_SETFOCUS,导致无法触发状态检测。
内容的提问来源于stack exchange,提问作者ARickman

