fry-qt6-progress-widget 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fry_qt6_progress_widget/__init__.py +16 -0
- fry_qt6_progress_widget/dock.py +74 -0
- fry_qt6_progress_widget/progress_widget.py +743 -0
- fry_qt6_progress_widget/py.typed +1 -0
- fry_qt6_progress_widget-0.1.0.dist-info/METADATA +126 -0
- fry_qt6_progress_widget-0.1.0.dist-info/RECORD +8 -0
- fry_qt6_progress_widget-0.1.0.dist-info/WHEEL +5 -0
- fry_qt6_progress_widget-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .dock import GeneralTaskProgressDock
|
|
2
|
+
from .progress_widget import GeneralTaskProgressWidget, InfoTypeConfig, ProgressResult
|
|
3
|
+
|
|
4
|
+
__version__ = "0.1.0"
|
|
5
|
+
|
|
6
|
+
TaskProgressWidget = GeneralTaskProgressWidget
|
|
7
|
+
TaskProgressDock = GeneralTaskProgressDock
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"GeneralTaskProgressWidget",
|
|
11
|
+
"GeneralTaskProgressDock",
|
|
12
|
+
"TaskProgressWidget",
|
|
13
|
+
"TaskProgressDock",
|
|
14
|
+
"InfoTypeConfig",
|
|
15
|
+
"ProgressResult",
|
|
16
|
+
]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Mapping, Optional
|
|
4
|
+
|
|
5
|
+
from PyQt6.QtCore import Qt, pyqtSlot
|
|
6
|
+
from PyQt6.QtWidgets import QDockWidget, QWidget
|
|
7
|
+
|
|
8
|
+
from .progress_widget import GeneralTaskProgressWidget, ProgressResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GeneralTaskProgressDock(QDockWidget):
|
|
12
|
+
"""Thin QDockWidget wrapper around GeneralTaskProgressWidget."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
parent: Optional[QWidget] = None,
|
|
17
|
+
*,
|
|
18
|
+
title: str = "任务进度",
|
|
19
|
+
widget: Optional[GeneralTaskProgressWidget] = None,
|
|
20
|
+
allowed_areas: Optional[Qt.DockWidgetArea] = None,
|
|
21
|
+
**widget_kwargs: Any,
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__(title, parent)
|
|
24
|
+
self.progress_widget = widget or GeneralTaskProgressWidget(parent=self, title=title, **widget_kwargs)
|
|
25
|
+
self.setWidget(self.progress_widget)
|
|
26
|
+
self.setAllowedAreas(
|
|
27
|
+
allowed_areas
|
|
28
|
+
or (Qt.DockWidgetArea.LeftDockWidgetArea | Qt.DockWidgetArea.RightDockWidgetArea)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def begin_task(self, *args: Any, **kwargs: Any) -> ProgressResult:
|
|
32
|
+
return self.progress_widget.begin_task(*args, **kwargs)
|
|
33
|
+
|
|
34
|
+
def update_progress(self, *args: Any, **kwargs: Any) -> ProgressResult:
|
|
35
|
+
return self.progress_widget.update_progress(*args, **kwargs)
|
|
36
|
+
|
|
37
|
+
def finish_task(self, *args: Any, **kwargs: Any) -> ProgressResult:
|
|
38
|
+
return self.progress_widget.finish_task(*args, **kwargs)
|
|
39
|
+
|
|
40
|
+
def append_message(self, *args: Any, **kwargs: Any) -> ProgressResult:
|
|
41
|
+
return self.progress_widget.append_message(*args, **kwargs)
|
|
42
|
+
|
|
43
|
+
def clear_info(self) -> ProgressResult:
|
|
44
|
+
return self.progress_widget.clear_info()
|
|
45
|
+
|
|
46
|
+
def set_paused_state(self, is_paused: bool) -> ProgressResult:
|
|
47
|
+
return self.progress_widget.set_paused_state(is_paused)
|
|
48
|
+
|
|
49
|
+
def set_project_filter(self, project_id: Optional[str]) -> ProgressResult:
|
|
50
|
+
return self.progress_widget.set_project_filter(project_id)
|
|
51
|
+
|
|
52
|
+
def get_params_dict(self) -> Dict[str, Any]:
|
|
53
|
+
return self.progress_widget.get_params_dict()
|
|
54
|
+
|
|
55
|
+
def load_params_dict(self, params_dict: Mapping[str, Any]) -> ProgressResult:
|
|
56
|
+
return self.progress_widget.load_params_dict(params_dict)
|
|
57
|
+
|
|
58
|
+
def fry_reload(self, params_dict: Optional[Mapping[str, Any]] = None) -> ProgressResult:
|
|
59
|
+
return self.progress_widget.fry_reload(params_dict)
|
|
60
|
+
|
|
61
|
+
def fry_reset(self) -> ProgressResult:
|
|
62
|
+
return self.progress_widget.fry_reset()
|
|
63
|
+
|
|
64
|
+
@pyqtSlot(str, str)
|
|
65
|
+
def handle_task_begin(self, project_id: str, total_task_name: str) -> ProgressResult:
|
|
66
|
+
return self.progress_widget.handle_task_begin(project_id, total_task_name)
|
|
67
|
+
|
|
68
|
+
@pyqtSlot(dict)
|
|
69
|
+
def handle_task_progress(self, task_data_dict: Dict[str, Any]) -> ProgressResult:
|
|
70
|
+
return self.progress_widget.handle_task_progress(task_data_dict)
|
|
71
|
+
|
|
72
|
+
@pyqtSlot(str)
|
|
73
|
+
def handle_task_finished(self, project_id: str) -> ProgressResult:
|
|
74
|
+
return self.progress_widget.handle_task_finished(project_id)
|
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from html import escape
|
|
6
|
+
import math
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, Callable, ClassVar, Dict, Mapping, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot
|
|
11
|
+
from PyQt6.QtGui import QTextCursor
|
|
12
|
+
from PyQt6.QtWidgets import (
|
|
13
|
+
QFrame,
|
|
14
|
+
QHBoxLayout,
|
|
15
|
+
QLabel,
|
|
16
|
+
QProgressBar,
|
|
17
|
+
QScrollArea,
|
|
18
|
+
QTextEdit,
|
|
19
|
+
QVBoxLayout,
|
|
20
|
+
QWidget,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class ProgressResult:
|
|
26
|
+
"""Small result object returned by public mutating methods."""
|
|
27
|
+
|
|
28
|
+
status: str
|
|
29
|
+
message: str = ""
|
|
30
|
+
data: Optional[Dict[str, Any]] = None
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def ok(self) -> bool:
|
|
34
|
+
return self.status != "failed"
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def success(cls, message: str = "", data: Optional[Dict[str, Any]] = None) -> "ProgressResult":
|
|
38
|
+
return cls(status="success", message=message, data=data)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def info(cls, message: str = "", data: Optional[Dict[str, Any]] = None) -> "ProgressResult":
|
|
42
|
+
return cls(status="info", message=message, data=data)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def warning(cls, message: str = "", data: Optional[Dict[str, Any]] = None) -> "ProgressResult":
|
|
46
|
+
return cls(status="warning", message=message, data=data)
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def failed(cls, message: str = "", data: Optional[Dict[str, Any]] = None) -> "ProgressResult":
|
|
50
|
+
return cls(status="failed", message=message, data=data)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class InfoTypeConfig:
|
|
55
|
+
"""Display colors for detail messages."""
|
|
56
|
+
|
|
57
|
+
color: str
|
|
58
|
+
icon: str
|
|
59
|
+
icon_color: str
|
|
60
|
+
|
|
61
|
+
TYPE_CONFIGS: ClassVar[Dict[str, Tuple[str, str, str]]] = {
|
|
62
|
+
"信息": ("#1F2937", "info-circle", "#4C566A"),
|
|
63
|
+
"成功": ("#2E7D32", "check-circle", "#2E7D32"),
|
|
64
|
+
"失败": ("#C62828", "times-circle", "#C62828"),
|
|
65
|
+
"异常": ("#C62828", "exclamation-triangle", "#C62828"),
|
|
66
|
+
"警告": ("#B45309", "exclamation-circle", "#B45309"),
|
|
67
|
+
"调试": ("#6D28D9", "bug", "#6D28D9"),
|
|
68
|
+
"重要": ("#1D4ED8", "star", "#1D4ED8"),
|
|
69
|
+
"info": ("#1F2937", "info-circle", "#4C566A"),
|
|
70
|
+
"success": ("#2E7D32", "check-circle", "#2E7D32"),
|
|
71
|
+
"error": ("#C62828", "times-circle", "#C62828"),
|
|
72
|
+
"warning": ("#B45309", "exclamation-circle", "#B45309"),
|
|
73
|
+
"debug": ("#6D28D9", "bug", "#6D28D9"),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def get_config(cls, info_type: str) -> "InfoTypeConfig":
|
|
78
|
+
config = cls.TYPE_CONFIGS.get(info_type, cls.TYPE_CONFIGS["信息"])
|
|
79
|
+
return cls(color=config[0], icon=config[1], icon_color=config[2])
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class GeneralTaskProgressWidget(QWidget):
|
|
83
|
+
"""A reusable PyQt6 widget for showing long-running task progress."""
|
|
84
|
+
|
|
85
|
+
taskStarted = pyqtSignal(str, str)
|
|
86
|
+
progressChanged = pyqtSignal(dict)
|
|
87
|
+
taskFinished = pyqtSignal(str)
|
|
88
|
+
messageAppended = pyqtSignal(str, str)
|
|
89
|
+
pausedChanged = pyqtSignal(bool)
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
parent: Optional[QWidget] = None,
|
|
94
|
+
*,
|
|
95
|
+
title: str = "任务进度",
|
|
96
|
+
accepted_project_id: Optional[str] = None,
|
|
97
|
+
auto_show: bool = False,
|
|
98
|
+
max_detail_blocks: int = 500,
|
|
99
|
+
logger: Optional[Callable[[str, str], None]] = None,
|
|
100
|
+
clock: Optional[Callable[[], float]] = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
super().__init__(parent)
|
|
103
|
+
self.title = title
|
|
104
|
+
self.accepted_project_id = accepted_project_id
|
|
105
|
+
self.auto_show = auto_show
|
|
106
|
+
self.max_detail_blocks = max_detail_blocks
|
|
107
|
+
self.logger = logger
|
|
108
|
+
self._clock = clock or time.monotonic
|
|
109
|
+
|
|
110
|
+
self.current_project_id = ""
|
|
111
|
+
self.current_task_name = ""
|
|
112
|
+
self.current_progress = 0
|
|
113
|
+
self.total_progress = 0
|
|
114
|
+
self.is_paused = False
|
|
115
|
+
self._task_time_records: Dict[str, Dict[str, Any]] = {}
|
|
116
|
+
# 滑动窗口大小:用最近这么多个进度样本估算吞吐率(越大越平滑,越小越灵敏)
|
|
117
|
+
self._eta_window_size = 20
|
|
118
|
+
|
|
119
|
+
self.setObjectName("FryTaskProgressWidget")
|
|
120
|
+
self._setup_ui()
|
|
121
|
+
self._apply_styles()
|
|
122
|
+
self.clear_info()
|
|
123
|
+
|
|
124
|
+
def set_project_filter(self, project_id: Optional[str]) -> ProgressResult:
|
|
125
|
+
"""Accept only updates for project_id. Pass None to accept all projects."""
|
|
126
|
+
self.accepted_project_id = project_id
|
|
127
|
+
return ProgressResult.success("项目过滤器已更新")
|
|
128
|
+
|
|
129
|
+
def begin_task(
|
|
130
|
+
self,
|
|
131
|
+
project_id: str = "",
|
|
132
|
+
task_name: str = "任务",
|
|
133
|
+
total: Optional[Any] = None,
|
|
134
|
+
*,
|
|
135
|
+
clear: bool = True,
|
|
136
|
+
) -> ProgressResult:
|
|
137
|
+
"""Start a task and reset the visible progress state."""
|
|
138
|
+
try:
|
|
139
|
+
if not self._should_accept_project(project_id):
|
|
140
|
+
return ProgressResult.info("忽略其他项目的任务开始信号")
|
|
141
|
+
|
|
142
|
+
if clear:
|
|
143
|
+
self.clear_info()
|
|
144
|
+
|
|
145
|
+
self.current_project_id = str(project_id or "")
|
|
146
|
+
self.current_task_name = str(task_name or "任务")
|
|
147
|
+
self.current_progress = 0
|
|
148
|
+
self.total_progress = self._coerce_int(total, 0)
|
|
149
|
+
self.is_paused = False
|
|
150
|
+
self._task_time_records.clear()
|
|
151
|
+
|
|
152
|
+
self.project_id_value.setText(self.current_project_id or "无")
|
|
153
|
+
self.task_name_value.setText(self.current_task_name)
|
|
154
|
+
self._set_status("running", "运行中")
|
|
155
|
+
self._render_progress()
|
|
156
|
+
self._append_detail_info("信息", f"任务开始: {self.current_task_name}", log=False)
|
|
157
|
+
|
|
158
|
+
if self.auto_show:
|
|
159
|
+
self.show()
|
|
160
|
+
|
|
161
|
+
self.taskStarted.emit(self.current_project_id, self.current_task_name)
|
|
162
|
+
self.progressChanged.emit(self.get_params_dict())
|
|
163
|
+
return ProgressResult.success("任务已开始", self.get_params_dict())
|
|
164
|
+
except Exception as exc: # pragma: no cover - defensive Qt boundary
|
|
165
|
+
return ProgressResult.failed(f"任务开始失败: {exc}")
|
|
166
|
+
|
|
167
|
+
def update_progress(
|
|
168
|
+
self,
|
|
169
|
+
current: Optional[Any] = None,
|
|
170
|
+
total: Optional[Any] = None,
|
|
171
|
+
*,
|
|
172
|
+
task_name: Optional[str] = None,
|
|
173
|
+
project_id: Optional[str] = None,
|
|
174
|
+
info_type: str = "信息",
|
|
175
|
+
detail_info: str = "",
|
|
176
|
+
) -> ProgressResult:
|
|
177
|
+
"""Update progress and optionally append one detail message."""
|
|
178
|
+
try:
|
|
179
|
+
resolved_project_id = self.current_project_id if project_id is None else str(project_id or "")
|
|
180
|
+
if not self._should_accept_project(resolved_project_id):
|
|
181
|
+
return ProgressResult.info("忽略其他项目的任务进度")
|
|
182
|
+
|
|
183
|
+
if resolved_project_id:
|
|
184
|
+
self.current_project_id = resolved_project_id
|
|
185
|
+
self.project_id_value.setText(resolved_project_id)
|
|
186
|
+
|
|
187
|
+
if task_name:
|
|
188
|
+
self.current_task_name = str(task_name)
|
|
189
|
+
self.task_name_value.setText(self.current_task_name)
|
|
190
|
+
|
|
191
|
+
if total is not None:
|
|
192
|
+
self.total_progress = self._coerce_int(total, self.total_progress)
|
|
193
|
+
|
|
194
|
+
if current is not None:
|
|
195
|
+
self.current_progress = self._coerce_int(current, self.current_progress)
|
|
196
|
+
|
|
197
|
+
self.current_progress = max(0, self.current_progress)
|
|
198
|
+
self.total_progress = max(0, self.total_progress)
|
|
199
|
+
if self.total_progress:
|
|
200
|
+
self.current_progress = min(self.current_progress, self.total_progress)
|
|
201
|
+
|
|
202
|
+
self._set_status("paused" if self.is_paused else "running", "已暂停" if self.is_paused else "运行中")
|
|
203
|
+
self._render_progress()
|
|
204
|
+
self._update_time_estimate()
|
|
205
|
+
|
|
206
|
+
if detail_info:
|
|
207
|
+
self._append_detail_info(info_type, detail_info)
|
|
208
|
+
|
|
209
|
+
if self.auto_show:
|
|
210
|
+
self.show()
|
|
211
|
+
|
|
212
|
+
self.progressChanged.emit(self.get_params_dict())
|
|
213
|
+
return ProgressResult.success("任务进度已更新", self.get_params_dict())
|
|
214
|
+
except Exception as exc: # pragma: no cover - defensive Qt boundary
|
|
215
|
+
self._log("异常", f"处理任务进度更新失败: {exc}")
|
|
216
|
+
return ProgressResult.failed(f"任务进度更新失败: {exc}")
|
|
217
|
+
|
|
218
|
+
def finish_task(
|
|
219
|
+
self,
|
|
220
|
+
project_id: Optional[str] = None,
|
|
221
|
+
*,
|
|
222
|
+
message: str = "任务已完成",
|
|
223
|
+
hide: bool = False,
|
|
224
|
+
) -> ProgressResult:
|
|
225
|
+
"""Mark the current task as finished."""
|
|
226
|
+
try:
|
|
227
|
+
resolved_project_id = self.current_project_id if project_id is None else str(project_id or "")
|
|
228
|
+
if not self._should_accept_project(resolved_project_id):
|
|
229
|
+
return ProgressResult.info("忽略其他项目的任务完成信号")
|
|
230
|
+
|
|
231
|
+
if self.total_progress > 0:
|
|
232
|
+
self.current_progress = self.total_progress
|
|
233
|
+
|
|
234
|
+
self.is_paused = False
|
|
235
|
+
self._set_status("success", "已完成")
|
|
236
|
+
self._render_progress()
|
|
237
|
+
self._update_time_estimate(force_done=True)
|
|
238
|
+
self._append_detail_info("成功", message)
|
|
239
|
+
self._task_time_records.clear()
|
|
240
|
+
|
|
241
|
+
if hide:
|
|
242
|
+
self.hide()
|
|
243
|
+
|
|
244
|
+
self.taskFinished.emit(resolved_project_id)
|
|
245
|
+
self.progressChanged.emit(self.get_params_dict())
|
|
246
|
+
return ProgressResult.success("任务已完成", self.get_params_dict())
|
|
247
|
+
except Exception as exc: # pragma: no cover - defensive Qt boundary
|
|
248
|
+
return ProgressResult.failed(f"任务完成处理失败: {exc}")
|
|
249
|
+
|
|
250
|
+
def append_message(self, info_type: str, message: str) -> ProgressResult:
|
|
251
|
+
"""Append a message to the detail area."""
|
|
252
|
+
return self._append_detail_info(info_type, message)
|
|
253
|
+
|
|
254
|
+
def clear_info(self) -> ProgressResult:
|
|
255
|
+
"""Reset all visible values."""
|
|
256
|
+
try:
|
|
257
|
+
self.current_project_id = ""
|
|
258
|
+
self.current_task_name = ""
|
|
259
|
+
self.current_progress = 0
|
|
260
|
+
self.total_progress = 0
|
|
261
|
+
self.is_paused = False
|
|
262
|
+
self._task_time_records.clear()
|
|
263
|
+
|
|
264
|
+
self.project_id_value.setText("无")
|
|
265
|
+
self.task_name_value.setText("无")
|
|
266
|
+
self.progress_label.setText("进度: 0 / 0")
|
|
267
|
+
self.progress_percent.setText("0%")
|
|
268
|
+
self.progress_bar.setRange(0, 100)
|
|
269
|
+
self.progress_bar.setValue(0)
|
|
270
|
+
self.progress_bar.setStyleSheet("")
|
|
271
|
+
self.time_estimate_label.setText("")
|
|
272
|
+
self.detail_text.clear()
|
|
273
|
+
self._set_status("idle", "未开始")
|
|
274
|
+
return ProgressResult.success("信息已清空")
|
|
275
|
+
except Exception as exc: # pragma: no cover - defensive Qt boundary
|
|
276
|
+
return ProgressResult.failed(f"清空信息失败: {exc}")
|
|
277
|
+
|
|
278
|
+
def set_paused_state(self, is_paused: bool) -> ProgressResult:
|
|
279
|
+
"""Switch between running and paused visual states."""
|
|
280
|
+
try:
|
|
281
|
+
self.is_paused = bool(is_paused)
|
|
282
|
+
if self.is_paused:
|
|
283
|
+
self._set_status("paused", "已暂停")
|
|
284
|
+
self._set_progress_bar_color("#D97706", "#F59E0B")
|
|
285
|
+
self._append_detail_info("警告", "任务已暂停")
|
|
286
|
+
else:
|
|
287
|
+
self._set_status("running", "运行中")
|
|
288
|
+
self.progress_bar.setStyleSheet("")
|
|
289
|
+
self._append_detail_info("信息", "任务已恢复")
|
|
290
|
+
|
|
291
|
+
self.pausedChanged.emit(self.is_paused)
|
|
292
|
+
self.progressChanged.emit(self.get_params_dict())
|
|
293
|
+
return ProgressResult.success(f"暂停状态已设置为: {self.is_paused}")
|
|
294
|
+
except Exception as exc: # pragma: no cover - defensive Qt boundary
|
|
295
|
+
return ProgressResult.failed(f"设置暂停状态失败: {exc}")
|
|
296
|
+
|
|
297
|
+
def get_params_dict(self) -> Dict[str, Any]:
|
|
298
|
+
"""Return a serializable snapshot of the widget state."""
|
|
299
|
+
percent = self._current_percent()
|
|
300
|
+
return {
|
|
301
|
+
"current_project_id": self.current_project_id,
|
|
302
|
+
"project_id_value": self.project_id_value.text(),
|
|
303
|
+
"task_name_value": self.task_name_value.text(),
|
|
304
|
+
"current_progress": self.current_progress,
|
|
305
|
+
"total_progress": self.total_progress,
|
|
306
|
+
"progress_value": self.progress_bar.value(),
|
|
307
|
+
"progress_percent_num": percent,
|
|
308
|
+
"progress_label": self.progress_label.text(),
|
|
309
|
+
"progress_percent": self.progress_percent.text(),
|
|
310
|
+
"status": self.status_value.text(),
|
|
311
|
+
"is_paused": self.is_paused,
|
|
312
|
+
"time_estimate": self.time_estimate_label.text(),
|
|
313
|
+
"detail_text": self.detail_text.toHtml(),
|
|
314
|
+
"detail_plain_text": self.detail_text.toPlainText(),
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
def load_params_dict(self, params_dict: Mapping[str, Any]) -> ProgressResult:
|
|
318
|
+
"""Restore a snapshot created by get_params_dict."""
|
|
319
|
+
try:
|
|
320
|
+
if not params_dict:
|
|
321
|
+
return ProgressResult.info("参数字典为空,跳过加载")
|
|
322
|
+
|
|
323
|
+
self.current_project_id = str(params_dict.get("current_project_id", self.current_project_id))
|
|
324
|
+
self.current_task_name = str(params_dict.get("task_name_value", self.current_task_name))
|
|
325
|
+
self.current_progress = self._coerce_int(params_dict.get("current_progress"), self.current_progress)
|
|
326
|
+
self.total_progress = self._coerce_int(params_dict.get("total_progress"), self.total_progress)
|
|
327
|
+
self.is_paused = bool(params_dict.get("is_paused", self.is_paused))
|
|
328
|
+
|
|
329
|
+
self.project_id_value.setText(str(params_dict.get("project_id_value", self.current_project_id or "无")))
|
|
330
|
+
self.task_name_value.setText(str(params_dict.get("task_name_value", self.current_task_name or "无")))
|
|
331
|
+
self.progress_label.setText(str(params_dict.get("progress_label", "进度: 0 / 0")))
|
|
332
|
+
self.progress_percent.setText(str(params_dict.get("progress_percent", "0%")))
|
|
333
|
+
self.time_estimate_label.setText(str(params_dict.get("time_estimate", "")))
|
|
334
|
+
|
|
335
|
+
progress_value = self._coerce_int(params_dict.get("progress_value"), 0)
|
|
336
|
+
self.progress_bar.setRange(0, 100)
|
|
337
|
+
self.progress_bar.setValue(max(0, min(progress_value, 100)))
|
|
338
|
+
|
|
339
|
+
if "detail_text" in params_dict:
|
|
340
|
+
self.detail_text.setHtml(str(params_dict["detail_text"]))
|
|
341
|
+
|
|
342
|
+
status_text = str(params_dict.get("status", "已暂停" if self.is_paused else "未开始"))
|
|
343
|
+
self._set_status("paused" if self.is_paused else "idle", status_text)
|
|
344
|
+
return ProgressResult.success("参数字典已加载")
|
|
345
|
+
except Exception as exc: # pragma: no cover - defensive Qt boundary
|
|
346
|
+
return ProgressResult.failed(f"加载参数字典失败: {exc}")
|
|
347
|
+
|
|
348
|
+
def fry_reload(self, params_dict: Optional[Mapping[str, Any]] = None) -> ProgressResult:
|
|
349
|
+
"""Compatibility alias for applications that use fry_reload."""
|
|
350
|
+
if params_dict is None:
|
|
351
|
+
return self.fry_reset()
|
|
352
|
+
return self.load_params_dict(params_dict)
|
|
353
|
+
|
|
354
|
+
def fry_reset(self) -> ProgressResult:
|
|
355
|
+
"""Compatibility alias for resetting the widget."""
|
|
356
|
+
return self.clear_info()
|
|
357
|
+
|
|
358
|
+
@pyqtSlot(str, str)
|
|
359
|
+
def handle_task_begin(self, project_id: str, total_task_name: str) -> ProgressResult:
|
|
360
|
+
"""Qt slot compatible with the original dock implementation."""
|
|
361
|
+
return self.begin_task(project_id=project_id, task_name=total_task_name)
|
|
362
|
+
|
|
363
|
+
@pyqtSlot(dict)
|
|
364
|
+
def handle_task_progress(self, task_data_dict: Dict[str, Any]) -> ProgressResult:
|
|
365
|
+
"""Qt slot accepting the original task progress dictionary keys."""
|
|
366
|
+
task_data_dict = task_data_dict or {}
|
|
367
|
+
project_id = task_data_dict.get("project_id_str", task_data_dict.get("project_id"))
|
|
368
|
+
task_name = task_data_dict.get("task_name_str", task_data_dict.get("task_name"))
|
|
369
|
+
current = task_data_dict.get("now_progress_num", task_data_dict.get("current_progress"))
|
|
370
|
+
total = task_data_dict.get("total_num", task_data_dict.get("total_progress"))
|
|
371
|
+
info_type = task_data_dict.get("info_type", "信息")
|
|
372
|
+
detail_info = task_data_dict.get("detail_info", task_data_dict.get("message", ""))
|
|
373
|
+
return self.update_progress(
|
|
374
|
+
current=current,
|
|
375
|
+
total=total,
|
|
376
|
+
task_name=task_name,
|
|
377
|
+
project_id=project_id,
|
|
378
|
+
info_type=info_type,
|
|
379
|
+
detail_info=detail_info,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
@pyqtSlot(str)
|
|
383
|
+
def handle_task_finished(self, project_id: str) -> ProgressResult:
|
|
384
|
+
"""Qt slot compatible with the original dock implementation."""
|
|
385
|
+
return self.finish_task(project_id=project_id)
|
|
386
|
+
|
|
387
|
+
def _setup_ui(self) -> None:
|
|
388
|
+
self.setMinimumWidth(320)
|
|
389
|
+
|
|
390
|
+
root_layout = QVBoxLayout(self)
|
|
391
|
+
root_layout.setContentsMargins(0, 0, 0, 0)
|
|
392
|
+
root_layout.setSpacing(0)
|
|
393
|
+
|
|
394
|
+
scroll_area = QScrollArea()
|
|
395
|
+
scroll_area.setWidgetResizable(True)
|
|
396
|
+
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
|
397
|
+
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
398
|
+
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
399
|
+
|
|
400
|
+
main_widget = QWidget()
|
|
401
|
+
main_widget.setObjectName("FryProgressContent")
|
|
402
|
+
main_layout = QVBoxLayout(main_widget)
|
|
403
|
+
main_layout.setContentsMargins(12, 12, 12, 12)
|
|
404
|
+
main_layout.setSpacing(10)
|
|
405
|
+
|
|
406
|
+
title_label = QLabel(self.title)
|
|
407
|
+
title_label.setObjectName("FryProgressTitle")
|
|
408
|
+
main_layout.addWidget(title_label)
|
|
409
|
+
|
|
410
|
+
self._create_project_info_section(main_layout)
|
|
411
|
+
self._add_separator(main_layout)
|
|
412
|
+
self._create_task_info_section(main_layout)
|
|
413
|
+
self._add_separator(main_layout)
|
|
414
|
+
self._create_progress_section(main_layout)
|
|
415
|
+
self._add_separator(main_layout)
|
|
416
|
+
self._create_detail_info_section(main_layout)
|
|
417
|
+
|
|
418
|
+
main_layout.addStretch()
|
|
419
|
+
scroll_area.setWidget(main_widget)
|
|
420
|
+
root_layout.addWidget(scroll_area)
|
|
421
|
+
|
|
422
|
+
def _create_project_info_section(self, parent_layout: QVBoxLayout) -> None:
|
|
423
|
+
project_label = QLabel("项目 ID:")
|
|
424
|
+
project_label.setObjectName("FrySectionLabel")
|
|
425
|
+
self.project_id_value = QLabel("无")
|
|
426
|
+
self.project_id_value.setWordWrap(True)
|
|
427
|
+
self.project_id_value.setObjectName("FryValueLabel")
|
|
428
|
+
parent_layout.addWidget(project_label)
|
|
429
|
+
parent_layout.addWidget(self.project_id_value)
|
|
430
|
+
|
|
431
|
+
def _create_task_info_section(self, parent_layout: QVBoxLayout) -> None:
|
|
432
|
+
status_layout = QHBoxLayout()
|
|
433
|
+
status_label = QLabel("状态:")
|
|
434
|
+
status_label.setObjectName("FrySectionLabel")
|
|
435
|
+
self.status_value = QLabel("未开始")
|
|
436
|
+
self.status_value.setObjectName("FryStatusValue")
|
|
437
|
+
status_layout.addWidget(status_label)
|
|
438
|
+
status_layout.addWidget(self.status_value)
|
|
439
|
+
status_layout.addStretch()
|
|
440
|
+
parent_layout.addLayout(status_layout)
|
|
441
|
+
|
|
442
|
+
task_layout = QHBoxLayout()
|
|
443
|
+
task_label = QLabel("任务名称:")
|
|
444
|
+
task_label.setObjectName("FrySectionLabel")
|
|
445
|
+
self.task_name_value = QLabel("无")
|
|
446
|
+
self.task_name_value.setObjectName("FryValueLabel")
|
|
447
|
+
self.task_name_value.setWordWrap(True)
|
|
448
|
+
task_layout.addWidget(task_label)
|
|
449
|
+
task_layout.addWidget(self.task_name_value, 1)
|
|
450
|
+
parent_layout.addLayout(task_layout)
|
|
451
|
+
|
|
452
|
+
def _create_progress_section(self, parent_layout: QVBoxLayout) -> None:
|
|
453
|
+
progress_info_layout = QHBoxLayout()
|
|
454
|
+
self.progress_label = QLabel("进度: 0 / 0")
|
|
455
|
+
self.progress_label.setMinimumWidth(150)
|
|
456
|
+
self.progress_label.setObjectName("FrySectionLabel")
|
|
457
|
+
|
|
458
|
+
self.progress_percent = QLabel("0%")
|
|
459
|
+
self.progress_percent.setObjectName("FryPercentLabel")
|
|
460
|
+
|
|
461
|
+
progress_info_layout.addWidget(self.progress_label)
|
|
462
|
+
progress_info_layout.addStretch()
|
|
463
|
+
progress_info_layout.addWidget(self.progress_percent)
|
|
464
|
+
parent_layout.addLayout(progress_info_layout)
|
|
465
|
+
|
|
466
|
+
self.progress_bar = QProgressBar()
|
|
467
|
+
self.progress_bar.setRange(0, 100)
|
|
468
|
+
self.progress_bar.setValue(0)
|
|
469
|
+
self.progress_bar.setTextVisible(False)
|
|
470
|
+
self.progress_bar.setMinimumHeight(24)
|
|
471
|
+
parent_layout.addWidget(self.progress_bar)
|
|
472
|
+
|
|
473
|
+
self.time_estimate_label = QLabel("")
|
|
474
|
+
self.time_estimate_label.setObjectName("FryTimeEstimate")
|
|
475
|
+
self.time_estimate_label.setWordWrap(True)
|
|
476
|
+
parent_layout.addWidget(self.time_estimate_label)
|
|
477
|
+
|
|
478
|
+
def _create_detail_info_section(self, parent_layout: QVBoxLayout) -> None:
|
|
479
|
+
detail_label = QLabel("详细信息:")
|
|
480
|
+
detail_label.setObjectName("FrySectionLabel")
|
|
481
|
+
parent_layout.addWidget(detail_label)
|
|
482
|
+
|
|
483
|
+
self.detail_text = QTextEdit()
|
|
484
|
+
self.detail_text.setReadOnly(True)
|
|
485
|
+
self.detail_text.setMinimumHeight(150)
|
|
486
|
+
self.detail_text.setMaximumHeight(320)
|
|
487
|
+
self.detail_text.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
|
|
488
|
+
self.detail_text.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
489
|
+
self.detail_text.document().setMaximumBlockCount(max(0, int(self.max_detail_blocks)))
|
|
490
|
+
parent_layout.addWidget(self.detail_text)
|
|
491
|
+
|
|
492
|
+
def _add_separator(self, parent_layout: QVBoxLayout) -> None:
|
|
493
|
+
separator = QFrame()
|
|
494
|
+
separator.setFrameShape(QFrame.Shape.HLine)
|
|
495
|
+
separator.setFrameShadow(QFrame.Shadow.Sunken)
|
|
496
|
+
parent_layout.addWidget(separator)
|
|
497
|
+
|
|
498
|
+
def _apply_styles(self) -> None:
|
|
499
|
+
self.setStyleSheet(
|
|
500
|
+
"""
|
|
501
|
+
QWidget#FryTaskProgressWidget,
|
|
502
|
+
QWidget#FryProgressContent {
|
|
503
|
+
background-color: #F8FAFC;
|
|
504
|
+
color: #111827;
|
|
505
|
+
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
|
|
506
|
+
}
|
|
507
|
+
QLabel#FryProgressTitle {
|
|
508
|
+
color: #111827;
|
|
509
|
+
font-size: 16px;
|
|
510
|
+
font-weight: 700;
|
|
511
|
+
padding-bottom: 2px;
|
|
512
|
+
}
|
|
513
|
+
QLabel#FrySectionLabel {
|
|
514
|
+
color: #4B5563;
|
|
515
|
+
font-size: 12px;
|
|
516
|
+
font-weight: 700;
|
|
517
|
+
}
|
|
518
|
+
QLabel#FryValueLabel {
|
|
519
|
+
color: #111827;
|
|
520
|
+
font-size: 12px;
|
|
521
|
+
padding: 3px 0;
|
|
522
|
+
}
|
|
523
|
+
QLabel#FryPercentLabel {
|
|
524
|
+
color: #1D4ED8;
|
|
525
|
+
font-size: 12px;
|
|
526
|
+
font-weight: 700;
|
|
527
|
+
}
|
|
528
|
+
QLabel#FryTimeEstimate {
|
|
529
|
+
color: #2563EB;
|
|
530
|
+
font-size: 11px;
|
|
531
|
+
font-style: italic;
|
|
532
|
+
}
|
|
533
|
+
QLabel#FryStatusValue {
|
|
534
|
+
border-radius: 4px;
|
|
535
|
+
color: white;
|
|
536
|
+
font-size: 11px;
|
|
537
|
+
font-weight: 700;
|
|
538
|
+
padding: 2px 8px;
|
|
539
|
+
}
|
|
540
|
+
QScrollArea {
|
|
541
|
+
background-color: #F8FAFC;
|
|
542
|
+
border: none;
|
|
543
|
+
}
|
|
544
|
+
QProgressBar {
|
|
545
|
+
background-color: #E5E7EB;
|
|
546
|
+
border: 1px solid #CBD5E1;
|
|
547
|
+
border-radius: 5px;
|
|
548
|
+
text-align: center;
|
|
549
|
+
}
|
|
550
|
+
QProgressBar::chunk {
|
|
551
|
+
background-color: qlineargradient(
|
|
552
|
+
x1:0, y1:0, x2:1, y2:0,
|
|
553
|
+
stop:0 #2563EB,
|
|
554
|
+
stop:1 #38BDF8
|
|
555
|
+
);
|
|
556
|
+
border-radius: 4px;
|
|
557
|
+
}
|
|
558
|
+
QTextEdit {
|
|
559
|
+
background-color: #FFFFFF;
|
|
560
|
+
border: 1px solid #CBD5E1;
|
|
561
|
+
border-radius: 4px;
|
|
562
|
+
color: #111827;
|
|
563
|
+
padding: 5px;
|
|
564
|
+
}
|
|
565
|
+
QFrame {
|
|
566
|
+
color: #E5E7EB;
|
|
567
|
+
}
|
|
568
|
+
"""
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
def _render_progress(self) -> None:
|
|
572
|
+
if self.total_progress <= 0:
|
|
573
|
+
self.progress_bar.setRange(0, 0)
|
|
574
|
+
self.progress_label.setText(f"进度: {self.current_progress} / 未知")
|
|
575
|
+
self.progress_percent.setText("计算中")
|
|
576
|
+
return
|
|
577
|
+
|
|
578
|
+
percent = self._current_percent()
|
|
579
|
+
self.progress_bar.setRange(0, 100)
|
|
580
|
+
self.progress_bar.setValue(int(round(percent)))
|
|
581
|
+
self.progress_label.setText(f"进度: {self.current_progress} / {self.total_progress}")
|
|
582
|
+
self.progress_percent.setText(f"{percent:g}%")
|
|
583
|
+
|
|
584
|
+
def _update_time_estimate(self, *, force_done: bool = False) -> None:
|
|
585
|
+
"""更新预估时间。
|
|
586
|
+
|
|
587
|
+
优化点:剩余时间用「滑动窗口内的最近吞吐率」估算,而非自起点的累计平均。
|
|
588
|
+
当任务速度随时间变化时(前慢后快 / 前快后慢),窗口估计更灵敏、更接近真实。
|
|
589
|
+
窗口不足时自动回退到累计平均。
|
|
590
|
+
"""
|
|
591
|
+
task_key = self.current_task_name or "__default__"
|
|
592
|
+
now = self._clock()
|
|
593
|
+
cur = float(self.current_progress)
|
|
594
|
+
record = self._task_time_records.get(task_key)
|
|
595
|
+
|
|
596
|
+
if record is None:
|
|
597
|
+
self._task_time_records[task_key] = {
|
|
598
|
+
"first_time": now,
|
|
599
|
+
"first_progress": cur,
|
|
600
|
+
"samples": [(now, cur)], # 滑动窗口样本 (时间, 进度)
|
|
601
|
+
}
|
|
602
|
+
self.time_estimate_label.setText("正在计算预估时间...")
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
# 追加样本并裁剪窗口(最多保留最近 self._eta_window_size 个样本)
|
|
606
|
+
samples = record["samples"]
|
|
607
|
+
samples.append((now, cur))
|
|
608
|
+
if len(samples) > self._eta_window_size:
|
|
609
|
+
del samples[: len(samples) - self._eta_window_size]
|
|
610
|
+
|
|
611
|
+
elapsed_total = max(0.0, now - record["first_time"])
|
|
612
|
+
if force_done or (self.total_progress > 0 and self.current_progress >= self.total_progress):
|
|
613
|
+
self.time_estimate_label.setText(f"已完成,用时: {self._format_time(elapsed_total)}")
|
|
614
|
+
return
|
|
615
|
+
|
|
616
|
+
if self.total_progress <= 0:
|
|
617
|
+
self.time_estimate_label.setText("正在计算预估时间...")
|
|
618
|
+
return
|
|
619
|
+
|
|
620
|
+
# 优先用窗口内(最早样本 -> 当前)的速率;不足则回退到累计平均
|
|
621
|
+
win_time0, win_prog0 = samples[0]
|
|
622
|
+
win_elapsed = now - win_time0
|
|
623
|
+
win_diff = cur - win_prog0
|
|
624
|
+
if win_diff > 0 and win_elapsed > 0:
|
|
625
|
+
time_per_unit = win_elapsed / win_diff
|
|
626
|
+
else:
|
|
627
|
+
cum_elapsed = elapsed_total
|
|
628
|
+
cum_diff = cur - record["first_progress"]
|
|
629
|
+
if cum_diff <= 0 or cum_elapsed <= 0:
|
|
630
|
+
self.time_estimate_label.setText("正在计算预估时间...")
|
|
631
|
+
return
|
|
632
|
+
time_per_unit = cum_elapsed / cum_diff
|
|
633
|
+
|
|
634
|
+
remaining_progress = max(0, self.total_progress - self.current_progress)
|
|
635
|
+
remaining_seconds = time_per_unit * remaining_progress
|
|
636
|
+
self.time_estimate_label.setText(
|
|
637
|
+
f"平均耗时: {self._format_time(time_per_unit)}/单位 | 预计剩余: {self._format_time(remaining_seconds)}"
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
def _format_time(self, seconds: float) -> str:
|
|
641
|
+
seconds = max(0.0, float(seconds))
|
|
642
|
+
if seconds < 1:
|
|
643
|
+
return f"{seconds:.2f} 秒"
|
|
644
|
+
if seconds < 60:
|
|
645
|
+
return f"{seconds:.1f} 秒"
|
|
646
|
+
if seconds < 3600:
|
|
647
|
+
return f"{seconds / 60:.1f} 分钟"
|
|
648
|
+
return f"{seconds / 3600:.1f} 小时"
|
|
649
|
+
|
|
650
|
+
def _append_detail_info(self, info_type: str, message: str, *, log: bool = True) -> ProgressResult:
|
|
651
|
+
try:
|
|
652
|
+
info_type = str(info_type or "信息")
|
|
653
|
+
message = str(message or "")
|
|
654
|
+
config = InfoTypeConfig.get_config(info_type)
|
|
655
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
656
|
+
|
|
657
|
+
cursor = self.detail_text.textCursor()
|
|
658
|
+
cursor.movePosition(QTextCursor.MoveOperation.End)
|
|
659
|
+
self.detail_text.setTextCursor(cursor)
|
|
660
|
+
|
|
661
|
+
safe_type = escape(info_type)
|
|
662
|
+
safe_message = escape(message).replace("\n", "<br>")
|
|
663
|
+
html_text = (
|
|
664
|
+
f'<div style="margin: 2px 0;">'
|
|
665
|
+
f'<span style="color: #6B7280;">[{timestamp}]</span> '
|
|
666
|
+
f'<span style="color: {config.color}; font-weight: 600;">[{safe_type}]</span> '
|
|
667
|
+
f'<span style="color: {config.color};">{safe_message}</span>'
|
|
668
|
+
f"</div>"
|
|
669
|
+
)
|
|
670
|
+
self.detail_text.insertHtml(html_text)
|
|
671
|
+
|
|
672
|
+
scrollbar = self.detail_text.verticalScrollBar()
|
|
673
|
+
scrollbar.setValue(scrollbar.maximum())
|
|
674
|
+
|
|
675
|
+
if log:
|
|
676
|
+
self._log(info_type, message)
|
|
677
|
+
|
|
678
|
+
self.messageAppended.emit(info_type, message)
|
|
679
|
+
return ProgressResult.success("详细信息已追加")
|
|
680
|
+
except Exception as exc: # pragma: no cover - defensive Qt boundary
|
|
681
|
+
return ProgressResult.failed(f"追加详细信息失败: {exc}")
|
|
682
|
+
|
|
683
|
+
def _set_status(self, status: str, text: str) -> None:
|
|
684
|
+
colors = {
|
|
685
|
+
"idle": "#64748B",
|
|
686
|
+
"running": "#2563EB",
|
|
687
|
+
"paused": "#D97706",
|
|
688
|
+
"success": "#16A34A",
|
|
689
|
+
"warning": "#B45309",
|
|
690
|
+
"error": "#DC2626",
|
|
691
|
+
}
|
|
692
|
+
color = colors.get(status, colors["idle"])
|
|
693
|
+
self.status_value.setText(text)
|
|
694
|
+
self.status_value.setStyleSheet(f"background-color: {color};")
|
|
695
|
+
|
|
696
|
+
def _set_progress_bar_color(self, start_color: str, end_color: str) -> None:
|
|
697
|
+
self.progress_bar.setStyleSheet(
|
|
698
|
+
f"""
|
|
699
|
+
QProgressBar {{
|
|
700
|
+
background-color: #E5E7EB;
|
|
701
|
+
border: 1px solid #CBD5E1;
|
|
702
|
+
border-radius: 5px;
|
|
703
|
+
text-align: center;
|
|
704
|
+
}}
|
|
705
|
+
QProgressBar::chunk {{
|
|
706
|
+
background-color: qlineargradient(
|
|
707
|
+
x1:0, y1:0, x2:1, y2:0,
|
|
708
|
+
stop:0 {start_color},
|
|
709
|
+
stop:1 {end_color}
|
|
710
|
+
);
|
|
711
|
+
border-radius: 4px;
|
|
712
|
+
}}
|
|
713
|
+
"""
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
def _current_percent(self) -> float:
|
|
717
|
+
if self.total_progress <= 0:
|
|
718
|
+
return 0.0
|
|
719
|
+
percent = self.current_progress * 100.0 / self.total_progress
|
|
720
|
+
if math.isnan(percent) or math.isinf(percent):
|
|
721
|
+
return 0.0
|
|
722
|
+
return round(max(0.0, min(percent, 100.0)), 2)
|
|
723
|
+
|
|
724
|
+
def _should_accept_project(self, project_id: Optional[str]) -> bool:
|
|
725
|
+
if self.accepted_project_id is None:
|
|
726
|
+
return True
|
|
727
|
+
return str(project_id or "") == str(self.accepted_project_id)
|
|
728
|
+
|
|
729
|
+
def _coerce_int(self, value: Optional[Any], default: int = 0) -> int:
|
|
730
|
+
if value is None:
|
|
731
|
+
return int(default)
|
|
732
|
+
try:
|
|
733
|
+
return int(float(value))
|
|
734
|
+
except (TypeError, ValueError):
|
|
735
|
+
return int(default)
|
|
736
|
+
|
|
737
|
+
def _log(self, info_type: str, message: str) -> None:
|
|
738
|
+
if self.logger is None:
|
|
739
|
+
return
|
|
740
|
+
try:
|
|
741
|
+
self.logger(info_type, message)
|
|
742
|
+
except Exception:
|
|
743
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fry-qt6-progress-widget
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A reusable PyQt6 QWidget for displaying task progress, messages, pause state, and time estimates.
|
|
5
|
+
Author: fry
|
|
6
|
+
Project-URL: Homepage, https://gitlab.com/fanrenyi33/fry_python_lib_d150_fry_qt6_progress_widget
|
|
7
|
+
Project-URL: Source, https://gitlab.com/fanrenyi33/fry_python_lib_d150_fry_qt6_progress_widget
|
|
8
|
+
Keywords: pyqt6,qt,progress,widget,task
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: X11 Applications :: Qt
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: PyQt6>=6.5
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: twine>=4.0; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# fry-qt6-progress-widget
|
|
28
|
+
|
|
29
|
+
`fry-qt6-progress-widget` 是一个可复用的 PyQt6 任务进度组件。核心类是 `GeneralTaskProgressWidget`,它继承自 `QWidget`,适合直接放进任意布局;如果你的程序需要 dock,可以使用附带的 `GeneralTaskProgressDock` 薄包装器,或者自己把 widget 放进 `QDockWidget`。
|
|
30
|
+
|
|
31
|
+
## 安装
|
|
32
|
+
|
|
33
|
+
发布到 PyPI 后:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install fry-qt6-progress-widget
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
本地开发安装:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install -e ".[dev]"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 最小用法
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import sys
|
|
49
|
+
|
|
50
|
+
from PyQt6.QtWidgets import QApplication
|
|
51
|
+
from fry_qt6_progress_widget import GeneralTaskProgressWidget
|
|
52
|
+
|
|
53
|
+
app = QApplication(sys.argv)
|
|
54
|
+
|
|
55
|
+
widget = GeneralTaskProgressWidget()
|
|
56
|
+
widget.begin_task(project_id="demo", task_name="导入数据", total=100)
|
|
57
|
+
widget.update_progress(current=30, total=100, detail_info="已完成 30 条")
|
|
58
|
+
widget.show()
|
|
59
|
+
|
|
60
|
+
sys.exit(app.exec())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 字典信号兼容用法
|
|
64
|
+
|
|
65
|
+
如果你已有任务系统会发出字典,可以直接调用 `handle_task_progress`:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
widget.handle_task_progress(
|
|
69
|
+
{
|
|
70
|
+
"project_id_str": "demo",
|
|
71
|
+
"task_name_str": "导入数据",
|
|
72
|
+
"now_progress_num": 30,
|
|
73
|
+
"total_num": 100,
|
|
74
|
+
"info_type": "信息",
|
|
75
|
+
"detail_info": "已完成 30 条",
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Dock 用法
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from PyQt6.QtCore import Qt
|
|
84
|
+
from PyQt6.QtWidgets import QMainWindow
|
|
85
|
+
from fry_qt6_progress_widget import GeneralTaskProgressDock
|
|
86
|
+
|
|
87
|
+
window = QMainWindow()
|
|
88
|
+
dock = GeneralTaskProgressDock(window)
|
|
89
|
+
window.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dock)
|
|
90
|
+
dock.begin_task(project_id="demo", task_name="后台任务", total=10)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 常用 API
|
|
94
|
+
|
|
95
|
+
- `begin_task(project_id="", task_name="任务", total=None)`: 开始任务并重置显示。
|
|
96
|
+
- `update_progress(current=None, total=None, task_name=None, project_id=None, info_type="信息", detail_info="")`: 更新进度和详情消息。
|
|
97
|
+
- `finish_task(project_id=None, message="任务已完成", hide=False)`: 标记任务完成。
|
|
98
|
+
- `set_paused_state(is_paused)`: 切换暂停/恢复状态。
|
|
99
|
+
- `append_message(info_type, message)`: 追加详情日志。
|
|
100
|
+
- `set_project_filter(project_id)`: 只接收指定项目 ID 的进度更新。
|
|
101
|
+
- `get_params_dict()` / `load_params_dict(snapshot)`: 保存和恢复组件状态。
|
|
102
|
+
|
|
103
|
+
## 示例和文档
|
|
104
|
+
|
|
105
|
+
- [examples/widget_demo.py](examples/widget_demo.py): 直接使用 QWidget。
|
|
106
|
+
- [examples/dock_demo.py](examples/dock_demo.py): 放进 QDockWidget。
|
|
107
|
+
- [docs/core-concepts.md](docs/core-concepts.md): 组件核心关键点。
|
|
108
|
+
|
|
109
|
+
## 开发与验证
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pytest
|
|
113
|
+
python -m build
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
构建后会生成 `dist/`,发布前建议检查:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
twine check dist/*
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
真正发布到 PyPI 需要维护者账号和 token:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
twine upload dist/*
|
|
126
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
fry_qt6_progress_widget/__init__.py,sha256=mJ7TE7vi2PqbjYcfJOBYGZIfHkuqnTSrlGGLPhDtCgg,432
|
|
2
|
+
fry_qt6_progress_widget/dock.py,sha256=YHNVJffy9CuzNeVoCsKnlVAHh0sr5NvehFNt2Qyd8mo,3049
|
|
3
|
+
fry_qt6_progress_widget/progress_widget.py,sha256=6eWxSxb8QFHEPDMdQAdwW2uYMGKXqvUjzdr6psVj6GY,31246
|
|
4
|
+
fry_qt6_progress_widget/py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
5
|
+
fry_qt6_progress_widget-0.1.0.dist-info/METADATA,sha256=OsZKNFMJgxwuEhk2BITow4GiZlxS4aHOwFf3ELdN_r0,4032
|
|
6
|
+
fry_qt6_progress_widget-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
fry_qt6_progress_widget-0.1.0.dist-info/top_level.txt,sha256=lh3Q_guArX17kDSWcZjuWLyUGWK5_FjLxhmHtTDsvO4,24
|
|
8
|
+
fry_qt6_progress_widget-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fry_qt6_progress_widget
|