新增统计栏/过滤/预设/深色主题/托盘/DPI适配/发送文件/RTS-DTR,修复配置保存与弹窗置顶
This commit is contained in:
@@ -16,3 +16,7 @@ network_debugger_repaired.py
|
|||||||
clean_consts.txt
|
clean_consts.txt
|
||||||
mod_doc.txt
|
mod_doc.txt
|
||||||
docs.json
|
docs.json
|
||||||
|
|
||||||
|
# 运行时生成的应用配置
|
||||||
|
app_config.json
|
||||||
|
*_profiles.json
|
||||||
+43
-24
@@ -1,31 +1,50 @@
|
|||||||
# 修改事项
|
# 修改事项
|
||||||
|
|
||||||
以下为本次相对于 e9b3100(初始提交)的改动记录。
|
以下为相对于上一提交(b43249f)的改动记录。
|
||||||
|
|
||||||
## 1. 接收区字体大小:Ctrl + 鼠标滚轮调整
|
## 1. 收发统计栏
|
||||||
- 新增 `RecvEdit` 类(继承 `QPlainTextEdit`),重写 `wheelEvent`:
|
- 底部新增统计标签,实时显示 **TX/RX 的字节数与帧数**。
|
||||||
按住 **Ctrl + 滚轮** 即可放大/缩小接收区字体(范围 8~40pt)。
|
|
||||||
- 接收区改用 `RecvEdit` 并通过 `QFont` 设置字体,替换原来的 stylesheet 方式。
|
|
||||||
|
|
||||||
## 2. 导出/导入配置扩展
|
## 2. 接收区右键菜单
|
||||||
- `export_config` 现在经 `collect_config` 导出以下内容:
|
- 新增 `RecvEdit.contextMenuEvent`:复制、全选、**另存为**、清空。
|
||||||
- 接收设置:显示格式(ASCII/HEX)、时间戳、INFO 勾选;
|
- "另存为"将接收区全部内容写入文本文件。
|
||||||
- 发送设置:内容格式(文本/HEX)、回车发送、回车换行、定时发送及间隔;
|
|
||||||
- 应答设置:自动应答、解析格式、回应格式勾选;
|
|
||||||
- 字体大小、应答规则、批量发送。
|
|
||||||
- **保存日志(chk_save_log)不参与导出/导入**,避免日志目录误恢复。
|
|
||||||
- `import_config` 通过 `apply_config` 统一恢复上述设置,兼容旧的 `reply_rules` / `batch_rows` 配置文件格式。
|
|
||||||
|
|
||||||
## 3. 关闭自动保存、重启自动恢复配置
|
## 3. 发送区 Ctrl+滚轮调字号
|
||||||
- `MainWindow` 持有串口/TCP/UDP 三个面板引用。
|
- `SendEdit` 支持 Ctrl+滚轮调整字号(8~40pt),字号随配置持久化。
|
||||||
- `closeEvent` 时把各面板配置写入同目录 `app_config.json`。
|
|
||||||
- 启动时自动读取并恢复,无需每次手动导入配置。
|
|
||||||
|
|
||||||
## 4. TCP 服务端发送改为广播所有客户端
|
## 4. 关键字过滤
|
||||||
- `NetPanel.actual_send` 服务端分支不再限定于下拉框选中的客户端,改为发送给**所有已连接客户端**。
|
- 接收设置组新增"过滤"勾选 + 关键字输入框:启用后只显示包含关键字的行。
|
||||||
|
- 过滤状态与关键字随配置保存/恢复。
|
||||||
|
|
||||||
## 5. 窗口置顶功能
|
## 5. 发送历史持久化
|
||||||
- 新增顶栏"窗口置顶"勾选框。
|
- 发送历史(最近 200 条)随 `app_config.json` 保存,重启后可用 ↑↓ 键继续浏览。
|
||||||
- 使用 Windows `SetWindowPos` API(Win32)调整 z 序实现置顶,无闪烁;
|
|
||||||
为 API 声明了 `argtypes` 防止 64 位句柄被截断,API 失败时回退到 `setWindowFlag`。
|
## 6. 多套配置预设(Profile)
|
||||||
- 置顶状态随 `app_config.json` 自动保存/恢复。
|
- 顶栏"预设"下拉 + "保存预设/删除预设"按钮,可一键切换整套配置。
|
||||||
|
- 预设保存于 `network_debugger_profiles.json`。
|
||||||
|
|
||||||
|
## 7. 深色主题切换
|
||||||
|
- 顶栏"深色模式"勾选框,基于 Fusion 风格切换亮/暗色板,状态持久化。
|
||||||
|
|
||||||
|
## 8. DPI 适配
|
||||||
|
- 启动时设置高分屏缩放舍入策略 `PassThrough`,避免高分屏模糊。
|
||||||
|
|
||||||
|
## 9. 串口 RTS/DTR 控制 + 发送文件
|
||||||
|
- 串口面板新增 RTS/DTR 勾选,打开串口时生效,随配置保存。
|
||||||
|
- 新增"发送文件"按钮:读取文件按 4KB 分块发送,避免阻塞界面。
|
||||||
|
|
||||||
|
## 10. 接收区性能优化
|
||||||
|
- 接收区改为**批量插入 + 定时(120ms)刷新**,高频率收发不再逐行刷新导致卡顿。
|
||||||
|
|
||||||
|
## 11. 窗口置顶修复:弹窗继承置顶
|
||||||
|
- 新增 `inherit_topmost()`:主窗口置顶时,批量发送/应答规则/自动应答弹窗自动带上置顶标志,避免被主窗口遮住。
|
||||||
|
- 修复 `BatchSendDialog` 未传 `parent` 导致无法继承置顶的问题。
|
||||||
|
|
||||||
|
## 12. 最小化到托盘
|
||||||
|
- 顶栏"最小化到托盘"勾选:勾选后显示托盘图标,**最小化或关闭**均隐藏到托盘,托盘菜单可"显示主界面/退出"。
|
||||||
|
- 修复托盘图标未调用 `show()` 导致功能无效的问题。
|
||||||
|
|
||||||
|
## 13. 配置保存修复与自动保存
|
||||||
|
- 修复残留隐藏 `app_config.json` 被锁定导致保存 `PermissionError`(静默失败)的问题。
|
||||||
|
- 新增每 30 秒自动保存兜底,异常退出也不易丢配置。
|
||||||
|
- 删除单实例锁,**默认支持多开**;`app_config.json` 加入 `.gitignore`。
|
||||||
+444
-38
@@ -18,15 +18,15 @@ from datetime import datetime
|
|||||||
|
|
||||||
import serial
|
import serial
|
||||||
import serial.tools.list_ports
|
import serial.tools.list_ports
|
||||||
from PySide6.QtCore import Qt, QTimer, Signal, QSize, QRect, QPoint
|
from PySide6.QtCore import Qt, QTimer, Signal, QSize, QRect, QPoint, QEvent
|
||||||
from PySide6.QtGui import QFont, QColor, QTextCharFormat, QTextCursor, QIcon
|
from PySide6.QtGui import QFont, QColor, QTextCharFormat, QTextCursor, QIcon, QPalette
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QApplication, QMainWindow, QWidget, QTabWidget, QVBoxLayout, QHBoxLayout,
|
QApplication, QMainWindow, QWidget, QTabWidget, QVBoxLayout, QHBoxLayout,
|
||||||
QGridLayout, QLabel, QComboBox, QLineEdit, QPushButton, QDialog,
|
QGridLayout, QLabel, QComboBox, QLineEdit, QPushButton, QDialog,
|
||||||
QPlainTextEdit, QCheckBox, QSpinBox, QSplitter, QMessageBox,
|
QPlainTextEdit, QCheckBox, QSpinBox, QSplitter, QMessageBox,
|
||||||
QTableWidget, QTableWidgetItem, QListWidget, QListWidgetItem,
|
QTableWidget, QTableWidgetItem, QListWidget, QListWidgetItem,
|
||||||
QGroupBox, QRadioButton, QHeaderView, QFileDialog, QWidgetItem, QLayout,
|
QGroupBox, QRadioButton, QHeaderView, QFileDialog, QWidgetItem, QLayout,
|
||||||
QToolBar,
|
QToolBar, QMenu, QInputDialog, QSystemTrayIcon,
|
||||||
)
|
)
|
||||||
|
|
||||||
STAMP_OPTS = ['无', 'ASCII(YYYY-MM-DD HH:MM:SS)', 'ASCII(HH:MM:SS)',
|
STAMP_OPTS = ['无', 'ASCII(YYYY-MM-DD HH:MM:SS)', 'ASCII(HH:MM:SS)',
|
||||||
@@ -41,6 +41,16 @@ WIDTH_OPTS = ['1', '2', '4']
|
|||||||
ENDIAN_OPTS = ['小端', '大端']
|
ENDIAN_OPTS = ['小端', '大端']
|
||||||
|
|
||||||
|
|
||||||
|
def inherit_topmost(dialog):
|
||||||
|
"""若主窗口处于置顶状态,则让弹出的对话框也置顶,避免被主窗口遮住"""
|
||||||
|
p = dialog.parentWidget()
|
||||||
|
while p is not None:
|
||||||
|
if getattr(p, '_topmost_active', False):
|
||||||
|
dialog.setWindowFlag(Qt.WindowStaysOnTopHint, True)
|
||||||
|
return
|
||||||
|
p = p.parentWidget()
|
||||||
|
|
||||||
|
|
||||||
class FlowLayout(QLayout):
|
class FlowLayout(QLayout):
|
||||||
"""可自动换行的流式布局:窗口过窄时控件自动换到下一行,不会显示不全"""
|
"""可自动换行的流式布局:窗口过窄时控件自动换到下一行,不会显示不全"""
|
||||||
|
|
||||||
@@ -117,11 +127,30 @@ class FlowLayout(QLayout):
|
|||||||
|
|
||||||
|
|
||||||
class SendEdit(QPlainTextEdit):
|
class SendEdit(QPlainTextEdit):
|
||||||
"""回车即发送的发送框,支持↑↓键浏览发送历史"""
|
"""回车即发送的发送框,支持↑↓键浏览发送历史,Ctrl+滚轮调整字号"""
|
||||||
|
|
||||||
enterPressed = Signal()
|
enterPressed = Signal()
|
||||||
histNav = Signal(int)
|
histNav = Signal(int)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._font_size = 11
|
||||||
|
|
||||||
|
def wheelEvent(self, event):
|
||||||
|
if event.modifiers() & Qt.ControlModifier:
|
||||||
|
delta = event.angleDelta().y()
|
||||||
|
if delta:
|
||||||
|
self._font_size = max(8, min(40, self._font_size + (1 if delta > 0 else -1)))
|
||||||
|
font = self.font()
|
||||||
|
font.setPointSize(self._font_size)
|
||||||
|
self.setFont(font)
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
super().wheelEvent(event)
|
||||||
|
|
||||||
|
def font_size(self):
|
||||||
|
return self._font_size
|
||||||
|
|
||||||
def keyPressEvent(self, e):
|
def keyPressEvent(self, e):
|
||||||
if e.key() in (Qt.Key_Return, Qt.Key_Enter) and not (e.modifiers() & Qt.ShiftModifier):
|
if e.key() in (Qt.Key_Return, Qt.Key_Enter) and not (e.modifiers() & Qt.ShiftModifier):
|
||||||
self.enterPressed.emit()
|
self.enterPressed.emit()
|
||||||
@@ -134,25 +163,18 @@ class SendEdit(QPlainTextEdit):
|
|||||||
|
|
||||||
|
|
||||||
class RecvEdit(QPlainTextEdit):
|
class RecvEdit(QPlainTextEdit):
|
||||||
"""接收区编辑框:Ctrl+滚轮 调整字体大小"""
|
"""接收区编辑框:Ctrl+滚轮 调整字体大小,右键菜单"""
|
||||||
|
|
||||||
MIN_FONT = 8
|
MIN_FONT = 8
|
||||||
MAX_FONT = 40
|
MAX_FONT = 40
|
||||||
DEFAULT_FONT = 11
|
DEFAULT_FONT = 11
|
||||||
|
saveRequested = Signal()
|
||||||
|
clearRequested = Signal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._font_size = self.DEFAULT_FONT
|
self._font_size = self.DEFAULT_FONT
|
||||||
|
|
||||||
def wheelEvent(self, event):
|
|
||||||
if event.modifiers() & Qt.ControlModifier:
|
|
||||||
delta = event.angleDelta().y()
|
|
||||||
if delta:
|
|
||||||
self.set_font_size(self._font_size + (1 if delta > 0 else -1))
|
|
||||||
event.accept()
|
|
||||||
return
|
|
||||||
super().wheelEvent(event)
|
|
||||||
|
|
||||||
def set_font_size(self, size):
|
def set_font_size(self, size):
|
||||||
self._font_size = max(self.MIN_FONT, min(self.MAX_FONT, int(size)))
|
self._font_size = max(self.MIN_FONT, min(self.MAX_FONT, int(size)))
|
||||||
font = self.font()
|
font = self.font()
|
||||||
@@ -162,6 +184,26 @@ class RecvEdit(QPlainTextEdit):
|
|||||||
def font_size(self):
|
def font_size(self):
|
||||||
return self._font_size
|
return self._font_size
|
||||||
|
|
||||||
|
def contextMenuEvent(self, event):
|
||||||
|
menu = self.createStandardContextMenu()
|
||||||
|
menu.addSeparator()
|
||||||
|
act_save = menu.addAction('另存为…')
|
||||||
|
act_clear = menu.addAction('清空')
|
||||||
|
ch = menu.exec(event.globalPos())
|
||||||
|
if ch == act_save:
|
||||||
|
self.saveRequested.emit()
|
||||||
|
elif ch == act_clear:
|
||||||
|
self.clearRequested.emit()
|
||||||
|
|
||||||
|
def wheelEvent(self, event):
|
||||||
|
if event.modifiers() & Qt.ControlModifier:
|
||||||
|
delta = event.angleDelta().y()
|
||||||
|
if delta:
|
||||||
|
self.set_font_size(self.font_size() + (1 if delta > 0 else -1))
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
super().wheelEvent(event)
|
||||||
|
|
||||||
|
|
||||||
class NoWheelSpinBox(QSpinBox):
|
class NoWheelSpinBox(QSpinBox):
|
||||||
def wheelEvent(self, event):
|
def wheelEvent(self, event):
|
||||||
@@ -519,6 +561,7 @@ def write_field(ftype, widget, field):
|
|||||||
class RuleEditDialog(QDialog):
|
class RuleEditDialog(QDialog):
|
||||||
def __init__(self, parent=None, rule=None):
|
def __init__(self, parent=None, rule=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
inherit_topmost(self)
|
||||||
self.setWindowTitle('应答规则')
|
self.setWindowTitle('应答规则')
|
||||||
self.setMinimumWidth(800)
|
self.setMinimumWidth(800)
|
||||||
v = QVBoxLayout(self)
|
v = QVBoxLayout(self)
|
||||||
@@ -673,6 +716,7 @@ class RuleEditDialog(QDialog):
|
|||||||
class AutoReplyManagerDialog(QDialog):
|
class AutoReplyManagerDialog(QDialog):
|
||||||
def __init__(self, rules, parent=None):
|
def __init__(self, rules, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
inherit_topmost(self)
|
||||||
self.setWindowTitle('自动应答规则')
|
self.setWindowTitle('自动应答规则')
|
||||||
self.setMinimumSize(520, 380)
|
self.setMinimumSize(520, 380)
|
||||||
self.rules = [dict(r) for r in rules]
|
self.rules = [dict(r) for r in rules]
|
||||||
@@ -784,6 +828,7 @@ class BatchSendDialog(QDialog):
|
|||||||
|
|
||||||
def __init__(self, panel, parent=None):
|
def __init__(self, panel, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
inherit_topmost(self)
|
||||||
self.panel = panel
|
self.panel = panel
|
||||||
self.setWindowTitle('批量发送')
|
self.setWindowTitle('批量发送')
|
||||||
self.setMinimumSize(560, 360)
|
self.setMinimumSize(560, 360)
|
||||||
@@ -1003,6 +1048,16 @@ class BasePanel(QWidget):
|
|||||||
self._log_hour = None
|
self._log_hour = None
|
||||||
self.log_rotate_timer = QTimer(self)
|
self.log_rotate_timer = QTimer(self)
|
||||||
self.log_rotate_timer.timeout.connect(self._rotate_log)
|
self.log_rotate_timer.timeout.connect(self._rotate_log)
|
||||||
|
self._stat_tx = 0
|
||||||
|
self._stat_rx = 0
|
||||||
|
self._stat_tx_frames = 0
|
||||||
|
self._stat_rx_frames = 0
|
||||||
|
self._filter_enabled = False
|
||||||
|
self._filter_text = ''
|
||||||
|
self._append_buffer = []
|
||||||
|
self._flush_timer = QTimer(self)
|
||||||
|
self._flush_timer.setSingleShot(True)
|
||||||
|
self._flush_timer.timeout.connect(self._flush_pending)
|
||||||
|
|
||||||
self.root = QVBoxLayout(self)
|
self.root = QVBoxLayout(self)
|
||||||
|
|
||||||
@@ -1021,6 +1076,8 @@ class BasePanel(QWidget):
|
|||||||
self.recv.setReadOnly(True)
|
self.recv.setReadOnly(True)
|
||||||
self.recv.setMaximumBlockCount(2000)
|
self.recv.setMaximumBlockCount(2000)
|
||||||
self.recv.setFont(QFont('Consolas', RecvEdit.DEFAULT_FONT))
|
self.recv.setFont(QFont('Consolas', RecvEdit.DEFAULT_FONT))
|
||||||
|
self.recv.saveRequested.connect(self.save_received_log)
|
||||||
|
self.recv.clearRequested.connect(self.recv.clear)
|
||||||
rv.addWidget(self.recv, 1)
|
rv.addWidget(self.recv, 1)
|
||||||
splitter.addWidget(recv_widget)
|
splitter.addWidget(recv_widget)
|
||||||
|
|
||||||
@@ -1030,7 +1087,7 @@ class BasePanel(QWidget):
|
|||||||
sv.addWidget(QLabel('发送区'))
|
sv.addWidget(QLabel('发送区'))
|
||||||
self.send = SendEdit()
|
self.send = SendEdit()
|
||||||
self.send.setMaximumBlockCount(2000)
|
self.send.setMaximumBlockCount(2000)
|
||||||
self.send.setStyleSheet('QPlainTextEdit{font-family:Consolas;font-size:11pt}')
|
self.send.setFont(QFont('Consolas', 11))
|
||||||
self.send.enterPressed.connect(self.on_enter_pressed)
|
self.send.enterPressed.connect(self.on_enter_pressed)
|
||||||
self.send.histNav.connect(self.history_nav)
|
self.send.histNav.connect(self.history_nav)
|
||||||
sv.addWidget(self.send)
|
sv.addWidget(self.send)
|
||||||
@@ -1069,6 +1126,15 @@ class BasePanel(QWidget):
|
|||||||
self.chk_info = QCheckBox('INFO')
|
self.chk_info = QCheckBox('INFO')
|
||||||
self.chk_info.setChecked(True)
|
self.chk_info.setChecked(True)
|
||||||
hr.addWidget(self.chk_info)
|
hr.addWidget(self.chk_info)
|
||||||
|
self.chk_filter = QCheckBox('过滤')
|
||||||
|
self.chk_filter.toggled.connect(self._on_filter_toggled)
|
||||||
|
self.edit_filter = QLineEdit()
|
||||||
|
self.edit_filter.setPlaceholderText('关键字')
|
||||||
|
self.edit_filter.setEnabled(False)
|
||||||
|
self.edit_filter.setMaximumWidth(130)
|
||||||
|
self.edit_filter.textChanged.connect(self._on_filter_text)
|
||||||
|
hr.addWidget(self.chk_filter)
|
||||||
|
hr.addWidget(self.edit_filter)
|
||||||
self.chk_save_log = QCheckBox('保存日志')
|
self.chk_save_log = QCheckBox('保存日志')
|
||||||
self.chk_save_log.toggled.connect(self.toggle_save_log)
|
self.chk_save_log.toggled.connect(self.toggle_save_log)
|
||||||
hr.addWidget(self.chk_save_log)
|
hr.addWidget(self.chk_save_log)
|
||||||
@@ -1134,19 +1200,24 @@ class BasePanel(QWidget):
|
|||||||
self.btn_import.clicked.connect(self.import_config)
|
self.btn_import.clicked.connect(self.import_config)
|
||||||
self.btn_send = QPushButton('发送')
|
self.btn_send = QPushButton('发送')
|
||||||
self.btn_batch = QPushButton('批量发送')
|
self.btn_batch = QPushButton('批量发送')
|
||||||
|
self.btn_send_file = QPushButton('发送文件')
|
||||||
self.btn_clear_recv = QPushButton('清空接收')
|
self.btn_clear_recv = QPushButton('清空接收')
|
||||||
self.btn_clear_send = QPushButton('清空发送')
|
self.btn_clear_send = QPushButton('清空发送')
|
||||||
self.btn_send.clicked.connect(self.doSend)
|
self.btn_send.clicked.connect(self.doSend)
|
||||||
self.btn_batch.clicked.connect(self.open_batch)
|
self.btn_batch.clicked.connect(self.open_batch)
|
||||||
|
self.btn_send_file.clicked.connect(self.send_file)
|
||||||
self.btn_clear_recv.clicked.connect(self.recv.clear)
|
self.btn_clear_recv.clicked.connect(self.recv.clear)
|
||||||
self.btn_clear_send.clicked.connect(self.send.clear)
|
self.btn_clear_send.clicked.connect(self.send.clear)
|
||||||
btn_row.addWidget(self.btn_export)
|
btn_row.addWidget(self.btn_export)
|
||||||
btn_row.addWidget(self.btn_import)
|
btn_row.addWidget(self.btn_import)
|
||||||
btn_row.addWidget(self.btn_send)
|
btn_row.addWidget(self.btn_send)
|
||||||
btn_row.addWidget(self.btn_batch)
|
btn_row.addWidget(self.btn_batch)
|
||||||
|
btn_row.addWidget(self.btn_send_file)
|
||||||
btn_row.addWidget(self.btn_clear_recv)
|
btn_row.addWidget(self.btn_clear_recv)
|
||||||
btn_row.addWidget(self.btn_clear_send)
|
btn_row.addWidget(self.btn_clear_send)
|
||||||
btn_row.addStretch()
|
btn_row.addStretch()
|
||||||
|
self.stat_label = QLabel('TX: 0 B / 0 帧 RX: 0 B / 0 帧')
|
||||||
|
btn_row.addWidget(self.stat_label)
|
||||||
self.root.addLayout(btn_row)
|
self.root.addLayout(btn_row)
|
||||||
|
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
@@ -1159,7 +1230,7 @@ class BasePanel(QWidget):
|
|||||||
self.doSend()
|
self.doSend()
|
||||||
|
|
||||||
def open_batch(self):
|
def open_batch(self):
|
||||||
dlg = BatchSendDialog(self)
|
dlg = BatchSendDialog(self, parent=self)
|
||||||
if self.batch_rows:
|
if self.batch_rows:
|
||||||
dlg.import_config(self.batch_rows)
|
dlg.import_config(self.batch_rows)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
@@ -1188,22 +1259,67 @@ class BasePanel(QWidget):
|
|||||||
payload = bytes_to_hex(data)
|
payload = bytes_to_hex(data)
|
||||||
else:
|
else:
|
||||||
payload = data.decode('utf-8', 'replace')
|
payload = data.decode('utf-8', 'replace')
|
||||||
|
if direction == 'TX':
|
||||||
|
self._stat_tx += len(data)
|
||||||
|
self._stat_tx_frames += 1
|
||||||
|
else:
|
||||||
|
self._stat_rx += len(data)
|
||||||
|
self._stat_rx_frames += 1
|
||||||
|
self._update_stats()
|
||||||
|
if self._filter_enabled and self._filter_text \
|
||||||
|
and self._filter_text.lower() not in payload.lower():
|
||||||
|
return
|
||||||
text = f'{ts()}' if self.chk_ts.isChecked() else ''
|
text = f'{ts()}' if self.chk_ts.isChecked() else ''
|
||||||
color = 'blue' if direction == 'TX' else 'green'
|
color = 'blue' if direction == 'TX' else 'green'
|
||||||
self._append_line(f'[{text}] {direction}: {payload}' if text else f'{direction}: {payload}', color)
|
self._append_line(f'[{text}] {direction}: {payload}' if text else f'{direction}: {payload}', color)
|
||||||
|
|
||||||
def _append_colored(self, text: str, color: str):
|
def _update_stats(self):
|
||||||
|
self.stat_label.setText(
|
||||||
|
f'TX: {self._stat_tx} B / {self._stat_tx_frames} 帧 '
|
||||||
|
f'RX: {self._stat_rx} B / {self._stat_rx_frames} 帧')
|
||||||
|
|
||||||
|
def _flush_pending(self):
|
||||||
|
if not self._append_buffer:
|
||||||
|
return
|
||||||
cur = self.recv.textCursor()
|
cur = self.recv.textCursor()
|
||||||
cur.movePosition(QTextCursor.End)
|
cur.movePosition(QTextCursor.End)
|
||||||
if color:
|
fmt_cache = {}
|
||||||
fmt = QTextCharFormat()
|
for text, color in self._append_buffer:
|
||||||
fmt.setForeground(QColor(color))
|
if color not in fmt_cache:
|
||||||
cur.setCharFormat(fmt)
|
f = QTextCharFormat()
|
||||||
|
f.setForeground(QColor(color))
|
||||||
|
fmt_cache[color] = f
|
||||||
|
cur.setCharFormat(fmt_cache[color])
|
||||||
cur.insertText(text + '\n')
|
cur.insertText(text + '\n')
|
||||||
|
self._append_buffer.clear()
|
||||||
self.recv.setTextCursor(cur)
|
self.recv.setTextCursor(cur)
|
||||||
bar = self.recv.verticalScrollBar()
|
bar = self.recv.verticalScrollBar()
|
||||||
bar.setValue(bar.maximum())
|
bar.setValue(bar.maximum())
|
||||||
|
|
||||||
|
def _append_colored(self, text: str, color: str):
|
||||||
|
self._append_buffer.append((text, color))
|
||||||
|
if not self._flush_timer.isActive():
|
||||||
|
self._flush_timer.start(120)
|
||||||
|
|
||||||
|
def _on_filter_toggled(self, checked: bool):
|
||||||
|
self._filter_enabled = checked
|
||||||
|
self.edit_filter.setEnabled(checked)
|
||||||
|
|
||||||
|
def _on_filter_text(self, text: str):
|
||||||
|
self._filter_text = text
|
||||||
|
|
||||||
|
def save_received_log(self):
|
||||||
|
path, _ = QFileDialog.getSaveFileName(self, '保存接收内容', 'recv.txt',
|
||||||
|
'文本文件 (*.txt);;所有文件 (*)')
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(self.recv.toPlainText())
|
||||||
|
self.info(f'接收内容已保存: {path}')
|
||||||
|
except OSError as e:
|
||||||
|
self.info(f'保存失败: {e}')
|
||||||
|
|
||||||
def _append_line(self, text: str, color: str = None):
|
def _append_line(self, text: str, color: str = None):
|
||||||
self._append_colored(text, color)
|
self._append_colored(text, color)
|
||||||
if self._log_fh or self._log_dir:
|
if self._log_fh or self._log_dir:
|
||||||
@@ -1263,6 +1379,36 @@ class BasePanel(QWidget):
|
|||||||
def actual_send(self, data: bytes) -> bool:
|
def actual_send(self, data: bytes) -> bool:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def send_file(self):
|
||||||
|
path, _ = QFileDialog.getOpenFileName(self, '选择要发送的文件', '')
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
except OSError as e:
|
||||||
|
QMessageBox.warning(self, '错误', f'读取文件失败:\n{e}')
|
||||||
|
return
|
||||||
|
if not data:
|
||||||
|
self.info('文件内容为空,未发送')
|
||||||
|
return
|
||||||
|
self._file_total = len(data)
|
||||||
|
self.info(f'开始发送文件({len(data)} B): {path}')
|
||||||
|
threading.Thread(target=self._send_file_worker, args=(data,),
|
||||||
|
daemon=True).start()
|
||||||
|
|
||||||
|
def _send_file_worker(self, data: bytes):
|
||||||
|
sent = 0
|
||||||
|
for i in range(0, len(data), 4096):
|
||||||
|
part = data[i:i + 4096]
|
||||||
|
if not self.actual_send(part):
|
||||||
|
self.info('发送文件已中止')
|
||||||
|
return
|
||||||
|
sent += len(part)
|
||||||
|
self.log_line('TX', part)
|
||||||
|
time.sleep(0.01)
|
||||||
|
self.info(f'文件发送完成,共 {sent} B')
|
||||||
|
|
||||||
def send_reply(self, reply: bytes, sock=None, addr=None):
|
def send_reply(self, reply: bytes, sock=None, addr=None):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -1389,6 +1535,8 @@ class BasePanel(QWidget):
|
|||||||
'display': 'HEX' if self.rb_recv_hex.isChecked() else 'ASCII',
|
'display': 'HEX' if self.rb_recv_hex.isChecked() else 'ASCII',
|
||||||
'ts': self.chk_ts.isChecked(),
|
'ts': self.chk_ts.isChecked(),
|
||||||
'info': self.chk_info.isChecked(),
|
'info': self.chk_info.isChecked(),
|
||||||
|
'filter': self._filter_enabled,
|
||||||
|
'filter_text': self._filter_text,
|
||||||
},
|
},
|
||||||
'send': {
|
'send': {
|
||||||
'format': 'HEX' if self.rb_send_hex.isChecked() else '文本',
|
'format': 'HEX' if self.rb_send_hex.isChecked() else '文本',
|
||||||
@@ -1396,6 +1544,7 @@ class BasePanel(QWidget):
|
|||||||
'crlf': self.chk_crlf.isChecked(),
|
'crlf': self.chk_crlf.isChecked(),
|
||||||
'auto': self.chk_auto.isChecked(),
|
'auto': self.chk_auto.isChecked(),
|
||||||
'interval': self.spin_interval.value(),
|
'interval': self.spin_interval.value(),
|
||||||
|
'font_size': self.send.font_size(),
|
||||||
},
|
},
|
||||||
'reply': {
|
'reply': {
|
||||||
'enabled': self.chk_reply.isChecked(),
|
'enabled': self.chk_reply.isChecked(),
|
||||||
@@ -1403,6 +1552,7 @@ class BasePanel(QWidget):
|
|||||||
'respond': 'HEX' if self.rb_reply_hex.isChecked() else 'ASCII',
|
'respond': 'HEX' if self.rb_reply_hex.isChecked() else 'ASCII',
|
||||||
},
|
},
|
||||||
'font_size': self.recv.font_size(),
|
'font_size': self.recv.font_size(),
|
||||||
|
'send_history': self.send_history[-200:],
|
||||||
'reply_rules': copy.deepcopy(self.reply_rules),
|
'reply_rules': copy.deepcopy(self.reply_rules),
|
||||||
'batch_rows': copy.deepcopy(self.batch_rows),
|
'batch_rows': copy.deepcopy(self.batch_rows),
|
||||||
}
|
}
|
||||||
@@ -1420,6 +1570,11 @@ class BasePanel(QWidget):
|
|||||||
self.chk_ts.setChecked(bool(recv['ts']))
|
self.chk_ts.setChecked(bool(recv['ts']))
|
||||||
if 'info' in recv:
|
if 'info' in recv:
|
||||||
self.chk_info.setChecked(bool(recv['info']))
|
self.chk_info.setChecked(bool(recv['info']))
|
||||||
|
if 'filter' in recv:
|
||||||
|
self.chk_filter.setChecked(bool(recv['filter']))
|
||||||
|
self.edit_filter.setEnabled(bool(recv['filter']))
|
||||||
|
if 'filter_text' in recv:
|
||||||
|
self.edit_filter.setText(recv.get('filter_text', ''))
|
||||||
msg.append('接收设置')
|
msg.append('接收设置')
|
||||||
send = cfg.get('send')
|
send = cfg.get('send')
|
||||||
if isinstance(send, dict):
|
if isinstance(send, dict):
|
||||||
@@ -1434,6 +1589,16 @@ class BasePanel(QWidget):
|
|||||||
self.chk_auto.setChecked(bool(send['auto']))
|
self.chk_auto.setChecked(bool(send['auto']))
|
||||||
if 'interval' in send:
|
if 'interval' in send:
|
||||||
self.spin_interval.setValue(int(send['interval']))
|
self.spin_interval.setValue(int(send['interval']))
|
||||||
|
if 'font_size' in send:
|
||||||
|
try:
|
||||||
|
size = max(8, min(40, int(send['font_size'])))
|
||||||
|
f = self.send.font()
|
||||||
|
f.setPointSize(size)
|
||||||
|
self.send.setFont(f)
|
||||||
|
if hasattr(self.send, '_font_size'):
|
||||||
|
self.send._font_size = size
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
msg.append('发送设置')
|
msg.append('发送设置')
|
||||||
reply = cfg.get('reply')
|
reply = cfg.get('reply')
|
||||||
if isinstance(reply, dict):
|
if isinstance(reply, dict):
|
||||||
@@ -1452,6 +1617,10 @@ class BasePanel(QWidget):
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
msg.append('字体大小')
|
msg.append('字体大小')
|
||||||
|
if 'send_history' in cfg and isinstance(cfg['send_history'], list):
|
||||||
|
self.send_history = [str(x) for x in cfg['send_history']][-200:]
|
||||||
|
self._hist_idx = len(self.send_history)
|
||||||
|
msg.append(f'发送历史 {len(self.send_history)} 条')
|
||||||
rr = cfg.get('reply_rules')
|
rr = cfg.get('reply_rules')
|
||||||
if isinstance(rr, list) and all(isinstance(x, dict) for x in rr):
|
if isinstance(rr, list) and all(isinstance(x, dict) for x in rr):
|
||||||
for r in rr:
|
for r in rr:
|
||||||
@@ -1599,6 +1768,10 @@ class SerialPanel(BasePanel):
|
|||||||
self.btn_refresh.clicked.connect(self.refresh_ports)
|
self.btn_refresh.clicked.connect(self.refresh_ports)
|
||||||
self.btn_open = QPushButton('打开串口')
|
self.btn_open = QPushButton('打开串口')
|
||||||
self.btn_open.clicked.connect(self.toggle_serial)
|
self.btn_open.clicked.connect(self.toggle_serial)
|
||||||
|
self.chk_rts = QCheckBox('RTS')
|
||||||
|
self.chk_dtr = QCheckBox('DTR')
|
||||||
|
self.chk_rts.setToolTip('打开串口后控制 RTS 引脚电平')
|
||||||
|
self.chk_dtr.setToolTip('打开串口后控制 DTR 引脚电平')
|
||||||
|
|
||||||
g.addWidget(QLabel('串口'), 0, 0)
|
g.addWidget(QLabel('串口'), 0, 0)
|
||||||
g.addWidget(self.combox_port, 0, 1)
|
g.addWidget(self.combox_port, 0, 1)
|
||||||
@@ -1611,7 +1784,9 @@ class SerialPanel(BasePanel):
|
|||||||
g.addWidget(self.combox_parity, 1, 4)
|
g.addWidget(self.combox_parity, 1, 4)
|
||||||
g.addWidget(QLabel('停止位'), 2, 0)
|
g.addWidget(QLabel('停止位'), 2, 0)
|
||||||
g.addWidget(self.combox_stop, 2, 1)
|
g.addWidget(self.combox_stop, 2, 1)
|
||||||
g.addWidget(self.btn_open, 2, 3, 1, 2)
|
g.addWidget(self.chk_rts, 2, 3)
|
||||||
|
g.addWidget(self.chk_dtr, 2, 4)
|
||||||
|
g.addWidget(self.btn_open, 3, 3, 1, 2)
|
||||||
g.setColumnStretch(1, 1)
|
g.setColumnStretch(1, 1)
|
||||||
g.setColumnStretch(4, 1)
|
g.setColumnStretch(4, 1)
|
||||||
w.setLayout(g)
|
w.setLayout(g)
|
||||||
@@ -1646,6 +1821,11 @@ class SerialPanel(BasePanel):
|
|||||||
stopbits=STOPBITS[self.combox_stop.currentText()],
|
stopbits=STOPBITS[self.combox_stop.currentText()],
|
||||||
timeout=0.1,
|
timeout=0.1,
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
self.ser.rts = self.chk_rts.isChecked()
|
||||||
|
self.ser.dtr = self.chk_dtr.isChecked()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self.btn_open.setText('关闭串口')
|
self.btn_open.setText('关闭串口')
|
||||||
self.combox_port.setEnabled(False)
|
self.combox_port.setEnabled(False)
|
||||||
self.info(f'串口 {port} 已打开')
|
self.info(f'串口 {port} 已打开')
|
||||||
@@ -1696,6 +1876,21 @@ class SerialPanel(BasePanel):
|
|||||||
QMessageBox.warning(self, '错误', f'发送失败\n{e}')
|
QMessageBox.warning(self, '错误', f'发送失败\n{e}')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def collect_config(self):
|
||||||
|
cfg = super().collect_config()
|
||||||
|
cfg['rts'] = self.chk_rts.isChecked()
|
||||||
|
cfg['dtr'] = self.chk_dtr.isChecked()
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
def apply_config(self, cfg):
|
||||||
|
msg = super().apply_config(cfg)
|
||||||
|
if isinstance(cfg, dict):
|
||||||
|
if 'rts' in cfg:
|
||||||
|
self.chk_rts.setChecked(bool(cfg['rts']))
|
||||||
|
if 'dtr' in cfg:
|
||||||
|
self.chk_dtr.setChecked(bool(cfg['dtr']))
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
class NetPanel(BasePanel):
|
class NetPanel(BasePanel):
|
||||||
"""TCP/UDP 通用面板,支持客户端与服务器模式"""
|
"""TCP/UDP 通用面板,支持客户端与服务器模式"""
|
||||||
@@ -2000,13 +2195,47 @@ class MainWindow(QMainWindow):
|
|||||||
self.top_btn = QCheckBox('窗口置顶')
|
self.top_btn = QCheckBox('窗口置顶')
|
||||||
self.top_btn.setToolTip('选中后窗口保持在最前端')
|
self.top_btn.setToolTip('选中后窗口保持在最前端')
|
||||||
self.top_btn.toggled.connect(self._toggle_topmost)
|
self.top_btn.toggled.connect(self._toggle_topmost)
|
||||||
|
self.chk_dark = QCheckBox('深色模式')
|
||||||
|
self.chk_dark.setToolTip('切换亮色/暗色主题')
|
||||||
|
self.chk_dark.toggled.connect(self._toggle_dark)
|
||||||
|
self.chk_tray = QCheckBox('最小化到托盘')
|
||||||
|
self.chk_tray.setToolTip('最小化或关闭窗口时隐藏到系统托盘')
|
||||||
|
self.chk_tray.toggled.connect(self._on_tray_toggled)
|
||||||
|
|
||||||
tb = QToolBar('选项')
|
tb = QToolBar('选项')
|
||||||
tb.setMovable(False)
|
tb.setMovable(False)
|
||||||
tb.addWidget(self.top_btn)
|
tb.addWidget(self.top_btn)
|
||||||
|
tb.addWidget(self.chk_dark)
|
||||||
|
tb.addWidget(self.chk_tray)
|
||||||
|
tb.addSeparator()
|
||||||
|
self.combo_profile = NoWheelComboBox()
|
||||||
|
self.combo_profile.setMinimumWidth(150)
|
||||||
|
self.combo_profile.currentIndexChanged.connect(self._on_profile_selected)
|
||||||
|
tb.addWidget(QLabel(' 预设 '))
|
||||||
|
tb.addWidget(self.combo_profile)
|
||||||
|
btn_psave = QPushButton('保存预设')
|
||||||
|
btn_pdel = QPushButton('删除预设')
|
||||||
|
btn_psave.clicked.connect(self._save_profile)
|
||||||
|
btn_pdel.clicked.connect(self._del_profile)
|
||||||
|
tb.addWidget(btn_psave)
|
||||||
|
tb.addWidget(btn_pdel)
|
||||||
self.addToolBar(tb)
|
self.addToolBar(tb)
|
||||||
|
|
||||||
|
self._profiles = {}
|
||||||
|
self._tray_enabled = False
|
||||||
|
self._topmost_active = False
|
||||||
|
self._light_palette = QApplication.instance().palette()
|
||||||
|
self.tray_icon = None
|
||||||
|
self._autosave = QTimer(self)
|
||||||
|
self._autosave.setInterval(30000)
|
||||||
|
self._autosave.timeout.connect(self._save_app_config)
|
||||||
|
self._autosave.start()
|
||||||
|
self._build_tray()
|
||||||
|
self._load_profiles()
|
||||||
self._load_app_config()
|
self._load_app_config()
|
||||||
|
|
||||||
def _toggle_topmost(self, on: bool):
|
def _toggle_topmost(self, on: bool):
|
||||||
|
self._topmost_active = on
|
||||||
try:
|
try:
|
||||||
import ctypes
|
import ctypes
|
||||||
user32 = ctypes.windll.user32
|
user32 = ctypes.windll.user32
|
||||||
@@ -2035,28 +2264,94 @@ class MainWindow(QMainWindow):
|
|||||||
QTimer.singleShot(0, lambda: (self.setWindowFlag(Qt.WindowStaysOnTopHint, on),
|
QTimer.singleShot(0, lambda: (self.setWindowFlag(Qt.WindowStaysOnTopHint, on),
|
||||||
self.show()))
|
self.show()))
|
||||||
|
|
||||||
def _save_app_config(self):
|
def _toggle_dark(self, on: bool):
|
||||||
data = {'topmost': self.top_btn.isChecked(),
|
app = QApplication.instance()
|
||||||
|
if on:
|
||||||
|
p = QPalette()
|
||||||
|
p.setColor(QPalette.Window, QColor('#2b2b2b'))
|
||||||
|
p.setColor(QPalette.WindowText, QColor('#f0f0f0'))
|
||||||
|
p.setColor(QPalette.Base, QColor('#232323'))
|
||||||
|
p.setColor(QPalette.AlternateBase, QColor('#3c3f41'))
|
||||||
|
p.setColor(QPalette.Text, QColor('#f0f0f0'))
|
||||||
|
p.setColor(QPalette.Button, QColor('#3c3f41'))
|
||||||
|
p.setColor(QPalette.ButtonText, QColor('#f0f0f0'))
|
||||||
|
p.setColor(QPalette.BrightText, QColor('#ff5555'))
|
||||||
|
p.setColor(QPalette.Highlight, QColor('#2a82da'))
|
||||||
|
p.setColor(QPalette.HighlightedText, QColor('#ffffff'))
|
||||||
|
p.setColor(QPalette.Link, QColor('#2a82da'))
|
||||||
|
p.setColor(QPalette.ToolTipBase, QColor('#f0f0f0'))
|
||||||
|
p.setColor(QPalette.ToolTipText, QColor('#2b2b2b'))
|
||||||
|
p.setColor(QPalette.Disabled, QPalette.Text, QColor('#787878'))
|
||||||
|
p.setColor(QPalette.Disabled, QPalette.ButtonText, QColor('#787878'))
|
||||||
|
p.setColor(QPalette.Disabled, QPalette.Base, QColor('#2b2b2b'))
|
||||||
|
app.setStyle('Fusion')
|
||||||
|
app.setPalette(p)
|
||||||
|
else:
|
||||||
|
app.setStyle('Fusion')
|
||||||
|
app.setPalette(self._light_palette)
|
||||||
|
|
||||||
|
def _build_tray(self):
|
||||||
|
icon = QIcon(self._icon_path())
|
||||||
|
if icon.isNull():
|
||||||
|
self.tray_icon = None
|
||||||
|
return
|
||||||
|
self.tray_icon = QSystemTrayIcon(icon, self)
|
||||||
|
menu = QMenu(self)
|
||||||
|
act_show = menu.addAction('显示主界面')
|
||||||
|
act_show.triggered.connect(self._show_from_tray)
|
||||||
|
menu.addSeparator()
|
||||||
|
act_quit = menu.addAction('退出')
|
||||||
|
act_quit.triggered.connect(self._quit_app)
|
||||||
|
self.tray_icon.setContextMenu(menu)
|
||||||
|
self.tray_icon.activated.connect(self._on_tray_activated)
|
||||||
|
|
||||||
|
def _on_tray_toggled(self, checked: bool):
|
||||||
|
self._tray_enabled = checked
|
||||||
|
if self.tray_icon:
|
||||||
|
if checked:
|
||||||
|
self.tray_icon.show()
|
||||||
|
else:
|
||||||
|
self.tray_icon.hide()
|
||||||
|
|
||||||
|
def _on_tray_activated(self, reason):
|
||||||
|
if reason == QSystemTrayIcon.Trigger:
|
||||||
|
self._show_from_tray()
|
||||||
|
|
||||||
|
def _show_from_tray(self):
|
||||||
|
self.showNormal()
|
||||||
|
self.show()
|
||||||
|
self.raise_()
|
||||||
|
self.activateWindow()
|
||||||
|
|
||||||
|
def _quit_app(self):
|
||||||
|
self._tray_enabled = False
|
||||||
|
if self.tray_icon:
|
||||||
|
self.tray_icon.hide()
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def changeEvent(self, event):
|
||||||
|
if event.type() == QEvent.WindowStateChange and self.isMinimized() \
|
||||||
|
and self._tray_enabled and self.tray_icon and self.tray_icon.isVisible():
|
||||||
|
QTimer.singleShot(0, self.hide)
|
||||||
|
super().changeEvent(event)
|
||||||
|
|
||||||
|
def info(self, text: str):
|
||||||
|
self.statusBar().showMessage(text, 3000)
|
||||||
|
|
||||||
|
def _collect_all_config(self):
|
||||||
|
return {
|
||||||
|
'topmost': self.top_btn.isChecked(),
|
||||||
|
'dark': self.chk_dark.isChecked(),
|
||||||
|
'tray': self.chk_tray.isChecked(),
|
||||||
'tabs': {
|
'tabs': {
|
||||||
'serial': self.serial_panel.collect_config(),
|
'serial': self.serial_panel.collect_config(),
|
||||||
'tcp': self.tcp_panel.collect_config(),
|
'tcp': self.tcp_panel.collect_config(),
|
||||||
'udp': self.udp_panel.collect_config(),
|
'udp': self.udp_panel.collect_config(),
|
||||||
}}
|
},
|
||||||
try:
|
}
|
||||||
with open(app_config_path(), 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _load_app_config(self):
|
def _apply_panels(self, data):
|
||||||
try:
|
|
||||||
with open(app_config_path(), 'r', encoding='utf-8') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
tabs_cfg = data.get('tabs') if isinstance(data, dict) else None
|
tabs_cfg = data.get('tabs') if isinstance(data, dict) else None
|
||||||
if isinstance(data, dict) and data.get('topmost'):
|
|
||||||
self.top_btn.setChecked(True)
|
|
||||||
if not isinstance(tabs_cfg, dict):
|
if not isinstance(tabs_cfg, dict):
|
||||||
return
|
return
|
||||||
for panel, key in ((self.serial_panel, 'serial'),
|
for panel, key in ((self.serial_panel, 'serial'),
|
||||||
@@ -2066,12 +2361,123 @@ class MainWindow(QMainWindow):
|
|||||||
if isinstance(cfg, dict):
|
if isinstance(cfg, dict):
|
||||||
panel.apply_config(cfg)
|
panel.apply_config(cfg)
|
||||||
|
|
||||||
|
def _save_app_config(self):
|
||||||
|
try:
|
||||||
|
with open(app_config_path(), 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self._collect_all_config(), f, ensure_ascii=False, indent=2)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _load_app_config(self):
|
||||||
|
try:
|
||||||
|
with open(app_config_path(), 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return
|
||||||
|
if data.get('topmost'):
|
||||||
|
self.top_btn.setChecked(True)
|
||||||
|
if data.get('dark'):
|
||||||
|
self.chk_dark.setChecked(True)
|
||||||
|
if data.get('tray'):
|
||||||
|
self.chk_tray.setChecked(True)
|
||||||
|
self._apply_panels(data)
|
||||||
|
|
||||||
|
def _profile_path(self):
|
||||||
|
return os.path.splitext(app_config_path())[0] + '_profiles.json'
|
||||||
|
|
||||||
|
def _load_profiles(self):
|
||||||
|
try:
|
||||||
|
with open(self._profile_path(), 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self._profiles = data if isinstance(data, dict) else {}
|
||||||
|
except Exception:
|
||||||
|
self._profiles = {}
|
||||||
|
self._reload_profiles_combo()
|
||||||
|
|
||||||
|
def _write_profiles(self):
|
||||||
|
try:
|
||||||
|
with open(self._profile_path(), 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self._profiles, f, ensure_ascii=False, indent=2)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _reload_profiles_combo(self, select=None):
|
||||||
|
self.combo_profile.blockSignals(True)
|
||||||
|
self.combo_profile.clear()
|
||||||
|
self.combo_profile.addItem('← 选择预设 →')
|
||||||
|
for name in self._profiles:
|
||||||
|
self.combo_profile.addItem(name)
|
||||||
|
select = self.combo_profile.currentText() if select is None else select
|
||||||
|
idx = self.combo_profile.findText(select)
|
||||||
|
if idx >= 0 and idx != 0:
|
||||||
|
self.combo_profile.setCurrentIndex(idx)
|
||||||
|
self.combo_profile.blockSignals(False)
|
||||||
|
|
||||||
|
def _on_profile_selected(self, index):
|
||||||
|
if index <= 0:
|
||||||
|
return
|
||||||
|
name = self.combo_profile.currentText()
|
||||||
|
cfg = self._profiles.get(name)
|
||||||
|
if not isinstance(cfg, dict):
|
||||||
|
return
|
||||||
|
if cfg.get('topmost'):
|
||||||
|
self.top_btn.setChecked(True)
|
||||||
|
else:
|
||||||
|
self.top_btn.setChecked(False)
|
||||||
|
if cfg.get('dark'):
|
||||||
|
self.chk_dark.setChecked(True)
|
||||||
|
else:
|
||||||
|
self.chk_dark.setChecked(False)
|
||||||
|
if cfg.get('tray'):
|
||||||
|
self.chk_tray.setChecked(True)
|
||||||
|
else:
|
||||||
|
self.chk_tray.setChecked(False)
|
||||||
|
self._apply_panels(cfg)
|
||||||
|
self._save_app_config()
|
||||||
|
self.info('预设已加载: ' + name)
|
||||||
|
|
||||||
|
def _save_profile(self):
|
||||||
|
text = self.combo_profile.currentText()
|
||||||
|
if text == '← 选择预设 →':
|
||||||
|
text = ''
|
||||||
|
name, ok = QInputDialog.getText(self, '保存预设', '预设名称:', QLineEdit.Normal, text)
|
||||||
|
if not ok or not name.strip():
|
||||||
|
return
|
||||||
|
self._profiles[name.strip()] = self._collect_all_config()
|
||||||
|
self._write_profiles()
|
||||||
|
self._reload_profiles_combo(name.strip())
|
||||||
|
self.info('预设已保存: ' + name.strip())
|
||||||
|
|
||||||
|
def _del_profile(self):
|
||||||
|
name = self.combo_profile.currentText()
|
||||||
|
if name == '← 选择预设 →' or name not in self._profiles:
|
||||||
|
return
|
||||||
|
if QMessageBox.question(self, '删除预设', f'确定删除预设「{name}」吗?') \
|
||||||
|
!= QMessageBox.Yes:
|
||||||
|
return
|
||||||
|
del self._profiles[name]
|
||||||
|
self._write_profiles()
|
||||||
|
self._reload_profiles_combo()
|
||||||
|
self.info('预设已删除: ' + name)
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
self._save_app_config()
|
self._save_app_config()
|
||||||
|
if self._tray_enabled and self.tray_icon and self.tray_icon.isVisible():
|
||||||
|
event.ignore()
|
||||||
|
self.hide()
|
||||||
|
self.tray_icon.showMessage(APP_NAME, '程序已最小化到托盘',
|
||||||
|
QSystemTrayIcon.Information, 2000)
|
||||||
|
return
|
||||||
|
if self.tray_icon:
|
||||||
|
self.tray_icon.hide()
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
||||||
|
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
app.setApplicationName(APP_NAME)
|
app.setApplicationName(APP_NAME)
|
||||||
app.setFont(QFont('Microsoft YaHei', 9))
|
app.setFont(QFont('Microsoft YaHei', 9))
|
||||||
|
|||||||
Reference in New Issue
Block a user