🔹 为啥要自定义右键菜单——场景共鸣

🔹 钩子函数到底是个啥——生活化比喻

🔹 菜单定义的核心代码骨架

🔹 实战:给浏览器卡片右键加上“查词典”

🔹 必知必看的注意事项与奇技淫巧

🧩 第一部分:那个让人抓狂的下午
起因的话,开头已经说过了,还有个需求就是在复习英语卡片时,遇到个生僻词,想查在线词典。
正常操作是:选中文本 → 右键复制 → 切到浏览器 → 粘贴搜索 → 再切回来。重复几次,耐心直接清零了,关键是费时呀!

当时我就想,如果能在Anki里右键直接调接口查词,还能顺手把释义填进字段,该多爽。
前几天咱们写的 FastAPI 自定义本地词典是不是也可以发光发热!

其实Anki插件体系还是很灵活的,关键是把“什么时候触发”和“触发后干什么”串起来。
前者靠钩子,后者靠你写的函数,而右键菜单或顶部菜单只是触发方式之一。

🎯 第二部分:核心原理——钩子就是厨房里的传菜铃
别被“钩子”这词吓到。把它想象成餐厅后厨的传菜铃:
厨师(Anki)做完一道工序,就“叮”地按一下铃(触发钩子),服务员(你的插件)听到后去上菜(执行自定义代码)。

其实就是这么简单,Anki内置了一堆钩子点,比如:

🔹 卡片浏览器加载完菜单时(browser.setupMenus)

🔹 复习界面显示问题前(reviewer.didShowQuestion)

🔹 卡片添加、删除、修改时……

我们定义右键菜单,其实就是告诉Anki:
“嗨,在浏览器右键菜单那个钩子触发时,帮我塞进去一个‘查词典’选项。”这背后对应的钩子叫 browser.setupMenus,

官方文档虽然列出了,但新手容易忽略的是,你必须在这个钩子回调里去调用菜单对象的 addAction,否则菜单项死活不会出现。

还有一个大坑提醒:老版本Anki的钩子命名不太一样,比如 setupEditorButtons 和 browser.setupMenus 容易搞混。
如果你用的 Anki 版本 >= 2.1.55,认准 browser.setupMenus 就行。版本太低的话,赶紧升吧,好多好用的钩子都没有。

再说一点就是,Anki就算升级到了新版本,还是要注意下它里面的有些属性和函数,因为版本差异,就是死活找不到,这时候,你只能去源码里“移至定义”,慢慢找了,结合出错信息提示,会快一些!

⚙️ 第三部分:实战演示——三步给菜单加“查词典”功能
好,咱们直接上代码骨架。假设你已经在插件文件夹里新建了 init.py。

第一步:导入必要的模块

from aqt import mw

from aqt.browser import Browser

from aqt.qt import QAction

from anki.hooks import addHook

import webbrowser

这里 mw 是主窗口对象,各种全局资源都通过它访问。

第二步:定义菜单添加函数,并绑定钩子

def setup_context_menu(browser: Browser):

# 创建一个QAction,这就是菜单项

action = QAction("🔍 一键查词典", browser)

# 当用户点击时,执行查询函数

action.triggered.connect(lambda: lookup_word(browser))

# 把action塞进浏览器菜单项里

browser.form.menuEdit.addAction(action)

addHook(“browser.setupMenus”, setup_context_menu)

注意啊,browser.form.menuEdit 是浏览器顶栏“编辑”菜单的对象。如果你想加到其他菜单项下,要看下源码里的命名,不然会报错,找不到对象。

如果要添加到右键菜单项中,我在卡片浏览器源码里没找到相应钩子,不过我需要的是在笔记编辑界面中添加右键菜单项,这个钩子函数我找到了,
就最下面代码里的editor_will_show_context_menu,使用方式上,和上面有点不同,但意思大差不差!

gui_hooks.editor_will_show_context_menu.append(setup_editor_context_menu)

第三步:实现查词函数

def lookup_word(browser: Browser):

# 获取浏览器当前选中的文本

selected = browser.current_card

if not selected:

    return

# 这里只是示例,实际可从卡片字段取词

word = selected.note()["Front"]

webbrowser.open(f"https://dict.youdao.com/search?q={word}")

这个例子是直接用电脑默认浏览器来查词典。
你完全可以结合 FastAPI 接口,把查询结果写回卡片字段,比如调个API后修改 note()[“Back”] 再 note().flush()。

你可能会问:“那我怎么知道有啥钩子能用?”
最好的办法不是死记硬背,因为它可能因为版本不同,就变了,而是去Anki源码里搜 runHook 或 addHook 等关键词,看它本身是怎么调用的,
或者在插件开发文档里找 Hooks Reference。

另外,用 aqt.utils.showInfo 打日志是最朴素的调试法,插件控制台不弹错误时,全靠它保命。

🔧 第四部分:注意事项与进阶思考
🔹 千万别在主线程里干重活:
像调API、大量IO操作,记得用 mw.taskman.run_in_background 包起来,否则Anki直接卡死给你看。

🔹 菜单对象生命周期:
不要在钩子外面预先创建 QAction 然后反复添加,每次 setupMenus 触发时都可能重建菜单,你只管每次新建并加入,系统会管理销毁,否则容易出重复菜单项。

🔹 多版本兼容:
如果你想让插件同时兼容新旧Anki,可以加个 if hasattr(Browser, ‘form’) 之类的判断,但不如直接声明最低版本要求,省心。

再说个容易翻车的点:修改了代码后,记得重启Anki,网上说的什么“开发者模式”下的“重载插件”功能,我找了半天,我这个版本里也没有找到,还是重启来的干脆,还省了缓存干扰。

💬 来个小结
其实自定义Anki插件就是个搭积木的过程——钩子告诉你“哪里能插”,菜单定义告诉它“长什么样”,你的函数决定“插进去干什么”。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐