功能:比对标记、保存修改
1. 安装依赖库
# 安装核心依赖
pip install pyqt5 intelhex
# 安装编译工具
pip install pyinstaller
2. 代码如下:
import sys
import os
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QSplitter, QTextEdit, QLabel, QPushButton, QFileDialog)
from PyQt5.QtCore import Qt, QMimeData
from PyQt5.QtGui import QFont, QColor, QTextCursor, QTextCharFormat
from intelhex import IntelHex, HexReaderError
import tempfile
class DraggableTextEdit(QTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.main_window = parent # 保存主窗口引用
self.setAcceptDrops(True)
self.setStyleSheet("""
QTextEdit {
background-color: #2b2b2b;
color: #a9b7c6;
border: 1px solid #3c3f41;
border-radius: 4px;
padding: 8px;
}
""")
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.acceptProposedAction()
def dropEvent(self, event):
urls = event.mimeData().urls()
if urls:
file_path = urls[0].toLocalFile()
if file_path.lower().endswith('.hex'):
self.main_window.load_hex_file(file_path, self)
class HexComparator(QMainWindow):
def __init__(self):
super().__init__()
self.left_file = None
self.right_file = None
self.init_ui()
def init_ui(self):
self.setWindowTitle("HEX 文件对比工具 v3.13")
self.setGeometry(100, 100, 1280, 800)
self.setStyleSheet("background-color: #3c3f41;")
# 主布局
main_widget = QWidget(self)
self.setCentralWidget(main_widget)
layout = QVBoxLayout(main_widget)
layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(12)
# 分屏视图
self.splitter = QSplitter(Qt.Horizontal)
self.splitter.setHandleWidth(8)
self.splitter.setStyleSheet("""
QSplitter::handle {
background: #4d4f52;
margin: 2px;
}
""")
# 左侧面板
self.left_panel = DraggableTextEdit(self)
self.left_panel.setFont(QFont("Consolas", 11))
# 右侧面板
self.right_panel = DraggableTextEdit(self)
self.right_panel.setFont(QFont("Consolas", 11))
self.splitter.addWidget(self.left_panel)
self.splitter.addWidget(self.right_panel)
layout.addWidget(self.splitter)
# 控制栏
control_bar = QHBoxLayout()
control_bar.setSpacing(12)
# 比较按钮
self.compare_btn = QPushButton("开始比较")
self.compare_btn.setStyleSheet("""
QPushButton {
background-color: #4e5257;
color: #dcdcdc;
border: 1px solid #5a5d63;
border-radius: 4px;
padding: 8px 16px;
min-width: 100px;
}
QPushButton:hover {
background-color: #5a5d63;
}
QPushButton:pressed {
background-color: #4a4d52;
}
""")
self.compare_btn.clicked.connect(self.compare_files)
control_bar.addWidget(self.compare_btn)
# 状态标签
self.status_label = QLabel("拖放HEX文件到左右面板或使用按钮选择文件")
self.status_label.setStyleSheet("color: #9da5b4; font: 10pt 'Segoe UI';")
control_bar.addWidget(self.status_label)
# 文件选择按钮
file_btn_style = """
QPushButton {
background-color: #4e5257;
color: #dcdcdc;
border: 1px solid #5a5d63;
border-radius: 4px;
padding: 8px 16px;
}
QPushButton:hover { background-color: #5a5d63; }
"""
self.left_btn = QPushButton("选择左文件")
self.left_btn.setStyleSheet(file_btn_style)
self.left_btn.clicked.connect(lambda: self.select_file(self.left_panel))
control_bar.addWidget(self.left_btn)
self.right_btn = QPushButton("选择右文件")
self.right_btn.setStyleSheet(file_btn_style)
self.right_btn.clicked.connect(lambda: self.select_file(self.right_panel))
control_bar.addWidget(self.right_btn)
# 添加保存按钮
self.save_btn = QPushButton("保存修改")
self.save_btn.setStyleSheet("background-color: #4CAF50; color: white;")
self.save_btn.clicked.connect(self.manual_save)
control_bar.insertWidget(1, self.save_btn)
layout.addLayout(control_bar)
def select_file(self, target_panel):
file_path, _ = QFileDialog.getOpenFileName(
self, "选择HEX文件", "", "HEX文件 (*.hex)"
)
if file_path:
self.load_hex_file(file_path, target_panel)
def load_hex_file(self, file_path, text_edit):
try:
# 安全断开信号连接
try:
text_edit.textChanged.disconnect()
except TypeError:
pass # 没有连接时忽略错误
# 直接加载文件原始内容
with open(file_path, 'r', encoding='utf-8') as f:
raw_content = f.read()
text_edit.setPlainText(raw_content)
# 记录文件路径
if text_edit == self.left_panel:
self.left_file = file_path
else:
self.right_file = file_path
# 重置修改状态
text_edit.document().setModified(False)
except Exception as e:
text_edit.setPlainText(f"文件加载失败: {str(e)}")
finally:
# 使用安全方式重新连接信号
if text_edit == self.left_panel:
text_edit.textChanged.connect(lambda: self.save_changes(self.left_panel, self.left_file))
else:
text_edit.textChanged.connect(lambda: self.save_changes(self.right_panel, self.right_file))
def format_hex_data(self, data):
lines = []
addresses = sorted(data.keys())
min_addr = min(addresses) if addresses else 0
max_addr = max(addresses) if addresses else 0
# 计算起始地址对齐到16字节边界
start_addr = min_addr - (min_addr % 16)
end_addr = max_addr + (16 - max_addr % 16) if max_addr % 16 != 0 else max_addr
for base in range(start_addr, end_addr + 1, 16):
hex_part = []
ascii_part = []
has_data = False
for offset in range(16):
addr = base + offset
if addr in data:
byte = data[addr]
hex_part.append(f"{byte:02X}")
ascii_part.append(chr(byte) if 32 <= byte <= 126 else "·")
has_data = True
else:
hex_part.append(" ")
ascii_part.append(" ")
if has_data:
# 分两列显示,每列8字节
line = (
f"{base:08X}: "
f"{' '.join(hex_part[:8])} " # 前8字节
f"{' '.join(hex_part[8:])} " # 后8字节
f"|{''.join(ascii_part)}|" # ASCII显示
)
lines.append(line)
return "\n".join(lines)
def save_changes(self, text_edit, file_path):
if not file_path:
raise ValueError("未选择保存文件")
try:
# 检查文件状态
if os.path.exists(file_path):
# 检查是否只读
if not os.access(file_path, os.W_OK):
raise PermissionError(f"文件为只读,请右键文件取消只读属性: {os.path.basename(file_path)}")
# 检查文件是否被占用
if self.is_file_locked(file_path):
raise PermissionError(f"文件被其他程序占用,请关闭: {os.path.basename(file_path)}")
# 使用备用保存方案
temp_success = False
try:
# 方案1: 使用临时文件替换
with tempfile.NamedTemporaryFile(mode='w',
encoding='utf-8',
delete=False,
dir=os.path.dirname(file_path)) as tmp:
tmp.write(text_edit.toPlainText())
tmp.close()
os.replace(tmp.name, file_path)
temp_success = True
except PermissionError:
temp_success = False
if not temp_success:
# 方案2: 直接覆盖写入
with open(file_path, 'w', encoding='utf-8') as f:
f.write(text_edit.toPlainText())
# 更新状态
text_edit.document().setModified(False)
except PermissionError as e:
raise PermissionError(str(e))
except Exception as e:
raise RuntimeError(f"保存失败: {str(e)}")
def is_file_locked(self, filepath):
"""检查文件是否被其他进程锁定"""
try:
# 尝试以追加模式打开文件
with open(filepath, 'a', encoding='utf-8') as f:
pass
return False
except IOError:
return True
def manual_save(self):
try:
# 显式检查文件权限
for path, panel in [(self.left_file, self.left_panel),
(self.right_file, self.right_panel)]:
if path and panel.document().isModified():
if not os.access(path, os.W_OK):
raise PermissionError(f"无权限: {os.path.basename(path)}")
# 执行保存
self.save_changes(panel, path)
self.status_label.setText("所有修改已保存")
except Exception as e:
self.status_label.setText(str(e))
def highlight_differences(self):
# 临时阻止修改信号
self.left_panel.blockSignals(True)
self.right_panel.blockSignals(True)
# 清除旧的高亮
self.clear_highlights()
left_content = self.left_panel.toPlainText().split("\n")
right_content = self.right_panel.toPlainText().split("\n")
# 行级差异高亮
line_format = QTextCharFormat()
line_format.setBackground(QColor(255, 245, 220)) # 浅黄色背景
# 字节级差异高亮
byte_format = QTextCharFormat()
byte_format.setForeground(QColor(255, 0, 0)) # 红色文字
byte_format.setFontWeight(QFont.Bold)
max_lines = max(len(left_content), len(right_content))
for i in range(max_lines):
left_line = left_content[i] if i < len(left_content) else ""
right_line = right_content[i] if i < len(right_content) else ""
# 行级高亮
if left_line != right_line:
self.highlight_line(self.left_panel, i, line_format)
self.highlight_line(self.right_panel, i, line_format)
# 字节级高亮
min_len = min(len(left_line), len(right_line))
for j in range(min_len):
if left_line[j] != right_line[j]:
self.highlight_byte(self.left_panel, i, j, byte_format)
self.highlight_byte(self.right_panel, i, j, byte_format)
# 恢复信号
self.left_panel.blockSignals(False)
self.right_panel.blockSignals(False)
def highlight_line(self, editor, line_num, fmt):
cursor = QTextCursor(editor.document().findBlockByLineNumber(line_num))
cursor.select(QTextCursor.LineUnderCursor)
cursor.mergeCharFormat(fmt)
def highlight_byte(self, editor, line_num, column, fmt):
cursor = QTextCursor(editor.document().findBlockByLineNumber(line_num))
cursor.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor, column)
cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, 1)
cursor.mergeCharFormat(fmt)
def clear_highlights(self):
default_fmt = QTextCharFormat()
for editor in [self.left_panel, self.right_panel]:
cursor = editor.textCursor()
cursor.select(QTextCursor.Document)
cursor.mergeCharFormat(default_fmt)
def compare_files(self):
# 保存所有修改
self.manual_save()
left_content = self.left_panel.toPlainText().split("\n")
right_content = self.right_panel.toPlainText().split("\n")
max_lines = max(len(left_content), len(right_content))
diffs = []
for i in range(max_lines):
left_line = left_content[i] if i < len(left_content) else ""
right_line = right_content[i] if i < len(right_content) else ""
if left_line != right_line:
diffs.append(i + 1) # 行号从1开始
if diffs:
diff_info = []
if len(diffs) > 5:
diff_info.append(f"首5处差异行号: {', '.join(map(str, diffs[:5]))}")
diff_info.append(f"总差异数: {len(diffs)}")
else:
diff_info.append(f"差异行号: {', '.join(map(str, diffs))}")
self.status_label.setText(" | ".join(diff_info))
else:
self.status_label.setText("文件内容完全一致")
self.highlight_differences() # 添加高亮调用
if __name__ == "__main__":
app = QApplication(sys.argv)
window = HexComparator()
window.show()
sys.exit(app.exec_())
3. python打包运行
pyinstaller --noconsole --onefile -w -i 图标.ico hex_compare_gui.py