许多人有着这样的默认需求 ------ 绝大多数的情况下,左键单击书签栏(包括界面横栏以及文件夹竖列)应在新的标签页打开书签网址。然而,chrome却没有提供这一选项。可以用AHK解决,几近完美。
AutoHotKey(自动化热键脚本语言)堪称是windows的用户脚本引擎,从xp时代开始就有了,用户很多,资料颇丰,能解决许多痛点。它简单而又高雅,在不为人知的论坛角落,有许多令我赞叹的作品。
说回正题,如何让chrome单击书签打开新页面呢?chrome可以ctrl+shift+单击,或ctrl+单击,或中键点击书签,从而在新页面打开。但这些方法要么需要按住额外的键盘按键,要么打开后新页面处于后台,都不符合"左键单击书签打开新页面"的简单需求。
经过一番思索,我首先定位到 tooltip ,因为鼠标悬浮在书签目上的时候,会弹出 tooltip 小窗口,里面两行文本,包含了书签名与URL。于是我尝试获取里面的文本,不太成功。
为了显示更多文字内容,新版 chrome (v100以上)的 tooltip 不再是win32 原生的tooltips_class32
,无法简单地用ControlGetText
获取文本。
而且就算能获取,也无法快速触发 tooltip 弹窗。原本我设想的是假如它是原生 tooltip,那么是否可以在不触发提示的情况下,直接获取提示的文本内容(toolInfo.lpszText = LPSTR_TEXTCALLBACK; // pszText
回调触发TTN_GETDISPINFO
窗口消息)?
文字识别还是...... ?
现在 chrome 使用自己的 Chrome_WidgetWin_1 (views 体系)绘制提示弹窗。如果要获取其中的文本,最好用什么办法呢?
文字识别?
windows的放大镜有一个小功能,点击上面的喇叭按钮,再点击其他窗口界面,就会大声朗读其中文本,有点像屏幕取词。
其中取词功能基于易用性接口(accessibility api)。AHK 有相关库,东拼西凑如下:
acc_lite.ahk
ahk
Acc_Init(Function := "") {
Static h
If Not h
h:=DllCall("LoadLibrary","Str","oleacc","Ptr")
If Function
return DllCall("GetProcAddress", "Ptr", h, "AStr", Function, "Ptr")
}
Acc_Query(Acc) { ; thanks Lexikos - www.autohotkey.com/forum/viewtopic.php?t=81731&p=509530#509530
try return ComObj(9, ComObjQuery(Acc,"{618736e0-3c3d-11cf-810c-00aa00389b71}"), 1)
}
Acc_Error(p="") {
static setting:=0
return p=""?setting:setting:=p
}
Acc_GetStateText(nState)
{
nSize := DllCall("oleacc\GetStateText", "Uint", nState, "Ptr", 0, "Uint", 0)
VarSetCapacity(sState, (A_IsUnicode?2:1)*nSize)
DllCall("oleacc\GetStateText", "Uint", nState, "str", sState, "Uint", nSize+1)
Return sState
}
; Acc_Child(Acc, ChildId=0) {
; try child:=Acc.accChild(ChildId)
; return child?Acc_Query(child):
; }
Acc_Children(Acc) {
if ComObjType(Acc,"Name") != "IAccessible"
ErrorLevel := "Invalid IAccessible Object"
else {
Acc_Init(), cChildren:=Acc.accChildCount, Children:=[]
if DllCall("oleacc\AccessibleChildren", "Ptr",ComObjValue(Acc), "Int",0, "Int",cChildren, "Ptr",VarSetCapacity(varChildren,cChildren*(8+2*A_PtrSize),0)*0+&varChildren, "Int*",cChildren)=0 {
Loop %cChildren%
i:=(A_Index-1)*(A_PtrSize*2+8)+8, child:=NumGet(varChildren,i), Children.Insert(NumGet(varChildren,i-8)=9?Acc_Query(child):child), NumGet(varChildren,i-8)=9?ObjRelease(child):
return Children.MaxIndex()?Children:
} else
ErrorLevel := "AccessibleChildren DllCall Failed"
}
if Acc_Error()
throw Exception(ErrorLevel,-1)
}
Acc_Location(Acc, ChildId=0, byref Position="") { ; adapted from Sean's code
try Acc.accLocation(ComObj(0x4003,&x:=0), ComObj(0x4003,&y:=0), ComObj(0x4003,&w:=0), ComObj(0x4003,&h:=0), ChildId)
catch
return
Position := "x" NumGet(x,0,"int") " y" NumGet(y,0,"int") " w" NumGet(w,0,"int") " h" NumGet(h,0,"int")
return {x:NumGet(x,0,"int"), y:NumGet(y,0,"int"), w:NumGet(w,0,"int"), h:NumGet(h,0,"int")}
}
Acc_State(Acc, ChildId=0) {
try return ComObjType(Acc,"Name")="IAccessible"?Acc_GetStateText(Acc.accState(ChildId)):"invalid object"
}
Acc_ObjectFromWindow(hWnd, idObject = -4)
{
Acc_Init()
If DllCall("oleacc\AccessibleObjectFromWindow", "Ptr", hWnd, "UInt", idObject&=0xFFFFFFFF, "Ptr", -VarSetCapacity(IID,16)+NumPut(idObject==0xFFFFFFF0?0x46000000000000C0:0x719B3800AA000C81,NumPut(idObject==0xFFFFFFF0?0x0000000000020400:0x11CF3C3D618736E0,IID,"Int64"),"Int64"), "Ptr*", pacc)=0
Return ComObjEnwrap(9,pacc,1)
}
Acc_ObjectFromPoint(ByRef ChildIdOut := "", x := 0, y := 0) {
static S_OK := 0
static address := Acc_Init("AccessibleObjectFromPoint")
point := x & 0xFFFFFFFF | y << 32
if (point = 0) {
DllCall("User32\GetCursorPos", "Int64*", point)
}
pAcc := 0
VarSetCapacity(child, A_PtrSize * 2 + 8, 0)
NTSTATUS := DllCall(address, "Int64", point, "Ptr*", pAcc, "Ptr", &child, "UInt")
if (NTSTATUS != S_OK) {
throw Exception("AccessibleObjectFromPoint() failed.", -1, A_LastError)
}
ChildIdOut := NumGet(child, 8, "UInt")
return ComObj(9, pAcc, 1)
}
使用其中的 Acc_ObjectFromPoint 从鼠标位置创建 acc 对象,然后调用 acc.accName(0) 就可以获取文本内容了!这也太简单了。
其实很多方法从c++代码转过来,它们又可以转回去,运用在其他的应用程序上。
鼠标取词!
既然可以获取鼠标下方的文本内容,那就不管什么 tooltip 了(算是弯路)。试了下可以成功获取书签的完整名称。chrome 和 edge 甚至其他程序都可以!
test_取词.ahk
ahk
#SingleInstance Force
#Include D:\Code\FigureOut\chrome\extesions\AutoHotKey\acc_lite.ahk
CoordMode, Mouse, Screen
AccGetText(acc) {
; WinGet, hwnd, ID, A
WinGetClass, WindowClass, ahk_id %hwnd%
t := acc.accName(0)
t .= " / " acc.accValue(0)
t .= " / "
For Each, Child In Acc_Children(acc) {
; If (5 or Acc_Location(acc, child).w) {
Try
{
t .= acc.accName(child) "`n"
}
; }
}
return " hwnd := " hwnd " acc := " acc "`n t := " t " " ErrorLevel
; return t
}
1::
MouseGetPos, xpos, ypos, MouseWindowUID, MouseControlID
acc := Acc_ObjectFromPoint(ChildIdOut, xpos, ypos)
msgbox, % AccGetText(acc)
return
最后,解决新页面打开书签。
分两种情况。
一、书签横栏
我的书签横栏上面大部分是文件夹,只有三个不是。那么就把少数的几个当特例,点击时附加 ctrl+shift,其余不变。
简而言之就是通过结合 鼠标点击位置 与鼠标取词结果,区分是否附加 ctrl+shift 按键修饰左键单击!
二、文件夹竖列
对于 Chrome v120,(书签栏展开的)文件夹竖列的窗口类别与众不同,它是 Chrome_WidgetWin_2。
对于文件夹竖列,大部分可以在新标签页打开(ctrl+shift),少部如 javascript 代码是特例,可以用书签名简单区分,比如 <<我是js代码>>。
竖列中的文件夹按住 ctrl+shift 点击无反应,所以不用区分。
如果还想在本页打开,可以------
- 根据点击位置,靠左、靠近图标位置,则强制在本页打开
- 增加全局开关
- 使用第三方的书签扩展,比如我的无限书签 ------ 多标签页的书签管理器!
示例代码:
js
#SingleInstance Force
#Include D:\Code\FigureOut\chrome\extesions\AutoHotKey\acc_lite.ahk
global leftDwn := 0
clickLeft(dwn=true) {
if(dwn!=leftDwn || dwn) {
leftDwn := dwn
if(dwn) {
send {LButton down}
} else
send {LButton up}
; 消息("clickLeft" dwn)
}
; else
; 消息("clickLeft nono " dwn)
}
StartsWith(str, t, only=false) {
l := StrLen(str)
n := StrLen(t)
if(l-n>=0 && SubStr(str, 1, n) = t) {
if(!only || indexOf(str, t, n)<0) {
return 1
}
}
return 0
}
$LButton::
WinGetTitle, ti
MouseGetPos, xpos, ypos, MouseWindowUID, MouseControlID
WinGetPos,x,y,w,h,A
if(xpos>=0 && xpos<=w && ypos>=0 && ypos<=h) {
browser := inGroup("browser_gp")
if(click_state=0) {
if browser and not InStr(ti, "New Tab"){
newTab := false
; 在新页面打开书签
WinGetClass, WindowClass, ahk_id %MouseWindowUID%
if(WindowClass="Chrome_WidgetWin_2") { ; 文件夹竖列
CoordMode, Mouse, Screen
MouseGetPos, sX, sY
WinGetPos, x, , , , ahk_id %MouseWindowUID%
CoordMode, Mouse, Relative
if(sX - x > 34) { ; 靠左近图标位置,则仍为本页打开
acc := Acc_ObjectFromPoint(ChildIdOut, sX, sY)
text := acc.accName(0)
; xx(text)
if(!StartsWith(text, "<<")
&& !StartsWith(text, "{{")
&& !StartsWith(text, "页面")
&& !StartsWith(text, "复制")
&& true) {
newTab := true
}
}
} else if(ypos < 160 && ypos > 110 && A_Cursor="Arrow") { ; 判断点击顶部横栏
CoordMode, Mouse, Screen
MouseGetPos, sX, sY
CoordMode, Mouse, Relative
acc := Acc_ObjectFromPoint(ChildIdOut, sX, sY)
text := acc.accName(0)
if(text = "website name") {
if(!InStr(ti, "website name")) { ; 判断重载
newTab := true
}
}
if(StartsWith(text, "http") ; and InStr(text, "未命名书签")
|| text="xxx") {
; 不是文件夹
newTab := true
}
}
; xx(ypos " " A_Cursor)
if newTab {
Send ^+{click}
return
}
}
}
; 消息("123")
clickLeft(1)
} else {
clickLeft(1)
}
return
$LButton up::
if(click_state=3)
click_state := 0
clickLeft(0) ; 这样允许拖拽
return