【AI编程】hex文件比对标记

功能:比对标记、保存修改

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值