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++的TextOutW或ExtTextOutW接口渲染文字的,我们可以通过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等
缺点:污染系统剪贴板,当用户有使用剪贴板历史软件时,会看到所有选中的文字