新增窗口置顶、接收区Ctrl+滚轮调字号、配置自动保存与恢复,TCP服务端发送改为广播所有客户端
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# 修改事项
|
||||
|
||||
以下为本次相对于 e9b3100(初始提交)的改动记录。
|
||||
|
||||
## 1. 接收区字体大小:Ctrl + 鼠标滚轮调整
|
||||
- 新增 `RecvEdit` 类(继承 `QPlainTextEdit`),重写 `wheelEvent`:
|
||||
按住 **Ctrl + 滚轮** 即可放大/缩小接收区字体(范围 8~40pt)。
|
||||
- 接收区改用 `RecvEdit` 并通过 `QFont` 设置字体,替换原来的 stylesheet 方式。
|
||||
|
||||
## 2. 导出/导入配置扩展
|
||||
- `export_config` 现在经 `collect_config` 导出以下内容:
|
||||
- 接收设置:显示格式(ASCII/HEX)、时间戳、INFO 勾选;
|
||||
- 发送设置:内容格式(文本/HEX)、回车发送、回车换行、定时发送及间隔;
|
||||
- 应答设置:自动应答、解析格式、回应格式勾选;
|
||||
- 字体大小、应答规则、批量发送。
|
||||
- **保存日志(chk_save_log)不参与导出/导入**,避免日志目录误恢复。
|
||||
- `import_config` 通过 `apply_config` 统一恢复上述设置,兼容旧的 `reply_rules` / `batch_rows` 配置文件格式。
|
||||
|
||||
## 3. 关闭自动保存、重启自动恢复配置
|
||||
- `MainWindow` 持有串口/TCP/UDP 三个面板引用。
|
||||
- `closeEvent` 时把各面板配置写入同目录 `app_config.json`。
|
||||
- 启动时自动读取并恢复,无需每次手动导入配置。
|
||||
|
||||
## 4. TCP 服务端发送改为广播所有客户端
|
||||
- `NetPanel.actual_send` 服务端分支不再限定于下拉框选中的客户端,改为发送给**所有已连接客户端**。
|
||||
|
||||
## 5. 窗口置顶功能
|
||||
- 新增顶栏"窗口置顶"勾选框。
|
||||
- 使用 Windows `SetWindowPos` API(Win32)调整 z 序实现置顶,无闪烁;
|
||||
为 API 声明了 `argtypes` 防止 64 位句柄被截断,API 失败时回退到 `setWindowFlag`。
|
||||
- 置顶状态随 `app_config.json` 自动保存/恢复。
|
||||
+207
-27
@@ -26,6 +26,7 @@ from PySide6.QtWidgets import (
|
||||
QPlainTextEdit, QCheckBox, QSpinBox, QSplitter, QMessageBox,
|
||||
QTableWidget, QTableWidgetItem, QListWidget, QListWidgetItem,
|
||||
QGroupBox, QRadioButton, QHeaderView, QFileDialog, QWidgetItem, QLayout,
|
||||
QToolBar,
|
||||
)
|
||||
|
||||
STAMP_OPTS = ['无', 'ASCII(YYYY-MM-DD HH:MM:SS)', 'ASCII(HH:MM:SS)',
|
||||
@@ -132,6 +133,36 @@ class SendEdit(QPlainTextEdit):
|
||||
super().keyPressEvent(e)
|
||||
|
||||
|
||||
class RecvEdit(QPlainTextEdit):
|
||||
"""接收区编辑框:Ctrl+滚轮 调整字体大小"""
|
||||
|
||||
MIN_FONT = 8
|
||||
MAX_FONT = 40
|
||||
DEFAULT_FONT = 11
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
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):
|
||||
self._font_size = max(self.MIN_FONT, min(self.MAX_FONT, int(size)))
|
||||
font = self.font()
|
||||
font.setPointSize(self._font_size)
|
||||
self.setFont(font)
|
||||
|
||||
def font_size(self):
|
||||
return self._font_size
|
||||
|
||||
|
||||
class NoWheelSpinBox(QSpinBox):
|
||||
def wheelEvent(self, event):
|
||||
event.ignore()
|
||||
@@ -191,6 +222,17 @@ def match_rule(hex_text: str, data: bytes) -> bool:
|
||||
return find_match(hex_text, data) != -1
|
||||
|
||||
APP_NAME = "网络调试助手"
|
||||
|
||||
|
||||
def app_config_path():
|
||||
"""返回自动保存配置的文件路径(与主程序同目录)"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
base = os.path.dirname(sys.executable)
|
||||
else:
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
return os.path.join(base, 'app_config.json')
|
||||
|
||||
|
||||
BAUDS = ['1200', '2400', '4800', '9600', '19200', '38400', '57600', '115200']
|
||||
PARITIES = {'无校验': serial.PARITY_NONE, '奇校验': serial.PARITY_ODD,
|
||||
'偶校验': serial.PARITY_EVEN}
|
||||
@@ -975,10 +1017,10 @@ class BasePanel(QWidget):
|
||||
rv = QVBoxLayout(recv_widget)
|
||||
rv.setContentsMargins(0, 0, 0, 0)
|
||||
rv.addWidget(QLabel('接收区'))
|
||||
self.recv = QPlainTextEdit()
|
||||
self.recv = RecvEdit()
|
||||
self.recv.setReadOnly(True)
|
||||
self.recv.setMaximumBlockCount(2000)
|
||||
self.recv.setStyleSheet('QPlainTextEdit{font-family:Consolas;font-size:11pt}')
|
||||
self.recv.setFont(QFont('Consolas', RecvEdit.DEFAULT_FONT))
|
||||
rv.addWidget(self.recv, 1)
|
||||
splitter.addWidget(recv_widget)
|
||||
|
||||
@@ -1341,9 +1383,91 @@ class BasePanel(QWidget):
|
||||
if dlg.exec() == QDialog.Accepted:
|
||||
self.reply_rules = dlg.result()
|
||||
|
||||
def collect_config(self):
|
||||
return {
|
||||
'recv': {
|
||||
'display': 'HEX' if self.rb_recv_hex.isChecked() else 'ASCII',
|
||||
'ts': self.chk_ts.isChecked(),
|
||||
'info': self.chk_info.isChecked(),
|
||||
},
|
||||
'send': {
|
||||
'format': 'HEX' if self.rb_send_hex.isChecked() else '文本',
|
||||
'enter_send': self.chk_enter_send.isChecked(),
|
||||
'crlf': self.chk_crlf.isChecked(),
|
||||
'auto': self.chk_auto.isChecked(),
|
||||
'interval': self.spin_interval.value(),
|
||||
},
|
||||
'reply': {
|
||||
'enabled': self.chk_reply.isChecked(),
|
||||
'parse': 'HEX' if self.rb_parse_hex.isChecked() else 'ASCII',
|
||||
'respond': 'HEX' if self.rb_reply_hex.isChecked() else 'ASCII',
|
||||
},
|
||||
'font_size': self.recv.font_size(),
|
||||
'reply_rules': copy.deepcopy(self.reply_rules),
|
||||
'batch_rows': copy.deepcopy(self.batch_rows),
|
||||
}
|
||||
|
||||
def apply_config(self, cfg):
|
||||
if not isinstance(cfg, dict):
|
||||
return []
|
||||
msg = []
|
||||
recv = cfg.get('recv')
|
||||
if isinstance(recv, dict):
|
||||
display = recv.get('display', 'HEX')
|
||||
self.rb_recv_hex.setChecked(display == 'HEX')
|
||||
self.rb_recv_ascii.setChecked(display != 'HEX')
|
||||
if 'ts' in recv:
|
||||
self.chk_ts.setChecked(bool(recv['ts']))
|
||||
if 'info' in recv:
|
||||
self.chk_info.setChecked(bool(recv['info']))
|
||||
msg.append('接收设置')
|
||||
send = cfg.get('send')
|
||||
if isinstance(send, dict):
|
||||
fmt = send.get('format', '文本')
|
||||
self.rb_send_hex.setChecked(fmt == 'HEX')
|
||||
self.rb_send_text.setChecked(fmt != 'HEX')
|
||||
if 'enter_send' in send:
|
||||
self.chk_enter_send.setChecked(bool(send['enter_send']))
|
||||
if 'crlf' in send:
|
||||
self.chk_crlf.setChecked(bool(send['crlf']))
|
||||
if 'auto' in send:
|
||||
self.chk_auto.setChecked(bool(send['auto']))
|
||||
if 'interval' in send:
|
||||
self.spin_interval.setValue(int(send['interval']))
|
||||
msg.append('发送设置')
|
||||
reply = cfg.get('reply')
|
||||
if isinstance(reply, dict):
|
||||
if 'enabled' in reply:
|
||||
self.chk_reply.setChecked(bool(reply['enabled']))
|
||||
if 'parse' in reply:
|
||||
self.rb_parse_hex.setChecked(reply['parse'] == 'HEX')
|
||||
self.rb_parse_ascii.setChecked(reply['parse'] != 'HEX')
|
||||
if 'respond' in reply:
|
||||
self.rb_reply_hex.setChecked(reply['respond'] == 'HEX')
|
||||
self.rb_reply_ascii.setChecked(reply['respond'] != 'HEX')
|
||||
msg.append('应答设置')
|
||||
if 'font_size' in cfg:
|
||||
try:
|
||||
self.recv.set_font_size(int(cfg['font_size']))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
msg.append('字体大小')
|
||||
rr = cfg.get('reply_rules')
|
||||
if isinstance(rr, list) and all(isinstance(x, dict) for x in rr):
|
||||
for r in rr:
|
||||
r.setdefault('enabled', True)
|
||||
self.reply_rules = rr
|
||||
msg.append(f'应答配置 {len(rr)} 条')
|
||||
br = cfg.get('batch_rows')
|
||||
if isinstance(br, list) and all(isinstance(x, dict) for x in br):
|
||||
self.batch_rows = [{'enabled': bool(x.get('enabled', True)),
|
||||
'delay': int(x.get('delay', 0)),
|
||||
'text': str(x.get('text', ''))} for x in br]
|
||||
msg.append(f'批量发送 {len(br)} 条')
|
||||
return msg
|
||||
|
||||
def export_config(self):
|
||||
data = {'reply_rules': copy.deepcopy(self.reply_rules),
|
||||
'batch_rows': copy.deepcopy(self.batch_rows)}
|
||||
data = self.collect_config()
|
||||
default = f'config_{datetime.now():%Y%m%d_%H%M%S}.json'
|
||||
path, _ = QFileDialog.getSaveFileName(self, '导出配置', default,
|
||||
'JSON 文件 (*.json);;所有文件 (*)')
|
||||
@@ -1370,22 +1494,7 @@ class BasePanel(QWidget):
|
||||
if not isinstance(data, dict):
|
||||
QMessageBox.warning(self, '导入失败', '配置文件格式无效')
|
||||
return
|
||||
msg = []
|
||||
rr = data.get('reply_rules')
|
||||
if isinstance(rr, list) and rr and all(isinstance(x, dict) for x in rr):
|
||||
for r in rr:
|
||||
r.setdefault('enabled', True)
|
||||
self.reply_rules = rr
|
||||
msg.append(f'应答配置 {len(rr)} 条')
|
||||
br = data.get('batch_rows')
|
||||
if isinstance(br, list):
|
||||
if not all(isinstance(x, dict) for x in br):
|
||||
QMessageBox.warning(self, '导入失败', '批量发送配置格式无效')
|
||||
return
|
||||
self.batch_rows = [{'enabled': bool(x.get('enabled', True)),
|
||||
'delay': int(x.get('delay', 0)),
|
||||
'text': str(x.get('text', ''))} for x in br]
|
||||
msg.append(f'批量发送 {len(br)} 条')
|
||||
msg = self.apply_config(data)
|
||||
if not msg:
|
||||
QMessageBox.warning(self, '导入失败', '配置文件中没有可导入的数据')
|
||||
return
|
||||
@@ -1847,11 +1956,7 @@ class NetPanel(BasePanel):
|
||||
QMessageBox.warning(self, '错误', f'发送失败\n{e}')
|
||||
return False
|
||||
# TCP 服务器
|
||||
sel = self.combox_client.currentText()
|
||||
with self.lock:
|
||||
if sel and sel in self.clients:
|
||||
targets = [self.clients[sel]]
|
||||
else:
|
||||
targets = list(self.clients.values())
|
||||
if not targets:
|
||||
QMessageBox.warning(self, '提示', '没有已连接的客户端')
|
||||
@@ -1885,10 +1990,85 @@ class MainWindow(QMainWindow):
|
||||
if icon:
|
||||
self.setWindowIcon(QIcon(icon))
|
||||
tabs = QTabWidget()
|
||||
tabs.addTab(SerialPanel(), '串口')
|
||||
tabs.addTab(NetPanel('tcp'), 'TCP')
|
||||
tabs.addTab(NetPanel('udp'), 'UDP')
|
||||
self.serial_panel = SerialPanel()
|
||||
self.tcp_panel = NetPanel('tcp')
|
||||
self.udp_panel = NetPanel('udp')
|
||||
tabs.addTab(self.serial_panel, '串口')
|
||||
tabs.addTab(self.tcp_panel, 'TCP')
|
||||
tabs.addTab(self.udp_panel, 'UDP')
|
||||
self.setCentralWidget(tabs)
|
||||
self.top_btn = QCheckBox('窗口置顶')
|
||||
self.top_btn.setToolTip('选中后窗口保持在最前端')
|
||||
self.top_btn.toggled.connect(self._toggle_topmost)
|
||||
tb = QToolBar('选项')
|
||||
tb.setMovable(False)
|
||||
tb.addWidget(self.top_btn)
|
||||
self.addToolBar(tb)
|
||||
self._load_app_config()
|
||||
|
||||
def _toggle_topmost(self, on: bool):
|
||||
try:
|
||||
import ctypes
|
||||
user32 = ctypes.windll.user32
|
||||
user32.SetWindowPos.argtypes = [
|
||||
ctypes.c_void_p, ctypes.c_void_p,
|
||||
ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int,
|
||||
ctypes.c_uint,
|
||||
]
|
||||
user32.SetWindowPos.restype = ctypes.c_int
|
||||
HWND_TOPMOST = -1
|
||||
HWND_NOTOPMOST = -2
|
||||
SWP_NOMOVE = 0x0002
|
||||
SWP_NOSIZE = 0x0001
|
||||
SWP_NOACTIVATE = 0x0010
|
||||
ok = user32.SetWindowPos(
|
||||
ctypes.c_void_p(int(self.winId())),
|
||||
ctypes.c_void_p(HWND_TOPMOST if on else HWND_NOTOPMOST),
|
||||
0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE) != 0
|
||||
if ok:
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
self._fallback_topmost(on)
|
||||
|
||||
def _fallback_topmost(self, on: bool):
|
||||
QTimer.singleShot(0, lambda: (self.setWindowFlag(Qt.WindowStaysOnTopHint, on),
|
||||
self.show()))
|
||||
|
||||
def _save_app_config(self):
|
||||
data = {'topmost': self.top_btn.isChecked(),
|
||||
'tabs': {
|
||||
'serial': self.serial_panel.collect_config(),
|
||||
'tcp': self.tcp_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):
|
||||
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
|
||||
if isinstance(data, dict) and data.get('topmost'):
|
||||
self.top_btn.setChecked(True)
|
||||
if not isinstance(tabs_cfg, dict):
|
||||
return
|
||||
for panel, key in ((self.serial_panel, 'serial'),
|
||||
(self.tcp_panel, 'tcp'),
|
||||
(self.udp_panel, 'udp')):
|
||||
cfg = tabs_cfg.get(key)
|
||||
if isinstance(cfg, dict):
|
||||
panel.apply_config(cfg)
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._save_app_config()
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user