Anki插件开发指南:扩展核心功能的完整教程
概述:为什么需要Anki插件?
Anki作为一款强大的间隔重复记忆软件,其核心功能已经非常完善。但每个用户的学习需求都是独特的——你可能需要特殊的卡片模板、自定义的复习算法、与外部工具的集成,或者特定的数据可视化方式。这就是Anki插件(Add-ons)的价值所在。
通过插件开发,你可以:
- 🎯 个性化学习体验:定制符合个人学习习惯的功能
- 🔧 扩展核心功能:添加Anki原生不支持的特性
- 🤖 自动化流程:减少重复性操作,提高学习效率
- 📊 深度数据分析:获取更详细的学习统计和洞察
开发环境准备
基础要求
# 确保已安装Python 3.8+
python --version
# 安装必要的开发工具
pip install black flake8 mypy
项目结构分析
Anki插件采用标准的Python包结构:
插件核心文件详解
1. manifest.json - 插件清单文件
{
"package": "my_awesome_addon",
"name": "我的超强插件",
"mod": 1735714800,
"conflicts": ["conflicting_addon_id"],
"min_point_version": 231001000,
"max_point_version": -231200000,
"human_version": "1.2.3",
"homepage": "https://example.com/addon",
"update_enabled": true
}
字段说明表:
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
package | string | ✅ | 插件文件夹名称,必须唯一 |
name | string | ✅ | 显示给用户的插件名称 |
mod | number | ❌ | 最后修改时间戳 |
conflicts | array | ❌ | 冲突插件ID列表 |
min_point_version | number | ❌ | 支持的最小Anki版本 |
max_point_version | number | ❌ | 支持的最大Anki版本(负值表示不支持更高版本) |
human_version | string | ❌ | 人类可读的版本号 |
homepage | string | ❌ | 插件主页URL |
update_enabled | boolean | ❌ | 是否启用自动更新 |
2. init.py - 插件入口文件
# Copyright: Your Name
# License: GNU AGPL, version 3 or later
from __future__ import annotations
import os
from typing import Any
from anki import hooks
from aqt import gui_hooks, mw
from aqt.qt import QAction, QMenu
from aqt.utils import showInfo, tooltip
# 插件配置管理
def get_config() -> dict[str, Any]:
"""获取插件配置"""
config = mw.addonManager.getConfig(__name__)
if config is None:
# 默认配置
return {
"enable_feature": True,
"max_cards": 100,
"auto_sync": False
}
return config
def update_config(new_config: dict[str, Any]) -> None:
"""更新插件配置"""
mw.addonManager.writeConfig(__name__, new_config)
# GUI钩子示例
def setup_browser_menu() -> None:
"""设置浏览器菜单"""
def on_browser_menu(browser) -> None:
action = QAction("我的插件功能", browser)
action.triggered.connect(lambda: my_browser_function(browser))
browser.form.menuEdit.addAction(action)
gui_hooks.browser_will_show_context_menu.append(on_browser_menu)
def my_browser_function(browser) -> None:
"""自定义浏览器功能"""
selected_notes = browser.selectedNotes()
if selected_notes:
showInfo(f"已选择 {len(selected_notes)} 个笔记")
else:
tooltip("请先选择笔记")
# 主菜单集成
def setup_main_menu() -> None:
"""设置主菜单"""
action = QAction("我的插件", mw)
action.triggered.connect(show_main_dialog)
mw.form.menuTools.addAction(action)
def show_main_dialog() -> None:
"""显示主对话框"""
from .dialog import MainDialog
dialog = MainDialog(mw)
dialog.exec()
# 初始化插件
def init_addon() -> None:
"""初始化插件"""
try:
setup_main_menu()
setup_browser_menu()
# 注册配置更新回调
mw.addonManager.setConfigUpdatedAction(__name__, on_config_updated)
print(f"插件 {__name__} 初始化成功")
except Exception as e:
print(f"插件初始化失败: {e}")
def on_config_updated(config: dict[str, Any]) -> None:
"""配置更新回调"""
print("配置已更新:", config)
# 启动时加载插件
if mw is not None:
init_addon()
else:
print("Anki未启动,插件延迟加载")
钩子系统深度解析
Anki提供了强大的钩子(Hooks)系统,允许插件在特定时刻介入执行流程。
GUI钩子分类
常用钩子示例
# 复习界面钩子
def on_show_question(text: str, card: Card, kind: str) -> str:
"""修改显示的问题文本"""
if "关键内容" in text:
return text + "<br><small>自定义提示</small>"
return text
gui_hooks.reviewer_will_show_question.append(on_show_question)
# 编辑器钩子
def on_editor_did_init(editor: Editor) -> None:
"""编辑器初始化时添加自定义按钮"""
button = editor.addButton("icon.png", "自定义功能", custom_editor_function)
# 保存按钮引用以便后续使用
editor.custom_button = button
gui_hooks.editor_did_init.append(on_editor_did_init)
# 配置文件钩子
def on_profile_did_open() -> None:
"""配置文件加载完成"""
print("用户配置文件已加载")
# 执行初始化操作
gui_hooks.profile_did_open.append(on_profile_did_open)
配置系统开发
配置架构设计
# config.schema.json - 配置验证架构
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"enable_feature": {
"type": "boolean",
"title": "启用功能",
"default": true,
"description": "是否启用主要功能"
},
"max_items": {
"type": "integer",
"title": "最大项目数",
"minimum": 1,
"maximum": 1000,
"default": 100,
"description": "允许处理的最大项目数量"
},
"color_scheme": {
"type": "string",
"title": "颜色方案",
"enum": ["default", "dark", "light", "custom"],
"default": "default",
"description": "选择界面颜色方案"
}
},
"required": ["enable_feature"]
}
# config.md - 配置说明文档
## 插件配置说明
### 基本设置
- **启用功能**: 控制插件的主要功能开关
- **最大项目数**: 限制处理的项目数量,避免性能问题
### 高级设置
- **颜色方案**: 选择适合你偏好的界面样式
### 注意事项
⚠️ 修改配置后需要重启Anki才能完全生效
配置对话框实现
from typing import Any
from aqt.qt import QDialog, QVBoxLayout, QCheckBox, QSpinBox, QComboBox
from aqt.utils import disable_help_button
class ConfigDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle("插件配置")
self.setup_ui()
self.load_config()
disable_help_button(self)
def setup_ui(self) -> None:
layout = QVBoxLayout()
self.enable_checkbox = QCheckBox("启用主要功能")
layout.addWidget(self.enable_checkbox)
self.max_items_spinbox = QSpinBox()
self.max_items_spinbox.setRange(1, 1000)
self.max_items_spinbox.setSuffix(" 个项目")
layout.addWidget(self.max_items_spinbox)
self.color_combo = QComboBox()
self.color_combo.addItems(["default", "dark", "light", "custom"])
layout.addWidget(self.color_combo)
self.setLayout(layout)
def load_config(self) -> None:
config = get_config()
self.enable_checkbox.setChecked(config.get("enable_feature", True))
self.max_items_spinbox.setValue(config.get("max_items", 100))
self.color_combo.setCurrentText(config.get("color_scheme", "default"))
def save_config(self) -> None:
new_config = {
"enable_feature": self.enable_checkbox.isChecked(),
"max_items": self.max_items_spinbox.value(),
"color_scheme": self.color_combo.currentText()
}
update_config(new_config)
高级功能开发
卡片处理流水线
批量操作实现
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List
def batch_process_notes(note_ids: List[int]) -> None:
"""批量处理笔记"""
total = len(note_ids)
processed = 0
with ThreadPoolExecutor(max_workers=4) as executor:
# 提交处理任务
future_to_note = {
executor.submit(process_single_note, note_id): note_id
for note_id in note_ids
}
# 处理完成的任务
for future in as_completed(future_to_note):
note_id = future_to_note[future]
try:
result = future.result()
processed += 1
progress = processed / total * 100
print(f"处理进度: {progress:.1f}%")
except Exception as e:
print(f"处理笔记 {note_id} 时出错: {e}")
def process_single_note(note_id: int) -> None:
"""处理单个笔记"""
note = mw.col.get_note(note_id)
if note:
# 应用自定义处理逻辑
modified = apply_custom_logic(note)
if modified:
mw.col.update_note(note)
return True
return False
调试与测试
调试技巧
import logging
import traceback
# 设置插件专用日志
logger = logging.getLogger("my_addon")
logger.setLevel(logging.DEBUG)
def debug_wrapper(func):
"""调试装饰器"""
def wrapper(*args, **kwargs):
try:
logger.debug(f"调用 {func.__name__} with args: {args}, kwargs: {kwargs}")
result = func(*args, **kwargs)
logger.debug(f"{func.__name__} 返回: {result}")
return result
except Exception as e:
logger.error(f"{func.__name__} 出错: {e}")
logger.error(traceback.format_exc())
raise
return wrapper
# 使用装饰器
@debug_wrapper
def risky_operation(data):
# 可能出错的操作
return data.process()
单元测试框架
import unittest
from unittest.mock import Mock, patch
class TestAddon(unittest.TestCase):
def setUp(self):
"""测试设置"""
self.config = {
"enable_feature": True,
"max_items": 100
}
@patch('aqt.mw')
def test_config_loading(self, mock_mw):
"""测试配置加载"""
mock_mw.addonManager.getConfig.return_value = self.config
result = get_config()
self.assertEqual(result, self.config)
def test_note_processing(self):
"""测试笔记处理逻辑"""
mock_note = Mock()
mock_note.fields = ["字段1", "字段2"]
result = process_single_note(mock_note)
self.assertTrue(result)
self.assertEqual(mock_note.fields[0], "处理后的字段1")
发布与分发
打包脚本
#!/bin/bash
# build_addon.sh
ADDON_NAME="my_awesome_addon"
VERSION="1.0.0"
OUTPUT_FILE="${ADDON_NAME}-${VERSION}.ankiaddon"
# 清理旧文件
rm -f "$OUTPUT_FILE"
# 创建临时目录
TEMP_DIR=$(mktemp -d)
# 复制必要文件
cp -r __init__.py manifest.json config.json config.md config.schema.json src/ "$TEMP_DIR/"
# 创建ZIP包
cd "$TEMP_DIR"
zip -r "$OUTPUT_FILE" .
mv "$OUTPUT_FILE" "$OLDPWD/"
# 清理
cd "$OLDPWD"
rm -rf "$TEMP_DIR"
echo "插件已打包: $OUTPUT_FILE"
版本管理策略
最佳实践与注意事项
性能优化建议
| 场景 | 问题 | 解决方案 |
|---|---|---|
| 批量处理 | 内存占用过高 | 分批次处理,使用生成器 |
| 界面操作 | UI卡顿 | 使用后台线程,QTimer延迟执行 |
| 数据库访问 | 查询缓慢 | 建立索引,批量操作 |
| 网络请求 | 超时失败 | 设置超时时间,重试机制 |
兼容性考虑
def check_compatibility():
"""检查Anki版本兼容性"""
from anki.utils import int_version
current_version = int_version()
required_version = 231001000 # Anki 23.10+
if current_version < required_version:
showWarning("此插件需要Anki 23.10或更高版本")
return False
# 检查其他依赖
try:
import some_required_module
except ImportError:
showWarning("缺少必要依赖: some_required_module")
return False
return True
总结
通过本教程,你已经掌握了Anki插件开发的核心技能:
- 理解了插件架构 - 从manifest配置到钩子系统
- 学会了配置管理 - 灵活的配置系统和用户界面
- 掌握了高级技巧 - 批量处理、多线程、错误处理
- 了解了发布流程 - 从开发到分发的完整流程
记住成功的插件开发关键在于:
- ✅ 充分理解用户需求
- ✅ 遵循Anki的最佳实践
- ✅ 提供清晰的文档和配置
- ✅ 保持良好的兼容性和性能
现在就开始你的第一个Anki插件项目吧!从一个小功能开始,逐步扩展,你会发现为Anki生态贡献自己的力量既有趣又有价值。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



