新增窗口置顶、接收区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` 自动保存/恢复。
|
||||||
+208
-28
@@ -26,6 +26,7 @@ from PySide6.QtWidgets import (
|
|||||||
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
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)',
|
||||||
@@ -132,6 +133,36 @@ class SendEdit(QPlainTextEdit):
|
|||||||
super().keyPressEvent(e)
|
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):
|
class NoWheelSpinBox(QSpinBox):
|
||||||
def wheelEvent(self, event):
|
def wheelEvent(self, event):
|
||||||
event.ignore()
|
event.ignore()
|
||||||
@@ -191,6 +222,17 @@ def match_rule(hex_text: str, data: bytes) -> bool:
|
|||||||
return find_match(hex_text, data) != -1
|
return find_match(hex_text, data) != -1
|
||||||
|
|
||||||
APP_NAME = "网络调试助手"
|
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']
|
BAUDS = ['1200', '2400', '4800', '9600', '19200', '38400', '57600', '115200']
|
||||||
PARITIES = {'无校验': serial.PARITY_NONE, '奇校验': serial.PARITY_ODD,
|
PARITIES = {'无校验': serial.PARITY_NONE, '奇校验': serial.PARITY_ODD,
|
||||||
'偶校验': serial.PARITY_EVEN}
|
'偶校验': serial.PARITY_EVEN}
|
||||||
@@ -975,10 +1017,10 @@ class BasePanel(QWidget):
|
|||||||
rv = QVBoxLayout(recv_widget)
|
rv = QVBoxLayout(recv_widget)
|
||||||
rv.setContentsMargins(0, 0, 0, 0)
|
rv.setContentsMargins(0, 0, 0, 0)
|
||||||
rv.addWidget(QLabel('接收区'))
|
rv.addWidget(QLabel('接收区'))
|
||||||
self.recv = QPlainTextEdit()
|
self.recv = RecvEdit()
|
||||||
self.recv.setReadOnly(True)
|
self.recv.setReadOnly(True)
|
||||||
self.recv.setMaximumBlockCount(2000)
|
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)
|
rv.addWidget(self.recv, 1)
|
||||||
splitter.addWidget(recv_widget)
|
splitter.addWidget(recv_widget)
|
||||||
|
|
||||||
@@ -1341,9 +1383,91 @@ class BasePanel(QWidget):
|
|||||||
if dlg.exec() == QDialog.Accepted:
|
if dlg.exec() == QDialog.Accepted:
|
||||||
self.reply_rules = dlg.result()
|
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):
|
def export_config(self):
|
||||||
data = {'reply_rules': copy.deepcopy(self.reply_rules),
|
data = self.collect_config()
|
||||||
'batch_rows': copy.deepcopy(self.batch_rows)}
|
|
||||||
default = f'config_{datetime.now():%Y%m%d_%H%M%S}.json'
|
default = f'config_{datetime.now():%Y%m%d_%H%M%S}.json'
|
||||||
path, _ = QFileDialog.getSaveFileName(self, '导出配置', default,
|
path, _ = QFileDialog.getSaveFileName(self, '导出配置', default,
|
||||||
'JSON 文件 (*.json);;所有文件 (*)')
|
'JSON 文件 (*.json);;所有文件 (*)')
|
||||||
@@ -1370,22 +1494,7 @@ class BasePanel(QWidget):
|
|||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
QMessageBox.warning(self, '导入失败', '配置文件格式无效')
|
QMessageBox.warning(self, '导入失败', '配置文件格式无效')
|
||||||
return
|
return
|
||||||
msg = []
|
msg = self.apply_config(data)
|
||||||
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)} 条')
|
|
||||||
if not msg:
|
if not msg:
|
||||||
QMessageBox.warning(self, '导入失败', '配置文件中没有可导入的数据')
|
QMessageBox.warning(self, '导入失败', '配置文件中没有可导入的数据')
|
||||||
return
|
return
|
||||||
@@ -1847,12 +1956,8 @@ class NetPanel(BasePanel):
|
|||||||
QMessageBox.warning(self, '错误', f'发送失败\n{e}')
|
QMessageBox.warning(self, '错误', f'发送失败\n{e}')
|
||||||
return False
|
return False
|
||||||
# TCP 服务器
|
# TCP 服务器
|
||||||
sel = self.combox_client.currentText()
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
if sel and sel in self.clients:
|
targets = list(self.clients.values())
|
||||||
targets = [self.clients[sel]]
|
|
||||||
else:
|
|
||||||
targets = list(self.clients.values())
|
|
||||||
if not targets:
|
if not targets:
|
||||||
QMessageBox.warning(self, '提示', '没有已连接的客户端')
|
QMessageBox.warning(self, '提示', '没有已连接的客户端')
|
||||||
return False
|
return False
|
||||||
@@ -1885,10 +1990,85 @@ class MainWindow(QMainWindow):
|
|||||||
if icon:
|
if icon:
|
||||||
self.setWindowIcon(QIcon(icon))
|
self.setWindowIcon(QIcon(icon))
|
||||||
tabs = QTabWidget()
|
tabs = QTabWidget()
|
||||||
tabs.addTab(SerialPanel(), '串口')
|
self.serial_panel = SerialPanel()
|
||||||
tabs.addTab(NetPanel('tcp'), 'TCP')
|
self.tcp_panel = NetPanel('tcp')
|
||||||
tabs.addTab(NetPanel('udp'), 'UDP')
|
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.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__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Reference in New Issue
Block a user