[AHK]中键菜单

发布时间 2023-06-25 00:08:28作者: 稻荷夜

开始

起因

一直挺烦那些繁琐又固定的操作,比如想打开某个文件夹、打开视频网站、打开文档手册等。这些工作都可以通过ahk脚本来完成,如打开文件夹和网页用run命令就可以。于是我便写了个整合脚本命令的ahk,可以方便的执行预定义的操作。
接下来,我会介绍它的使用,及出现的问题和改进,如果想看原理的话,在最后有简单的原理说明(很少)。

使用

长按鼠标中键即可弹出一串选项菜单,左键选择子项就可执行自定义行为了。

介绍

版本一

菜单的样式如下图。
image
它最多可以有19个菜单项,因为是使用Tooltip实现的,ahk规定最多只能同时存在20个。不过19也挺多了,如果没用太多需求的话……
但我使用了些许日子后,果然,19远远不够,还有了更大的问题——无分类显得杂乱。
于是我改进了第二版,增加了一个父级菜单用来分类,分类下是具体的子菜单。如此,分类可以有19个,而每个分类也有19个子项,足够我使用了!

版本二

菜单现在是这样的,父菜单:
image
子菜单:
image
我只做了些日需的分类,但仅仅使用了6个分类,还用13个分类可用喔。

代码

也有github地址喔,里面还有些ahk的小玩意:
https://github.com/refiz/ahk_vL_scripts.git
版本一

#SingleInstance, Force

CoordMode,ToolTip,Screen
CoordMode,Mouse,Screen

;========自定义
Global AllCharMenu:={ 1:"StartUp" ;在此按序号添加菜单项,需要自行添加及修改Label内容
    ,2:"Flypy.Com"
    ,3:"Ahk.Com"
    ,4:"Bili.Com"
    ,5:"AConvert.Com"
    ,6:"YouDao.Com"
    ,7:"Book"
    ,8:"Note"
    ,9:""
    ,10:""
    ,11:""
    ,12:""
    ,13:""
    ,14:""
    ,15:""
    ,16:""
    ,17:""
    ,18:""
,19:"TheMemo"}
Global SimpleCharMenu:={1:"笔记" ;此处为简字符菜单,适合圆形菜单形式
    ,2:"查形"
    ,3:"AHK"
    ,4:"哔哩"
    ,5:"转换"
    ,6:"翻译"
    ,7:"小说"
    ,8:"占十"
    ,9:"WEB"
    ,10:"占七"
    ,11:"占三"
    ,12:"虎牙"
    ,13:"占五"
    ,14:"占六"
    ,15:"JAPI"
    ,16:"UAPI"
    ,17:"菜鸟"
    ,18:"自启"
,19:"备忘"}
Global MenuStyle:="Circle" ;可选:长条形() 圆形(Circle)Circle
Global TipStyle:="SingleChar" ;可选:单字符(SingleChar) 全字符(AllChar)
Global Delay:=20 ;在此定义按住中键唤起菜单所需的时间
Global Radius:=50 ;在此定义圆形菜单的半径
Global MaxItems:=6 ;在此定义第一圈的最大项数,第二圈(最多)则为2*MaxItems项,总项数不超过19。关系为:(0-7]:[3-6],(0-9]:3,(9-13]:4,(13-16]:5,(16-19]:6
Global PI:=3.1415926
;========更换托盘图标相关,分激活与停用两种状态,双击托盘图标以停用脚本
Global IsOn:=True
Global IsStyle:=True
Global M_Open:="Open"
Global M_Close:="Close"

Menu,Tray,Icon,%A_Desktop%/Icon图集/MidMenu_Open.ico ;自备两个ICO
Menu,Tray,NoStandard ;去除默认菜单项
Menu,Tray,Add,Rect,M_MenuStyle
Menu,Tray,Add,%M_Close%,M_Toggle ;增加切换选项
Menu,Tray,Default,%M_Close% ;将切换选项设为默认
Menu,Tray,Click,2 ;设置双击响应
Menu,Tray,Add,&Exit,M_MenuExit ;添加退出按钮

#If IsOn
    ~MButton::
    HowLong:=0 ;初始化
    Loop{
        HowLong++ ;计时
        Sleep,10
        If !(GetKeyState("MButton"))
            Return
        ; ToolTip,% HowLong ;显示进度
    } Until HowLong>=Delay ;达到预定时间
    Send,{MButton} ;在浏览器中有用
    OptimizeShowEffect() ;显示菜单
    HotKey,~LButton,MenuClick ;设置单击热键,当单击时进行判断
    If (GetKeyState("MButton"))
        Return
    HotKey,~LButton,On ;启用热键
Return
#If

MenuClick:
    HotKey,~LButton,Off ;单击后关闭热键
    Sleep,100 ;等待ToolTip被激活
    Result:=CheckMenu() ;接受返回的数组
    If Result[1]!=1 ;此次单击是否将菜单项激活
    {
        CleanMenu() ;未激活则清除菜单并退出
        Return
    }
    OptimizeCloseEffect(Result[2]) ;优化菜单关闭效果
    Gosub,% Result[2] ;追求效率将此条放在上条上面
Return

M_MenuStyle:
    If(IsStyle)
    {
        Menu,Tray,Rename,Rect,Circle
        MenuStyle:="Rect"
        IsStyle:=False

    }Else{
        Menu,Tray,Rename,Circle,Rect
        MenuStyle:="Circle"
        IsStyle:=True
    }
Return

M_Toggle:
    If(IsOn) ;关闭
    {
        Menu,Tray,Rename,%M_Close%,%M_Open%
        Menu,Tray,Icon,%A_Desktop%/Icon图集/MidMenu_Close.ico
        IsOn:=False
    }Else{ ;开启
        Menu,Tray,Rename,%M_Open%,%M_Close%
        Menu,Tray,Icon,%A_Desktop%/Icon图集/MidMenu_Open.ico
        IsOn:=True
    }
Return

M_MenuExit:
ExitApp
;=========================Function
CheckMenu() ;通过检测是否将ToolTip激活,以检查是否选择了菜单项
{
    Loop,% AllCharMenu.Count() ;请确保菜单项与Label数量一致
    {
        If (WinActive(AllCharMenu[A_Index]) OR WinActive(SimpleCharMenu[A_Index]))
            Return [True,A_Index] ;返回线性数组,下标一为选择了菜单项,下标二为选择菜单项的下标
        Else
            Continue ;检测下一条
    }
}

CleanMenu() ;清除菜单
{
    Loop, % SimpleCharMenu.Count()
    {
        If (SimpleCharMenu[A_Index]="")
            Continue
        ToolTip,,,,A_Index
        Sleep,20 ;效果优化
    }
}

OptimizeShowEffect() ;优化显示菜单效果
{
    R:=Radius
    MouseGetPos,PosX,PosY ;获取坐标,用于计算Tip的坐标
    If (MenuStyle="Circle") ;以下为不同的菜单形式
    {
        PosX-=18,PosY-=10 ;稍微调整坐标,让光标置于中心
    }
    Loop,% SimpleCharMenu.Count()
    {
        If GetKeyState("RButton")
            Return
        If (SimpleCharMenu[A_Index]="")
            Continue
        If % (AllCharMenu.Count()<=MaxItems) ;总数未超过设定数
        {
            Radians:=(PI/180)*Round(360/AllCharMenu.Count()) ;不变
        }Else ;第二圈
        {
            If (A_Index>MaxItems) ;第二圈时更改半径及等分点
            {
                R:=2*Radius ;在此调节第二圈的半径
                Radians:=(PI/180)*Round(360/(2*MaxItems))
            }
            Else ;第一圈时
                Radians:=(PI/180)*Round(360/MaxItems)
        }
        If (MenuStyle="Circle") ;以下为不同的菜单形式
        {
            X:=PosX+R*Cos(Radians*A_Index)
            Y:=PosY+R*Sin(Radians*A_Index)
        }Else If (MenuStyle="Rect")
        {
            If (A_Index>1)
                PosY+=22
            X:=PosX
            Y:=PosY
        }
        If (TipStyle="SingleChar")
        {
            If % (A_Index=SimpleCharMenu.Count())
            {
                Sleep,30
                PopUpStr(SimpleCharMenu[SimpleCharMenu.Count()],PosX,PosY,SimpleCharMenu.Count())
            }
            Else
                PopUpStr(SimpleCharMenu[A_Index],X,Y,A_Index,15) ;在此调节弹出的速度

        }Else
        {
            If % (A_Index=AllCharMenu.Count())
                PopUpStr(AllCharMenu[AllCharMenu.Count()],PosX,PosY,AllCharMenu.Count())
            Else 
                PopUpStr(AllCharMenu[A_Index],X,Y,A_Index)
        }
    }
}

OptimizeCloseEffect(SelectIndex) ;优化选取效果
{
    Loop, % AllCharMenu.Count()
    {
        If % A_Index=SelectIndex ;跳过选中的菜单项
            Continue
        ToolTip,,,,A_Index
        Sleep,20
    }
    Sleep,% 25*AllCharMenu.Count() ;保留所选取的菜单项到最后
    ToolTip,,,,%SelectIndex%
}

PopUpStr(String,PosX,PosY,Weight,Speed:=0) ;建议保存此函数,在许多脚本中可使用
{
    ClipLenth:=2
    Loop,% StrLen(StrReplace(String, A_Space, "")) ;去除空格并获取长度
    {
        SplitStr:=SubStr(String,1,ClipLenth)
        ToolTip,%SplitStr%,%PosX%,%PosY%,%Weight%
        ClipLenth+=1
        Sleep,8
    }
    Sleep,%Speed%
Return
}

;==============================CustomizeLabel
1:
    Run,G:\笔记
Return

2:
    Run,http://react.xhup.club/search
Return

3:
    Run,https://wyagd001.github.io/zh-cn/docs/AutoHotkey.htm
Return

4:
    Run,https://www.bilibili.com/
Return

5:
    Run,https://www.aconvert.com/cn/icon/jpg-to-ico/
Return

6:
    Run,https://fanyi.youdao.com/
Return

7:
    Run,D:\小说
Return

8:

Return

9:
    Run,https://developer.mozilla.org/zh-CN/
Return

10:

Return

11:

Return

12:
    Run,https://www.huya.com/786724
Return
13:

Return

14:

Return

15:
    Run,https://www.runoob.com/manual/jdk11api/index.html
Return

16:
    Run,https://docs.unity.cn/cn/2020.3/Manual/UnityManual.html
Return

17:
    Run,https://www.runoob.com/
Return

18:
    Run,%A_Startup%
Return

19:
    Run,%A_Desktop%\备忘录.txt
Return

版本二

;拥有更多菜单了!
#SingleInstance, Force

CoordMode,ToolTip,Screen
CoordMode,Mouse,Screen

;此脚本菜单样式为圆形
Global Delay:=20 ;在此定义按住中键唤起菜单所需的时间
Global BaseRadius:=60 ;在此定义父级圆形菜单的半径(子菜单需要在方法调用中修改)
Global MaxItems:=6 ;在此定义第一圈的最大项数,第二圈(最多)则为2*MaxItems项,总项数不超过19。关系为:(0-7]:[3-6],(0-9]:3,(9-13]:4,(13-16]:5,(16-19]:6
Global PI:=3.1415926
Global PosX,PosY ;记录菜单弹出的坐标,使子菜单与父菜单在同一位置激活
;========更换托盘图标相关,分激活与停用两种状态,双击托盘图标以停用脚本
Global IsOn:=True
Global IsStyle:=True
Global M_Open:="Open"
Global M_Close:="Close"
;=============基础菜单
;!同样,键只能使用数字!
;帮助:多字可使用`n换行,此时将半径适当调大可更好选中。父级建议都是四字,子级随意。
Global BaseMenu:={ 1:"API`n手册"
    ,2:"工具`n网站"
    ,3:"视频`n网站"
    ,4:"本地`n文件"
    ,5:"杂项`n网站"
    ,6:"占位`n占位"
,7:"学习`n网站"}

;=============对应的展开菜单
Global ExMenu_1:={ 1:"JAVA`n文档"
    ,2:"UNIT`n手册"
    ,3:"MDN`n文档"
    ,4:"W3C`n文档"
    ,5:"AHK`n文档"
    ,6:"占位"
,7:"占位"}

Global ExMenu_2:={ 1:"正则`n在线"
    ,2:"图标`n转换"
    ,3:"字符`n百科"
    ,4:"Json`n在线"
    ,5:"颜色`n进制"
    ,6:"正则`n菜鸟"
    ,7:"小鹤`n查形"
,8:"有道`n翻译"}

Global ExMenu_3:={ 1:"BILI`nBILI"
    ,2:"虎牙`n直播"
    ,3:"斗鱼`n直播"
    ,4:"占位"
    ,5:"占位"
    ,6:"占位"
,7:"占位"}

Global ExMenu_4:={ 1:"笔记`nFD"
    ,2:"小说`n合集"
    ,3:"自启`nFD"
    ,4:"文档`nFD"
    ,5:"CHM`nFD"
    ,6:"PDF`nFD"
,7:"备忘`n文件"
,8:"AHK`n合集"}

Global ExMenu_5:={ 1:"QQ`n邮箱"
    ,2:"学校`n官网"
    ,3:"占位"
    ,4:"占位"
    ,5:"占位"
    ,6:"占位"
,7:"占位"}

Global ExMenu_7:={ 1:"MDN`n网站"
    ,2:"菜鸟`n网站"
    ,3:"占位"
    ,4:"占位"
    ,5:"占位"
    ,6:"占位"
,7:"占位"}
;     ,8:"占位"
;     ,9:"占位"
;     ,10:"占位"
;     ,11:"占位"
;     ,12:"占位"
;     ,13:"占位"
;     ,14:"占位"
;     ,15:"占位"
;     ,16:"占位"
;     ,17:"占位"
;     ,18:"占位"
; ,19:"占位"}

Menu,Tray,Icon,%A_Desktop%/Icon图集/MidMenu_Open.ico ;自备两个ICO
Menu,Tray,NoStandard ;去除默认菜单项
Menu,Tray,Add,%M_Close%,M_Toggle ;增加切换选项
Menu,Tray,Default,%M_Close% ;将切换选项设为默认
Menu,Tray,Click,2 ;设置双击响应
Menu,Tray,Add,&Exit,M_MenuExit ;添加退出按钮

#If IsOn
~MButton::
    HowLong:=0 ;初始化
    Loop{
        HowLong++ ;计时
        Sleep,10
        If !(GetKeyState("MButton"))
            Return
        ;ToolTip,% HowLong ;显示进度,可删除
    } Until HowLong>=Delay ;达到预定时间
    Send,{MButton} ;在浏览器中可去除中键的副作用
    MouseGetPos,PosX,PosY ;获取坐标,用于计算Tip的坐标
    PosX-=18,PosY-=20 ;稍微调整坐标,让光标置于中心,可能需要自己调整为合适位置
    PopUpMenu(BaseMenu,BaseRadius) ;显示基础菜单
    HotKey,~LButton,MenuClick ;设置单击热键,当单击时进行判断(这是一种方式,另一种见下方)
    HotKey,~LButton,On ;启用热键
Return
#If
;使用标签的方式处理父级菜单
MenuClick:
    HotKey,~LButton,Off ;单击后关闭热键
    Sleep,100 ;等待ToolTip被激活
    Result:=CheckMenu(BaseMenu) ;接受返回的数组
    If Result[1]!=1 ;此次单击是否将菜单项激活
    {
        CleanMenu(BaseMenu) ;未激活则清除菜单并退出
        Return
    }
    OptimizeCloseEffect(Result[2],BaseMenu) ;优化菜单关闭效果
    ; Gosub, % "B_"Result[2] ;追求效率将此条放在上条上面
    ; 讨论在此使用标签还是函数
    ; SelectedSubMenu:="ExMenu_" . Result[2]
    ; ExtendsMenu(SelectedSubMenu) ;真正传入的是字符串
    Switch Result[2] ;在此增加子菜单的入口
    {
        Case 1:ExtendsMenu(ExMenu_1,1,60) ;在此可定义每个子菜单的半径
        Case 2:ExtendsMenu(ExMenu_2,2,60) ;注意正确填写对应的参数
        Case 3:ExtendsMenu(ExMenu_3,3,60)
        Case 4:ExtendsMenu(ExMenu_4,4,60)
        Case 5:ExtendsMenu(ExMenu_5,5,60)
        Case 7:ExtendsMenu(ExMenu_7,7,60)
    }
Return

;弹出菜单
PopUpMenu(Arrays,Radius:=60){
    R:=Radius
    Loop,% Arrays.Count()
    {
        If (Arrays[A_Index]="")
            Continue
        If % (Arrays.Count()<=MaxItems) ;总数未超过设定数
        {
            Radians:=(PI/180)*Round(360/(Arrays.Count()-1))
        }Else ;第二圈
        {
            If (A_Index>MaxItems) ;第二圈时更改半径及等分点
            {
                R:=2*Radius ;在此调节第二圈的半径
                Radians:=(PI/180)*Round(360/(2*MaxItems))
            }
            Else ;第一圈时
                Radians:=(PI/180)*Round(360/MaxItems)
        }
        X:=PosX+R*Cos(Radians*A_Index)
        Y:=PosY+R*Sin(Radians*A_Index)
        If % (A_Index=Arrays.Count())
        {
            Sleep,30
            PopUpStr(Arrays[Arrays.Count()],PosX,PosY,Arrays.Count())
        }
        Else
            PopUpStr(Arrays[A_Index],X,Y,A_Index,15) ;在此调节弹出的速度
    }
}

;优化选取效果
OptimizeCloseEffect(SelectIndex,Arrays,SleepTime:=0)
{
    Loop, % Arrays.Count()
    {
        If % A_Index=SelectIndex ;跳过选中的菜单项
            Continue
        ToolTip,,,,A_Index
        Sleep,% SleepTime
    }
    Sleep,% 25*SleepTime ;保留所选取的菜单项到最后
    ToolTip,,,,%SelectIndex%
}

;弹出字符串,可调节速度
;参数:一、传入字符串,二三、弹出坐标,四、ToolTip的权重(1-20),五、两个ToolTip间隔的速度
PopUpStr(String,PosX,PosY,Weight,Speed:=0)
{
    ClipLenth:=2
    Loop,% StrLen(StrReplace(String, A_Space, "")) ;去除空格并获取长度
    {
        SplitStr:=SubStr(String,1,ClipLenth)
        ToolTip,%SplitStr%,%PosX%,%PosY%,%Weight%
        ClipLenth+=1
        ; Sleep,5 ;在此调节弹出速度
    }
    Sleep,%Speed%
    Return
}

;通过检测是否将ToolTip激活,以检查是否选择了菜单项
CheckMenu(Arrays)
{
    Loop,% Arrays.Count() ;请确保菜单项与Label数量一致
    {
        If (WinActive(Arrays[A_Index]))
            Return [True,A_Index] ;返回线性数组,下标一为选择了菜单项,下标二为选择菜单项的下标
        Else
            Continue ;检测下一条
    }
}

;清除菜单
CleanMenu(Arrays)
{
    Loop, % Arrays.Count()
    {
        If (Arrays[A_Index]="")
            Continue
        ToolTip,,,,A_Index
        Sleep,20 ;效果优化
    }
}

;使用函数的方式处理子菜单的弹出与选中
;参数一传入子菜单名,二为子菜单在父级中的序号,三为子菜单的半径
ExtendsMenu(Arrays,Which,R)
{
    If (Arrays.Count()=0)
        Return
    PopUpMenu(Arrays,R)
    Sleep,200
    KeyWait, LButton,D ;另一中检测选中的方式,不使用热键,而是中止程序等待左键键击
    Result:=CheckMenu(Arrays)
    If Result[1]!=1 ;此次单击是否将菜单项激活
    {
        CleanMenu(Arrays) ;未激活则清除菜单并退出
        Return
    }
    OptimizeCloseEffect(Result[2],Arrays,20)
    ; msgbox,% "Ex_"Which "_"Result[2] ;使用时改为GoSub,并自行添加相应标签,命名
    GoSub,% "Ex_"Which "_"Result[2]
    Return
}

M_Toggle:
    If(IsOn) ;关闭
    {
        Menu,Tray,Rename,%M_Close%,%M_Open%
        Menu,Tray,Icon,%A_Desktop%/Icon图集/MidMenu_Close.ico
        IsOn:=False
    }Else{ ;开启
        Menu,Tray,Rename,%M_Open%,%M_Close%
        Menu,Tray,Icon,%A_Desktop%/Icon图集/MidMenu_Open.ico
        IsOn:=True
    }
Return

M_MenuExit:
ExitApp
;=====================api手册
Ex_1_1:
    Run,https://www.runoob.com/manual/jdk11api/index.html
Return
Ex_1_2:
    Run,https://docs.unity.cn/cn/2020.3/Manual/UnityManual.html
Return
Ex_1_3:
    Run,https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element
Return
Ex_1_4:
    Run,https://www.w3school.com.cn/tags/index.asp
Return
Ex_1_5:
    Run,https://wyagd001.github.io/zh-cn/docs/
Return

;====================工具网站
Ex_2_1:
    Run,https://regexr-cn.com/
Return
Ex_2_2:
    Run,https://www.aconvert.com/cn/icon/jpg-to-ico/
Return
Ex_2_3:
    Run,https://unicode-table.com/cn/
Return
Ex_2_4:

Return
Ex_2_5:
    Run,https://c.runoob.com/front-end/55/
Return
Ex_2_6:
    Run,https://c.runoob.com/front-end/854/
Return
Ex_2_7:
    Run,http://react.xhup.club/search
Return
Ex_2_8:
    Run,https://fanyi.youdao.com/
Return
;====================视频网站
Ex_3_1:
    Run,https://www.bilibili.com/
Return
Ex_3_2:
    Run,https://www.huya.com/786724
Return
Ex_3_3:
    Run,https://www.douyu.com/
Return
;====================本地文件
Ex_4_1:
    Run,G:\笔记
Return
Ex_4_2:
    Run,D:\小说
Return
Ex_4_3:
    Run,%A_Startup%
Return
Ex_4_4:
    Run,%A_MyDocuments%
Return
Ex_4_5:
    Run,G:\CHM
Return
Ex_4_6:
    Run,G:\PDF
Return
Ex_4_7:
    Run,%A_Desktop%\备忘录.txt
Return
Ex_4_8:
    Run,G:\AHK\AHK脚本合集
Return
;====================杂项网站
Ex_5_1:
    Run,https://mail.qq.com/cgi-bin/frame_html?sid=dsj_Hrro6ARguRh7&r=97a7af1edd5c52db38404bf341e107a9&lang=zh
Return
Ex_5_2:
    Run,http://www.gnust.edu.cn/
Return
;====================学习网站
Ex_7_1:
    Run,https://developer.mozilla.org/zh-CN/docs/Web
Return
Ex_7_2:
    Run,https://www.runoob.com/
Return

原理

我太懒,嫌麻烦,就不讲代码了,只说些思路,共两步:
1、根据三角函数确定tooltip生成的坐标,可看下面的示例脚本。

#SingleInstance, Force

CoordMode,ToolTip,Screen

String:=["a","b","c","d","e","f","g","h","i","j","k","l"]

PI:=3.1415926
Radius:=100
Radians:=(PI/180)*Round(360/String.Length())

~LButton::
    MouseGetPos,PosX,PosY
    Loop,% String.Length()
    {
        Y:=PosY+Radius*Cos(radians*A_Index)
        X:=PosX+Radius*Sin(radians*A_Index)
        PopUpStr(String[A_Index],X,Y,A_Index)
    }
Return

PopUpStr(String,PosX,PosY,Weight)
{
    ClipLenth:=2
    Loop,% StrLen(StrReplace(String, A_Space, "")) ;去除空格并获取长度
    {
        SplitStr:=SubStr(String,1,ClipLenth)
        ToolTip,%SplitStr%,%PosX%,%PosY%,%Weight%
        ClipLenth+=1
        Sleep,10
    }
    Sleep,150
Return
}

2、tooltip属于ahk的gui部分,如果没有给tooltip显式使用v选项赋值,则其name默认为tooltip的内容(所以菜单项不能重名)。那么便可使用if winactive判断鼠标点击时具体激活哪个tooltip,再执行对应操作即可。

其他

  • 版本一还定义了另一种菜单样式--矩形:
    image
    实际可能需要调整每个tooltip的y坐标差值。
  • 因为我使用的是100%缩放,所以tooltip很小,不占空间。缩放125%可能显的过大了,所以不一定适合所有人……
  • 如果直接copy是一定会报找不到icon的错误的,需要注释掉menu命令中关于tray的,或在对应路径上准备两个icon。
  • 另外,脚本还有许多细节我没有说,具体的自己尝试吧~