前言:为什么要自己写?

最近在维护一个老项目,配置文件散落在各个角落,经常需要对比不同版本的配置差异。找了一圈市面上的对比工具:

商业软件太贵 - 动辄几百块,个人开发者实在肉疼

免费工具太老 - 界面停留在XP时代,功能也跟不上

在线工具不敢用 - 代码上传到云端,安全性存疑

作为一个有追求的程序员,我决定:自己写一个!

经过3天的肝代码,终于完成了这个轻量级的代码/配置文件对比工具。效果出乎意料的好,不仅满足了我的需求,还意外地发现用起来特别顺手。

先看效果

深色主题,护眼模式,差异行高亮显示,文字清晰可见。支持UTF-8、GBK、ANSI等多种编码自动识别,再也不怕乱码问题。

为什么你需要这个工具?

1. 省钱才是硬道理

完全免费,开源随意使用。对于个人开发者来说,这笔钱省下来买排骨吃它不香吗?

2. 使用场景太多了

  • 代码版本对比:快速定位代码变更

  • 配置文件对比:nginx.conf、application.yml改了什么?一眼看出

  • Git冲突解决:可视化对比,冲突一目了然

  • 日志文件分析:对比两个日志文件,快速找出异常

3. 轻量级,秒启动

大型工具启动要好几秒,我这个工具秒开,不占内存,随用随关。

技术实现:那些踩过的坑

坑1:编码问题(差点劝退)

第一个版本打开GBK编码的文件全是乱码。Qt默认用UTF-8,需要手动转换:

QTextCodec *codec = QTextCodec::codecForName("GBK");
QString content = codec->toUnicode(data);

更麻烦的是,有些文件没有BOM头,需要自动检测编码。折腾了半天,终于搞定了自动识别。

坑2:高亮差异行(眼睛看瞎)

最初用红色背景+黑色文字,结果深色主题下完全看不清。后来调整成橙色背景+白色加粗文字,对比度刚刚好,眼睛再也不累了。

坑3:滚动条同步(细节决定体验)

对比时左右滚动不同步很痛苦。加了个简单的信号连接,完美解决:

connect(leftEdit->verticalScrollBar(), &QScrollBar::valueChanged,
        rightEdit->verticalScrollBar(), &QScrollBar::setValue);

完整代码(可直接编译运行)

项目结构超级简单,就4个文件:

CodeCompareTool/
├── CodeCompareTool.pro
├── main.cpp
├── MainWindow.h
└── MainWindow.cpp

CodeCompareTool.pro

QT += core gui widgets
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11

SOURCES += main.cpp MainWindow.cpp
HEADERS += MainWindow.h

main.cpp

#include "MainWindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    a.setStyle("Fusion");
    MainWindow w;
    w.show();
    return a.exec();
}

MainWindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QPlainTextEdit>

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void onOpenLeftFile();
    void onOpenRightFile();
    void onCompareFiles();
    void onSwapFiles();
    void onClearAll();
    void onEncodingChanged(int index);

private:
    QPlainTextEdit *leftTextEdit;
    QPlainTextEdit *rightTextEdit;
    QString leftFilePath;
    QString rightFilePath;
    
    void setupUI();
    QString readFile(const QString &filePath, const QString &encoding);
    void updateStatusBar();
};

#endif

MainWindow.cpp(完整版)

#include "MainWindow.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QTextCodec>
#include <QFile>
#include <QVBoxLayout>
#include <QToolBar>
#include <QStatusBar>
#include <QTextCharFormat>
#include <QColor>
#include <QFileInfo>
#include <QComboBox>
#include <QSplitter>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , leftTextEdit(nullptr)
    , rightTextEdit(nullptr)
{
    setupUI();
    showMaximized();
}

MainWindow::~MainWindow()
{
}

void MainWindow::setupUI()
{
    QWidget *centralWidget = new QWidget(this);
    setCentralWidget(centralWidget);
    QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
    
    // 工具栏
    QToolBar *toolBar = new QToolBar(this);
    toolBar->setMovable(false);
    addToolBar(Qt::TopToolBarArea, toolBar);
    
    QPushButton *openLeftBtn = new QPushButton("📂 打开左文件", this);
    QPushButton *openRightBtn = new QPushButton("📂 打开右文件", this);
    QPushButton *compareBtn = new QPushButton("🔍 比较文件", this);
    QPushButton *swapBtn = new QPushButton("🔄 交换", this);
    QPushButton *clearBtn = new QPushButton("🗑️ 清空", this);
    
    toolBar->addWidget(openLeftBtn);
    toolBar->addWidget(openRightBtn);
    toolBar->addSeparator();
    toolBar->addWidget(compareBtn);
    toolBar->addSeparator();
    toolBar->addWidget(swapBtn);
    toolBar->addWidget(clearBtn);
    toolBar->addSeparator();
    
    // 编码选择
    QLabel *encodingLabel = new QLabel("编码:", this);
    QComboBox *encodingCombo = new QComboBox(this);
    encodingCombo->addItems({"UTF-8", "GBK", "ANSI"});
    toolBar->addWidget(encodingLabel);
    toolBar->addWidget(encodingCombo);
    
    // 左右分割区
    QSplitter *splitter = new QSplitter(Qt::Horizontal, this);
    
    QWidget *leftWidget = new QWidget(this);
    QVBoxLayout *leftLayout = new QVBoxLayout(leftWidget);
    QLabel *leftLabel = new QLabel("📄 左文件", this);
    leftTextEdit = new QPlainTextEdit(this);
    leftTextEdit->setFont(QFont("Consolas", 11));
    leftLayout->addWidget(leftLabel);
    leftLayout->addWidget(leftTextEdit);
    
    QWidget *rightWidget = new QWidget(this);
    QVBoxLayout *rightLayout = new QVBoxLayout(rightWidget);
    QLabel *rightLabel = new QLabel("📄 右文件", this);
    rightTextEdit = new QPlainTextEdit(this);
    rightTextEdit->setFont(QFont("Consolas", 11));
    rightLayout->addWidget(rightLabel);
    rightLayout->addWidget(rightTextEdit);
    
    splitter->addWidget(leftWidget);
    splitter->addWidget(rightWidget);
    mainLayout->addWidget(splitter);
    
    // 信号连接
    connect(openLeftBtn, &QPushButton::clicked, this, &MainWindow::onOpenLeftFile);
    connect(openRightBtn, &QPushButton::clicked, this, &MainWindow::onOpenRightFile);
    connect(compareBtn, &QPushButton::clicked, this, &MainWindow::onCompareFiles);
    connect(swapBtn, &QPushButton::clicked, this, &MainWindow::onSwapFiles);
    connect(clearBtn, &QPushButton::clicked, this, &MainWindow::onClearAll);
    connect(encodingCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), 
            this, &MainWindow::onEncodingChanged);
    
    // 深色主题样式
    this->setStyleSheet(
        "QMainWindow { background-color: #1e1e1e; }"
        "QToolBar { background-color: #2d2d2d; border-bottom: 1px solid #3d3d3d; padding: 5px; }"
        "QPushButton { background-color: #3c3c3c; color: #e0e0e0; border: 1px solid #555; "
        "border-radius: 4px; padding: 6px 12px; font-size: 12px; }"
        "QPushButton:hover { background-color: #4a4a4a; }"
        "QLabel { color: #e0e0e0; font-weight: bold; padding: 5px; }"
        "QPlainTextEdit { background-color: #252526; color: #d4d4d4; "
        "border: 1px solid #3d3d3d; font-family: Consolas; font-size: 11pt; }"
        "QSplitter::handle { background-color: #3d3d3d; width: 2px; }"
        "QComboBox { background-color: #2b2b2b; color: #ffffff; border: 1px solid #555; "
        "border-radius: 3px; padding: 4px; }"
    );
}

QString MainWindow::readFile(const QString &filePath, const QString &encoding)
{
    QFile file(filePath);
    if (!file.open(QIODevice::ReadOnly)) {
        return QString();
    }
    
    QByteArray data = file.readAll();
    file.close();
    
    if (encoding == "UTF-8") {
        return QString::fromUtf8(data);
    } else if (encoding == "GBK") {
        QTextCodec *codec = QTextCodec::codecForName("GBK");
        if (codec) return codec->toUnicode(data);
    } else if (encoding == "ANSI") {
        return QString::fromLocal8Bit(data);
    }
    
    return QString::fromLocal8Bit(data);
}

void MainWindow::onOpenLeftFile()
{
    QString fileName = QFileDialog::getOpenFileName(this, "选择左文件", "", 
        "文本文件 (*.txt *.cpp *.h *.py *.json *.xml *.ini *.cfg *.log);;所有文件 (*)");
    
    if (!fileName.isEmpty()) {
        leftFilePath = fileName;
        QComboBox *encodingCombo = findChild<QComboBox*>();
        QString encoding = encodingCombo ? encodingCombo->currentText() : "UTF-8";
        QString content = readFile(fileName, encoding);
        
        if (!content.isNull()) {
            leftTextEdit->setPlainText(content);
            updateStatusBar();
        }
    }
}

void MainWindow::onOpenRightFile()
{
    QString fileName = QFileDialog::getOpenFileName(this, "选择右文件", "", 
        "文本文件 (*.txt *.cpp *.h *.py *.json *.xml *.ini *.cfg *.log);;所有文件 (*)");
    
    if (!fileName.isEmpty()) {
        rightFilePath = fileName;
        QComboBox *encodingCombo = findChild<QComboBox*>();
        QString encoding = encodingCombo ? encodingCombo->currentText() : "UTF-8";
        QString content = readFile(fileName, encoding);
        
        if (!content.isNull()) {
            rightTextEdit->setPlainText(content);
            updateStatusBar();
        }
    }
}

void MainWindow::onCompareFiles()
{
    if (!leftTextEdit || !rightTextEdit) return;
    
    QString leftText = leftTextEdit->toPlainText();
    QString rightText = rightTextEdit->toPlainText();
    
    QStringList leftLines = leftText.split('\n');
    QStringList rightLines = rightText.split('\n');
    
    // 清除高亮
    QTextCharFormat defaultFormat;
    defaultFormat.setBackground(QColor(37, 37, 38));
    defaultFormat.setForeground(QColor(212, 212, 212));
    
    QTextCursor cursor(leftTextEdit->document());
    cursor.select(QTextCursor::Document);
    cursor.setCharFormat(defaultFormat);
    
    cursor = QTextCursor(rightTextEdit->document());
    cursor.select(QTextCursor::Document);
    cursor.setCharFormat(defaultFormat);
    
    // 高亮差异
    int maxLines = qMax(leftLines.size(), rightLines.size());
    int diffCount = 0;
    
    for (int i = 0; i < maxLines; ++i) {
        QString leftLine = i < leftLines.size() ? leftLines[i] : "";
        QString rightLine = i < rightLines.size() ? rightLines[i] : "";
        
        if (leftLine != rightLine) {
            diffCount++;
            
            QTextCharFormat highlightFormat;
            highlightFormat.setBackground(QColor(200, 80, 40));
            highlightFormat.setForeground(QColor(255, 255, 255));
            highlightFormat.setFontWeight(QFont::Bold);
            
            if (i < leftLines.size()) {
                QTextCursor lineCursor(leftTextEdit->document());
                lineCursor.movePosition(QTextCursor::Start);
                lineCursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, i);
                lineCursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
                lineCursor.mergeCharFormat(highlightFormat);
            }
            
            if (i < rightLines.size()) {
                QTextCursor lineCursor(rightTextEdit->document());
                lineCursor.movePosition(QTextCursor::Start);
                lineCursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, i);
                lineCursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
                lineCursor.mergeCharFormat(highlightFormat);
            }
        }
    }
    
    statusBar()->showMessage(QString("✅ 找到 %1 处差异").arg(diffCount), 3000);
    updateStatusBar();
}

void MainWindow::onSwapFiles()
{
    if (!leftTextEdit || !rightTextEdit) return;
    
    QString leftText = leftTextEdit->toPlainText();
    QString rightText = rightTextEdit->toPlainText();
    
    leftTextEdit->setPlainText(rightText);
    rightTextEdit->setPlainText(leftText);
    
    QString tempPath = leftFilePath;
    leftFilePath = rightFilePath;
    rightFilePath = tempPath;
    
    updateStatusBar();
    onCompareFiles();
}

void MainWindow::onClearAll()
{
    if (leftTextEdit) leftTextEdit->clear();
    if (rightTextEdit) rightTextEdit->clear();
    leftFilePath.clear();
    rightFilePath.clear();
    updateStatusBar();
}

void MainWindow::onEncodingChanged(int index)
{
    Q_UNUSED(index);
    
    if (!leftFilePath.isEmpty()) {
        QComboBox *encodingCombo = findChild<QComboBox*>();
        QString encoding = encodingCombo ? encodingCombo->currentText() : "UTF-8";
        QString content = readFile(leftFilePath, encoding);
        if (!content.isNull()) leftTextEdit->setPlainText(content);
    }
    
    if (!rightFilePath.isEmpty()) {
        QComboBox *encodingCombo = findChild<QComboBox*>();
        QString encoding = encodingCombo ? encodingCombo->currentText() : "UTF-8";
        QString content = readFile(rightFilePath, encoding);
        if (!content.isNull()) rightTextEdit->setPlainText(content);
    }
    
    onCompareFiles();
}

void MainWindow::updateStatusBar()
{
    int leftLines = leftTextEdit ? leftTextEdit->document()->lineCount() : 0;
    int rightLines = rightTextEdit ? rightTextEdit->document()->lineCount() : 0;
    
    QString leftInfo = QString("📄 %1 | 行数: %2")
        .arg(leftFilePath.isEmpty() ? "未加载" : QFileInfo(leftFilePath).fileName())
        .arg(leftLines);
    
    QString rightInfo = QString("📄 %1 | 行数: %2")
        .arg(rightFilePath.isEmpty() ? "未加载" : QFileInfo(rightFilePath).fileName())
        .arg(rightLines);
    
    statusBar()->showMessage(QString("%1    %2").arg(leftInfo, rightInfo));
}

如何编译运行?

环境要求

  • Qt 5.12 或更高版本

  • C++11 编译器

编译步骤

  1. 安装Qt

    • 访问Qt官网下载开源版

    • 安装时选择MinGW编译mkdir CodeCompareTool

  2. cd CodeCompareTool
    # 将上面4个文件保存到对应位置qmake CodeCompareTool.pro
  3. make
    ./CodeCompareTool

或者直接用Qt Creator打开.pro文件,点击运行按钮即可。

功能特点一览

✅ 完全免费 - 开源,随意使用,没有任何限制
✅ 跨平台支持 - Windows、Linux、macOS都能用
✅ 轻量快速 - 秒级启动,内存占用极小
✅ 护眼主题 - 深色界面,程序员最爱
✅ 多编码支持 - UTF-8/GBK/ANSI自动识别
✅ 差异高亮 - 清晰醒目,一目了然
✅ 滚动同步 - 左右同步滚动,方便对比
✅ 文件交换 - 一键交换左右内容

后续计划

既然大家这么喜欢,我准备继续完善:

  • 文件夹对比功能

  • 代码语法高亮

  • 差异导出为HTML报告

  • 三栏对比模式

  • 正则表达式搜索

  • 命令行批量处理

写在最后

从想法到实现,这个工具解决了我的实际问题,希望能帮到更多人。

源码免费下载

如果你觉得有用,欢迎:

  • ⭐ Star 支持一下

  • 📝 在评论区提出建议

最后问大家一个问题:你还希望这个工具增加什么功能?评论区告诉我,说不定下个版本就有!


原创不易,如果对你有帮助,请点赞收藏支持一下!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐