从 VSTO 加载项检测 Word 2016 中的文本更改
Detecting text changes in Word 2016 from VSTO add-in
此问题与 How to get the “KeyPress” event from a Word 2010 Addin (developed in C#)? 密切相关(实际上包括该问题答案中的示例代码),但这是专门针对 Visual Studio(专业版)2015 开发的Word 2016 运行宁 Windows 10.
我正在尝试通过 VSTO 加载项检测 Word 文档中的文本何时更改。我从
了解到
- How to get the “KeyPress” event from a Word 2010 Addin (developed in C#)?(2011 年 11 月 14 日)
- Capturing keydown event of MS Word using C#(2012 年 10 月 21 日)
- How to raise an event on MS word Keypress(2012 年 10 月 24 日)
- How to trap keypress event in MSword using VSTO?(2012 年 11 月 5 日)
没有事件驱动的方法来做到这一点。当文本更改时,Word 根本不发送事件。
我看到讨论了两个解决方法:
- 使用WindowSelectionChange事件。不幸的是,当通过按箭头键、使用鼠标、执行撤消或重做以及可能的其他操作更改选择时,似乎会发送此事件,但在键入或删除时不会发送。
- 使用低级 keydown 事件挂钩。这已在其中几个 Whosebug 问题中进行了讨论,并且在 2014 年 2 月的 a thread on a Visual Studio forum 中也被称为“广泛传播的技术”。
我正在尝试使用 How to get the “KeyPress” event from a Word 2010 Addin (developed in C#)? 答案中的代码,它似乎观察到每个按键事件 除了 那些发送到 Word 2016 的事件。
这是我使用的代码,方便参考。
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
namespace KeydownWordAddIn
{
public partial class ThisAddIn
{
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private static IntPtr hookId = IntPtr.Zero;
private delegate IntPtr HookProcedure(int nCode, IntPtr wParam, IntPtr lParam);
private static HookProcedure procedure = HookCallback;
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, HookProcedure lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
private static IntPtr SetHook(HookProcedure procedure)
{
using (Process process = Process.GetCurrentProcess())
using (ProcessModule module = process.MainModule)
return SetWindowsHookEx(WH_KEYBOARD_LL, procedure, GetModuleHandle(module.ModuleName), 0);
}
private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
{
int pointerCode = Marshal.ReadInt32(lParam);
string pressedKey = ((Keys)pointerCode).ToString();
// Do some sort of processing on key press.
var thread = new Thread(() => {
Debug.WriteLine(pressedKey);
});
thread.Start();
}
return CallNextHookEx(hookId, nCode, wParam, lParam);
}
private void ThisAddIn_Startup(object sender, EventArgs e)
{
hookId = SetHook(procedure);
}
private void ThisAddIn_Shutdown(object sender, EventArgs e)
{
UnhookWindowsHookEx(hookId);
}
#region VSTO generated code
/// <summary>
/// Required method for Designer support.
/// </summary>
private void InternalStartup()
{
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
#endregion
}
}
当我使用此加载项 运行 Word 2016 时,我看到按键事件发送到 Edge 浏览器甚至 Visual Studio,但没有发送到 Word 本身。
在 Word 2016 中是否以某种方式阻止了 keydown 挂钩,还是我做错了什么?
我在 Word 2013 中也遇到过同样的问题,不得不想出一个 "creative" 的解决方案。它使用 diffplex 来监视活动文档文本的变化,并在发生变化时触发事件。这不太理想,但我们会做我们必须做的事情来使事情顺利进行。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using Word = Microsoft.Office.Interop.Word;
using Office = Microsoft.Office.Core;
using Microsoft.Office.Tools.Word;
using System.ComponentModel;
namespace WordUtils {
public class TextChangeDetector {
public Word.Application Application;
private BackgroundWorker bg;
public delegate void TextChangeHandler(object sender, TextChangedEventArgs e);
public event TextChangeHandler OnTextChanged;
public TextChangeDetector(Word.Application app) {
this.Application = app;
}
public void Start() {
bg = new BackgroundWorker();
bg.WorkerReportsProgress = true;
bg.WorkerSupportsCancellation = true;
bg.ProgressChanged += bg_ProgressChanged;
bg.DoWork += bg_DoWork;
bg.RunWorkerAsync(this.Application);
}
private void bg_ProgressChanged(object sender, ProgressChangedEventArgs e) {
switch (e.ProgressPercentage) {
case 50: //change
if (OnTextChanged != null) {
OnTextChanged(this, new TextChangedEventArgs((char)e.UserState));
}
break;
}
}
private void bg_DoWork(object sender, DoWorkEventArgs e) {
Word.Application wordApp = e.Argument as Word.Application;
BackgroundWorker bg = sender as BackgroundWorker;
string lastPage = string.Empty;
while (true) {
try {
if (Application.Documents.Count > 0) {
if (Application.ActiveDocument.Words.Count > 0) {
var currentPage = Application.ActiveDocument.Bookmarks["\Page"].Range.Text;
if (currentPage != null && currentPage != lastPage) {
var differ = new DiffPlex.Differ();
var builder = new DiffPlex.DiffBuilder.InlineDiffBuilder(differ);
var difference = builder.BuildDiffModel(lastPage, currentPage);
var change = from d in difference.Lines where d.Type != DiffPlex.DiffBuilder.Model.ChangeType.Unchanged select d;
if (change.Any()) {
bg.ReportProgress(50, change.Last().Text.Last());
}
lastPage = currentPage;
}
}
}
} catch (Exception) {
}
if (bg.CancellationPending) {
break;
}
System.Threading.Thread.Sleep(100);
}
}
public void Stop() {
if (bg != null && !bg.CancellationPending) {
bg.CancelAsync();
}
}
}
public class TextChangedEventArgs : EventArgs {
public char Letter;
public TextChangedEventArgs(char letter) {
this.Letter = letter;
}
}
}
用法:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Word = Microsoft.Office.Interop.Word;
using Office = Microsoft.Office.Core;
using Microsoft.Office.Tools.Word;
using WordUtils;
namespace WordAddIn1 {
public partial class ThisAddIn {
TextChangeDetector detector;
private void ThisAddIn_Startup(object sender, System.EventArgs e) {
detector = new TextChangeDetector(Application);
detector.OnTextChanged += detector_OnTextChanged;
detector.Start();
}
void detector_OnTextChanged(object sender, TextChangedEventArgs e) {
Console.WriteLine(e.Letter);
}
private void ThisAddIn_Shutdown(object sender, System.EventArgs e) {
detector.Stop();
}
#region VSTO generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InternalStartup() {
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
#endregion
}
}
如果您不在 VSTO 加载项中使用低级挂钩,一切都应该正常工作。
[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetCurrentThreadId();
const int WH_KEYBOARD = 2;
private static IntPtr SetHook(HookProcedure procedure)
{
var threadId = (uint)SafeNativeMethods.GetCurrentThreadId();
return SetWindowsHookEx(WH_KEYBOARD, procedure, IntPtr.Zero, threadId);
}
请注意,您可能还需要创建一个钩子来拦截鼠标消息,因为仅通过鼠标交互(例如通过功能区或上下文菜单复制和粘贴)就可以修改文档的文本。
VSTO 示例
这是一个完整的工作 VSTO 示例,包括键盘和鼠标挂钩:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Office = Microsoft.Office.Core;
namespace SampleAddinWithKeyboardHook
{
public partial class ThisAddIn
{
// NOTE: We need a backing field to prevent the delegate being garbage collected
private SafeNativeMethods.HookProc _mouseProc;
private SafeNativeMethods.HookProc _keyboardProc;
private IntPtr _hookIdMouse;
private IntPtr _hookIdKeyboard;
private void ThisAddIn_Startup(object sender, EventArgs e)
{
_mouseProc = MouseHookCallback;
_keyboardProc = KeyboardHookCallback;
SetWindowsHooks();
}
private void ThisAddIn_Shutdown(object sender, EventArgs e)
{
UnhookWindowsHooks();
}
private void SetWindowsHooks()
{
uint threadId = (uint)SafeNativeMethods.GetCurrentThreadId();
_hookIdMouse =
SafeNativeMethods.SetWindowsHookEx(
(int)SafeNativeMethods.HookType.WH_MOUSE,
_mouseProc,
IntPtr.Zero,
threadId);
_hookIdKeyboard =
SafeNativeMethods.SetWindowsHookEx(
(int)SafeNativeMethods.HookType.WH_KEYBOARD,
_keyboardProc,
IntPtr.Zero,
threadId);
}
private void UnhookWindowsHooks()
{
SafeNativeMethods.UnhookWindowsHookEx(_hookIdKeyboard);
SafeNativeMethods.UnhookWindowsHookEx(_hookIdMouse);
}
private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
var mouseHookStruct =
(SafeNativeMethods.MouseHookStructEx)
Marshal.PtrToStructure(lParam, typeof(SafeNativeMethods.MouseHookStructEx));
// handle mouse message here
var message = (SafeNativeMethods.WindowMessages)wParam;
Debug.WriteLine(
"{0} event detected at position {1} - {2}",
message,
mouseHookStruct.pt.X,
mouseHookStruct.pt.Y);
}
return SafeNativeMethods.CallNextHookEx(
_hookIdKeyboard,
nCode,
wParam,
lParam);
}
private IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
// handle key message here
Debug.WriteLine("Key event detected.");
}
return SafeNativeMethods.CallNextHookEx(
_hookIdKeyboard,
nCode,
wParam,
lParam);
}
#region VSTO generated code
/// <summary>
/// Required method for Designer support.
/// </summary>
private void InternalStartup()
{
Startup += ThisAddIn_Startup;
Shutdown += ThisAddIn_Shutdown;
}
#endregion
}
internal static class SafeNativeMethods
{
public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
public enum HookType
{
WH_KEYBOARD = 2,
WH_MOUSE = 7
}
public enum WindowMessages : uint
{
WM_KEYDOWN = 0x0100,
WM_KEYFIRST = 0x0100,
WM_KEYLAST = 0x0108,
WM_KEYUP = 0x0101,
WM_LBUTTONDBLCLK = 0x0203,
WM_LBUTTONDOWN = 0x0201,
WM_LBUTTONUP = 0x0202,
WM_MBUTTONDBLCLK = 0x0209,
WM_MBUTTONDOWN = 0x0207,
WM_MBUTTONUP = 0x0208,
WM_MOUSEACTIVATE = 0x0021,
WM_MOUSEFIRST = 0x0200,
WM_MOUSEHOVER = 0x02A1,
WM_MOUSELAST = 0x020D,
WM_MOUSELEAVE = 0x02A3,
WM_MOUSEMOVE = 0x0200,
WM_MOUSEWHEEL = 0x020A,
WM_MOUSEHWHEEL = 0x020E,
WM_RBUTTONDBLCLK = 0x0206,
WM_RBUTTONDOWN = 0x0204,
WM_RBUTTONUP = 0x0205,
WM_SYSDEADCHAR = 0x0107,
WM_SYSKEYDOWN = 0x0104,
WM_SYSKEYUP = 0x0105
}
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr SetWindowsHookEx(
int idHook,
HookProc lpfn,
IntPtr hMod,
uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr CallNextHookEx(
IntPtr hhk,
int nCode,
IntPtr wParam,
IntPtr lParam);
[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetCurrentThreadId();
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
public int X;
public int Y;
public Point(int x, int y)
{
X = x;
Y = y;
}
public static implicit operator System.Drawing.Point(Point p)
{
return new System.Drawing.Point(p.X, p.Y);
}
public static implicit operator Point(System.Drawing.Point p)
{
return new Point(p.X, p.Y);
}
}
[StructLayout(LayoutKind.Sequential)]
public struct MouseHookStructEx
{
public Point pt;
public IntPtr hwnd;
public uint wHitTestCode;
public IntPtr dwExtraInfo;
public int MouseData;
}
}
}
VBE 加载项示例
这里是 VBA 编辑器(VBE 加载项)的工作示例:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Extensibility;
namespace VbeAddin
{
[ComVisible(true)]
[ProgId("VbeAddin.Connect")]
[Guid("95840C70-5A1A-4EDB-B436-40E8BF030469")]
public class Connect : StandardOleMarshalObject, IDTExtensibility2
{
// NOTE: We need a backing field to prevent the delegate being garbage collected
private SafeNativeMethods.HookProc _mouseProc;
private SafeNativeMethods.HookProc _keyboardProc;
private IntPtr _hookIdMouse;
private IntPtr _hookIdKeyboard;
#region IDTExtensibility2 Members
public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
{
_mouseProc = MouseHookCallback;
_keyboardProc = KeyboardHookCallback;
SetWindowsHooks();
}
public void OnDisconnection(ext_DisconnectMode removeMode, ref Array custom)
{
UnhookWindowsHooks();
}
public void OnAddInsUpdate(ref Array custom)
{
}
public void OnStartupComplete(ref Array custom)
{
}
public void OnBeginShutdown(ref Array custom)
{
}
#endregion
private void SetWindowsHooks()
{
uint threadId = (uint)SafeNativeMethods.GetCurrentThreadId();
_hookIdMouse =
SafeNativeMethods.SetWindowsHookEx(
(int)SafeNativeMethods.HookType.WH_MOUSE,
_mouseProc,
IntPtr.Zero,
threadId);
_hookIdKeyboard =
SafeNativeMethods.SetWindowsHookEx(
(int)SafeNativeMethods.HookType.WH_KEYBOARD,
_keyboardProc,
IntPtr.Zero,
threadId);
}
private void UnhookWindowsHooks()
{
SafeNativeMethods.UnhookWindowsHookEx(_hookIdKeyboard);
SafeNativeMethods.UnhookWindowsHookEx(_hookIdMouse);
}
private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
var mouseHookStruct =
(SafeNativeMethods.MouseHookStructEx)
Marshal.PtrToStructure(
lParam,
typeof(SafeNativeMethods.MouseHookStructEx));
// handle mouse message here
var message = (SafeNativeMethods.WindowMessages)wParam;
Debug.WriteLine(
"{0} event detected at position {1} - {2}",
message,
mouseHookStruct.pt.X,
mouseHookStruct.pt.Y);
}
return SafeNativeMethods.CallNextHookEx(
_hookIdKeyboard,
nCode,
wParam,
lParam);
}
private IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
// handle key message here
Debug.WriteLine("Key event detected.");
}
return SafeNativeMethods.CallNextHookEx(
_hookIdKeyboard,
nCode,
wParam,
lParam);
}
}
internal static class SafeNativeMethods
{
public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
public enum HookType
{
WH_KEYBOARD = 2,
WH_MOUSE = 7
}
public enum WindowMessages : uint
{
WM_KEYDOWN = 0x0100,
WM_KEYFIRST = 0x0100,
WM_KEYLAST = 0x0108,
WM_KEYUP = 0x0101,
WM_LBUTTONDBLCLK = 0x0203,
WM_LBUTTONDOWN = 0x0201,
WM_LBUTTONUP = 0x0202,
WM_MBUTTONDBLCLK = 0x0209,
WM_MBUTTONDOWN = 0x0207,
WM_MBUTTONUP = 0x0208,
WM_MOUSEACTIVATE = 0x0021,
WM_MOUSEFIRST = 0x0200,
WM_MOUSEHOVER = 0x02A1,
WM_MOUSELAST = 0x020D,
WM_MOUSELEAVE = 0x02A3,
WM_MOUSEMOVE = 0x0200,
WM_MOUSEWHEEL = 0x020A,
WM_MOUSEHWHEEL = 0x020E,
WM_RBUTTONDBLCLK = 0x0206,
WM_RBUTTONDOWN = 0x0204,
WM_RBUTTONUP = 0x0205,
WM_SYSDEADCHAR = 0x0107,
WM_SYSKEYDOWN = 0x0104,
WM_SYSKEYUP = 0x0105
}
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr SetWindowsHookEx(
int idHook,
HookProc lpfn,
IntPtr hMod,
uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr CallNextHookEx(
IntPtr hhk,
int nCode,
IntPtr wParam,
IntPtr lParam);
[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetCurrentThreadId();
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential)]
public struct MouseHookStructEx
{
public Point pt;
public IntPtr hwnd;
public uint wHitTestCode;
public IntPtr dwExtraInfo;
public int MouseData;
}
}
}
此问题与 How to get the “KeyPress” event from a Word 2010 Addin (developed in C#)? 密切相关(实际上包括该问题答案中的示例代码),但这是专门针对 Visual Studio(专业版)2015 开发的Word 2016 运行宁 Windows 10.
我正在尝试通过 VSTO 加载项检测 Word 文档中的文本何时更改。我从
了解到- How to get the “KeyPress” event from a Word 2010 Addin (developed in C#)?(2011 年 11 月 14 日)
- Capturing keydown event of MS Word using C#(2012 年 10 月 21 日)
- How to raise an event on MS word Keypress(2012 年 10 月 24 日)
- How to trap keypress event in MSword using VSTO?(2012 年 11 月 5 日)
没有事件驱动的方法来做到这一点。当文本更改时,Word 根本不发送事件。
我看到讨论了两个解决方法:
- 使用WindowSelectionChange事件。不幸的是,当通过按箭头键、使用鼠标、执行撤消或重做以及可能的其他操作更改选择时,似乎会发送此事件,但在键入或删除时不会发送。
- 使用低级 keydown 事件挂钩。这已在其中几个 Whosebug 问题中进行了讨论,并且在 2014 年 2 月的 a thread on a Visual Studio forum 中也被称为“广泛传播的技术”。
我正在尝试使用 How to get the “KeyPress” event from a Word 2010 Addin (developed in C#)? 答案中的代码,它似乎观察到每个按键事件 除了 那些发送到 Word 2016 的事件。
这是我使用的代码,方便参考。
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
namespace KeydownWordAddIn
{
public partial class ThisAddIn
{
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private static IntPtr hookId = IntPtr.Zero;
private delegate IntPtr HookProcedure(int nCode, IntPtr wParam, IntPtr lParam);
private static HookProcedure procedure = HookCallback;
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, HookProcedure lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
private static IntPtr SetHook(HookProcedure procedure)
{
using (Process process = Process.GetCurrentProcess())
using (ProcessModule module = process.MainModule)
return SetWindowsHookEx(WH_KEYBOARD_LL, procedure, GetModuleHandle(module.ModuleName), 0);
}
private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
{
int pointerCode = Marshal.ReadInt32(lParam);
string pressedKey = ((Keys)pointerCode).ToString();
// Do some sort of processing on key press.
var thread = new Thread(() => {
Debug.WriteLine(pressedKey);
});
thread.Start();
}
return CallNextHookEx(hookId, nCode, wParam, lParam);
}
private void ThisAddIn_Startup(object sender, EventArgs e)
{
hookId = SetHook(procedure);
}
private void ThisAddIn_Shutdown(object sender, EventArgs e)
{
UnhookWindowsHookEx(hookId);
}
#region VSTO generated code
/// <summary>
/// Required method for Designer support.
/// </summary>
private void InternalStartup()
{
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
#endregion
}
}
当我使用此加载项 运行 Word 2016 时,我看到按键事件发送到 Edge 浏览器甚至 Visual Studio,但没有发送到 Word 本身。
在 Word 2016 中是否以某种方式阻止了 keydown 挂钩,还是我做错了什么?
我在 Word 2013 中也遇到过同样的问题,不得不想出一个 "creative" 的解决方案。它使用 diffplex 来监视活动文档文本的变化,并在发生变化时触发事件。这不太理想,但我们会做我们必须做的事情来使事情顺利进行。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using Word = Microsoft.Office.Interop.Word;
using Office = Microsoft.Office.Core;
using Microsoft.Office.Tools.Word;
using System.ComponentModel;
namespace WordUtils {
public class TextChangeDetector {
public Word.Application Application;
private BackgroundWorker bg;
public delegate void TextChangeHandler(object sender, TextChangedEventArgs e);
public event TextChangeHandler OnTextChanged;
public TextChangeDetector(Word.Application app) {
this.Application = app;
}
public void Start() {
bg = new BackgroundWorker();
bg.WorkerReportsProgress = true;
bg.WorkerSupportsCancellation = true;
bg.ProgressChanged += bg_ProgressChanged;
bg.DoWork += bg_DoWork;
bg.RunWorkerAsync(this.Application);
}
private void bg_ProgressChanged(object sender, ProgressChangedEventArgs e) {
switch (e.ProgressPercentage) {
case 50: //change
if (OnTextChanged != null) {
OnTextChanged(this, new TextChangedEventArgs((char)e.UserState));
}
break;
}
}
private void bg_DoWork(object sender, DoWorkEventArgs e) {
Word.Application wordApp = e.Argument as Word.Application;
BackgroundWorker bg = sender as BackgroundWorker;
string lastPage = string.Empty;
while (true) {
try {
if (Application.Documents.Count > 0) {
if (Application.ActiveDocument.Words.Count > 0) {
var currentPage = Application.ActiveDocument.Bookmarks["\Page"].Range.Text;
if (currentPage != null && currentPage != lastPage) {
var differ = new DiffPlex.Differ();
var builder = new DiffPlex.DiffBuilder.InlineDiffBuilder(differ);
var difference = builder.BuildDiffModel(lastPage, currentPage);
var change = from d in difference.Lines where d.Type != DiffPlex.DiffBuilder.Model.ChangeType.Unchanged select d;
if (change.Any()) {
bg.ReportProgress(50, change.Last().Text.Last());
}
lastPage = currentPage;
}
}
}
} catch (Exception) {
}
if (bg.CancellationPending) {
break;
}
System.Threading.Thread.Sleep(100);
}
}
public void Stop() {
if (bg != null && !bg.CancellationPending) {
bg.CancelAsync();
}
}
}
public class TextChangedEventArgs : EventArgs {
public char Letter;
public TextChangedEventArgs(char letter) {
this.Letter = letter;
}
}
}
用法:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Word = Microsoft.Office.Interop.Word;
using Office = Microsoft.Office.Core;
using Microsoft.Office.Tools.Word;
using WordUtils;
namespace WordAddIn1 {
public partial class ThisAddIn {
TextChangeDetector detector;
private void ThisAddIn_Startup(object sender, System.EventArgs e) {
detector = new TextChangeDetector(Application);
detector.OnTextChanged += detector_OnTextChanged;
detector.Start();
}
void detector_OnTextChanged(object sender, TextChangedEventArgs e) {
Console.WriteLine(e.Letter);
}
private void ThisAddIn_Shutdown(object sender, System.EventArgs e) {
detector.Stop();
}
#region VSTO generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InternalStartup() {
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
#endregion
}
}
如果您不在 VSTO 加载项中使用低级挂钩,一切都应该正常工作。
[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetCurrentThreadId();
const int WH_KEYBOARD = 2;
private static IntPtr SetHook(HookProcedure procedure)
{
var threadId = (uint)SafeNativeMethods.GetCurrentThreadId();
return SetWindowsHookEx(WH_KEYBOARD, procedure, IntPtr.Zero, threadId);
}
请注意,您可能还需要创建一个钩子来拦截鼠标消息,因为仅通过鼠标交互(例如通过功能区或上下文菜单复制和粘贴)就可以修改文档的文本。
VSTO 示例
这是一个完整的工作 VSTO 示例,包括键盘和鼠标挂钩:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Office = Microsoft.Office.Core;
namespace SampleAddinWithKeyboardHook
{
public partial class ThisAddIn
{
// NOTE: We need a backing field to prevent the delegate being garbage collected
private SafeNativeMethods.HookProc _mouseProc;
private SafeNativeMethods.HookProc _keyboardProc;
private IntPtr _hookIdMouse;
private IntPtr _hookIdKeyboard;
private void ThisAddIn_Startup(object sender, EventArgs e)
{
_mouseProc = MouseHookCallback;
_keyboardProc = KeyboardHookCallback;
SetWindowsHooks();
}
private void ThisAddIn_Shutdown(object sender, EventArgs e)
{
UnhookWindowsHooks();
}
private void SetWindowsHooks()
{
uint threadId = (uint)SafeNativeMethods.GetCurrentThreadId();
_hookIdMouse =
SafeNativeMethods.SetWindowsHookEx(
(int)SafeNativeMethods.HookType.WH_MOUSE,
_mouseProc,
IntPtr.Zero,
threadId);
_hookIdKeyboard =
SafeNativeMethods.SetWindowsHookEx(
(int)SafeNativeMethods.HookType.WH_KEYBOARD,
_keyboardProc,
IntPtr.Zero,
threadId);
}
private void UnhookWindowsHooks()
{
SafeNativeMethods.UnhookWindowsHookEx(_hookIdKeyboard);
SafeNativeMethods.UnhookWindowsHookEx(_hookIdMouse);
}
private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
var mouseHookStruct =
(SafeNativeMethods.MouseHookStructEx)
Marshal.PtrToStructure(lParam, typeof(SafeNativeMethods.MouseHookStructEx));
// handle mouse message here
var message = (SafeNativeMethods.WindowMessages)wParam;
Debug.WriteLine(
"{0} event detected at position {1} - {2}",
message,
mouseHookStruct.pt.X,
mouseHookStruct.pt.Y);
}
return SafeNativeMethods.CallNextHookEx(
_hookIdKeyboard,
nCode,
wParam,
lParam);
}
private IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
// handle key message here
Debug.WriteLine("Key event detected.");
}
return SafeNativeMethods.CallNextHookEx(
_hookIdKeyboard,
nCode,
wParam,
lParam);
}
#region VSTO generated code
/// <summary>
/// Required method for Designer support.
/// </summary>
private void InternalStartup()
{
Startup += ThisAddIn_Startup;
Shutdown += ThisAddIn_Shutdown;
}
#endregion
}
internal static class SafeNativeMethods
{
public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
public enum HookType
{
WH_KEYBOARD = 2,
WH_MOUSE = 7
}
public enum WindowMessages : uint
{
WM_KEYDOWN = 0x0100,
WM_KEYFIRST = 0x0100,
WM_KEYLAST = 0x0108,
WM_KEYUP = 0x0101,
WM_LBUTTONDBLCLK = 0x0203,
WM_LBUTTONDOWN = 0x0201,
WM_LBUTTONUP = 0x0202,
WM_MBUTTONDBLCLK = 0x0209,
WM_MBUTTONDOWN = 0x0207,
WM_MBUTTONUP = 0x0208,
WM_MOUSEACTIVATE = 0x0021,
WM_MOUSEFIRST = 0x0200,
WM_MOUSEHOVER = 0x02A1,
WM_MOUSELAST = 0x020D,
WM_MOUSELEAVE = 0x02A3,
WM_MOUSEMOVE = 0x0200,
WM_MOUSEWHEEL = 0x020A,
WM_MOUSEHWHEEL = 0x020E,
WM_RBUTTONDBLCLK = 0x0206,
WM_RBUTTONDOWN = 0x0204,
WM_RBUTTONUP = 0x0205,
WM_SYSDEADCHAR = 0x0107,
WM_SYSKEYDOWN = 0x0104,
WM_SYSKEYUP = 0x0105
}
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr SetWindowsHookEx(
int idHook,
HookProc lpfn,
IntPtr hMod,
uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr CallNextHookEx(
IntPtr hhk,
int nCode,
IntPtr wParam,
IntPtr lParam);
[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetCurrentThreadId();
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
public int X;
public int Y;
public Point(int x, int y)
{
X = x;
Y = y;
}
public static implicit operator System.Drawing.Point(Point p)
{
return new System.Drawing.Point(p.X, p.Y);
}
public static implicit operator Point(System.Drawing.Point p)
{
return new Point(p.X, p.Y);
}
}
[StructLayout(LayoutKind.Sequential)]
public struct MouseHookStructEx
{
public Point pt;
public IntPtr hwnd;
public uint wHitTestCode;
public IntPtr dwExtraInfo;
public int MouseData;
}
}
}
VBE 加载项示例
这里是 VBA 编辑器(VBE 加载项)的工作示例:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Extensibility;
namespace VbeAddin
{
[ComVisible(true)]
[ProgId("VbeAddin.Connect")]
[Guid("95840C70-5A1A-4EDB-B436-40E8BF030469")]
public class Connect : StandardOleMarshalObject, IDTExtensibility2
{
// NOTE: We need a backing field to prevent the delegate being garbage collected
private SafeNativeMethods.HookProc _mouseProc;
private SafeNativeMethods.HookProc _keyboardProc;
private IntPtr _hookIdMouse;
private IntPtr _hookIdKeyboard;
#region IDTExtensibility2 Members
public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
{
_mouseProc = MouseHookCallback;
_keyboardProc = KeyboardHookCallback;
SetWindowsHooks();
}
public void OnDisconnection(ext_DisconnectMode removeMode, ref Array custom)
{
UnhookWindowsHooks();
}
public void OnAddInsUpdate(ref Array custom)
{
}
public void OnStartupComplete(ref Array custom)
{
}
public void OnBeginShutdown(ref Array custom)
{
}
#endregion
private void SetWindowsHooks()
{
uint threadId = (uint)SafeNativeMethods.GetCurrentThreadId();
_hookIdMouse =
SafeNativeMethods.SetWindowsHookEx(
(int)SafeNativeMethods.HookType.WH_MOUSE,
_mouseProc,
IntPtr.Zero,
threadId);
_hookIdKeyboard =
SafeNativeMethods.SetWindowsHookEx(
(int)SafeNativeMethods.HookType.WH_KEYBOARD,
_keyboardProc,
IntPtr.Zero,
threadId);
}
private void UnhookWindowsHooks()
{
SafeNativeMethods.UnhookWindowsHookEx(_hookIdKeyboard);
SafeNativeMethods.UnhookWindowsHookEx(_hookIdMouse);
}
private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
var mouseHookStruct =
(SafeNativeMethods.MouseHookStructEx)
Marshal.PtrToStructure(
lParam,
typeof(SafeNativeMethods.MouseHookStructEx));
// handle mouse message here
var message = (SafeNativeMethods.WindowMessages)wParam;
Debug.WriteLine(
"{0} event detected at position {1} - {2}",
message,
mouseHookStruct.pt.X,
mouseHookStruct.pt.Y);
}
return SafeNativeMethods.CallNextHookEx(
_hookIdKeyboard,
nCode,
wParam,
lParam);
}
private IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
// handle key message here
Debug.WriteLine("Key event detected.");
}
return SafeNativeMethods.CallNextHookEx(
_hookIdKeyboard,
nCode,
wParam,
lParam);
}
}
internal static class SafeNativeMethods
{
public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
public enum HookType
{
WH_KEYBOARD = 2,
WH_MOUSE = 7
}
public enum WindowMessages : uint
{
WM_KEYDOWN = 0x0100,
WM_KEYFIRST = 0x0100,
WM_KEYLAST = 0x0108,
WM_KEYUP = 0x0101,
WM_LBUTTONDBLCLK = 0x0203,
WM_LBUTTONDOWN = 0x0201,
WM_LBUTTONUP = 0x0202,
WM_MBUTTONDBLCLK = 0x0209,
WM_MBUTTONDOWN = 0x0207,
WM_MBUTTONUP = 0x0208,
WM_MOUSEACTIVATE = 0x0021,
WM_MOUSEFIRST = 0x0200,
WM_MOUSEHOVER = 0x02A1,
WM_MOUSELAST = 0x020D,
WM_MOUSELEAVE = 0x02A3,
WM_MOUSEMOVE = 0x0200,
WM_MOUSEWHEEL = 0x020A,
WM_MOUSEHWHEEL = 0x020E,
WM_RBUTTONDBLCLK = 0x0206,
WM_RBUTTONDOWN = 0x0204,
WM_RBUTTONUP = 0x0205,
WM_SYSDEADCHAR = 0x0107,
WM_SYSKEYDOWN = 0x0104,
WM_SYSKEYUP = 0x0105
}
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr SetWindowsHookEx(
int idHook,
HookProc lpfn,
IntPtr hMod,
uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr CallNextHookEx(
IntPtr hhk,
int nCode,
IntPtr wParam,
IntPtr lParam);
[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetCurrentThreadId();
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential)]
public struct MouseHookStructEx
{
public Point pt;
public IntPtr hwnd;
public uint wHitTestCode;
public IntPtr dwExtraInfo;
public int MouseData;
}
}
}