🔌
插件开发指南
课堂点名计时器支持通过插件系统扩展功能。本文档将引导你编写、部署和分发自己的插件。
课堂点名计时器 (ClassroomTimer) 支持通过插件系统扩展功能。插件分为三个层级:
| 层级 | 存放位置 | 属性 | 说明 |
| 系统内置 | plugins/__init__.py 中的 BUILTINS 列表 | builtin=True, system=True | 核心功能,不可移除,如时间岛 |
| 捆绑插件 | 项目源码 plugins/*.py | bundled=True | 随发行包分发,显示在"插件市场",用户可开关 |
| 用户插件 | %APPDATA%/ClassroomTimer/plugins/*.py | bundled=False | 用户自行拖放安装,支持热更新 |
创建一个 .py 文件,文件开头用注释声明元数据,然后实现生命周期函数:
# @name: 我的插件
# @icon: 🔧
# @desc: 这是插件的功能描述,会在主界面中展示。
def on_load(app):
"""插件首次加载时调用(应用启动时)。"""
print("插件已加载")
def on_enable(app):
"""用户点击"启用"按钮时调用。"""
print("插件已启用")
def on_disable(app):
"""用户点击"停用"按钮时调用。"""
print("插件已停用")
| 函数签名 | 调用时机 |
on_load(app) | 应用启动时,PluginManager 初始化完成后 |
on_enable(app) | 用户在"工具&插件"页面点击"启用"时 |
on_disable(app) | 用户在"工具&插件"页面点击"停用"时 |
app 参数是 main.py 中的 App 实例,提供以下可用接口:
app._main_win # MainWindow 主窗口实例
app._ball # FloatingBall 浮球实例
app._anno_bar # AnnotationToolbar 批注工具栏
app._anno_canvas # AnnotationCanvas 批注画布(懒加载,可能为 None)
app._whiteboard # WhiteboardCanvas 白板画布(懒加载,可能为 None)
app._time_island # TimeIslandWidget 时间岛(懒加载)
app._island_panel # TimeIslandDetailPanel 时间岛详情面板(懒加载)
app._async_win # AsyncPickWindow 异步抽签窗口
app._pick_flash # PickFlashOverlay 抽签闪光效果
app._fast_mode # bool 快速模式
app._animation_enabled # bool 自动化全局开关
app._plugin_mgr # PluginManager 插件管理器
from utils import config as cfg # 配置读写
from utils import logger as log # 日志输出
from utils.config import get_resource_dir # 获取资源目录
from utils import config as cfg
cfg.get('key', default) # 读取配置
cfg.set('key', value) # 写入配置(自动持久化)
cfg.load() # 返回完整配置字典
cfg.save({'key': 'val'}) # 批量写入
cfg.load_roster('name') # 加载名单
cfg.save_roster('name', list) # 保存名单
from utils import logger as log
log.info('消息')
log.warning('警告')
log.error('错误')
log.debug('调试')
# @name: 计时提醒
# @icon: ⏰
# @desc: 每隔指定分钟数弹出一个托盘提醒,帮助教师把控课堂节奏。
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QSystemTrayIcon
_INTERVAL = 5 * 60 * 1000 # 5分钟
_timer = None
def on_load(app):
"""初始化但不启动。"""
pass
def on_enable(app):
"""启动定时器。"""
global _timer
_timer = QTimer()
_timer.setInterval(_INTERVAL)
_timer.timeout.connect(lambda: _notify(app))
_timer.start()
from utils import logger as log
log.info('计时提醒插件已启动')
def on_disable(app):
"""停止定时器并清理。"""
global _timer
if _timer:
_timer.stop()
_timer = None
def _notify(app):
"""发送托盘通知。"""
tray = None
for w in app._app.topLevelWidgets():
if hasattr(w, '_tray'):
tray = w._tray
break
if tray:
tray.showMessage('计时提醒', '已经过了预设时间,请注意课堂节奏。',
QSystemTrayIcon.Information, 3000)
方式一:作为捆绑插件(随发行包分发)
将 .py 文件放入项目 plugins/ 目录,并在 rollcall.spec 中添加:
hiddenimports 中添加 'plugins.你的文件名'
datas 中已包含 ('plugins', 'plugins'),无需额外修改
应用启动时通过 get_resource_dir() / 'plugins' 扫描加载。
方式二:作为用户插件(拖放安装)
用户将 .py 文件拖放到"工具&插件"页面的虚线拖放区域,系统自动复制到 %APPDATA%/ClassroomTimer/plugins/ 并立即识别显示。
方式三:手动安装
将 .py 文件放入 %APPDATA%/ClassroomTimer/plugins/ 目录,应用会通过 QFileSystemWatcher 在 ~150ms 内自动检测并加载。
用户插件目录由 QFileSystemWatcher 实时监控:
| 操作 | 触发方式 | 效果 |
| 新增 | 拖放或复制 .py 到目录 | plugin_added 信号 → 界面新增卡片 |
| 删除 | 从目录删除 .py | plugin_removed 信号 → 界面移除卡片 |
| 修改 | 覆盖已有文件 | 需重启应用生效(当前版本不热重载代码) |
- 文件名 = 插件 ID:
hello_world.py 的插件 ID 为 hello_world,保持唯一且不与内置插件冲突
- 以下划线开头 (
_foo.py) 的文件会被忽略,不会被加载为插件
- 编码:文件必须以 UTF-8 编码保存
- 不阻塞主线程:
on_load / on_enable / on_disable 在主线程中同步调用,避免在回调中执行耗时操作
- 第三方依赖:如果插件依赖额外库(如
requests),需确保目标环境已安装
- PyQt5 可用:插件运行在 Qt 事件循环中,可直接使用 PyQt5 的
QTimer、信号槽等
- 不存在默认开启:所有插件的
enabled 初始状态为 False,除非在 config.json 中有保存状态
用户对插件的启用/停用状态自动保存在配置中:
| 插件 | 配置字段 |
time_island | config.json 的 island_enabled 字段 |
annotation | config.json 的 annotation_enabled 字段 |
automation | config.json 的 automation_enabled 字段 |
| 其他插件 | config.json 的 plugin_states 字典 |
PluginManager.save_enabled_state() 在每次切换时自动调用,无需手动处理。