Обнаружение изменений текста в Word 2016 из надстройки VSTO
этот вопрос очень тесно связан с Как получить событие "нажатие клавиши" из Word 2010 Addin (разработанного на C#)? (и фактически включает в себя пример кода из ответа на этот вопрос), но это конкретно о разработке в Visual Studio (Professional) 2015 для Word 2016, работающего в Windows 10.
Я пытаюсь определить, когда текст изменяется в документе Word из надстройки VSTO. Я понимаю из
- Как получить Событие" нажатие клавиши " из Word 2010 Addin (разработано на C#)? (14 ноября 2011)
- захват события keydown MS Word с помощью C# (21 октября 2012)
- Как поднять событие на MS word Keypress (24 октября 2012)
- как поймать событие нажатия клавиши в MSword с помощью VSTO? (5 ноября 2012)
что нет никакого управляемого событиями способа сделать это. Слово просто не отправляет события, когда текст изменения.
Я видел, как обсуждались два обходных пути:
- использовать WindowSelectionChange событие. К сожалению, это событие появляется, когда выбор изменяется нажатием клавиш со стрелками, с помощью мыши, выполнения отмены или повтора и, возможно, других действий, но не при вводе или удалении.
- используйте низкоуровневый крюк события keydown. Это обсуждалось в нескольких из этих вопросов StackOverflow, а также называлось " широко распространение техники " в поток на форуме Visual Studio в феврале 2014 года.
Я пытаюсь использовать код, в ответ Как получить событие "нажатие клавиши" из Word 2010 Addin (разработанного на C#)?, и, кажется, наблюдать за каждым событием keydown за исключением отправленные в 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 с этой надстройкой, я вижу keydown события отправляются в браузер Edge и даже в Visual Studio, но не в Word.
крючки keydown каким-то образом предотвращены в Word 2016, или я делаю что-то неправильно?
2 ответов
Everthing должен работать нормально, если вы не используете низкоуровневый крюк в надстройке 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;
}
}
}
Я испытал ту же самую проблему в Word 2013 и должен был придумать несколько "креативное" решение. Он использует 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
}
}