from __future__ import annotations from collections.abc import Callable from typing import Any from PySide6.QtCore import QModelIndex, QObject, QRunnable, QSize, Qt, QThreadPool, QTimer, Signal, Slot from PySide6.QtGui import QCloseEvent, QColor, QFont, QGuiApplication, QIcon, QPainter, QPixmap from PySide6.QtWidgets import ( QAbstractItemView, QApplication, QCheckBox, QFrame, QHBoxLayout, QHeaderView, QLabel, QLineEdit, QMainWindow, QMessageBox, QProgressBar, QPushButton, QScrollArea, QSizePolicy, QSplitter, QStackedWidget, QStyle, QTableView, QTextEdit, QVBoxLayout, QWidget, ) from processdock.models import ( PROCESS_ROLE, ActionButtonDelegate, ProcessFilterProxyModel, ProcessTableModel, ) from processdock.services import ActionResult, ProcessDockError, ProcessService from processdock.styles import APP_STYLE class WorkerSignals(QObject): result = Signal(object) error = Signal(str) finished = Signal() class FunctionWorker(QRunnable): def __init__(self, function: Callable[[], Any]) -> None: super().__init__() self.function = function self.signals = WorkerSignals() @Slot() def run(self) -> None: try: result = self.function() except Exception as error: # delivered to the GUI thread self.signals.error.emit(str(error) or error.__class__.__name__) else: self.signals.result.emit(result) finally: self.signals.finished.emit() class SummaryCard(QFrame): def __init__(self, icon: str, title: str, value: str, subtitle: str, parent=None) -> None: super().__init__(parent) self.setObjectName("SummaryCard") self.setMinimumHeight(92) layout = QHBoxLayout(self) layout.setContentsMargins(16, 13, 16, 13) layout.setSpacing(12) icon_label = QLabel(icon) icon_label.setObjectName("CardIcon") icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(icon_label) text_layout = QVBoxLayout() text_layout.setSpacing(1) title_label = QLabel(title) title_label.setObjectName("CardTitle") self.value_label = QLabel(value) self.value_label.setObjectName("CardValue") self.subtitle_label = QLabel(subtitle) self.subtitle_label.setObjectName("CardSubtitle") text_layout.addWidget(title_label) text_layout.addWidget(self.value_label) text_layout.addWidget(self.subtitle_label) layout.addLayout(text_layout, 1) def update_value(self, value: str, subtitle: str | None = None) -> None: self.value_label.setText(value) if subtitle is not None: self.subtitle_label.setText(subtitle) class DetailRow(QWidget): def __init__(self, key: str, parent=None) -> None: super().__init__(parent) layout = QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(12) key_label = QLabel(key) key_label.setObjectName("DetailKey") key_label.setFixedWidth(76) key_label.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft) self.value_label = QLabel("—") self.value_label.setObjectName("DetailValue") self.value_label.setTextFormat(Qt.TextFormat.PlainText) self.value_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) self.value_label.setWordWrap(True) layout.addWidget(key_label) layout.addWidget(self.value_label, 1) def set_value(self, value: str) -> None: self.value_label.setText(value or "—") class DetailPanel(QFrame): open_requested = Signal(object) copy_requested = Signal(str) terminate_requested = Signal(object, bool) def __init__(self, parent=None) -> None: super().__init__(parent) self.setObjectName("DetailCard") self.setMinimumWidth(350) self.setMaximumWidth(430) self._process: dict[str, Any] | None = None outer = QVBoxLayout(self) outer.setContentsMargins(0, 0, 0, 0) outer.setSpacing(0) heading = QWidget() heading_layout = QHBoxLayout(heading) heading_layout.setContentsMargins(18, 16, 18, 12) title = QLabel("进程详情") title.setObjectName("SectionTitle") hint = QLabel("实时信息") hint.setObjectName("MutedLabel") heading_layout.addWidget(title) heading_layout.addStretch() heading_layout.addWidget(hint) outer.addWidget(heading) self.stack = QStackedWidget() outer.addWidget(self.stack, 1) self._build_empty_page() self._build_detail_page() self.stack.setCurrentWidget(self.empty_page) def _build_empty_page(self) -> None: self.empty_page = QWidget() layout = QVBoxLayout(self.empty_page) layout.setContentsMargins(34, 34, 34, 70) layout.addStretch() icon = QLabel("⌁") icon.setObjectName("EmptyIcon") icon.setFixedSize(54, 54) icon.setAlignment(Qt.AlignmentFlag.AlignCenter) self.empty_title = QLabel("选择一个进程") self.empty_title.setObjectName("EmptyTitle") self.empty_title.setAlignment(Qt.AlignmentFlag.AlignCenter) self.empty_hint = QLabel("点击左侧任意一行,查看路径、命令行和资源占用。") self.empty_hint.setObjectName("MutedLabel") self.empty_hint.setWordWrap(True) self.empty_hint.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(icon, alignment=Qt.AlignmentFlag.AlignCenter) layout.addSpacing(12) layout.addWidget(self.empty_title) layout.addSpacing(4) layout.addWidget(self.empty_hint) layout.addStretch() self.stack.addWidget(self.empty_page) def _build_detail_page(self) -> None: self.detail_page = QWidget() page_layout = QVBoxLayout(self.detail_page) page_layout.setContentsMargins(0, 0, 0, 0) page_layout.setSpacing(0) scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setFrameShape(QFrame.Shape.NoFrame) detail_content = QWidget() layout = QVBoxLayout(detail_content) layout.setContentsMargins(18, 6, 18, 18) layout.setSpacing(12) identity = QWidget() identity_layout = QHBoxLayout(identity) identity_layout.setContentsMargins(0, 0, 0, 6) identity_layout.setSpacing(12) self.avatar = QLabel("P") self.avatar.setObjectName("ProcessAvatar") self.avatar.setFixedSize(50, 50) self.avatar.setAlignment(Qt.AlignmentFlag.AlignCenter) name_layout = QVBoxLayout() name_layout.setSpacing(4) self.process_name = QLabel("—") self.process_name.setObjectName("ProcessName") self.process_name.setTextFormat(Qt.TextFormat.PlainText) self.pid_badge = QLabel("PID —") self.pid_badge.setObjectName("PidBadge") self.pid_badge.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) name_layout.addWidget(self.process_name) name_layout.addWidget(self.pid_badge) identity_layout.addWidget(self.avatar) identity_layout.addLayout(name_layout, 1) layout.addWidget(identity) self.limited_banner = QLabel("部分信息受系统权限限制") self.limited_banner.setObjectName("LimitedBanner") self.limited_banner.setWordWrap(True) self.limited_banner.hide() layout.addWidget(self.limited_banner) self.rows: dict[str, DetailRow] = {} for key, label in ( ("ports", "监听端口"), ("exe", "程序路径"), ("create_time", "启动时间"), ("cpu", "CPU 使用率"), ("memory", "内存占用"), ("username", "运行用户"), ("status", "进程状态"), ("ppid", "父进程 PID"), ): row = DetailRow(label) if key == "ports": row.value_label.setObjectName("PortValue") self.rows[key] = row layout.addWidget(row) command_title = QLabel("启动命令") command_title.setObjectName("DetailKey") layout.addWidget(command_title) self.command_box = QTextEdit() self.command_box.setObjectName("CommandBox") self.command_box.setReadOnly(True) self.command_box.setMinimumHeight(76) self.command_box.setMaximumHeight(108) layout.addWidget(self.command_box) layout.addStretch() actions = QWidget() actions_layout = QVBoxLayout(actions) actions_layout.setContentsMargins(18, 10, 18, 18) actions_layout.setSpacing(8) utility_layout = QHBoxLayout() utility_layout.setSpacing(8) self.open_button = QPushButton("打开文件位置") self.open_button.setObjectName("SecondaryButton") self.copy_button = QPushButton("复制启动命令") self.copy_button.setObjectName("SecondaryButton") utility_layout.addWidget(self.open_button) utility_layout.addWidget(self.copy_button) actions_layout.addLayout(utility_layout) danger_layout = QHBoxLayout() danger_layout.setSpacing(8) self.normal_button = QPushButton("正常结束") self.normal_button.setObjectName("SoftDangerButton") self.force_button = QPushButton("强制结束") self.force_button.setObjectName("DangerButton") danger_layout.addWidget(self.normal_button) danger_layout.addWidget(self.force_button) actions_layout.addLayout(danger_layout) self.open_button.clicked.connect(self._request_open) self.copy_button.clicked.connect(self._request_copy) self.normal_button.clicked.connect(lambda: self._request_terminate(False)) self.force_button.clicked.connect(lambda: self._request_terminate(True)) scroll.setWidget(detail_content) page_layout.addWidget(scroll, 1) page_layout.addWidget(actions) self.stack.addWidget(self.detail_page) self.detail_scroll = scroll def set_process(self, process: dict[str, Any]) -> None: self._process = process name = process["name"] self.avatar.setText((name[:1] or "P").upper()) self.process_name.setText(name) self.pid_badge.setText(f'PID {process["pid"]}') self.limited_banner.setVisible(bool(process["access_limited"])) ports = "、".join(str(port) for port in process["ports"]) or "未监听端口" port_tooltip = "\n".join( f'{item["protocol"]} {item["address"]}:{item["port"]}' for item in process["port_details"] ) self.rows["ports"].set_value(ports) self.rows["ports"].value_label.setToolTip(port_tooltip) self.rows["exe"].set_value(process["exe"]) self.rows["create_time"].set_value(process["create_time_text"]) self.rows["cpu"].set_value(f'{process["cpu_percent"]:.1f}%') self.rows["memory"].set_value( f'{process["memory_text"]} ({process["memory_percent"]:.1f}%)' ) self.rows["username"].set_value(process["username"]) self.rows["status"].set_value(process["status_label"]) self.rows["ppid"].set_value(str(process["ppid"]) if process["ppid"] else "—") self.command_box.setPlainText(process["command"] or "—") self.open_button.setEnabled(bool(process["can_open_location"])) self.copy_button.setEnabled(bool(process["command"])) self.normal_button.setEnabled(bool(process["can_terminate"])) self.force_button.setEnabled(bool(process["can_terminate"])) self.normal_button.setToolTip( "向进程发送常规结束请求" if process["can_terminate"] else "不能结束 ProcessDock 自身" ) self.force_button.setToolTip("立即终止进程,未保存的数据可能丢失") self.stack.setCurrentIndex(1) def clear(self, message: str = "点击左侧任意一行,查看路径、命令行和资源占用。") -> None: self._process = None self.empty_hint.setText(message) self.stack.setCurrentWidget(self.empty_page) def set_action_busy(self, busy: bool) -> None: if not self._process: return self.open_button.setEnabled(not busy and bool(self._process["can_open_location"])) self.copy_button.setEnabled(not busy and bool(self._process["command"])) self.normal_button.setEnabled(not busy and bool(self._process["can_terminate"])) self.force_button.setEnabled(not busy and bool(self._process["can_terminate"])) def _request_open(self) -> None: if self._process: self.open_requested.emit(self._process) def _request_copy(self) -> None: if self._process and self._process["command"]: self.copy_requested.emit(self._process["command"]) def _request_terminate(self, force: bool) -> None: if self._process: self.terminate_requested.emit(self._process, force) class MainWindow(QMainWindow): def __init__( self, service: ProcessService | None = None, auto_refresh: bool = True, parent=None ) -> None: super().__init__(parent) self.service = service or ProcessService() self.thread_pool = QThreadPool.globalInstance() self._workers: set[FunctionWorker] = set() self._refreshing = False self._refresh_pending = False self._closing = False self._selected_pid: int | None = None self._action_busy = False self.setWindowTitle("ProcessDock · 进程与端口管理器") self.setWindowIcon(make_app_icon()) self.setMinimumSize(1080, 700) self.resize(1360, 820) self.setStyleSheet(APP_STYLE) self._build_ui(auto_refresh) self.auto_timer = QTimer(self) self.auto_timer.setInterval(3000) self.auto_timer.timeout.connect(self.refresh) self.auto_checkbox.toggled.connect(self._toggle_auto_refresh) if auto_refresh: self.auto_timer.start() QTimer.singleShot(0, self.refresh) def _build_ui(self, auto_refresh: bool) -> None: root = QWidget() root.setObjectName("AppRoot") self.setCentralWidget(root) outer = QVBoxLayout(root) outer.setContentsMargins(20, 18, 20, 16) outer.setSpacing(12) header = QFrame() header.setObjectName("HeaderCard") header_layout = QHBoxLayout(header) header_layout.setContentsMargins(16, 11, 16, 11) header_layout.setSpacing(12) mark = QLabel("P") mark.setObjectName("BrandMark") mark.setFixedSize(42, 42) mark.setAlignment(Qt.AlignmentFlag.AlignCenter) header_layout.addWidget(mark) brand = QVBoxLayout() brand.setSpacing(0) title = QLabel("ProcessDock") title.setObjectName("BrandTitle") subtitle = QLabel("进程与端口管理器") subtitle.setObjectName("BrandSubtitle") brand.addWidget(title) brand.addWidget(subtitle) header_layout.addLayout(brand) header_layout.addStretch() self.search_box = QLineEdit() self.search_box.setObjectName("SearchBox") self.search_box.setPlaceholderText("搜索进程、PID、端口或路径") self.search_box.setClearButtonEnabled(True) self.search_box.textChanged.connect(self._filter_changed) header_layout.addWidget(self.search_box) self.refresh_button = QPushButton("刷新") self.refresh_button.setObjectName("PrimaryButton") self.refresh_button.setIcon( self.style().standardIcon(QStyle.StandardPixmap.SP_BrowserReload) ) self.refresh_button.clicked.connect(self.refresh) header_layout.addWidget(self.refresh_button) self.auto_checkbox = QCheckBox("自动刷新 3 秒") self.auto_checkbox.setChecked(auto_refresh) header_layout.addWidget(self.auto_checkbox) outer.addWidget(header) cards_layout = QHBoxLayout() cards_layout.setSpacing(10) self.process_card = SummaryCard("进", "运行进程", "—", "正在读取") self.port_card = SummaryCard("端", "监听端口", "—", "TCP 与 UDP") self.cpu_card = SummaryCard("CPU", "系统 CPU", "—", "所有逻辑核心") self.memory_card = SummaryCard("存", "系统内存", "—", "正在读取") for card in (self.process_card, self.port_card, self.cpu_card, self.memory_card): cards_layout.addWidget(card, 1) outer.addLayout(cards_layout) self.warning_banner = QLabel() self.warning_banner.setObjectName("WarningBanner") self.warning_banner.setWordWrap(True) self.warning_banner.hide() outer.addWidget(self.warning_banner) splitter = QSplitter(Qt.Orientation.Horizontal) splitter.setChildrenCollapsible(False) outer.addWidget(splitter, 1) table_card = QFrame() table_card.setObjectName("TableCard") table_layout = QVBoxLayout(table_card) table_layout.setContentsMargins(0, 0, 0, 0) table_layout.setSpacing(0) table_heading = QWidget() table_heading_layout = QHBoxLayout(table_heading) table_heading_layout.setContentsMargins(18, 14, 18, 11) table_title = QLabel("正在运行的进程") table_title.setObjectName("SectionTitle") self.count_label = QLabel("准备读取…") self.count_label.setObjectName("MutedLabel") table_heading_layout.addWidget(table_title) table_heading_layout.addStretch() table_heading_layout.addWidget(self.count_label) table_layout.addWidget(table_heading) self.loading_bar = QProgressBar() self.loading_bar.setObjectName("LoadingBar") self.loading_bar.setRange(0, 0) self.loading_bar.setTextVisible(False) self.loading_bar.hide() table_layout.addWidget(self.loading_bar) self.model = ProcessTableModel(self) self.proxy = ProcessFilterProxyModel(self) self.proxy.setSourceModel(self.model) self.table = QTableView() self.table.setModel(self.proxy) self.table.setAlternatingRowColors(True) self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) self.table.setShowGrid(False) self.table.setWordWrap(False) self.table.setSortingEnabled(True) self.table.sortByColumn(1, Qt.SortOrder.AscendingOrder) self.table.verticalHeader().hide() self.table.verticalHeader().setDefaultSectionSize(48) header_view = self.table.horizontalHeader() header_view.setDefaultAlignment(Qt.AlignmentFlag.AlignCenter) header_view.setMinimumSectionSize(62) header_view.setSectionResizeMode(QHeaderView.ResizeMode.Interactive) header_view.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) widths = (78, 190, 135, 82, 105, 130, 88, 82) for column, width in enumerate(widths): self.table.setColumnWidth(column, width) self.table.selectionModel().currentRowChanged.connect(self._selection_changed) self.action_delegate = ActionButtonDelegate(self.table) self.action_delegate.terminate_requested.connect( lambda process: self._confirm_terminate(process, False) ) self.table.setItemDelegateForColumn(7, self.action_delegate) self.table_stack = QStackedWidget() self.table_stack.addWidget(self.table) self.no_results_page = self._build_no_results_page() self.table_stack.addWidget(self.no_results_page) table_layout.addWidget(self.table_stack, 1) splitter.addWidget(table_card) self.detail_panel = DetailPanel() self.detail_panel.open_requested.connect(self._open_location) self.detail_panel.copy_requested.connect(self._copy_command) self.detail_panel.terminate_requested.connect(self._confirm_terminate) splitter.addWidget(self.detail_panel) splitter.setStretchFactor(0, 1) splitter.setStretchFactor(1, 0) splitter.setSizes([910, 390]) footer = QHBoxLayout() self.footer_status = QLabel("就绪") self.footer_status.setObjectName("FooterStatus") self.footer_time = QLabel("尚未刷新") self.footer_time.setObjectName("FooterStatus") footer.addWidget(self.footer_status) footer.addStretch() footer.addWidget(self.footer_time) outer.addLayout(footer) @staticmethod def _build_no_results_page() -> QWidget: page = QWidget() layout = QVBoxLayout(page) layout.addStretch() icon = QLabel("⌕") icon.setObjectName("EmptyIcon") icon.setFixedSize(54, 54) icon.setAlignment(Qt.AlignmentFlag.AlignCenter) title = QLabel("没有匹配的进程") title.setObjectName("EmptyTitle") title.setAlignment(Qt.AlignmentFlag.AlignCenter) hint = QLabel("换一个进程名称、PID 或端口试试。") hint.setObjectName("MutedLabel") hint.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(icon, alignment=Qt.AlignmentFlag.AlignCenter) layout.addSpacing(10) layout.addWidget(title) layout.addWidget(hint) layout.addStretch() return page @Slot() def refresh(self) -> None: if self._refreshing: self._refresh_pending = True return self._refreshing = True self.refresh_button.setEnabled(False) self.loading_bar.show() self._set_status("正在读取进程和端口…") worker = FunctionWorker(self.service.snapshot) worker.signals.result.connect(self._apply_snapshot) worker.signals.error.connect(self._refresh_failed) worker.signals.finished.connect(self._refresh_finished) self._track_worker(worker) self.thread_pool.start(worker) @Slot(object) def _apply_snapshot(self, snapshot: dict[str, Any]) -> None: if self._closing: return processes = snapshot["processes"] self.model.set_processes(processes) summary = snapshot["summary"] self.process_card.update_value(str(summary["process_count"]), "本机可见进程") self.port_card.update_value(str(summary["port_count"]), "TCP 监听 / UDP 绑定") self.cpu_card.update_value(f'{summary["cpu_percent"]:.1f}%', "所有逻辑核心") self.memory_card.update_value( f'{summary["memory_percent"]:.1f}%', f'{summary["memory_used"]} / {summary["memory_total"]}', ) warnings = snapshot.get("warnings") or [] self.warning_banner.setText(" ".join(warnings)) self.warning_banner.setVisible(bool(warnings)) self.footer_time.setText(f'上次刷新 {snapshot["refreshed_at"]}') self._set_status("数据已更新") self._update_visible_count() if self._selected_pid is not None: selected = next( (process for process in processes if process["pid"] == self._selected_pid), None, ) if selected is None: self._selected_pid = None self.detail_panel.clear("该进程已经结束,请重新选择。") else: self.detail_panel.set_process(selected) self._reselect_process(self._selected_pid) @Slot(str) def _refresh_failed(self, message: str) -> None: self.warning_banner.setText(f"刷新失败:{message}") self.warning_banner.show() self._set_status("刷新失败") @Slot() def _refresh_finished(self) -> None: self._refreshing = False self.refresh_button.setEnabled(True) self.loading_bar.hide() if self._refresh_pending and not self._closing: self._refresh_pending = False QTimer.singleShot(0, self.refresh) def _track_worker(self, worker: FunctionWorker) -> None: self._workers.add(worker) worker.signals.finished.connect(lambda current=worker: self._workers.discard(current)) @Slot(str) def _filter_changed(self, query: str) -> None: self.proxy.set_query(query) self._update_visible_count() def _update_visible_count(self) -> None: visible = self.proxy.rowCount() total = self.model.rowCount() self.count_label.setText( f"{visible} / {total} 个进程" if self.search_box.text().strip() else f"{total} 个进程" ) self.table_stack.setCurrentWidget(self.table if visible else self.no_results_page) @Slot(QModelIndex, QModelIndex) def _selection_changed(self, current: QModelIndex, previous: QModelIndex) -> None: del previous if not current.isValid(): return process = current.data(PROCESS_ROLE) if process: self._selected_pid = process["pid"] self.detail_panel.set_process(process) def _reselect_process(self, pid: int) -> None: for source_row, process in enumerate(self.model.processes): if process["pid"] != pid: continue source_index = self.model.index(source_row, 0) proxy_index = self.proxy.mapFromSource(source_index) if proxy_index.isValid(): self.table.selectRow(proxy_index.row()) return @Slot(bool) def _toggle_auto_refresh(self, enabled: bool) -> None: if enabled: self.auto_timer.start() self._set_status("已开启自动刷新") else: self.auto_timer.stop() self._set_status("已暂停自动刷新") @Slot(object, bool) def _confirm_terminate(self, process: dict[str, Any], force: bool) -> None: if self._action_busy or not process.get("can_terminate"): return name = process["name"] pid = process["pid"] box = QMessageBox(self) box.setIcon(QMessageBox.Icon.Warning) box.setWindowTitle("确认强制结束" if force else "确认结束进程") box.setText(f"确定要结束 {name}(PID {pid})吗?") box.setInformativeText( "强制结束可能导致未保存的数据丢失。" if force else "将先发送常规结束请求;如果进程没有响应,可再强制结束。" ) confirm = box.addButton( "强制结束" if force else "正常结束", QMessageBox.ButtonRole.AcceptRole ) box.addButton("取消", QMessageBox.ButtonRole.RejectRole) box.setDefaultButton(confirm) box.exec() if box.clickedButton() is not confirm: return self._action_busy = True if self._selected_pid == pid: self.detail_panel.set_action_busy(True) self._set_status(f"正在结束 {name}…") worker = FunctionWorker( lambda: self.service.terminate( pid, force=force, expected_create_time=process.get("create_time"), ) ) worker.signals.result.connect(self._termination_finished) worker.signals.error.connect(self._action_failed) worker.signals.finished.connect(self._action_worker_finished) self._track_worker(worker) self.thread_pool.start(worker) @Slot(object) def _termination_finished(self, result: ActionResult) -> None: self._set_status(result.message) if result.still_running: QMessageBox.warning(self, "进程仍在运行", result.message) self.refresh() @Slot(str) def _action_failed(self, message: str) -> None: self._set_status("操作失败") QMessageBox.critical(self, "操作失败", message) @Slot() def _action_worker_finished(self) -> None: self._action_busy = False self.detail_panel.set_action_busy(False) @Slot(object) def _open_location(self, process: dict[str, Any]) -> None: if self._action_busy: return self._action_busy = True self.detail_panel.set_action_busy(True) worker = FunctionWorker( lambda: self.service.open_file_location( process["pid"], expected_create_time=process.get("create_time") ) ) worker.signals.result.connect(lambda result: self._set_status(result.message)) worker.signals.error.connect(self._action_failed) worker.signals.finished.connect(self._action_worker_finished) self._track_worker(worker) self.thread_pool.start(worker) @Slot(str) def _copy_command(self, command: str) -> None: QGuiApplication.clipboard().setText(command) self._set_status("启动命令已复制到剪贴板") def _set_status(self, text: str) -> None: self.footer_status.setText(text) def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 self._closing = True self.auto_timer.stop() self.thread_pool.clear() super().closeEvent(event) def make_app_icon() -> QIcon: pixmap = QPixmap(64, 64) pixmap.fill(Qt.GlobalColor.transparent) painter = QPainter(pixmap) painter.setRenderHint(QPainter.RenderHint.Antialiasing) painter.setPen(Qt.PenStyle.NoPen) painter.setBrush(QColor("#5B5BD6")) painter.drawRoundedRect(4, 4, 56, 56, 15, 15) painter.setPen(QColor("#FFFFFF")) font = QFont("Arial", 29, QFont.Weight.Bold) painter.setFont(font) painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "P") painter.end() return QIcon(pixmap) def create_window_for_testing(service: ProcessService | None = None) -> MainWindow: """Small test hook used by the off-screen smoke check.""" if QApplication.instance() is None: raise RuntimeError("A QApplication must exist before creating the window.") return MainWindow(service=service, auto_refresh=False)