SerialDebugger 采用模块化插件架构,旨在提供一个可扩展的核心平台,允许开发者通过创建和集成独立的功能模块(面板插件)来增强应用程序的能力。本指南详细阐述了插件开发的标准规范、完整的生命周期管理机制以及与主程序高效交互的最佳实践,助力开发者构建高质量、易于维护的插件。
所有外部插件都应作为独立的Python包(包含init.py的目录)存放于主应用程序根目录下的 panel_plugins/ 文件夹内。推荐的结构如下:
project_root/
├── main.py # 主程序入口
├── panel_interface.py # 面板接口定义 (所有插件面板必须继承)
├── plugin_manager.py # 插件管理器实现
└── panel_plugins/ # 外部插件标准存放目录
└── my_data_monitor/ # 示例插件包名 (遵循命名约定)
├── __init__.py # 插件注册入口,必须存在
├── monitor_panel.py # 插件面板核心实现 (文件名可自定义)
├── resources/ # (可选) 存放图标、qss等静态资源
│ └── icons/
│ └── monitor_icon.png
└── README.md # (推荐) 插件说明文档
- 插件包名 (目录名): 全小写,单词间使用下划线分隔 (例如:
data_monitor,advanced_plotter) - 插件模块名 (导入时使用): 例如
panel_plugins.data_monitor - 面板类名: 遵循Python的PascalCase命名规范 (例如
DataMonitorPanel) - PANEL_TYPE_NAME (静态类属性):
- 必须在插件面板类中定义
- 全局唯一的字符串标识符,用于系统内部识别面板类型
- 推荐使用小写字母和下划线 (例如:"data_monitor_panel")
- PANEL_DISPLAY_NAME (静态类属性):
- 必须在插件面板类中定义
- 用户友好的名称,将显示在菜单、插件管理器等UI界面中 (例如:"数据监视器")
所有插件面板必须继承自项目根目录 panel_interface.py 中定义的 PanelInterface 类。
# panel_plugins/example_custom_panel/__init__.py
"""
自定义面板插件包
这个模块提供了一个示例自定义面板的完整实现,包括:
- 面板组件的定义和实现
- 插件注册逻辑
- 元数据管理
"""
from typing import Dict, Any, List, Optional, Union
import logging
# 导入面板组件
try:
from .custom_panel_widget import MyCustomPanelWidget
except ImportError as e:
logging.error(f"无法导入 MyCustomPanelWidget: {e}")
MyCustomPanelWidget = None
# 插件版本和基本信息
PLUGIN_VERSION = "1.0.0"
PLUGIN_NAME = "example_custom_panel"
PLUGIN_DISPLAY_NAME = "我的自定义插件包示例"
PLUGIN_AUTHOR = "您的名字"
# 设置日志记录器
logger = logging.getLogger(__name__)
# 调试模式开关 - 设置为 False 可关闭所有调试输出
DEBUG_MODE = False
def _debug_print(message: str) -> None:
"""
条件调试打印函数
Args:
message: 要打印的调试信息
"""
if DEBUG_MODE:
print(f"DEBUG: {message}")
def register_plugin_panels(plugin_manager) -> bool:
"""
注册插件提供的所有面板类型
Args:
plugin_manager: 插件管理器实例
Returns:
bool: 注册是否成功
"""
try:
_debug_print(f"==> REGISTER_PLUGIN_PANELS CALLED for module: {__name__} <==")
logger.info(f"开始注册插件面板: {__name__}")
# 检查组件是否成功导入
if MyCustomPanelWidget is None:
error_msg = f"面板组件 MyCustomPanelWidget 导入失败,跳过注册"
logger.error(error_msg)
return False
# 验证面板组件的必要属性
if not _validate_panel_widget(MyCustomPanelWidget):
return False
module_full_name = __name__
# 注册面板类型
try:
_debug_print(f"==> Attempting to register panel type: {MyCustomPanelWidget.PANEL_TYPE_NAME} <==")
_debug_print(f"==> Panel display name: {MyCustomPanelWidget.PANEL_DISPLAY_NAME} <==")
_debug_print(f"==> Module name: {module_full_name} <==")
result = plugin_manager.register_panel_type(
MyCustomPanelWidget,
module_name=module_full_name
)
_debug_print(f"==> register_panel_type returned: {result} (type: {type(result)}) <==")
# 处理不同的返回值类型
if result is None:
# 如果返回 None,假设注册成功(某些实现可能不返回值)
_debug_print(f"==> register_panel_type returned None, assuming success <==")
success = True
elif isinstance(result, bool):
success = result
else:
# 如果返回其他类型,转换为布尔值
success = bool(result)
_debug_print(f"==> register_panel_type returned non-boolean: {result}, converted to: {success} <==")
if success:
_debug_print(
f"==> Successfully registered {MyCustomPanelWidget.PANEL_DISPLAY_NAME} from {module_full_name} <==")
logger.info(f"面板类型注册成功: {MyCustomPanelWidget.PANEL_DISPLAY_NAME}")
return True
else:
logger.error(f"面板类型注册失败: {MyCustomPanelWidget.PANEL_DISPLAY_NAME}")
return False
except Exception as reg_error:
logger.exception(f"面板注册过程中发生异常: {str(reg_error)}")
return False
except Exception as e:
error_msg = f"注册插件面板时发生异常: {str(e)}"
logger.exception(error_msg)
return False
def _validate_panel_widget(panel_class) -> bool:
"""
验证面板组件类是否符合接口要求
Args:
panel_class: 要验证的面板类
Returns:
bool: 验证是否通过
"""
try:
_debug_print(f"==> Validating panel class: {panel_class.__name__} <==")
# 检查必要的类属性
required_attrs = ['PANEL_TYPE_NAME', 'PANEL_DISPLAY_NAME']
for attr in required_attrs:
if not hasattr(panel_class, attr):
logger.error(f"面板类验证失败: 缺少属性 {attr}")
return False
# 检查属性值是否为空
value = getattr(panel_class, attr)
_debug_print(f"==> {attr} = '{value}' (type: {type(value)}) <==")
if not value or not isinstance(value, str):
logger.error(f"面板类验证失败: 属性 {attr} 值无效")
return False
# 检查必要的方法
required_methods = ['get_config', 'apply_config', 'get_initial_dock_title']
for method in required_methods:
if not hasattr(panel_class, method):
logger.error(f"面板类验证失败: 缺少方法 {method}")
return False
elif not callable(getattr(panel_class, method)):
logger.error(f"面板类验证失败: {method} 不可调用")
return False
else:
_debug_print(f"==> Method {method} found and callable <==")
# 检查是否继承自正确的基类
try:
# 尝试检查是否有 PanelInterface 基类
mro = panel_class.__mro__
base_class_names = [cls.__name__ for cls in mro]
_debug_print(f"==> Panel class MRO: {base_class_names} <==")
if 'PanelInterface' not in base_class_names:
logger.warning(f"面板类可能没有继承正确的基类: {panel_class.__name__}")
except Exception as e:
_debug_print(f"==> Could not check base class inheritance: {e} <==")
_debug_print(f"==> Panel class {panel_class.__name__} validation passed <==")
logger.debug(f"面板类 {panel_class.__name__} 验证通过")
return True
except Exception as e:
logger.exception("面板类验证异常")
return False
def get_plugin_metadata() -> Dict[str, Any]:
"""
获取插件元数据信息
Returns:
Dict[str, Any]: 包含插件详细信息的字典
"""
# 定义面板类型信息的类型
panel_types_info: List[Dict[str, Any]] = []
metadata: Dict[str, Any] = {
"module_name": __name__,
"plugin_name": PLUGIN_NAME,
"display_name": PLUGIN_DISPLAY_NAME,
"version": PLUGIN_VERSION,
"description": "这是一个自定义面板的完整示例插件,演示了如何创建、注册和管理自定义面板组件。",
"author": PLUGIN_AUTHOR,
"panel_types_info": panel_types_info,
"dependencies": [],
"min_app_version": "1.0.0",
"tags": ["示例", "自定义面板", "教程"]
}
# 动态添加面板类型信息
if MyCustomPanelWidget is not None:
try:
panel_info: Dict[str, Any] = {
"type_name": MyCustomPanelWidget.PANEL_TYPE_NAME,
"display_name": MyCustomPanelWidget.PANEL_DISPLAY_NAME,
"description": "一个功能完整的自定义面板示例,包含配置管理、主题支持和与主窗口的交互功能。",
"features": [
"配置保存和加载",
"主题适配",
"串口状态监控",
"动态标题更新",
"资源自动清理"
],
"version": "1.0.0"
}
panel_types_info.append(panel_info)
except AttributeError as e:
logger.warning(f"获取面板类型信息时出错: {e}")
# 如果面板类属性不完整,提供基本信息
error_panel_info: Dict[str, Any] = {
"type_name": "example_custom_panel",
"display_name": "自定义面板示例",
"description": "面板类型信息不完整",
"error": str(e)
}
panel_types_info.append(error_panel_info)
return metadata
def get_supported_panel_types() -> List[str]:
"""
获取此插件支持的面板类型列表
Returns:
List[str]: 面板类型名称列表
"""
if MyCustomPanelWidget is not None:
try:
return [MyCustomPanelWidget.PANEL_TYPE_NAME]
except AttributeError:
logger.warning("无法获取面板类型名称")
return []
return []
def check_plugin_compatibility(app_version: str) -> Dict[str, Any]:
"""
检查插件与应用程序的兼容性
Args:
app_version: 应用程序版本
Returns:
Dict[str, Any]: 兼容性检查结果
"""
try:
# 简单的版本比较(实际项目中可能需要更复杂的版本解析)
metadata = get_plugin_metadata()
min_version = metadata.get("min_app_version", "1.0.0")
# 这里简化处理,实际应用中应该有更严格的版本比较逻辑
compatible = True # 假设兼容
result = {
"compatible": compatible,
"plugin_version": PLUGIN_VERSION,
"min_app_version": min_version,
"current_app_version": app_version,
"warnings": [],
"errors": []
}
# 检查面板组件是否可用
if MyCustomPanelWidget is None:
result["compatible"] = False
result["errors"].append("面板组件导入失败")
return result
except Exception as e:
logger.exception("兼容性检查失败")
return {
"compatible": False,
"error": str(e),
"plugin_version": PLUGIN_VERSION
}
# 插件卸载清理函数(可选)
def cleanup_plugin() -> None:
"""
插件卸载时的清理工作
"""
try:
logger.info(f"正在清理插件: {PLUGIN_DISPLAY_NAME}")
# 执行必要的清理工作
# 例如:关闭文件、断开连接、清理缓存等
logger.info(f"插件清理完成: {PLUGIN_DISPLAY_NAME}")
except Exception as e:
logger.exception(f"插件清理时发生错误: {e}")
# 模块级别的信息(用于调试和文档)
__version__ = PLUGIN_VERSION
__author__ = PLUGIN_AUTHOR
__description__ = "自定义面板插件示例"
# 导出的公共接口
__all__ = [
'register_plugin_panels',
'get_plugin_metadata',
'get_supported_panel_types',
'check_plugin_compatibility',
'cleanup_plugin',
'MyCustomPanelWidget'
]
# panel_plugins/example_custom_panel/custom_panel_widget.py
from PySide6.QtWidgets import QVBoxLayout, QLabel, QPushButton, QWidget
from PySide6.QtCore import Slot
from typing import Dict, Any, Optional
# 导入 PanelInterface,支持不同的导入路径
try:
from core.panel_interface import PanelInterface
except ImportError:
import sys
from pathlib import Path
project_root = Path(__file__).resolve().parent.parent.parent
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
from core.panel_interface import PanelInterface
class MyCustomPanelWidget(PanelInterface):
"""
自定义面板示例组件
这个组件演示了如何实现一个完整的自定义面板,包括:
- UI初始化
- 配置管理
- 与主窗口交互
- 资源清理
"""
# PanelInterface 必须定义的静态属性
PANEL_TYPE_NAME: str = "example_custom_panel"
PANEL_DISPLAY_NAME: str = "我的自定义面板"
def __init__(self,
panel_id: int,
# SerialDebugger的实际引用在PanelInterface中已经实现,该警告可以忽略
main_window_ref: 'SerialDebugger',
initial_config: Optional[Dict[str, Any]] = None,
parent: Optional[QWidget] = None):
"""
初始化自定义面板
Args:
panel_id: 面板唯一标识符
main_window_ref: 主窗口引用
initial_config: 初始配置数据
parent: 父级组件
"""
super().__init__(panel_id, main_window_ref, initial_config, parent)
# 记录初始化日志
if self.error_logger:
self.error_logger.log_debug(
f"MyCustomPanelWidget (ID: {panel_id}) 开始初始化",
self.PANEL_TYPE_NAME
)
# 初始化组件特定属性
self.custom_setting: str = "初始默认值"
self._click_count: int = 0
# 初始化UI组件引用
self.info_label: Optional[QLabel] = None
self.my_button: Optional[QPushButton] = None
self.config_display_label: Optional[QLabel] = None
# 构建用户界面
self._init_ui()
# 应用初始配置或设置默认状态
if initial_config:
self.apply_config(initial_config)
else:
self._update_ui_state()
# 更新标题
self._update_title()
def _init_ui(self) -> None:
"""构建和初始化面板的用户界面"""
try:
if self.error_logger:
self.error_logger.log_debug(
f"MyCustomPanelWidget (ID: {self.panel_id}) 开始构建UI",
self.PANEL_TYPE_NAME
)
layout = QVBoxLayout(self)
# 创建信息标签
self.info_label = QLabel(f"这是 {self.PANEL_DISPLAY_NAME} (ID: {self.panel_id})")
self.info_label.setWordWrap(True)
layout.addWidget(self.info_label)
# 创建操作按钮
self.my_button = QPushButton("点击我!")
self.my_button.clicked.connect(self._on_my_button_clicked)
layout.addWidget(self.my_button)
# 创建配置显示标签
self.config_display_label = QLabel(f"当前设置: {self.custom_setting}")
self.config_display_label.setWordWrap(True)
layout.addWidget(self.config_display_label)
self.setLayout(layout)
except Exception as e:
if self.error_logger:
self.error_logger.log_error(
f"MyCustomPanelWidget UI初始化失败: {str(e)}",
self.PANEL_TYPE_NAME
)
raise
def _update_ui_state(self) -> None:
"""更新UI状态以反映当前配置"""
if self.config_display_label:
self.config_display_label.setText(f"当前设置: {self.custom_setting}")
if self.info_label:
self.info_label.setText(
f"这是 {self.PANEL_DISPLAY_NAME} (ID: {self.panel_id})\n"
f"点击次数: {self._click_count}"
)
def _update_title(self) -> None:
"""根据内部状态更新停靠窗口标题"""
try:
# 构建新标题,限制长度避免UI过于拥挤
setting_preview = self.custom_setting[:15]
if len(self.custom_setting) > 15:
setting_preview += "..."
new_title = f"{self.PANEL_DISPLAY_NAME} [{self.panel_id}] - {setting_preview}"
self.dock_title_changed.emit(new_title)
except Exception as e:
if self.error_logger:
self.error_logger.log_error(
f"更新面板标题失败: {str(e)}",
self.PANEL_TYPE_NAME
)
@Slot()
def _on_my_button_clicked(self) -> None:
"""处理按钮点击事件"""
try:
if self.error_logger:
self.error_logger.log_info(
f"自定义面板 {self.panel_id} 的按钮被点击",
self.PANEL_TYPE_NAME
)
# 获取当前串口连接状态
port_info = self._get_port_info()
# 更新点击计数和设置
self._click_count += 1
self.custom_setting = f"点击次数: {self._click_count}"
# 更新UI显示
if self.info_label:
self.info_label.setText(f"按钮已点击! 串口: {port_info}")
self._update_ui_state()
self._update_title()
except Exception as e:
if self.error_logger:
self.error_logger.log_error(
f"按钮点击处理失败: {str(e)}",
self.PANEL_TYPE_NAME
)
def _get_port_info(self) -> str:
"""获取当前串口连接信息"""
try:
if (hasattr(self.main_window_ref, 'serial_manager') and
self.main_window_ref.serial_manager.is_connected and
hasattr(self.main_window_ref, 'current_serial_config') and
self.main_window_ref.current_serial_config):
return self.main_window_ref.current_serial_config.port_name or "未知端口"
return "未连接"
except Exception:
return "状态未知"
# === PanelInterface 必须实现的方法 ===
def get_config(self) -> Dict[str, Any]:
"""返回面板当前状态的配置数据"""
return {
"version": "1.0",
"custom_setting": self.custom_setting,
"click_count": self._click_count,
"panel_type": self.PANEL_TYPE_NAME
}
def apply_config(self, config: Dict[str, Any]) -> None:
"""应用配置数据恢复面板状态"""
try:
# 验证配置版本(可选)
config_version = config.get("version", "1.0")
if config_version != "1.0":
if self.error_logger:
self.error_logger.log_warning(
f"配置版本不匹配: {config_version}, 预期: 1.0",
self.PANEL_TYPE_NAME
)
# 应用配置值
self.custom_setting = config.get("custom_setting", "从配置加载的默认值")
self._click_count = config.get("click_count", 0)
# 更新UI状态
self._update_ui_state()
self._update_title()
if self.error_logger:
self.error_logger.log_debug(
f"面板 {self.panel_id} 配置应用成功",
self.PANEL_TYPE_NAME
)
except Exception as e:
if self.error_logger:
self.error_logger.log_error(
f"应用配置失败: {str(e)}",
self.PANEL_TYPE_NAME
)
def get_initial_dock_title(self) -> str:
"""返回面板停靠窗口的初始标题"""
return f"{self.PANEL_DISPLAY_NAME} ({self.panel_id})"
# === PanelInterface 可选实现的方法 ===
def on_panel_added(self) -> None:
"""面板被添加到主窗口后的回调"""
super().on_panel_added()
if self.error_logger:
self.error_logger.log_info(
f"面板 '{self.PANEL_DISPLAY_NAME}' (ID: {self.panel_id}) 已成功添加",
self.PANEL_TYPE_NAME
)
# 可以在这里执行需要主窗口完全初始化后才能进行的操作
# 例如:连接主窗口的特定信号、获取初始数据等
def on_panel_removed(self) -> None:
"""面板即将被移除前的清理回调"""
super().on_panel_removed()
if self.error_logger:
self.error_logger.log_info(
f"面板 '{self.PANEL_DISPLAY_NAME}' (ID: {self.panel_id}) 正在清理资源",
self.PANEL_TYPE_NAME
)
try:
# 断开信号连接(通常Qt会自动处理,但手动断开更安全)
if self.my_button:
self.my_button.clicked.disconnect()
# 清理其他资源
# 例如:停止定时器、关闭文件句柄、断开网络连接等
except Exception as e:
if self.error_logger:
self.error_logger.log_error(
f"资源清理时发生错误: {str(e)}",
self.PANEL_TYPE_NAME
)
def update_theme(self) -> None:
"""主题变化时的更新回调"""
super().update_theme()
if self.error_logger:
self.error_logger.log_debug(
f"面板 '{self.PANEL_DISPLAY_NAME}' (ID: {self.panel_id}) 主题更新",
self.PANEL_TYPE_NAME
)
# 根据主题更新样式
try:
if hasattr(self.main_window_ref, 'theme_manager'):
theme_manager = self.main_window_ref.theme_manager
# 示例:根据主题调整样式
if hasattr(theme_manager, 'is_dark_theme') and theme_manager.is_dark_theme():
# 深色主题样式
if self.info_label:
self.info_label.setStyleSheet("QLabel { color: #ffffff; }")
else:
# 浅色主题样式
if self.info_label:
self.info_label.setStyleSheet("QLabel { color: #000000; }")
except Exception as e:
if self.error_logger:
self.error_logger.log_error(
f"主题更新失败: {str(e)}",
self.PANEL_TYPE_NAME
)3.2 生命周期与回调方法
| 方法/属性 | 类型/签名 | 必须实现 | 说明 |
|---|---|---|---|
| PANEL_TYPE_NAME | str (静态类属性) |
✓ | 全局唯一的内部类型名。 |
| PANEL_DISPLAY_NAME | str (静态类属性) |
✓ | 显示给用户的名称。 |
| init | init(self, panel_id, main_window_ref, initial_config=None, parent=None) | ✓ | 构造函数,必须调用 super().__init__(...)。 |
| _init_ui | _init_ui(self) -> None | ✓ | 构建和初始化面板的用户界面。 |
| get_config | get_config(self) -> Dict[str, Any] | ✓ | 返回面板当前状态的 JSON 可序列化字典。 |
| apply_config | apply_config(self, config: Dict[str, Any]) -> None | ✓ | 从字典加载并应用配置,恢复面板状态。 |