当用户执行以下操作之一时,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
{
}
}
}
}
我仍然无法弄清楚为什么没有调用
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;
}
}