通过 NativeWindow 子类监视 Excel 的编辑模式在 Excel DNA 中不起作用

问题描述 投票:0回答:1

当用户执行以下操作之一时,Excel 进入“编辑模式”:双击单元格,开始在其上键入内容,按 F2 键,或单击编辑栏。当“编辑模式”被激活时,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
            {
            }
        }
    }
}
c# excel winapi excel-dna
1个回答
0
投票

我仍然无法弄清楚为什么没有调用

WndProc
覆盖。除此之外,正如 @IInspectable 指出的那样,Excel 开发团队总是有可能更改编辑器窗口的类名,这无论如何都会使代码无用。

通过大量研究,我发现了一种使用 Excel SDK 中未记录的函数LPenHelper

 可靠地检测 Excel 编辑模式
(不依赖 WinAPI 和子类化)的方法。通过定期调用此函数并检查其返回值,可以在模式更改时引发事件。

背景

LPenHelper
函数在XLCALL32.h头文件中定义,在XLCALL32.DLL中实现。 XLCALL32.h 定义了许多重要的常量、数据类型、结构和函数,可以通过 Excel SDK 访问,但是为了监控 Excel 的编辑模式,我们只需要定义以下内容:

private const int xlSpecial = 0x4000;
private const int xlGetFmlaInfo = (14 | xlSpecial);

[StructLayout(LayoutKind.Sequential)]
private struct FmlaInfo
{
    public int wPointMode;  // current edit mode.  0 => rest of struct undefined
    public int cch;         // count of characters in formula
    public IntPtr lpch;     // pointer to formula characters.  READ ONLY!!!
    public int ichFirst;    // char offset to start of selection
    public int ichLast;     // char offset to end of selection (may be > cch)
    public int ichCaret;    // char offset to blinking caret
}

[DllImport("XLCALL32.DLL")]
private static extern int LPenHelper(int wCode, ref FmlaInfo fmlaInfo);

/// <summary>
/// Represents the the current cell editing mode displayed on the left side of 
/// Excel's status bar.
/// </summary> 
public enum xlEditMode
{
    /// <summary>
    /// This value is does not correlate with an actual Excel cell edit mode,
    /// but is used as a return value when an exception occurs while attempting
    /// to poll the current state.
    /// </summary> 
    Undefined = -1,

    /// <summary>
    /// Represents the default state. 
    /// </summary> 
    Ready = 0,

    /// <summary>
    /// Indicates when a cell is selected and typing has begun, or when F2 is keyed 
    /// twice.
    /// </summary> 
    Enter = 1,

    /// <summary>
    /// Indicates in-cell editing, which occurs as a result of double clicking a
    /// cell, keying F2 to enter data. 
    /// </summary> 
    Edit = 2,

    /// <summary>
    /// Indicates in-cell formula selection mode, which occurs as a result of entering
    /// a formula, and clicking the cells/ranges to include in that formula.
    /// </summary> 
    Point = 4
}

实际监控只包括定期轮询 Excel

ThreadPool
并在检测到状态更改时引发事件。我不喜欢投票,但我真的看不到任何其他选择。

private Timer _timer;
private const int PollingFrequency = 10; // milliseconds
private readonly CancellationTokenSource _internalTokenSource;
private readonly CancellationToken _internalToken;
private readonly CancellationToken _externalToken;
private xlEditMode _lastEditMode = xlEditMode.Ready;
private xlEditMode _currentEditMode = xlEditMode.Ready;

public xlEditMode LastEditMode => _lastEditMode;
public xlEditMode CurrentEditMode => _currentEditMode;

public event EventHandler<ExcelEditModeMonitorEventArgs> EditModeChange; 

public ExcelEditModeMonitor(CancellationToken cancellationToken)
{
    _internalTokenSource = new CancellationTokenSource();
    _internalToken = _internalTokenSource.Token; 
    _externalToken = cancellationToken;
    
    // run the timer every 10 milliseconds
   _timer = new Timer(_timer_Elapsed, null, 0, PollingFrequency);
}

private void _timer_Elapsed(object state)
{
    if (_externalToken.IsCancellationRequested || _internalToken.IsCancellationRequested)
    {
        _timer.Change(Timeout.Infinite, Timeout.Infinite);
        return; // stop the timer
    }

    _lastEditMode = _currentEditMode; 
    _currentEditMode = GetEditMode();

    if(_lastEditMode != _currentEditMode)
    {
        EditModeChange?.Invoke(this, new ExcelEditModeMonitorEventArgs(_lastEditMode, _currentEditMode));
    }
}

private xlEditMode GetEditMode()
{
    try
    {
        FmlaInfo fmlaInfo = new FmlaInfo();
        int retVal = LPenHelper(xlGetFmlaInfo, ref fmlaInfo);
        if (retVal != 0)
        {
            return xlEditMode.Undefined;
        }

        return (xlEditMode)fmlaInfo.wPointMode;
    }
    catch
    {
        return xlEditMode.Undefined;
    }
}

测试
然后将其全部连接起来,我们可以执行以下操作:

public sealed class ExcelDnaAddIn : IExcelAddIn
{
    CancellationTokenSource _globalCancellationTokenSource; 
    ExcelEditModeMonitor _editModeMonitor; 
    public void AutoOpen()
    {
        _globalCancellationTokenSource = new CancellationTokenSource(); 
        _editModeMonitor = new ExcelEditModeMonitor(_globalCancellationTokenSource); 
        _editModeMonitor.EditModeChange += OnEditModeChange; 
    }
    
    public void AutoClose()
    {
        _editModeMonitor.Dispose(); 
    }
    
    private void OnExcelEditModeChange(object sender, ExcelEditModeMonitorEventArgs e)
    {
        var last = Enum.GetName(typeof(ExcelEditModeMonitor.xlEditMode), e.LastEditMode);
        var current = Enum.GetName(typeof(ExcelEditModeMonitor.xlEditMode), e.CurrentEditMode);

        System.Diagnostics.Debug.WriteLine($"Excel Edit Mode Changed From {last} to {current}");
    }
}

以下是完整代码供参考:

using System;
using System.Runtime.InteropServices;
using System.Threading;

/// <summary>
/// Represents the "cell edit Mode" of Excel by periodically polling
/// its state via the XLCALL32.dll function "LPenHelper" and inspecting
/// its return value. 
/// </summary>
public sealed class ExcelEditModeMonitor: IDisposable
{
    // Constants, struct, and LPenHelper are all defined in the XLCALL32.h file
    private const int xlSpecial = 0x4000;
    private const int xlGetFmlaInfo = (14 | xlSpecial);

    [StructLayout(LayoutKind.Sequential)]
    private struct FmlaInfo
    {
        public int wPointMode;  // current edit mode.  0 => rest of struct undefined
        public int cch;         // count of characters in formula
        public IntPtr lpch;     // pointer to formula characters.  READ ONLY!!!
        public int ichFirst;    // char offset to start of selection
        public int ichLast;     // char offset to end of selection (may be > cch)
        public int ichCaret;    // char offset to blinking caret
    }

    [DllImport("XLCALL32.DLL")]
    private static extern int LPenHelper(int wCode, ref FmlaInfo fmlaInfo);

    private Timer _timer;
    private long _disposedCount = 0;
    private const int PollingFrequency = 10; // milliseconds
    private readonly CancellationTokenSource _internalTokenSource;
    private readonly CancellationToken _internalToken;
    private readonly CancellationToken _externalToken;
    private xlEditMode _lastEditMode = xlEditMode.Ready;
    private xlEditMode _currentEditMode = xlEditMode.Ready;

    /// <summary>
    /// Represents the the current cell editing mode displayed on the left side of 
    /// Excel's status bar.
    /// </summary> 
    public enum xlEditMode
    {
        /// <summary>
        /// This value is does not correlate with an actual Excel cell edit mode,
        /// but is used as a return value when an exception occurs while attempting
        /// to poll the current state.
        /// </summary> 
        Undefined = -1,

        /// <summary>
        /// Represents the default state. 
        /// </summary> 
        Ready = 0,

        /// <summary>
        /// Indicates when a cell is selected and typing has begun, or when F2 is keyed 
        /// twice.
        /// </summary> 
        Enter = 1,

        /// <summary>
        /// Indicates in-cell editing, which occurs as a result of double clicking a
        /// cell, keying F2 to enter data. 
        /// </summary> 
        Edit = 2,

        /// <summary>
        /// Indicates in-cell formula selection mode, which occurs as a result of entering
        /// a formula, and clicking the cells/ranges to include in that formula.
        /// </summary> 
        Point = 4
    }

    public xlEditMode LastEditMode => _lastEditMode;
    public xlEditMode CurrentEditMode => _currentEditMode;

    public event EventHandler<ExcelEditModeMonitorEventArgs> EditModeChange; 

    public ExcelEditModeMonitor(CancellationToken cancellationToken)
    {
        _internalTokenSource = new CancellationTokenSource();
        _internalToken = _internalTokenSource.Token; 
        _externalToken = cancellationToken;

       _timer = new Timer(_timer_Elapsed, null, 0, PollingFrequency);
    }

    public void Dispose()
    {
        // don't dispose more than once
        if (Interlocked.Read(ref _disposedCount) > 0)
            return;

        _internalTokenSource.Cancel(); 
        _timer.Change(Timeout.Infinite, Timeout.Infinite);
        _timer.Dispose();
        EditModeChange = null; 

        Interlocked.Increment(ref _disposedCount);
    }

    private void _timer_Elapsed(object state)
    {
        if (_externalToken.IsCancellationRequested || _internalToken.IsCancellationRequested)
        {
            _timer.Change(Timeout.Infinite, Timeout.Infinite);
            return; 
        }

        _lastEditMode = _currentEditMode; 
        _currentEditMode = GetEditMode();

        if(_lastEditMode != _currentEditMode)
        {
            EditModeChange?.Invoke(this, new ExcelEditModeMonitorEventArgs(_lastEditMode, _currentEditMode));
        }
    }

    private xlEditMode GetEditMode()
    {
        try
        {
            FmlaInfo fmlaInfo = new FmlaInfo();
            int retVal = LPenHelper(xlGetFmlaInfo, ref fmlaInfo);
            if (retVal != 0)
            {
                return xlEditMode.Undefined;
            }

            return (xlEditMode)fmlaInfo.wPointMode;
        }
        catch
        {
            return xlEditMode.Undefined;
        }
    }
}

public sealed class ExcelEditModeMonitorEventArgs : EventArgs
{
    public ExcelEditModeMonitor.xlEditMode LastEditMode { get; set; }
    public ExcelEditModeMonitor.xlEditMode CurrentEditMode { get; set; }

    public ExcelEditModeMonitorEventArgs(ExcelEditModeMonitor.xlEditMode lastEditMode, ExcelEditModeMonitor.xlEditMode currentEditMode)
    {
        LastEditMode = lastEditMode;
        CurrentEditMode = currentEditMode;
    }
}

编辑
显然,从 Excel 主 UI 线程以外的任何线程调用

LPenHelper
时,Excel 往往会间歇性崩溃。基于 ExcelDna.Intellisense Issue #87 中讨论的问题,我实施了一些同步来防止这种情况发生。我还使用
HandleProcessCorruptedStateExceptions
属性装饰了 GetEditMode 函数。请参阅下面的修订代码。

using System;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

/// <summary>
/// Represents the "cell edit Mode" of Excel by periodically polling
/// its state via the XLCALL32.dll function "LPenHelper" and inspecting
/// its return value. 
/// </summary>
public sealed class ExcelEditModeMonitor: IDisposable
{
    // Constants, struct, and LPenHelper are all defined in the XLCALL32.h file
    private const int xlSpecial = 0x4000;
    private const int xlGetFmlaInfo = (14 | xlSpecial);

    [StructLayout(LayoutKind.Sequential)]
    private struct FmlaInfo
    {
        public int wPointMode;  // current edit mode.  0 => rest of struct undefined
        public int cch;         // count of characters in formula
        public IntPtr lpch;     // pointer to formula characters.  READ ONLY!!!
        public int ichFirst;    // char offset to start of selection
        public int ichLast;     // char offset to end of selection (may be > cch)
        public int ichCaret;    // char offset to blinking caret
    }

    [DllImport("XLCALL32.DLL")]
    private static extern int LPenHelper(int wCode, ref FmlaInfo fmlaInfo);

    private readonly WindowsFormsSynchronizationContext _excelMainThreadSyncContext;
    private System.Threading.Timer _timer;
    private long _disposedCount = 0;
    private const int PollingFrequency = 10; // milliseconds
    private readonly CancellationTokenSource _internalTokenSource;
    private readonly CancellationToken _internalToken;
    private readonly CancellationToken _externalToken;
    private xlEditMode _lastEditMode = xlEditMode.Ready;
    private xlEditMode _currentEditMode = xlEditMode.Ready;

    /// <summary>
    /// Represents the the current cell editing mode displayed on the left side of 
    /// Excel's status bar.
    /// </summary> 
    public enum xlEditMode
    {
        /// <summary>
        /// This value is does not correlate with an actual Excel cell edit mode,
        /// but is used as a return value when an exception occurs while attempting
        /// to poll the current state.
        /// </summary> 
        Undefined = -1,

        /// <summary>
        /// Represents the default state. 
        /// </summary> 
        Ready = 0,

        /// <summary>
        /// Indicates when a cell is selected and typing has begun, or when F2 is keyed 
        /// twice.
        /// </summary> 
        Enter = 1,

        /// <summary>
        /// Indicates in-cell editing, which occurs as a result of double clicking a
        /// cell, keying F2 to enter data. 
        /// </summary> 
        Edit = 2,

        /// <summary>
        /// Indicates in-cell formula selection mode, which occurs as a result of entering
        /// a formula, and clicking the cells/ranges to include in that formula.
        /// </summary> 
        Point = 4
    }

    public xlEditMode LastEditMode => _lastEditMode;
    public xlEditMode CurrentEditMode => _currentEditMode;

    public event EventHandler<ExcelEditModeMonitorEventArgs> EditModeChange; 

    public ExcelEditModeMonitor(CancellationToken cancellationToken)
    {
        _excelMainThreadSyncContext = new WindowsFormsSynchronizationContext();
        _internalTokenSource = new CancellationTokenSource();
        _internalToken = _internalTokenSource.Token; 
        _externalToken = cancellationToken;

       _timer = new System.Threading.Timer(_timer_Elapsed, null, 0, PollingFrequency);
    }

    public void Dispose()
    {
        // don't dispose more than once
        if (Interlocked.Read(ref _disposedCount) > 0)
            return;

        _internalTokenSource.Cancel(); 
        _timer.Change(Timeout.Infinite, Timeout.Infinite);
        _timer.Dispose();
        _excelMainThreadSyncContext.Dispose();
        EditModeChange = null; 

        Interlocked.Increment(ref _disposedCount);
    }

    private void _timer_Elapsed(object state)
    {
        if (_externalToken.IsCancellationRequested || _internalToken.IsCancellationRequested)
        {
            _timer.Change(Timeout.Infinite, Timeout.Infinite);
            return; 
        }

        // Switches Excel's Main UI thread, and updates state
        _excelMainThreadSyncContext.Post(_ =>
        {
            _lastEditMode = _currentEditMode;
            _currentEditMode = GetEditMode();
            
            if(_lastEditMode != _currentEditMode)
            {
                EditModeChange?.Invoke(this, new ExcelEditModeMonitorEventArgs(_lastEditMode, _currentEditMode));
            }
        }, null);
    }
    
    [HandleProcessCorruptedStateExceptions]
    private xlEditMode GetEditMode()
    {
        try
        {
            FmlaInfo fmlaInfo = new FmlaInfo();
            int retVal = LPenHelper(xlGetFmlaInfo, ref fmlaInfo);
            if (retVal != 0)
            {
                return xlEditMode.Undefined;
            }

            return (xlEditMode)fmlaInfo.wPointMode;
        }
        catch
        {
            return xlEditMode.Undefined;
        }
    }
}

public sealed class ExcelEditModeMonitorEventArgs : EventArgs
{
    public ExcelEditModeMonitor.xlEditMode LastEditMode { get; set; }
    public ExcelEditModeMonitor.xlEditMode CurrentEditMode { get; set; }

    public ExcelEditModeMonitorEventArgs(ExcelEditModeMonitor.xlEditMode lastEditMode, ExcelEditModeMonitor.xlEditMode currentEditMode)
    {
        LastEditMode = lastEditMode;
        CurrentEditMode = currentEditMode;
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.