C# .NET 文件比较工具 WinForms

文件比较器:

  • 单文件:文本对比(并排高亮差异)/ 二进制对比
  • 目录对比:递归比对文件名、大小、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
相关推荐
再写一行代码就下班1 小时前
Cursor配置Java环境、创建Spring Boot项目的步骤
java·开发语言·spring boot
零陵上将军_xdr1 小时前
后端转全栈学习-Day5-JavaScript 基础-3
开发语言·javascript·学习
oqX0Cazj21 小时前
2026超火Go-Zero实战:从架构原理到高并发接口落地,彻底解决接口超时、雪崩问题
开发语言·架构·golang
学会去珍惜1 小时前
C语言简介
c语言·开发语言
糖不吃1 小时前
WPF值转换器
c#
思麟呀1 小时前
C++11 核心特性(三):强类型枚举、static_assert 与 std::tuple
开发语言·c++
hoiii1872 小时前
Qt 实现屏幕截图功能
开发语言·qt·命令模式
学以智用2 小时前
.NET Core Swagger 超详细讲解(从入门到企业级)
后端·.net
小白学大数据2 小时前
爬虫性能天花板:asyncio赋能 Aiohttp,并发提速 10 倍
开发语言·爬虫·数据分析