Skip to content

Commit 97de686

Browse files
committed
feat: 实现插件自定义配置系统
新增功能: - 添加 OtherInfoBase 配置容器基类 - 支持多种配置类型自动生成 UI 控件: - BoolConfig -> QCheckBox - IntConfig -> QSpinBox - FloatConfig -> QDoubleSpinBox - ChoiceConfig -> QComboBox - TextConfig -> QLineEdit - ColorConfig -> 颜色选择器 - FileConfig -> 文件选择器 - PathConfig -> 目录选择器 - LongTextConfig -> QTextEdit - RangeConfig -> 数值范围控件 - 配置自动持久化到 data/plugin_data/<name>/config.json - 配置变化信号 config_changed 支持实时响应 - 设置对话框中显示插件自定义配置 - 支持继承 BaseConfig 创建自定义配置类型 修复: - 修复 WindowMode/LogLevel 继承自 str 的比较问题 - 修复 set_plugin_log_level 的 Level 类型错误 - 修复配置加载时错误使用插件默认值的问题 - 修复窗口模式即时切换功能 文档: - 在 plugin-dev-tutorial.md 中添加完整的配置系统章节 - 更新快速参考卡片和 API 文档
1 parent 26558af commit 97de686

23 files changed

+2097
-27
lines changed

plugin-dev-tutorial.md

Lines changed: 309 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
- [三、理解插件发现机制](#三理解插件发现机制)
1212
- [四、编写第一个插件(Hello World)](#四编写第一个插件hello-world)
1313
- [五、核心 API 详解](#五核心-api-详解)
14-
- [六、实战:带 GUI 的完整插件示例](#六实战带-gui-的完整插件示例)
15-
- [七、VS Code 调试指南](#七vs-code-调试指南)
16-
- [八、常见问题与最佳实践](#八常见问题与最佳实践)
14+
- [六、插件自定义配置系统](#六插件自定义配置系统)
15+
- [七、实战:带 GUI 的完整插件示例](#七实战带-gui-的完整插件示例)
16+
- [八、VS Code 调试指南](#八vs-code-调试指南)
17+
- [九、常见问题与最佳实践](#九常见问题与最佳实践)
1718

1819
---
1920

@@ -344,6 +345,8 @@ class HelloPlugin(BasePlugin):
344345
| `self.log_level` | `LogLevel` | 当前的日志级别 |
345346
| `self.plugin_icon` | `QIcon` | 插件图标 |
346347
| `self.logger` | `loguru.Logger` | **已绑定插件名称的日志器**(直接用!) |
348+
| `self.other_info` | `OtherInfoBase \| None` | 插件自定义配置对象 |
349+
| `self.config_changed` | `pyqtSignal` | 配置变化信号,参数 `(name, value)` |
347350

348351
### 5.2 事件订阅 API
349352

@@ -523,6 +526,7 @@ class PluginInfo:
523526
log_level: LogLevel = "DEBUG" # 默认日志级别
524527
icon: QIcon | None = None # 图标
525528
log_config: LogConfig | None = None # 高级日志配置
529+
other_info: type[OtherInfoBase] | None = None # 👈 自定义配置类
526530
```
527531

528532
**WindowMode 含义:**
@@ -535,7 +539,265 @@ class PluginInfo:
535539

536540
---
537541

538-
## 六、实战:带 GUI 的完整插件示例
542+
## 六、插件自定义配置系统
543+
544+
插件可以定义自己的配置项,这些配置会:
545+
- 自动生成 UI 控件(在设置对话框中)
546+
- 自动持久化到 `data/plugin_data/<plugin_name>/config.json`
547+
- 支持配置变化事件通知
548+
549+
### 6.1 配置类型一览
550+
551+
| 类型 | UI 控件 | 用途示例 |
552+
|------|---------|----------|
553+
| `BoolConfig` | QCheckBox | 开关选项(启用/禁用功能) |
554+
| `IntConfig` | QSpinBox / QSlider | 整数设置(数量、超时时间) |
555+
| `FloatConfig` | QDoubleSpinBox | 浮点数设置(阈值、系数) |
556+
| `ChoiceConfig` | QComboBox | 下拉选择(主题、模式) |
557+
| `TextConfig` | QLineEdit | 文本输入(名称、路径、密码) |
558+
| `ColorConfig` | 颜色按钮 + QColorDialog | 颜色选择(主题颜色) |
559+
| `FileConfig` | QLineEdit + 文件对话框 | 文件路径选择 |
560+
| `PathConfig` | QLineEdit + 目录对话框 | 目录路径选择 |
561+
| `LongTextConfig` | QTextEdit | 多行文本(脚本、描述) |
562+
| `RangeConfig` | 两个 QSpinBox | 数值范围(最小/最大值) |
563+
564+
### 6.2 定义配置类
565+
566+
继承 `OtherInfoBase` 并声明配置字段:
567+
568+
```python
569+
from plugin_manager.config_types import (
570+
OtherInfoBase, BoolConfig, IntConfig, FloatConfig,
571+
ChoiceConfig, TextConfig, ColorConfig, FileConfig,
572+
PathConfig, LongTextConfig, RangeConfig,
573+
)
574+
575+
class MyPluginConfig(OtherInfoBase):
576+
"""我的插件配置"""
577+
578+
# ── 基础类型 ─────────────────────────
579+
enable_auto_save = BoolConfig(
580+
default=True,
581+
label="自动保存",
582+
description="游戏结束后自动保存录像",
583+
)
584+
585+
max_records = IntConfig(
586+
default=100,
587+
label="最大记录数",
588+
min_value=10,
589+
max_value=10000,
590+
step=10,
591+
)
592+
593+
min_rtime = FloatConfig(
594+
default=0.0,
595+
label="最小用时筛选",
596+
min_value=0.0,
597+
max_value=999.0,
598+
decimals=2,
599+
)
600+
601+
theme = ChoiceConfig(
602+
default="dark",
603+
label="主题",
604+
choices=[
605+
("light", "明亮"),
606+
("dark", "暗黑"),
607+
("auto", "跟随系统"),
608+
],
609+
)
610+
611+
player_name = TextConfig(
612+
default="",
613+
label="玩家名称",
614+
placeholder="输入名称...",
615+
)
616+
617+
api_token = TextConfig(
618+
default="",
619+
label="API Token",
620+
password=True, # 密码模式
621+
placeholder="输入密钥...",
622+
)
623+
624+
# ── 高级类型 ─────────────────────────
625+
theme_color = ColorConfig(
626+
default="#1976d2",
627+
label="主题颜色",
628+
)
629+
630+
export_file = FileConfig(
631+
default="",
632+
label="导出文件",
633+
filter="JSON (*.json)", # 文件过滤器
634+
save_mode=True, # 保存文件模式
635+
)
636+
637+
log_directory = PathConfig(
638+
default="",
639+
label="日志目录",
640+
)
641+
642+
description = LongTextConfig(
643+
default="",
644+
label="描述",
645+
placeholder="输入描述...",
646+
max_height=100,
647+
)
648+
649+
time_range = RangeConfig(
650+
default=(0, 300),
651+
label="时间范围(秒)",
652+
min_value=0,
653+
max_value=999,
654+
)
655+
```
656+
657+
### 6.3 绑定配置到插件
658+
659+
`PluginInfo` 中通过 `other_info` 属性绑定:
660+
661+
```python
662+
class MyPlugin(BasePlugin):
663+
664+
@classmethod
665+
def plugin_info(cls) -> PluginInfo:
666+
return PluginInfo(
667+
name="my_plugin",
668+
version="1.0.0",
669+
description="我的插件",
670+
other_info=MyPluginConfig, # 👈 绑定配置类
671+
)
672+
```
673+
674+
### 6.4 访问配置值
675+
676+
```python
677+
class MyPlugin(BasePlugin):
678+
679+
def on_initialized(self):
680+
# 访问配置值
681+
if self.other_info:
682+
max_records = self.other_info.max_records
683+
theme = self.other_info.theme
684+
self.logger.info(f"配置: max_records={max_records}, theme={theme}")
685+
686+
def _handle_event(self, event):
687+
# 使用配置
688+
if self.other_info and self.other_info.enable_auto_save:
689+
self._save_record(event)
690+
```
691+
692+
### 6.5 监听配置变化
693+
694+
```python
695+
class MyPlugin(BasePlugin):
696+
697+
def on_initialized(self):
698+
# 连接配置变化信号
699+
self.config_changed.connect(self._on_config_changed)
700+
701+
def _on_config_changed(self, name: str, value: Any):
702+
"""配置变化时调用(在主线程执行)"""
703+
self.logger.info(f"配置变化: {name} = {value}")
704+
705+
if name == "theme":
706+
self._apply_theme(value)
707+
elif name == "max_records":
708+
self._resize_buffer(value)
709+
```
710+
711+
### 6.6 配置相关属性和方法
712+
713+
| 属性/方法 | 说明 |
714+
|-----------|------|
715+
| `self.other_info` | 配置对象实例(可能为 None) |
716+
| `self.config_changed` | 配置变化信号,参数 `(name, value)` |
717+
| `self.save_config()` | 手动保存配置到文件 |
718+
| `self.other_info.to_dict()` | 导出配置为字典 |
719+
| `self.other_info.from_dict(data)` | 从字典加载配置 |
720+
| `self.other_info.reset_to_defaults()` | 重置为默认值 |
721+
722+
### 6.7 配置存储位置
723+
724+
配置自动保存到:
725+
```
726+
data/plugin_data/<plugin_name>/config.json
727+
```
728+
729+
示例:
730+
```json
731+
{
732+
"enable_auto_save": true,
733+
"max_records": 100,
734+
"theme": "dark",
735+
"player_name": "Player1",
736+
"theme_color": "#1976d2"
737+
}
738+
```
739+
740+
### 6.8 自定义配置类型
741+
742+
如果预定义的配置类型不满足需求,可以继承 `BaseConfig` 创建自定义类型:
743+
744+
```python
745+
from plugin_manager.config_types.base_config import BaseConfig
746+
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QDial
747+
from PyQt5.QtCore import Qt
748+
749+
class DialConfig(BaseConfig[int]):
750+
"""旋钮配置 → QDial 控件"""
751+
widget_type = "dial"
752+
753+
def __init__(
754+
self,
755+
default: int = 0,
756+
label: str = "",
757+
min_value: int = 0,
758+
max_value: int = 100,
759+
**kwargs,
760+
):
761+
super().__init__(default, label, **kwargs)
762+
self.min_value = min_value
763+
self.max_value = max_value
764+
765+
def create_widget(self):
766+
"""创建自定义 UI 控件,返回 (控件, 获取值函数, 设置值函数)"""
767+
widget = QDial()
768+
widget.setRange(self.min_value, self.max_value)
769+
widget.setValue(int(self.default))
770+
widget.setNotchesVisible(True)
771+
772+
if self.description:
773+
widget.setToolTip(self.description)
774+
775+
return widget, widget.value, widget.setValue
776+
777+
def to_storage(self, value: int) -> int:
778+
return int(value)
779+
780+
def from_storage(self, data) -> int:
781+
return int(data)
782+
783+
# 使用自定义配置类型
784+
class MyConfig(OtherInfoBase):
785+
volume = DialConfig(50, "音量", min_value=0, max_value=100)
786+
sensitivity = DialConfig(5, "灵敏度", min_value=1, max_value=10)
787+
```
788+
789+
**自定义配置类型要点:**
790+
791+
| 方法/属性 | 说明 |
792+
|-----------|------|
793+
| `widget_type` | 控件类型标识 |
794+
| `create_widget()` | 返回 `(控件, getter, setter)` 三元组 |
795+
| `to_storage(value)` | 将值转换为 JSON 可序列化格式 |
796+
| `from_storage(data)` | 从 JSON 数据恢复值 |
797+
798+
---
799+
800+
## 七、实战:带 GUI 的完整插件示例
539801

540802
下面是一个更完整的示例——**实时统计面板插件**,展示计数器、表格等常见 UI 元素的用法:
541803

@@ -712,7 +974,7 @@ class StatsPlugin(BasePlugin):
712974
```
713975
---
714976

715-
## 、VS Code 调试指南
977+
## 、VS Code 调试指南
716978

717979
### 最简开发方式(推荐)
718980

@@ -749,7 +1011,7 @@ code <安装目录>
7491011

7501012
---
7511013

752-
## 、常见问题与最佳实践
1014+
## 、常见问题与最佳实践
7531015

7541016
### Q1: 我的插件为什么没有被加载?
7551017

@@ -775,6 +1037,33 @@ code <安装目录>
7751037

7761038
### Q3: 如何存储插件的持久化数据?
7771039

1040+
**方式一:使用配置系统(推荐)**
1041+
1042+
定义配置类并绑定到插件,配置会自动保存和加载:
1043+
1044+
```python
1045+
class MyConfig(OtherInfoBase):
1046+
setting1 = BoolConfig(True, "设置1")
1047+
setting2 = IntConfig(100, "设置2")
1048+
1049+
class MyPlugin(BasePlugin):
1050+
@classmethod
1051+
def plugin_info(cls) -> PluginInfo:
1052+
return PluginInfo(name="my_plugin", other_info=MyConfig)
1053+
1054+
def on_initialized(self):
1055+
# 访问配置
1056+
if self.other_info:
1057+
value = self.other_info.setting1
1058+
1059+
def on_shutdown(self):
1060+
# 配置在设置对话框确认时自动保存
1061+
# 也可以手动保存
1062+
self.save_config()
1063+
```
1064+
1065+
**方式二:使用 self.data_dir**
1066+
7781067
使用 `self.data_dir` —— 它指向 `<exe_dir>/data/plugin_data/<PluginClassName>/`
7791068

7801069
```python
@@ -911,9 +1200,15 @@ class ConsumerPlugin(BasePlugin):
9111200
# ═══ 最小可行插件模板 ═══
9121201

9131202
from plugin_manager import BasePlugin, PluginInfo, make_plugin_icon, WindowMode
1203+
from plugin_manager.config_types import OtherInfoBase, BoolConfig, IntConfig # 可选
9141204
from shared_types.events import VideoSaveEvent # 按需导入
9151205
from shared_types.services.my_service import MyService # 服务接口(可选)
9161206

1207+
# ═══ 配置类定义(可选) ═══
1208+
class MyConfig(OtherInfoBase):
1209+
enable_feature = BoolConfig(True, "启用功能")
1210+
max_count = IntConfig(100, "最大数量", min_value=1, max_value=1000)
1211+
9171212
class MyPlugin(BasePlugin):
9181213

9191214
@classmethod
@@ -923,6 +1218,7 @@ class MyPlugin(BasePlugin):
9231218
description="插件描述",
9241219
window_mode=WindowMode.TAB, # TAB / DETACHED / CLOSED
9251220
icon=make_plugin_icon("#1976D2", "M"),
1221+
other_info=MyConfig, # 👈 绑定配置类(可选)
9261222
)
9271223

9281224
def _setup_subscriptions(self) -> None:
@@ -940,6 +1236,13 @@ class MyPlugin(BasePlugin):
9401236
# 获取服务代理(如果是服务使用者)
9411237
# if self.has_service(MyService):
9421238
# self._service = self.get_service_proxy(MyService)
1239+
1240+
# 连接配置变化信号(可选)
1241+
# self.config_changed.connect(self._on_config_changed)
1242+
1243+
# 访问配置值(可选)
1244+
# if self.other_info:
1245+
# max_count = self.other_info.max_count
9431246

9441247
def on_shutdown(self): # 可选:资源清理
9451248
pass

0 commit comments

Comments
 (0)