文件比较器:
- 单文件:文本对比(并排高亮差异)/ 二进制对比
- 目录对比:递归比对文件名、大小、Hash,结果树/表格展示
- 导出报告 → 文本 / HTML
不依赖第三方 diff 库;文本比较用的是经典 LCS 差异算法(Myers-like 简化版)
一、新建工程
1.1 用 .NET 6/7/8
bash
dotnet new winforms -n FileDiffTool
cd FileDiffTool
# 把下面两个文件丢进项目目录
# FormMain.cs
# DiffEngine.cs
dotnet run
1.2 或用 VS → 新建「Windows 窗体应用(.NET Framework)」
把代码粘进去即可(命名空间自己保持统一)。
二、核心:差异引擎 DiffEngine.cs
csharp
// DiffEngine.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace FileDiffTool
{
/// <summary>
/// 统一 diff 结果行
/// Side: Left | Right | Both
/// </summary>
public enum DiffSide { Left, Right, Both }
public class DiffLine
{
public DiffSide Side { get; set; } // 属于哪一边
public int LeftLine { get; set; } = -1; // 左边行号(从1开始,-1表示不存在)
public int RightLine { get; set; } = -1; // 右边行号
public string Text { get; set; } = "";
public bool IsDifferent => Side != DiffSide.Both;
}
public static class DiffEngine
{
/// <summary>
/// 文本逐行 diff(LCS为基础,稳定可解释)
/// </summary>
public static List<DiffLine> CompareLines(IList<string> left, IList<string> right)
{
int n = left.Count, m = right.Count;
int[,] dp = new int[n + 1, m + 1];
// LCS DP
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
dp[i, j] = left[i - 1] == right[j - 1]
? dp[i - 1, j - 1] + 1
: Math.Max(dp[i - 1, j], dp[i, j - 1]);
var result = new List<DiffLine>();
int i = n, j = m;
while (i > 0 || j > 0)
{
if (i > 0 && j > 0 && left[i - 1] == right[j - 1])
{
result.Add(new DiffLine
{
Side = DiffSide.Both,
LeftLine = i,
RightLine = j,
Text = left[i - 1]
});
i--; j--;
}
else if (j > 0 && (i == 0 || dp[i, j - 1] >= dp[i - 1, j]))
{
// 右侧新增
result.Add(new DiffLine
{
Side = DiffSide.Right,
LeftLine = -1,
RightLine = j,
Text = right[j - 1]
});
j--;
}
else
{
// 左侧删除
result.Add(new DiffLine
{
Side = DiffSide.Left,
LeftLine = i,
RightLine = -1,
Text = left[i - 1]
});
i--;
}
}
result.Reverse(); // 回溯出来是反的
return result;
}
/// <summary>
/// 快速二进制相等判断
/// </summary>
public static bool BinaryEqual(string pathA, string pathB)
{
using var a = System.IO.File.OpenRead(pathA);
using var b = System.IO.File.OpenRead(pathB);
if (a.Length != b.Length) return false;
const int BUF = 8192;
byte[] ba = new byte[BUF], bb = new byte[BUF];
int ra, rb;
while ((ra = a.Read(ba, 0, BUF)) > 0)
{
rb = b.Read(bb, 0, BUF);
if (ra != rb) return false;
for (int i = 0; i < ra; i++)
if (ba[i] != bb[i]) return false;
}
return true;
}
}
}
三、GUI:FormMain.cs(WinForms)
csharp
// FormMain.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Windows.Forms;
namespace FileDiffTool
{
public partial class FormMain : Form
{
private SplitContainer splitMain;
private RichTextBox rtbLeft;
private RichTextBox rtbRight;
private TreeView treeDir;
private ListView lvMatches;
public FormMain()
{
InitializeComponent();
}
private void InitializeComponent()
{
Text = "File Diff Tool (.NET)";
Width = 1200; Height = 720;
StartPosition = FormStartPosition.CenterScreen;
// === 工具栏 ===
var tb = new ToolStrip();
var btnSelA = new ToolStripButton(" 选择文件A... ");
var btnSelB = new ToolStripButton(" 选择文件B... ");
var btnDirA = new ToolStripButton(" 目录A... ");
var btnDirB = new ToolStripButton(" 目录B... ");
var btnExport = new ToolStripButton(" 导出报告... ");
tb.Items.AddRange(new ToolStripItem[] { btnSelA, btnSelB, new ToolStripSeparator(), btnDirA, btnDirB, new ToolStripSeparator(), btnExport });
Controls.Add(tb);
// === 主体:左侧目录树 + 右侧文本对比 ===
splitMain = new SplitContainer
{
Dock = DockStyle.Fill,
SplitterDistance = 280,
Top = tb.Height
};
treeDir = new TreeView { Dock = DockStyle.Fill };
splitMain.Panel1.Controls.Add(treeDir);
var splitView = new SplitContainer
{
Dock = DockStyle.Fill,
SplitterDistance = (Width - 280) / 2
};
rtbLeft = new RichTextBox
{
Dock = DockStyle.Fill,
Font = new System.Drawing.Font("Consolas", 10f),
ReadOnly = true,
BackColor = System.Drawing.Color.FromArgb(245, 248, 255)
};
rtbRight = new RichTextBox
{
Dock = DockStyle.Fill,
Font = new System.Drawing.Font("Consolas", 10f),
ReadOnly = true,
BackColor = System.Drawing.Color.FromArgb(255, 248, 245)
};
splitView.Panel1.Controls.Add(new Label { Text = "◀ 文件A", Dock = DockStyle.Top, Height = 24 });
splitView.Panel2.Controls.Add(new Label { Text = "文件B ▶", Dock = DockStyle.Top, Height = 24 });
splitView.Panel1.Controls.Add(rtbLeft);
splitView.Panel2.Controls.Add(rtbRight);
splitMain.Panel2.Controls.Add(splitView);
Controls.Add(splitMain);
// === 事件 ===
btnSelA.Click += (_, __) => CompareTwoFilesPick();
btnSelB.Click += (_, __) => { }; // 其实在 SelA 里一起选B更直观;下面我用"A+B"方式
btnDirA.Click += (_, __) =>
{
var dlg = new FolderBrowserDialog();
if (dlg.ShowDialog() == DialogResult.OK)
CompareDirectories(dlg.SelectedPath, PickFolder());
};
btnDirB.Click += (_, __) => { };
btnExport.Click += (_, __) => ExportReport();
}
// ---------- 文件对比入口 ----------
void CompareTwoFilesPick()
{
var ofd1 = new OpenFileDialog { Title = "选择文件 A" };
if (ofd1.ShowDialog() != DialogResult.OK) return;
var ofd2 = new OpenFileDialog { Title = "选择文件 B", InitialDirectory = Path.GetDirectoryName(ofd1.FileName) };
if (ofd2.ShowDialog() != DialogResult.OK) return;
CompareFiles(ofd1.FileName, ofd2.FileName);
}
void CompareFiles(string pathA, string pathB)
{
Text = $"Diff: {Path.GetFileName(pathA)} ↔ {Path.GetFileName(pathB)}";
// 1)先试文本:能当成 UTF-8 / ASCII 就读行
string[] linesA, linesB;
try
{
linesA = File.ReadAllLines(pathA, Encoding.UTF8);
linesB = File.ReadAllLines(pathB, Encoding.UTF8);
}
catch
{
// 2)fallback:二进制
bool eq = DiffEngine.BinaryEqual(pathA, pathB);
rtbLeft.Clear(); rtbRight.Clear();
rtbLeft.AppendText(eq ? "二进制:完全相同" : "二进制:不同(不可显示)");
return;
}
var diffs = DiffEngine.CompareLines(linesA, linesB);
RenderDiff(diffs, linesA, linesB);
}
// ---------- 渲染文本差异 ----------
void RenderDiff(List<DiffLine> diffs, string[] leftLines, string[] rightLines)
{
rtbLeft.Clear(); rtbRight.Clear();
const string TAG_DEL = "◀",
TAG_ADD = "▶";
foreach (var d in diffs)
{
Color c = Color.Black;
string tag = " ";
if (d.Side == DiffSide.Left)
{
c = Color.Red; tag = TAG_DEL + " ";
}
else if (d.Side == DiffSide.Right)
{
c = Color.Green; tag = TAG_ADD + " ";
}
if (d.Side != DiffSide.Right) // 左面板
{
rtbLeft.SelectionColor = c;
rtbLeft.AppendText($"{tag}{d.LeftLine,4}: {d.Text}\n");
}
else
{
rtbLeft.SelectionColor = Color.Gray;
rtbLeft.AppendText($" \n");
}
if (d.Side != DiffSide.Left) // 右面板
{
rtbRight.SelectionColor = c;
rtbRight.AppendText($"{tag}{d.RightLine,4}: {d.Text}\n");
}
else
{
rtbRight.SelectionColor = Color.Gray;
rtbRight.AppendText($" \n");
}
}
}
// ---------- 目录对比 ----------
string PickFolder()
{
var dlg = new FolderBrowserDialog();
return dlg.ShowDialog() == DialogResult.OK ? dlg.SelectedPath : null;
}
void CompareDirectories(string dirA, string dirB)
{
if (dirB == null) return;
treeDir.Nodes.Clear();
var root = treeDir.Nodes.Add($"{dirA} ↔ {dirB}");
var filesA = Directory.GetFiles(dirA, "*.*", SearchOption.AllDirectories)
.Select(p => p[(dirA.Length + 1)..]).ToDictionary(p => p, p => p);
var filesB = Directory.GetFiles(dirB, "*.*", SearchOption.AllDirectories)
.Select(p => p[(dirB.Length + 1)..]).ToList();
var md5 = MD5.Create();
string Hash(string f)
{
using var s = File.OpenRead(f);
return BitConverter.ToString(md5.ComputeHash(s)).Replace("-", "");
}
foreach (var rel in filesA.Keys.Union(filesB).OrderBy(x => x))
{
bool inA = filesA.ContainsKey(rel);
bool inB = filesB.Contains(rel);
if (inA && inB &&
File.GetAttributes(Path.Combine(dirA, rel)) == File.GetAttributes(Path.Combine(dirB, rel)) &&
Hash(Path.Combine(dirA, rel)) == Hash(Path.Combine(dirB, rel)))
{
root.Nodes.Add(rel + " (same)");
}
else
{
var n = root.Nodes.Add(rel + " ★ DIFF");
n.ForeColor = Color.DarkOrange;
if (!inA) n.Text = rel + " (only B)";
if (!inB) n.Text = rel + " (only A)";
}
}
root.Expand();
}
// ---------- 导出报告 ----------
void ExportReport()
{
var sfd = new SaveFileDialog { Filter = "HTML报告|*.html|纯文本|*.txt", FileName = "diff_report.html" };
if (sfd.ShowDialog() != DialogResult.OK) return;
File.WriteAllText(sfd.FileName,
"<html><body><h2>Diff Report</h2><pre>" +
HttpUtility.HtmlEncode(rtbLeft.Text + "\n\n=====>>>>>>\n\n" + rtbRight.Text) +
"</pre></body></html>");
MessageBox.Show("报告已导出:" + sfd.FileName);
}
}
}
上面
HttpUtility.HtmlEncode需要System.Web引用(或你改成简单的< >替换也行)。如果只是
.txt:把导出换成File.WriteAllText(sfd.FileName, rtbLeft.Text + "\n====>>>>\n" + rtbRight.Text);
参考代码 C#.NET文件比较工具 www.youwenfan.com/contentcsv/116036.html
四、运行效果
- 并排左右 :
- 红色
◀:A里有、B里删 - 绿色
▶:B新增 - 黑色:两边都一致
- 红色
- 目录模式 :树里标
★ DIFF / only A / only B