Skip to content

Commit 5272480

Browse files
committed
refactor: 配置类型返回信号对象,支持插件自定义配置类型
修改: - BaseConfig.create_widget() 返回四个值: (控件, getter, setter, 信号) - 所有预定义配置类型更新返回值 - config_widget.py 使用信号对象连接变化,不再自动检测控件类型 - 修复 ColorConfig/RangeConfig 信号对象被垃圾回收的问题 - 更新 plugin-dev-tutorial.md 文档 优点: - 插件开发者可在自己插件中定义配置类型,无需修改插件管理器代码 - 配置类型完全控制自己的 UI 和信号连接方式
1 parent 97de686 commit 5272480

File tree

14 files changed

+188
-67
lines changed

14 files changed

+188
-67
lines changed

plugin-dev-tutorial.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,7 @@ class DialConfig(BaseConfig[int]):
763763
self.max_value = max_value
764764

765765
def create_widget(self):
766-
"""创建自定义 UI 控件,返回 (控件, 获取值函数, 设置值函数)"""
766+
"""创建自定义 UI 控件,返回 (控件, getter, setter, 信号)"""
767767
widget = QDial()
768768
widget.setRange(self.min_value, self.max_value)
769769
widget.setValue(int(self.default))
@@ -772,7 +772,8 @@ class DialConfig(BaseConfig[int]):
772772
if self.description:
773773
widget.setToolTip(self.description)
774774

775-
return widget, widget.value, widget.setValue
775+
# 返回控件、getter、setter、以及 valueChanged 信号
776+
return widget, widget.value, widget.setValue, widget.valueChanged
776777

777778
def to_storage(self, value: int) -> int:
778779
return int(value)
@@ -791,10 +792,31 @@ class MyConfig(OtherInfoBase):
791792
| 方法/属性 | 说明 |
792793
|-----------|------|
793794
| `widget_type` | 控件类型标识 |
794-
| `create_widget()` | 返回 `(控件, getter, setter)` 三元组 |
795+
| `create_widget()` | 返回 `(控件, getter, setter, 信号)` 四元组 |
795796
| `to_storage(value)` | 将值转换为 JSON 可序列化格式 |
796797
| `from_storage(data)` | 从 JSON 数据恢复值 |
797798

799+
**信号对象说明:**
800+
801+
信号对象可以是:
802+
- Qt 控件的内置信号(如 `widget.valueChanged``widget.textChanged`
803+
- 自定义 `pyqtSignal`(需要通过 QObject 子类定义)
804+
805+
```python
806+
# 方式一:使用控件的内置信号
807+
return widget, widget.value, widget.setValue, widget.valueChanged
808+
809+
# 方式二:自定义信号(复杂控件)
810+
from PyQt5.QtCore import QObject, pyqtSignal
811+
812+
class MySignal(QObject):
813+
changed = pyqtSignal()
814+
815+
signal_emitter = MySignal(parent=container) # parent 防止垃圾回收
816+
# ... 控件变化时调用 signal_emitter.changed.emit()
817+
return container, get_value, set_value, signal_emitter.changed
818+
```
819+
798820
---
799821

800822
## 七、实战:带 GUI 的完整插件示例

src/plugin_manager/config_types/base_config.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing import Any, Callable, ClassVar, Generic, TypeVar
1212

1313
from PyQt5.QtWidgets import QWidget
14+
from PyQt5.QtCore import pyqtSignal, QObject
1415

1516
T = TypeVar("T")
1617

@@ -47,12 +48,15 @@ def __post_init__(self) -> None:
4748
self.label = ""
4849

4950
@abstractmethod
50-
def create_widget(self) -> tuple[QWidget, Callable[[], T], Callable[[T], None]]:
51+
def create_widget(self) -> tuple[QWidget, Callable[[], T], Callable[[T], None], QObject]:
5152
"""
5253
创建 PyQt 控件
5354
5455
Returns:
55-
(控件, 获取值函数, 设置值函数)
56+
(控件, 获取值函数, 设置值函数, 值变化信号对象)
57+
58+
信号对象应该是一个有 connect 方法的 QObject(如 pyqtSignal)。
59+
当值变化时,配置系统会自动连接这个信号来同步值。
5660
"""
5761
pass
5862

src/plugin_manager/config_types/bool_config.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Any, Callable
88

99
from PyQt5.QtWidgets import QCheckBox
10+
from PyQt5.QtCore import QObject
1011

1112
from .base_config import BaseConfig
1213

@@ -26,13 +27,13 @@ def __post_init__(self) -> None:
2627
"""确保默认值是布尔类型"""
2728
self.default = bool(self.default)
2829

29-
def create_widget(self) -> tuple[QCheckBox, Callable[[], bool], Callable[[bool], None]]:
30-
"""创建 QCheckBox 控件"""
30+
def create_widget(self) -> tuple[QCheckBox, Callable[[], bool], Callable[[bool], None], QObject]:
31+
"""创建 QCheckBox 控件,返回 (控件, getter, setter, 信号)"""
3132
widget = QCheckBox()
3233
widget.setChecked(self.default)
3334
if self.description:
3435
widget.setToolTip(self.description)
35-
return widget, widget.isChecked, widget.setChecked
36+
return widget, widget.isChecked, widget.setChecked, widget.stateChanged
3637

3738
def to_storage(self, value: bool) -> bool:
3839
"""转换为存储格式"""

src/plugin_manager/config_types/choice_config.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from dataclasses import dataclass, field
88
from typing import Any, Callable
99

10+
from PyQt5.QtCore import QObject
1011
from PyQt5.QtWidgets import QComboBox
1112

1213
from .base_config import BaseConfig
@@ -39,8 +40,8 @@ def __post_init__(self) -> None:
3940
"""确保默认值是字符串类型"""
4041
self.default = str(self.default)
4142

42-
def create_widget(self) -> tuple[QComboBox, Callable[[], str], Callable[[str], None]]:
43-
"""创建 QComboBox 控件"""
43+
def create_widget(self) -> tuple[QComboBox, Callable[[], str], Callable[[str], None], QObject]:
44+
"""创建 QComboBox 控件,返回 (控件, getter, setter, 信号)"""
4445

4546
widget = QComboBox()
4647
for value, text in self.choices:
@@ -63,7 +64,7 @@ def set_value(value: str) -> None:
6364
if idx >= 0:
6465
widget.setCurrentIndex(idx)
6566

66-
return widget, get_value, set_value
67+
return widget, get_value, set_value, widget.currentIndexChanged
6768

6869
def to_storage(self, value: str) -> str:
6970
"""转换为存储格式"""

src/plugin_manager/config_types/color_config.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@
77
from dataclasses import dataclass
88
from typing import Any, Callable
99

10-
from PyQt5.QtCore import Qt
10+
from PyQt5.QtCore import QObject, pyqtSignal
1111
from PyQt5.QtGui import QColor
1212
from PyQt5.QtWidgets import QPushButton, QColorDialog, QHBoxLayout, QWidget
1313

1414
from .base_config import BaseConfig
1515

1616

17+
class ColorChangeSignal(QObject):
18+
"""颜色变化信号发射器"""
19+
changed = pyqtSignal()
20+
21+
1722
@dataclass
1823
class ColorConfig(BaseConfig[str]):
1924
"""
@@ -37,15 +42,18 @@ def __post_init__(self) -> None:
3742
if not self.default.startswith("#"):
3843
self.default = "#" + self.default
3944

40-
def create_widget(self) -> tuple[QWidget, Callable[[], str], Callable[[str], None]]:
41-
"""创建颜色选择按钮"""
45+
def create_widget(self) -> tuple[QWidget, Callable[[], str], Callable[[str], None], QObject]:
46+
"""创建颜色选择按钮,返回 (控件, getter, setter, 信号)"""
4247

43-
# 创建容器
4448
container = QWidget()
4549
layout = QHBoxLayout(container)
4650
layout.setContentsMargins(0, 0, 0, 0)
4751
layout.setSpacing(4)
4852

53+
# 创建信号发射器,并保存为容器的属性(防止垃圾回收)
54+
signal_emitter = ColorChangeSignal(parent=container)
55+
changed_signal = signal_emitter.changed
56+
4957
# 颜色预览按钮
5058
btn = QPushButton()
5159
btn.setFixedSize(40, 24)
@@ -70,6 +78,7 @@ def on_click():
7078
current_color[0] = color_str
7179
btn.setStyleSheet(f"background-color: {color_str}; border: 1px solid #999;")
7280
text_btn.setText(color_str)
81+
changed_signal.emit()
7382

7483
btn.clicked.connect(on_click)
7584
text_btn.clicked.connect(on_click)
@@ -83,7 +92,7 @@ def set_value(value: str) -> None:
8392
btn.setStyleSheet(f"background-color: {value}; border: 1px solid #999;")
8493
text_btn.setText(value)
8594

86-
return container, get_value, set_value
95+
return container, get_value, set_value, changed_signal
8796

8897
def to_storage(self, value: str) -> str:
8998
"""转换为存储格式"""

src/plugin_manager/config_types/file_config.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from dataclasses import dataclass, field
88
from typing import Any, Callable
99

10+
from PyQt5.QtCore import QObject
1011
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLineEdit, QPushButton, QFileDialog
1112

1213
from .base_config import BaseConfig
@@ -39,8 +40,8 @@ def __post_init__(self) -> None:
3940
"""确保默认值是字符串"""
4041
self.default = str(self.default) if self.default else ""
4142

42-
def create_widget(self) -> tuple[QWidget, Callable[[], str], Callable[[str], None]]:
43-
"""创建文件选择器"""
43+
def create_widget(self) -> tuple[QWidget, Callable[[], str], Callable[[str], None], QObject]:
44+
"""创建文件选择器,返回 (控件, getter, setter, 信号)"""
4445

4546
container = QWidget()
4647
layout = QHBoxLayout(container)
@@ -71,7 +72,7 @@ def on_browse():
7172
layout.addWidget(line_edit, 1)
7273
layout.addWidget(btn)
7374

74-
return container, line_edit.text, line_edit.setText
75+
return container, line_edit.text, line_edit.setText, line_edit.textChanged
7576

7677
def to_storage(self, value: str) -> str:
7778
"""转换为存储格式"""

src/plugin_manager/config_types/float_config.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from dataclasses import dataclass
88
from typing import Any, Callable
99

10+
from PyQt5.QtCore import QObject
1011
from PyQt5.QtWidgets import QDoubleSpinBox
1112

1213
from .base_config import BaseConfig
@@ -44,16 +45,16 @@ def __post_init__(self) -> None:
4445

4546
def create_widget(
4647
self,
47-
) -> tuple[QDoubleSpinBox, Callable[[], float], Callable[[float], None]]:
48-
"""创建 QDoubleSpinBox 控件"""
48+
) -> tuple[QDoubleSpinBox, Callable[[], float], Callable[[float], None], QObject]:
49+
"""创建 QDoubleSpinBox 控件,返回 (控件, getter, setter, 信号)"""
4950
widget = QDoubleSpinBox()
5051
widget.setRange(self.min_value, self.max_value)
5152
widget.setValue(self.default)
5253
widget.setSingleStep(self.step)
5354
widget.setDecimals(self.decimals)
5455
if self.description:
5556
widget.setToolTip(self.description)
56-
return widget, widget.value, widget.setValue
57+
return widget, widget.value, widget.setValue, widget.valueChanged
5758

5859
def to_storage(self, value: float) -> float:
5960
"""转换为存储格式"""

src/plugin_manager/config_types/int_config.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,24 +46,26 @@ def __post_init__(self) -> None:
4646

4747
def create_widget(
4848
self,
49-
) -> tuple[QSpinBox | QSlider, Callable[[], int], Callable[[int], None]]:
50-
"""创建 QSpinBox 或 QSlider 控件"""
49+
) -> tuple[QSpinBox | QSlider, Callable[[], int], Callable[[int], None], QObject]:
50+
"""创建 QSpinBox 或 QSlider 控件,返回 (控件, getter, setter, 信号)"""
51+
from PyQt5.QtCore import QObject
52+
5153
if self.use_slider:
5254
widget = QSlider(Qt.Horizontal)
5355
widget.setRange(self.min_value, self.max_value)
5456
widget.setValue(self.default)
5557
widget.setSingleStep(self.step)
5658
if self.description:
5759
widget.setToolTip(self.description)
58-
return widget, widget.value, widget.setValue
60+
return widget, widget.value, widget.setValue, widget.valueChanged
5961
else:
6062
widget = QSpinBox()
6163
widget.setRange(self.min_value, self.max_value)
6264
widget.setValue(self.default)
6365
widget.setSingleStep(self.step)
6466
if self.description:
6567
widget.setToolTip(self.description)
66-
return widget, widget.value, widget.setValue
68+
return widget, widget.value, widget.setValue, widget.valueChanged
6769

6870
def to_storage(self, value: int) -> int:
6971
"""转换为存储格式"""

src/plugin_manager/config_types/long_text_config.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from dataclasses import dataclass
88
from typing import Any, Callable
99

10+
from PyQt5.QtCore import QObject
1011
from PyQt5.QtWidgets import QTextEdit
1112

1213
from .base_config import BaseConfig
@@ -39,8 +40,8 @@ def __post_init__(self) -> None:
3940
"""确保默认值是字符串"""
4041
self.default = str(self.default) if self.default else ""
4142

42-
def create_widget(self) -> tuple[QTextEdit, Callable[[], str], Callable[[str], None]]:
43-
"""创建多行文本编辑器"""
43+
def create_widget(self) -> tuple[QTextEdit, Callable[[], str], Callable[[str], None], QObject]:
44+
"""创建多行文本编辑器,返回 (控件, getter, setter, 信号)"""
4445
widget = QTextEdit()
4546
widget.setPlainText(str(self.default))
4647
widget.setMaximumHeight(self.max_height)
@@ -51,7 +52,7 @@ def create_widget(self) -> tuple[QTextEdit, Callable[[], str], Callable[[str], N
5152
if self.description:
5253
widget.setToolTip(self.description)
5354

54-
return widget, widget.toPlainText, widget.setPlainText
55+
return widget, widget.toPlainText, widget.setPlainText, widget.textChanged
5556

5657
def to_storage(self, value: str) -> str:
5758
"""转换为存储格式"""

src/plugin_manager/config_types/path_config.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from dataclasses import dataclass
88
from typing import Any, Callable
99

10+
from PyQt5.QtCore import QObject
1011
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLineEdit, QPushButton, QFileDialog
1112

1213
from .base_config import BaseConfig
@@ -34,8 +35,8 @@ def __post_init__(self) -> None:
3435
"""确保默认值是字符串"""
3536
self.default = str(self.default) if self.default else ""
3637

37-
def create_widget(self) -> tuple[QWidget, Callable[[], str], Callable[[str], None]]:
38-
"""创建目录选择器"""
38+
def create_widget(self) -> tuple[QWidget, Callable[[], str], Callable[[str], None], QObject]:
39+
"""创建目录选择器,返回 (控件, getter, setter, 信号)"""
3940

4041
container = QWidget()
4142
layout = QHBoxLayout(container)
@@ -61,7 +62,7 @@ def on_browse():
6162
layout.addWidget(line_edit, 1)
6263
layout.addWidget(btn)
6364

64-
return container, line_edit.text, line_edit.setText
65+
return container, line_edit.text, line_edit.setText, line_edit.textChanged
6566

6667
def to_storage(self, value: str) -> str:
6768
"""转换为存储格式"""

0 commit comments

Comments
 (0)