587 lines
21 KiB
Python
587 lines
21 KiB
Python
# pip install pyserial PyQt5
|
||
|
||
import sys
|
||
import re
|
||
import serial
|
||
import serial.tools.list_ports
|
||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||
QLabel, QComboBox, QPushButton, QTextEdit, QLineEdit,
|
||
QSpinBox, QCheckBox, QMessageBox, QGroupBox, QSplitter,
|
||
QScrollArea, QSizePolicy, QFrame)
|
||
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QDateTime, QTimer
|
||
from PyQt5.QtGui import QTextCursor, QColor, QTextCharFormat, QFont
|
||
|
||
|
||
class SerialThread(QThread):
|
||
data_received = pyqtSignal(str, str) # data, timestamp
|
||
|
||
def __init__(self, serial_port):
|
||
super().__init__()
|
||
self.serial_port = serial_port
|
||
self.running = True
|
||
|
||
def run(self):
|
||
while self.running and self.serial_port.is_open:
|
||
try:
|
||
if self.serial_port.in_waiting:
|
||
data = self.serial_port.read(self.serial_port.in_waiting).decode('utf-8', errors='ignore')
|
||
timestamp = QDateTime.currentDateTime().toString("hh:mm:ss.zzz")
|
||
self.data_received.emit(data, timestamp)
|
||
except Exception as e:
|
||
print(f"Serial read error: {e}")
|
||
break
|
||
|
||
def stop(self):
|
||
self.running = False
|
||
|
||
|
||
class SerialDebugger(QMainWindow):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.serial = None
|
||
self.serial_thread = None
|
||
self.auto_send_timer = None
|
||
self.send_count = 0
|
||
self.receive_count = 0
|
||
self.init_ui()
|
||
self.refresh_ports()
|
||
|
||
def init_ui(self):
|
||
self.setWindowTitle('高级串口调试助手')
|
||
self.setGeometry(100, 100, 1200, 800)
|
||
|
||
# 主窗口部件
|
||
main_widget = QWidget()
|
||
self.setCentralWidget(main_widget)
|
||
main_layout = QHBoxLayout(main_widget)
|
||
main_layout.setContentsMargins(5, 5, 5, 5)
|
||
|
||
# 左侧控制面板
|
||
control_panel = QWidget()
|
||
control_panel.setMaximumWidth(300)
|
||
control_layout = QVBoxLayout(control_panel)
|
||
control_layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
# 串口设置区域
|
||
port_group = QGroupBox("串口设置")
|
||
port_layout = QVBoxLayout()
|
||
|
||
# 串口选择
|
||
port_select_layout = QHBoxLayout()
|
||
self.port_label = QLabel('串口:')
|
||
self.port_combo = QComboBox()
|
||
self.refresh_btn = QPushButton('刷新')
|
||
self.refresh_btn.clicked.connect(self.refresh_ports)
|
||
port_select_layout.addWidget(self.port_label)
|
||
port_select_layout.addWidget(self.port_combo)
|
||
port_select_layout.addWidget(self.refresh_btn)
|
||
port_layout.addLayout(port_select_layout)
|
||
|
||
# 波特率选择
|
||
baud_layout = QHBoxLayout()
|
||
self.baud_label = QLabel('波特率:')
|
||
self.baud_combo = QComboBox()
|
||
common_baudrates = ['9600', '19200', '38400', '57600', '115200',
|
||
'230400', '460800', '500000', '576000', '750000',
|
||
'921600', '1000000', '1152000', '1500000', '2000000']
|
||
self.baud_combo.addItems(common_baudrates)
|
||
self.baud_combo.setCurrentText('2000000')
|
||
self.baud_combo.setEditable(True)
|
||
baud_layout.addWidget(self.baud_label)
|
||
baud_layout.addWidget(self.baud_combo)
|
||
port_layout.addLayout(baud_layout)
|
||
|
||
# 其他串口参数
|
||
param_layout = QHBoxLayout()
|
||
self.data_bits_label = QLabel('数据位:')
|
||
self.data_bits_combo = QComboBox()
|
||
self.data_bits_combo.addItems(['5', '6', '7', '8'])
|
||
self.data_bits_combo.setCurrentText('8')
|
||
|
||
self.stop_bits_label = QLabel('停止位:')
|
||
self.stop_bits_combo = QComboBox()
|
||
self.stop_bits_combo.addItems(['1', '1.5', '2'])
|
||
self.stop_bits_combo.setCurrentText('1')
|
||
|
||
param_layout.addWidget(self.data_bits_label)
|
||
param_layout.addWidget(self.data_bits_combo)
|
||
param_layout.addWidget(self.stop_bits_label)
|
||
param_layout.addWidget(self.stop_bits_combo)
|
||
port_layout.addLayout(param_layout)
|
||
|
||
# 校验位
|
||
parity_layout = QHBoxLayout()
|
||
self.parity_label = QLabel('校验位:')
|
||
self.parity_combo = QComboBox()
|
||
self.parity_combo.addItems(['无', '奇校验', '偶校验', '标记', '空格'])
|
||
self.parity_combo.setCurrentText('无')
|
||
parity_layout.addWidget(self.parity_label)
|
||
parity_layout.addWidget(self.parity_combo)
|
||
port_layout.addLayout(parity_layout)
|
||
|
||
# 控制按钮
|
||
ctrl_btn_layout = QHBoxLayout()
|
||
self.open_btn = QPushButton('打开串口')
|
||
self.open_btn.clicked.connect(self.toggle_serial)
|
||
self.dtr_check = QCheckBox('DTR')
|
||
self.dtr_check.stateChanged.connect(self.set_dtr)
|
||
self.rts_check = QCheckBox('RTS')
|
||
self.rts_check.stateChanged.connect(self.set_rts)
|
||
|
||
ctrl_btn_layout.addWidget(self.open_btn)
|
||
ctrl_btn_layout.addWidget(self.dtr_check)
|
||
ctrl_btn_layout.addWidget(self.rts_check)
|
||
port_layout.addLayout(ctrl_btn_layout)
|
||
|
||
port_group.setLayout(port_layout)
|
||
control_layout.addWidget(port_group)
|
||
|
||
# 接收选项
|
||
rx_group = QGroupBox("接收设置")
|
||
rx_layout = QVBoxLayout()
|
||
|
||
self.hex_display = QCheckBox('十六进制显示')
|
||
self.hex_display.stateChanged.connect(self.update_receive_display)
|
||
|
||
self.timestamp_check = QCheckBox('显示时间戳')
|
||
# self.timestamp_check.setChecked(True)
|
||
|
||
self.terminal_mode = QCheckBox('自动滚动')
|
||
self.terminal_mode.setChecked(True)
|
||
self.terminal_mode.stateChanged.connect(self.toggle_terminal_mode)
|
||
|
||
self.clear_rx_btn = QPushButton('清空接收区')
|
||
self.clear_rx_btn.clicked.connect(self.clear_receive)
|
||
|
||
rx_layout.addWidget(self.hex_display)
|
||
rx_layout.addWidget(self.timestamp_check)
|
||
rx_layout.addWidget(self.terminal_mode)
|
||
rx_layout.addWidget(self.clear_rx_btn)
|
||
rx_group.setLayout(rx_layout)
|
||
control_layout.addWidget(rx_group)
|
||
|
||
# 发送选项
|
||
tx_group = QGroupBox("发送设置")
|
||
tx_layout = QVBoxLayout()
|
||
|
||
self.hex_send = QCheckBox('十六进制发送')
|
||
|
||
self.auto_send_check = QCheckBox('自动发送')
|
||
self.auto_send_check.stateChanged.connect(self.toggle_auto_send)
|
||
self.auto_send_interval = QSpinBox()
|
||
self.auto_send_interval.setRange(100, 10000)
|
||
self.auto_send_interval.setValue(1000)
|
||
self.auto_send_interval.setSuffix('ms')
|
||
|
||
auto_send_layout = QHBoxLayout()
|
||
auto_send_layout.addWidget(self.auto_send_check)
|
||
auto_send_layout.addWidget(self.auto_send_interval)
|
||
|
||
self.clear_tx_btn = QPushButton('清空发送区')
|
||
self.clear_tx_btn.clicked.connect(lambda: self.send_text.clear())
|
||
|
||
tx_layout.addWidget(self.hex_send)
|
||
tx_layout.addLayout(auto_send_layout)
|
||
tx_layout.addWidget(self.clear_tx_btn)
|
||
tx_group.setLayout(tx_layout)
|
||
control_layout.addWidget(tx_group)
|
||
|
||
# 自定义命令区域(可折叠)
|
||
self.cmd_group = QGroupBox("自定义命令 ▼")
|
||
self.cmd_group.setCheckable(True)
|
||
self.cmd_group.setChecked(True)
|
||
self.cmd_group.toggled.connect(self.toggle_cmd_panel)
|
||
cmd_layout = QVBoxLayout()
|
||
|
||
# 命令列表
|
||
self.cmd_scroll = QScrollArea()
|
||
self.cmd_scroll.setWidgetResizable(True)
|
||
self.cmd_scroll_widget = QWidget()
|
||
self.cmd_scroll_layout = QVBoxLayout(self.cmd_scroll_widget)
|
||
self.cmd_scroll_layout.setContentsMargins(2, 2, 2, 2)
|
||
self.cmd_scroll.setWidget(self.cmd_scroll_widget)
|
||
|
||
# 添加命令控件
|
||
add_cmd_layout = QHBoxLayout()
|
||
self.new_cmd_input = QLineEdit()
|
||
self.new_cmd_input.setPlaceholderText("输入新命令")
|
||
self.add_cmd_btn = QPushButton('添加')
|
||
self.add_cmd_btn.clicked.connect(self.add_custom_command)
|
||
add_cmd_layout.addWidget(self.new_cmd_input)
|
||
add_cmd_layout.addWidget(self.add_cmd_btn)
|
||
|
||
cmd_layout.addWidget(self.cmd_scroll)
|
||
cmd_layout.addLayout(add_cmd_layout)
|
||
self.cmd_group.setLayout(cmd_layout)
|
||
control_layout.addWidget(self.cmd_group)
|
||
|
||
# 添加一些示例命令
|
||
self.add_example_commands()
|
||
|
||
control_layout.addStretch()
|
||
main_layout.addWidget(control_panel)
|
||
|
||
# 右侧通信区域
|
||
comm_panel = QWidget()
|
||
comm_layout = QVBoxLayout(comm_panel)
|
||
comm_layout.setContentsMargins(5, 0, 0, 0)
|
||
|
||
# 接收区
|
||
rx_frame = QFrame()
|
||
rx_frame.setFrameShape(QFrame.StyledPanel)
|
||
rx_frame_layout = QVBoxLayout(rx_frame)
|
||
rx_frame_layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
self.receive_text = QTextEdit()
|
||
self.receive_text.setReadOnly(True)
|
||
self.receive_text.setLineWrapMode(QTextEdit.NoWrap)
|
||
font = QFont("Courier New", 10)
|
||
self.receive_text.setFont(font)
|
||
|
||
rx_frame_layout.addWidget(self.receive_text)
|
||
comm_layout.addWidget(rx_frame)
|
||
|
||
# 发送区
|
||
tx_frame = QFrame()
|
||
tx_frame.setFrameShape(QFrame.StyledPanel)
|
||
tx_frame_layout = QVBoxLayout(tx_frame)
|
||
tx_frame_layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
self.send_text = QTextEdit()
|
||
self.send_text.setFont(font)
|
||
|
||
send_btn_layout = QHBoxLayout()
|
||
self.send_btn = QPushButton('发送')
|
||
self.send_btn.clicked.connect(self.send_data)
|
||
send_btn_layout.addStretch()
|
||
send_btn_layout.addWidget(self.send_btn)
|
||
|
||
tx_frame_layout.addWidget(self.send_text)
|
||
tx_frame_layout.addLayout(send_btn_layout)
|
||
comm_layout.addWidget(tx_frame)
|
||
|
||
main_layout.addWidget(comm_panel, stretch=1)
|
||
|
||
# 状态栏
|
||
self.status_bar = self.statusBar()
|
||
self.status_label = QLabel("发送: 0 字节 | 接收: 0 字节")
|
||
self.status_bar.addPermanentWidget(self.status_label)
|
||
self.status_bar.showMessage('就绪')
|
||
|
||
# 颜色格式
|
||
self.default_format = QTextCharFormat()
|
||
self.default_format.setForeground(QColor('black'))
|
||
|
||
self.error_format = QTextCharFormat()
|
||
self.error_format.setForeground(QColor('red'))
|
||
|
||
self.warning_format = QTextCharFormat()
|
||
self.warning_format.setForeground(QColor('orange'))
|
||
|
||
self.success_format = QTextCharFormat()
|
||
self.success_format.setForeground(QColor('green'))
|
||
|
||
self.info_format = QTextCharFormat()
|
||
self.info_format.setForeground(QColor('blue'))
|
||
|
||
def add_example_commands(self):
|
||
"""添加一些示例命令"""
|
||
example_commands = [
|
||
("55 55 55 55 55 55", True),
|
||
("BOUFFALOLAB5555DTR1", False),
|
||
("BOUFFALOLAB5555DTR0", False),
|
||
("BOUFFALOLAB5555RTS0", False),
|
||
("BOUFFALOLAB5555RTS1", False)
|
||
]
|
||
|
||
for cmd, is_hex in example_commands:
|
||
self.add_command_to_panel(cmd, is_hex)
|
||
|
||
def add_command_to_panel(self, cmd, is_hex=False):
|
||
"""添加一个命令到命令面板"""
|
||
cmd_widget = QWidget()
|
||
cmd_layout = QHBoxLayout(cmd_widget)
|
||
cmd_layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
cmd_label = QLabel(cmd)
|
||
cmd_label.setWordWrap(True)
|
||
|
||
hex_check = QCheckBox('HEX')
|
||
hex_check.setChecked(is_hex)
|
||
|
||
send_btn = QPushButton('发送')
|
||
send_btn.setMaximumWidth(60)
|
||
send_btn.clicked.connect(lambda _, c=cmd, h=hex_check: self.send_custom_command(c, h.isChecked()))
|
||
|
||
del_btn = QPushButton('×')
|
||
del_btn.setMaximumWidth(30)
|
||
del_btn.clicked.connect(lambda _, w=cmd_widget: self.remove_command(w))
|
||
|
||
cmd_layout.addWidget(cmd_label, stretch=1)
|
||
cmd_layout.addWidget(hex_check)
|
||
cmd_layout.addWidget(send_btn)
|
||
cmd_layout.addWidget(del_btn)
|
||
|
||
self.cmd_scroll_layout.addWidget(cmd_widget)
|
||
|
||
def remove_command(self, widget):
|
||
"""从命令面板移除一个命令"""
|
||
self.cmd_scroll_layout.removeWidget(widget)
|
||
widget.deleteLater()
|
||
|
||
def toggle_cmd_panel(self, checked):
|
||
"""切换命令面板的展开/折叠状态"""
|
||
if checked:
|
||
self.cmd_group.setTitle("自定义命令 ▼")
|
||
else:
|
||
self.cmd_group.setTitle("自定义命令 ►")
|
||
|
||
def refresh_ports(self):
|
||
self.port_combo.clear()
|
||
ports = serial.tools.list_ports.comports()
|
||
for port in ports:
|
||
self.port_combo.addItem(port.device)
|
||
if not ports:
|
||
self.port_combo.addItem('无可用串口')
|
||
|
||
def toggle_serial(self):
|
||
if self.serial and self.serial.is_open:
|
||
self.close_serial()
|
||
self.open_btn.setText('打开串口')
|
||
self.status_bar.showMessage('串口已关闭')
|
||
else:
|
||
if self.open_serial():
|
||
self.open_btn.setText('关闭串口')
|
||
self.status_bar.showMessage('串口已打开')
|
||
|
||
def open_serial(self):
|
||
port = self.port_combo.currentText()
|
||
if not port or port == '无可用串口':
|
||
QMessageBox.warning(self, '警告', '没有可用的串口!')
|
||
return False
|
||
|
||
try:
|
||
baudrate = int(self.baud_combo.currentText())
|
||
except ValueError:
|
||
QMessageBox.warning(self, '警告', '请输入有效的波特率!')
|
||
return False
|
||
|
||
# 获取其他串口参数
|
||
bytesize = {
|
||
'5': serial.FIVEBITS,
|
||
'6': serial.SIXBITS,
|
||
'7': serial.SEVENBITS,
|
||
'8': serial.EIGHTBITS
|
||
}.get(self.data_bits_combo.currentText(), serial.EIGHTBITS)
|
||
|
||
stopbits = {
|
||
'1': serial.STOPBITS_ONE,
|
||
'1.5': serial.STOPBITS_ONE_POINT_FIVE,
|
||
'2': serial.STOPBITS_TWO
|
||
}.get(self.stop_bits_combo.currentText(), serial.STOPBITS_ONE)
|
||
|
||
parity = {
|
||
'无': serial.PARITY_NONE,
|
||
'奇校验': serial.PARITY_ODD,
|
||
'偶校验': serial.PARITY_EVEN,
|
||
'标记': serial.PARITY_MARK,
|
||
'空格': serial.PARITY_SPACE
|
||
}.get(self.parity_combo.currentText(), serial.PARITY_NONE)
|
||
|
||
try:
|
||
self.serial = serial.Serial(
|
||
port=port,
|
||
baudrate=baudrate,
|
||
bytesize=bytesize,
|
||
parity=parity,
|
||
stopbits=stopbits,
|
||
timeout=1
|
||
)
|
||
|
||
# 设置初始DTR/RTS状态
|
||
self.serial.dtr = self.dtr_check.isChecked()
|
||
self.serial.rts = self.rts_check.isChecked()
|
||
|
||
self.serial_thread = SerialThread(self.serial)
|
||
self.serial_thread.data_received.connect(self.receive_data)
|
||
self.serial_thread.start()
|
||
|
||
return True
|
||
except Exception as e:
|
||
QMessageBox.critical(self, '错误', f'无法打开串口: {e}')
|
||
return False
|
||
|
||
def close_serial(self):
|
||
self.toggle_auto_send(False)
|
||
|
||
if self.serial_thread:
|
||
self.serial_thread.stop()
|
||
self.serial_thread.wait()
|
||
self.serial_thread = None
|
||
|
||
if self.serial and self.serial.is_open:
|
||
self.serial.close()
|
||
self.serial = None
|
||
|
||
def receive_data(self, data, timestamp):
|
||
self.receive_count += len(data.encode('utf-8'))
|
||
self.update_status()
|
||
|
||
cursor = self.receive_text.textCursor()
|
||
cursor.movePosition(QTextCursor.End)
|
||
|
||
if self.timestamp_check.isChecked():
|
||
timestamp_str = f"[{timestamp}] "
|
||
cursor.insertText(timestamp_str, self.default_format)
|
||
|
||
# 智能颜色判断
|
||
fmt = self.determine_text_format(data)
|
||
|
||
if self.hex_display.isChecked():
|
||
hex_data = ' '.join([f'{ord(c):02X}' for c in data])
|
||
cursor.insertText(hex_data, fmt)
|
||
else:
|
||
cursor.insertText(data, fmt)
|
||
|
||
if self.terminal_mode.isChecked():
|
||
cursor.movePosition(QTextCursor.End)
|
||
self.receive_text.setTextCursor(cursor)
|
||
|
||
def determine_text_format(self, text):
|
||
# 根据内容判断使用哪种颜色格式
|
||
text_lower = text.lower()
|
||
'''
|
||
if re.search(r'error|fail|failed|err|非法|错误', text_lower):
|
||
return self.error_format
|
||
elif re.search(r'warn|alert|警告|注意', text_lower):
|
||
return self.warning_format
|
||
elif re.search(r'success|ok|完成|成功', text_lower):
|
||
return self.success_format
|
||
elif re.search(r'info|version|信息|版本', text_lower):
|
||
return self.info_format
|
||
else:
|
||
return self.default_format
|
||
'''
|
||
return self.default_format
|
||
|
||
def send_data(self):
|
||
if not self.serial or not self.serial.is_open:
|
||
QMessageBox.warning(self, '警告', '请先打开串口!')
|
||
return
|
||
|
||
text = self.send_text.toPlainText()
|
||
if not text:
|
||
return
|
||
|
||
try:
|
||
if self.hex_send.isChecked():
|
||
# 处理十六进制发送
|
||
text = text.strip()
|
||
hex_bytes = []
|
||
for part in text.split():
|
||
try:
|
||
hex_bytes.append(int(part, 16))
|
||
except ValueError:
|
||
QMessageBox.warning(self, '警告', f'无效的十六进制数据: {part}')
|
||
return
|
||
data = bytes(hex_bytes)
|
||
else:
|
||
data = text.encode('utf-8')
|
||
|
||
self.serial.write(data)
|
||
self.send_count += len(data)
|
||
self.update_status()
|
||
self.status_bar.showMessage(f'已发送 {len(data)} 字节', 2000)
|
||
|
||
except Exception as e:
|
||
QMessageBox.critical(self, '错误', f'发送失败: {e}')
|
||
|
||
def update_receive_display(self):
|
||
# 当切换十六进制显示时,更新显示内容
|
||
if self.serial and self.serial.is_open:
|
||
current_text = self.receive_text.toPlainText()
|
||
if current_text:
|
||
if self.hex_display.isChecked():
|
||
hex_data = ' '.join([f'{ord(c):02X}' for c in current_text if not c.isspace()])
|
||
self.receive_text.setPlainText(hex_data)
|
||
else:
|
||
try:
|
||
# 尝试将十六进制数据转换回字符
|
||
bytes_data = bytes.fromhex(current_text.replace(' ', ''))
|
||
text = bytes_data.decode('utf-8', errors='ignore')
|
||
self.receive_text.setPlainText(text)
|
||
except:
|
||
# 如果不是有效的十六进制数据,保持原样
|
||
pass
|
||
|
||
def toggle_terminal_mode(self, state):
|
||
if state:
|
||
self.receive_text.setLineWrapMode(QTextEdit.NoWrap)
|
||
cursor = self.receive_text.textCursor()
|
||
cursor.movePosition(QTextCursor.End)
|
||
self.receive_text.setTextCursor(cursor)
|
||
else:
|
||
self.receive_text.setLineWrapMode(QTextEdit.WidgetWidth)
|
||
|
||
def set_dtr(self, state):
|
||
if self.serial and self.serial.is_open:
|
||
self.serial.dtr = state == Qt.Checked
|
||
|
||
def set_rts(self, state):
|
||
if self.serial and self.serial.is_open:
|
||
self.serial.rts = state == Qt.Checked
|
||
|
||
def toggle_auto_send(self, state):
|
||
if state:
|
||
interval = self.auto_send_interval.value()
|
||
self.auto_send_timer = QTimer()
|
||
self.auto_send_timer.timeout.connect(self.send_data)
|
||
self.auto_send_timer.start(interval)
|
||
else:
|
||
if self.auto_send_timer:
|
||
self.auto_send_timer.stop()
|
||
self.auto_send_timer = None
|
||
|
||
def add_custom_command(self):
|
||
cmd = self.new_cmd_input.text().strip()
|
||
if cmd:
|
||
self.add_command_to_panel(cmd)
|
||
self.new_cmd_input.clear()
|
||
|
||
def send_custom_command(self, cmd, is_hex):
|
||
if not self.serial or not self.serial.is_open:
|
||
QMessageBox.warning(self, '警告', '请先打开串口!')
|
||
return
|
||
|
||
# 保存当前状态
|
||
current_hex = self.hex_send.isChecked()
|
||
|
||
# 使用命令的HEX设置
|
||
self.hex_send.setChecked(is_hex)
|
||
|
||
# 设置发送文本并发送
|
||
self.send_text.setPlainText(cmd)
|
||
self.send_data()
|
||
|
||
# 恢复原状态
|
||
self.hex_send.setChecked(current_hex)
|
||
|
||
def clear_receive(self):
|
||
self.receive_text.clear()
|
||
self.receive_count = 0
|
||
self.update_status()
|
||
|
||
def update_status(self):
|
||
self.status_label.setText(f"发送: {self.send_count} 字节 | 接收: {self.receive_count} 字节")
|
||
|
||
def closeEvent(self, event):
|
||
self.close_serial()
|
||
event.accept()
|
||
|
||
|
||
if __name__ == '__main__':
|
||
app = QApplication(sys.argv)
|
||
debugger = SerialDebugger()
|
||
debugger.show()
|
||
sys.exit(app.exec_())
|