Chrome : 点击书签栏在新页面打开的完美解决方案!(AHK脚本分享)

许多人有着这样的默认需求 ------ 绝大多数的情况下,左键单击书签栏(包括界面横栏以及文件夹竖列)应在新的标签页打开书签网址。然而,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 点击无反应,所以不用区分。

如果还想在本页打开,可以------

  1. 根据点击位置,靠左、靠近图标位置,则强制在本页打开
  2. 增加全局开关
  3. 使用第三方的书签扩展,比如我的无限书签 ------ 多标签页的书签管理器!

示例代码:

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
相关推荐
凄凄迷人10 分钟前
如何调试 chrome 崩溃日志(MAC)
前端·chrome·macos·crash
浏览器爱好者11 小时前
如何定制谷歌浏览器的外观主题
chrome
无敌糖果14 小时前
Python+Selenium无头浏览器实现网页截图
chrome·爬虫·selenium·测试工具
Narutolxy1 天前
一篇专业且实用的技术博客:从离线安装 Nginx 到动态适配依赖升级20241125
运维·chrome·nginx
星月昭铭1 天前
浏览器控制台中使用ajax下载文件(没有postman等情况下)
前端·chrome·ajax·postman
孤帝@2 天前
Shell编程完结
前端·chrome
荼靡6032 天前
shell完结
前端·chrome
一颗青果2 天前
【Linux】详解shell代码实现(上)
linux·运维·服务器·前端·chrome·算法·1024程序员节
航月2 天前
linux基本命令2
linux·运维·chrome
想喝冰拿铁2 天前
bash笔记
chrome·笔记·bash