Featured image of post Windows下获取鼠标当前选中文字

Windows下获取鼠标当前选中文字

Mac下的PopClip是我很喜欢的应用,一直想在Windows下找个替代的,但一直没找到,就准备自己简单实现一个。但研究后发现,Windows下是没有统一的API接口来获取当前用户选中文字的。商业化的GetWord组件可以实现,效果不错,但是这组件是不能免费在非商业应用中使用的。

经过一番网络查找后,发现有几个方案可以获取到,但都有优缺点,都不完美,需要把几种方案结合起来使用才能获取大部分情景下的选中文字。下面记录下这几种不同的实现方案:

Microsoft UI Automation

Microsoft UI Automation是UI的自动化测试框架,提供对windows用户界面(UI)相关信息的编程访问。它提供的IUIAutomationTextPattern接口,可以用来获取文本控件的选中内容,获取示例(C#):

var el = AutomationElement.FocusedElement;
object pattern;
if (el.TryGetCurrentPattern(TextPattern.Pattern, out pattern))
{
    var selectRanges = ((TextPattern)pattern).GetSelection();
    foreach (var rng in selectRanges)
    {
        Console.WriteLine(rng.GetText(-1));
    }
}

优点:官方支持的接口,edge、chrome等浏览器内容都能获取到

缺点:很多应用不支持这接口,或支持得不好,测试发现有些使用.Net组件能获取的,但自己用Go+Win32就没法正确获取

EM_GETSEL & WM_GETTEXT message

向windows窗口发送EM_GETSEL消息,可以获取到编辑控件当前选中文本的起始和结束索引位置,结合WM_GETTEXT获取编辑控件当前文本内容,就能计算出选中的文字,获取示例(Go):

func getSelection() (string, error) {
    // attach to other windows thread
	var processId uint32
	var attach = false
	forgroundWnd := win.GetForegroundWindow()
	targetThreadId := GetWindowThreadProcessId(forgroundWnd, &processId)
	currentThreadId := GetCurrentThreadId()
	if targetThreadId != currentThreadId {
		attach := AttachThreadInput(currentThreadId, targetThreadId, true)
		if attach == 0 {
			return "", fmt.Errorf("AttachThreadInput failed")
		}
	}

	focusWnd := win.GetFocus()

	//Get total text length
	textlength := uint32(win.SendMessage(focusWnd, win.WM_GETTEXTLENGTH, 0, 0))

	//Have any text at all?
	if textlength > 0 {
		textlength = textlength + 1

		//Get selection
		selstart := 0
		selend := 0
		win.SendMessage(focusWnd, win.EM_GETSEL, uintptr(unsafe.Pointer(&selstart)), uintptr(unsafe.Pointer(&selend)))
	
		sb := make([]uint16, textlength)
		win.SendMessage(focusWnd, win.WM_GETTEXT, uintptr(textlength), uintptr(unsafe.Pointer(&sb[0])))

		//Slice out selection
		value := syscall.UTF16ToString(sb)
		length := len(value)
		if (length > 0) && (selend-selstart > 0) && (selstart < length) && (selend <= length) {
			if attach {
				AttachThreadInput(currentThreadId, targetThreadId, false)
			}
			return value[selstart:selend], nil
		}
	}

	if attach {
		AttachThreadInput(currentThreadId, targetThreadId, false)
	}
	//Failed :(
	return "", fmt.Errorf("get selected failed")
}

优点:支持旧的winform、win32应用

缺点:新的WPF、UWP应用不支持

Hook TextOut API

部分windows应用或游戏是通过底层GDI++的TextOutWExtTextOutW接口渲染文字的,我们可以通过HOOK技术,把这些接口替换为我们自己的接口,调用InvalidateRect让应用重绘下鼠标划过的区域,这样应用再调用ExTextOutW接口渲染时,就能拿到相应的文字了。具体原理可以参考这篇论文:Principle of Capturing Word from Screen and Its Implement Methods,相关示例项目可参考:

优点:支持获取旧游戏的文本

缺点:实现难度大,现在大多数应用都是通过TrueType字体文件渲染的,只能拿到GLYPH_INDEX,拿不到最原始的文字。

OCR

截图鼠标划过的区域,再通过OCR出来文字,很多词典软件的鼠标取词通过这方式实现

优点:任何windows应用都支持

缺点:OCR处理慢,混合多语言文字准确度不高

Clipboard

通过模拟复制命令,把选中文字先复制到剪贴板,再从剪贴板取出来。使用示例(Go):

func getSelectionByClipboard() (string, error) {
	hwnd := win.GetForegroundWindow()
	winText := make([]uint16, 1000)
	GetWindowText(hwnd, &winText[0], uint32(len(winText)-1))
	windowName := syscall.UTF16ToString(winText)

	// save old clipboard content
	oldClipboardText, _ := GetClipboardText()
	// current sequentNumber
	oldSeqNumber, _ := GetClipboardSequenceNumber()

	// send ctrl+c copy event
	if strings.Contains(windowName, "Internet Explore") || strings.Contains(windowName, "Microsoft Edge") {
		// Internet Explore ignore ctrl+c click event
		win.PostMessage(hwnd, win.WM_COMMAND, 0x0001000f, 0)
	} else {
		// send selected to clipboard
		SendCopy()
	}

	clipboardChange := <-waitForClipboardChange(oldSeqNumber)
	if clipboardChange {

		text, _ := GetClipboardText()

		// restore old clipborad state
		UpdateClipboardText(oldClipboardText)

		return text, nil
	}

	//Failed :(
	return "", fmt.Errorf("get selected failed")
}

func waitForClipboardChange(oldSeqNumber uint32) chan bool {
	waitChangeCh := make(chan bool)

	timeout := 200
	runTime := 0
	changeCheckTimer := time.NewTimer(10 * time.Microsecond)

	go func() {

		for {
			<-changeCheckTimer.C

			seqNumber, _ := GetClipboardSequenceNumber()
			if seqNumber != oldSeqNumber {
				waitChangeCh <- true
				return
			}

			runTime += 10
			if runTime > timeout {
				waitChangeCh <- false
				return
			}
			changeCheckTimer.Reset(10 * time.Microsecond)
		}
	}()

	return waitChangeCh
}

优点:支持复制的应用都可以支持,如excel,word等

缺点:污染系统剪贴板,当用户有使用剪贴板历史软件时,会看到所有选中的文字

Built with Hugo
主题 StackJimmy 设计