从零开发一款代码对比工具,开源分享!
前言:为什么要自己写?
最近在维护一个老项目,配置文件散落在各个角落,经常需要对比不同版本的配置差异。找了一圈市面上的对比工具:
商业软件太贵 - 动辄几百块,个人开发者实在肉疼
免费工具太老 - 界面停留在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 编译器
编译步骤
-
安装Qt
-
访问Qt官网下载开源版
-
安装时选择MinGW编译mkdir CodeCompareTool
-
-
cd CodeCompareTool # 将上面4个文件保存到对应位置qmake CodeCompareTool.pro
-
make ./CodeCompareTool
或者直接用Qt Creator打开.pro文件,点击运行按钮即可。
功能特点一览
✅ 完全免费 - 开源,随意使用,没有任何限制
✅ 跨平台支持 - Windows、Linux、macOS都能用
✅ 轻量快速 - 秒级启动,内存占用极小
✅ 护眼主题 - 深色界面,程序员最爱
✅ 多编码支持 - UTF-8/GBK/ANSI自动识别
✅ 差异高亮 - 清晰醒目,一目了然
✅ 滚动同步 - 左右同步滚动,方便对比
✅ 文件交换 - 一键交换左右内容
后续计划
既然大家这么喜欢,我准备继续完善:
-
文件夹对比功能
-
代码语法高亮
-
差异导出为HTML报告
-
三栏对比模式
-
正则表达式搜索
-
命令行批量处理
写在最后
从想法到实现,这个工具解决了我的实际问题,希望能帮到更多人。
如果你觉得有用,欢迎:
-
⭐ Star 支持一下
-
📝 在评论区提出建议
最后问大家一个问题:你还希望这个工具增加什么功能?评论区告诉我,说不定下个版本就有!
原创不易,如果对你有帮助,请点赞收藏支持一下!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)