You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在C#中实现类似Babylon的Ctrl+右键取词功能?

嘿,要实现类似Babylon那种Ctrl+右键一键取词的功能,确实得靠Windows API来搞定全局监听和跨窗口文本提取,我之前做过类似的小工具,下面给你一步步拆解实现思路和核心代码:

核心实现步骤拆解

这个功能主要分三个关键环节:全局捕获Ctrl+右键组合事件、提取鼠标位置的单词、调用词典展示结果。

一、全局监听Ctrl+右键事件

要在所有Windows应用里响应这个组合键,得用低级鼠标钩子(WH_MOUSE_LL)——它不需要注入到其他进程,相对安全,是做全局鼠标监听的首选。

先写一个全局钩子的封装类,核心是调用SetWindowsHookEx注册钩子,然后在回调里判断是否是右键按下且Ctrl键处于按住状态:

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

public class GlobalMouseHook
{
    // 钩子类型和消息常量
    private const int WH_MOUSE_LL = 14;
    private const int WM_RBUTTONDOWN = 0x0204;
    private const int VK_CONTROL = 0x11;

    // 钩子回调委托
    private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
    private LowLevelMouseProc _hookCallback;
    private IntPtr _hookId = IntPtr.Zero;

    // 对外暴露的事件,触发Ctrl+右键时调用
    public event EventHandler<Point> CtrlRightClicked;

    public GlobalMouseHook()
    {
        _hookCallback = HookProc;
    }

    // 注册钩子
    public void StartHook()
    {
        _hookId = SetWindowsHookEx(WH_MOUSE_LL, _hookCallback, GetModuleHandle(null), 0);
    }

    // 取消钩子,程序退出时一定要调用!
    public void StopHook()
    {
        if (_hookId != IntPtr.Zero)
        {
            UnhookWindowsHookEx(_hookId);
            _hookId = IntPtr.Zero;
        }
    }

    // 钩子处理逻辑
    private IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && wParam == (IntPtr)WM_RBUTTONDOWN)
        {
            // 检查Ctrl键是否按下(GetKeyState返回的高位为1表示按住)
            if ((GetKeyState(VK_CONTROL) & 0x8000) != 0)
            {
                var mouseInfo = Marshal.PtrToStructure<MSLLHOOKSTRUCT>(lParam);
                CtrlRightClicked?.Invoke(this, mouseInfo.pt);
            }
        }
        // 必须调用下一个钩子,否则系统会出问题
        return CallNextHookEx(_hookId, nCode, wParam, lParam);
    }

    // 鼠标钩子结构体
    [StructLayout(LayoutKind.Sequential)]
    private struct MSLLHOOKSTRUCT
    {
        public Point pt;
        public uint mouseData;
        public uint flags;
        public uint time;
        public IntPtr dwExtraInfo;
    }

    // Windows API导入
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr GetModuleHandle(string lpModuleName);

    [DllImport("user32.dll")]
    private static extern short GetKeyState(int nVirtKey);
}

然后在你的主程序里初始化钩子:

var mouseHook = new GlobalMouseHook();
mouseHook.CtrlRightClicked += OnCtrlRightClicked;
mouseHook.StartHook();

// 程序退出时记得取消钩子
// mouseHook.StopHook();

二、提取鼠标位置的单词

这是最麻烦的一步——不同应用的文本存储方式差异极大,得针对性处理,这里给你几个主流场景的实现方案:

1. 用UI自动化框架(推荐)

Windows的**UI Automation(UIA)**是官方提供的自动化接口,支持大多数现代应用(浏览器、WPF、Office、WinForms等),通用性最强:

using System.Windows.Automation;
using System.Windows; // 注意这里用的是WPF的Point,不是WinForms的

private string GetWordFromUIA(Point screenPoint)
{
    // 获取鼠标指向的自动化元素
    AutomationElement targetElement = AutomationElement.FromPoint(new System.Windows.Point(screenPoint.X, screenPoint.Y));
    if (targetElement == null) return string.Empty;

    // 获取文本模式
    TextPattern textPattern = targetElement.GetCurrentPattern(TextPattern.Pattern) as TextPattern;
    if (textPattern == null) return string.Empty;

    // 获取鼠标位置对应的文本范围,然后扩展到整个单词
    TextPatternRange wordRange = textPattern.RangeFromPoint(new System.Windows.Point(screenPoint.X, screenPoint.Y));
    wordRange?.ExpandToEnclosingUnit(TextUnit.Word);
    
    return wordRange?.GetText(-1).Trim() ?? string.Empty;
}

2. 标准Win32文本控件(Edit/RichEdit)

对于老式的Win32文本控件,可以通过发送Windows消息来提取文本:

using System.Text;

private string GetWordFromWin32Edit(IntPtr hWnd, Point clientPoint)
{
    // 获取鼠标位置对应的字符索引
    IntPtr charPosResult = SendMessage(hWnd, EM_CHARFROMPOS, IntPtr.Zero, (IntPtr)(clientPoint.X | (clientPoint.Y << 16)));
    int charIndex = (short)Marshal.ReadInt16(charPosResult);
    if (charIndex < 0) return string.Empty;

    // 获取控件内的全部文本
    int textLength = SendMessage(hWnd, EM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero);
    StringBuilder sb = new StringBuilder(textLength + 1);
    SendMessage(hWnd, EM_GETTEXT, (IntPtr)sb.Capacity, sb);
    string fullText = sb.ToString();

    // 手动遍历找到单词边界(字母/数字为单词字符)
    int start = charIndex;
    while (start > 0 && char.IsLetterOrDigit(fullText[start - 1])) start--;
    
    int end = charIndex;
    while (end < fullText.Length && char.IsLetterOrDigit(fullText[end])) end++;

    return fullText.Substring(start, end - start);
}

// 消息常量和API导入
private const int EM_CHARFROMPOS = 0x00D7;
private const int EM_GETTEXT = 0x000D;
private const int EM_GETTEXTLENGTH = 0x000E;

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);

[DllImport("user32.dll")]
private static extern bool ScreenToClient(IntPtr hWnd, ref Point lpPoint);

3. Office应用(Word/Excel)

Office提供了COM接口可以直接访问文档内容,以Word为例:

using Microsoft.Office.Interop.Word;
using System.Reflection;

private string GetWordFromWord(Point screenPoint)
{
    try
    {
        // 获取当前活跃的Word实例
        Application wordApp = (Application)Marshal.GetActiveObject("Word.Application");
        if (wordApp?.ActiveWindow == null) return string.Empty;

        // 转换屏幕坐标为Word客户端坐标
        Point clientPoint = wordApp.ActiveWindow.PointToClient(screenPoint);
        Range wordRange = wordApp.ActiveWindow.RangeFromPoint(clientPoint.X, clientPoint.Y);
        
        // 扩展到整个单词
        wordRange?.Expand(WdUnits.wdWord);
        return wordRange?.Text.Trim() ?? string.Empty;
    }
    catch (Exception)
    {
        // 处理未找到Word实例的情况
        return string.Empty;
    }
}

注意:需要引用Office的Interop组件,或者用dynamic类型避免强依赖。

三、整合逻辑并调用词典

OnCtrlRightClicked事件里,先获取鼠标位置,然后尝试用不同的提取方法拿到单词,最后调用你的词典逻辑:

private void OnCtrlRightClicked(object sender, Point screenPoint)
{
    string word = string.Empty;

    // 先尝试UIA(通用性最强)
    word = GetWordFromUIA(new System.Windows.Point(screenPoint.X, screenPoint.Y));
    
    // 如果UIA没拿到,再尝试Win32控件
    if (string.IsNullOrEmpty(word))
    {
        IntPtr hWnd = WindowFromPoint(screenPoint);
        if (hWnd != IntPtr.Zero)
        {
            Point clientPoint = screenPoint;
            ScreenToClient(hWnd, ref clientPoint);
            word = GetWordFromWin32Edit(hWnd, clientPoint);
        }
    }

    // 如果还是没拿到,尝试Office应用
    if (string.IsNullOrEmpty(word))
    {
        word = GetWordFromWord(screenPoint);
    }

    // 拿到单词后,调用你的词典搜索逻辑
    if (!string.IsNullOrEmpty(word))
    {
        ShowDictionaryResult(word); // 这里替换成你的弹窗/查询逻辑
    }
}

// 补充API导入
[DllImport("user32.dll")]
private static extern IntPtr WindowFromPoint(Point pt);

关键注意事项

  • 权限问题:如果你的工具要在管理员权限的应用里工作,自身也需要以管理员权限运行,否则钩子无法捕获这些窗口的事件。
  • 钩子生命周期:程序退出时一定要调用StopHook取消钩子,否则可能导致系统不稳定。
  • 兼容性适配:有些自定义控件可能不支持UIA或标准消息,这时候可能需要针对特定应用做适配,但大部分主流应用用UIA都能搞定。

内容的提问来源于stack exchange,提问作者MirrorMirror

火山引擎 最新活动