You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Excel DNA中通过NativeWindow子类化监控Excel编辑模式失效问题

Excel编辑模式检测中WndProc未被调用的原因分析

当用户双击单元格、直接输入内容、按下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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 14:27:36