C# WinForms记事本源码解析:从UI设计到文件编码处理
2026/9/13 15:06:08 网站建设 项目流程

简介:这是一份基于C# Windows Forms开发的仿Windows记事本源码项目,面向C#初学者与桌面应用入门开发者,旨在通过完整可运行的实例掌握GUI编程核心技能。资源共51个文件,包含17个C#源文件(如NotePadForm.cs、Program.cs、findForm.cs等)、6个资源文件(.resources)、6个本地化资源(.resx)、3个可执行文件(.exe)及配套配置(.config)、图标(.ico)、项目定义(.csproj)和解决方案(.sln)等,整体压缩包仅142KB,轻量易读。已有457人学习下载,适合边学边练:代码结构清晰,涵盖窗体设计、MenuStrip菜单与快捷键绑定、RichTextBox文本处理、Open/SaveFileDialog文件操作、多级对话框(查找、替换、定位、关于)及基础异常处理,所有功能模块均以独立.cs文件组织,便于逐个理解与调试。

1. 这不是玩具项目:一个能真实运行、可调试、带完整菜单逻辑的 C# 记事本源码包

你手头这个NotePad++.rar压缩包,不是网上常见的“Hello World 式窗体 demo”,而是一个结构完整、功能闭环、已通过 Visual Studio 编译验证的 Windows Forms 桌面应用工程。它实现了 Windows 原生记事本 90% 的核心交互:文件新建/打开/保存(含编码自动识别)、编辑撤销/重做、查找/替换(支持区分大小写与全字匹配)、转到行号、字体设置、状态栏实时统计字符数与行数,甚至包含独立的“关于”对话框和图标资源(n2.ico)。整个项目采用标准.sln+.csproj结构,目录下清晰分离了PropertiesobjbinForm设计文件(.Designer.cs)、资源文件(.resx)和业务逻辑(.cs),完全符合 C# WinForms 工程规范。它适合两类人:一是刚学完 C# 基础语法、正卡在“怎么把代码变成可点击窗口”阶段的初学者——这里每一步 UI 绑定、事件注册、文件读写都有明确实现;二是需要快速复用成熟 UI 框架结构的上位机开发者——你可以直接提取GotoForm.cs的行号跳转逻辑,或复用replaceForm.resx的多语言资源组织方式,无需从零造轮子。


2. 窗体架构与控件绑定:从NotePadForm.cs看 WinForms 的三层分离实践

2.1 主窗体类结构解析:NotePadForm.cs是行为中枢,NotePadForm.Designer.cs是界面蓝图

WinForms 应用的典型分层体现在NotePadForm.csNotePadForm.Designer.cs的严格分工。前者是开发者编写业务逻辑的主战场,后者由 Visual Studio 自动生成,负责控件实例化与初始属性设置。打开NotePadForm.cs,你会看到:

public partial class NotePadForm : Form { private RichTextBox richTextBox; private MenuStrip menuStrip1; private ToolStripMenuItem fileToolStripMenuItem; private ToolStripMenuItem editToolStripMenuItem; private ToolStripMenuItem helpToolStripMenuItem; private ToolStripMenuItem newToolStripMenuItem; private ToolStripMenuItem openToolStripMenuItem; private ToolStripMenuItem saveToolStripMenuItem; // ... 其他控件字段声明 }

注意:所有控件字段都标记为private且未在NotePadForm.csnew实例化——它们全部由NotePadForm.Designer.cs中的InitializeComponent()方法创建并挂载到窗体上。这是 WinForms 的强制约定,破坏该约定会导致设计器无法加载或运行时NullReferenceException

InitializeComponent()方法内部关键代码节选如下(简化示意):

// NotePadForm.Designer.cs private void InitializeComponent() { this.richTextBox = new System.Windows.Forms.RichTextBox(); this.menuStrip1 = new System.Windows.Forms.MenuStrip(); this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.newToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); // 设置 RichTextBox 属性 this.richTextBox.Dock = System.Windows.Forms.DockStyle.Fill; this.richTextBox.Font = new System.Drawing.Font("Consolas", 10F); this.richTextBox.Location = new System.Drawing.Point(0, 24); this.richTextBox.Size = new System.Drawing.Size(800, 450); // 绑定菜单项点击事件 this.newToolStripMenuItem.Click += new System.EventHandler(this.newToolStripMenuItem_Click); this.openToolStripMenuItem.Click += new System.EventHandler(this.openToolStripMenuItem_Click); // 将控件添加到窗体控件集合 this.Controls.Add(this.richTextBox); this.Controls.Add(this.menuStrip1); }

这段代码揭示了 WinForms 的核心机制:控件生命周期由设计器托管,开发者只负责事件响应与数据操作Dock = DockStyle.FillRichTextBox自动填充客户区;Font属性直接指定等宽字体,避免中文显示错位;而Click += ...则将用户点击动作与具体方法绑定——这正是事件驱动编程的物理落地点。

2.2 菜单系统实现:MenuStrip与快捷键的硬编码映射

NotePadForm.cs中的菜单逻辑并非仅靠设计器拖拽完成,其快捷键(如 Ctrl+N、Ctrl+O)需在代码中显式注册。查看newToolStripMenuItem_Click方法:

private void newToolStripMenuItem_Click(object sender, EventArgs e) { if (CheckSaveBeforeClose()) { richTextBox.Clear(); currentFilePath = null; this.Text = "无标题 - NotePad++"; UpdateStatusBar(); } }

但快捷键生效的关键,在于InitializeComponent()中对ToolStripMenuItem.ShortcutKeys的设置:

// NotePadForm.Designer.cs this.newToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.N))); this.openToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.O))); this.saveToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.S)));

提示Keys.Control | Keys.N是位运算组合,生成Ctrl+N键值。若你修改快捷键(如改为Alt+N),必须同步更新此处位运算表达式,并确保newToolStripMenuItemName属性未被意外更改——WinForms 依赖Name字段进行设计器与代码的双向绑定。

2.3 状态栏动态更新:ToolStripStatusLabel的实时刷新策略

状态栏显示字符数与行数,不是静态文本,而是随输入实时变化。NotePadForm.cs中定义了UpdateStatusBar()方法:

private void UpdateStatusBar() { int charCount = richTextBox.Text.Length; int lineCount = richTextBox.Lines.Length; statusLabel.Text = $"Ln {richTextBox.GetLineFromCharIndex(richTextBox.SelectionStart) + 1}, Col {richTextBox.SelectionStart - richTextBox.GetFirstCharIndexOfCurrentLine() + 1} 字符: {charCount} 行: {lineCount}"; }

该方法被三处调用:richTextBox.TextChanged事件、richTextBox.SelectionChanged事件、以及所有文件操作后。其中TextChanged的注册在InitializeComponent()中:

this.richTextBox.TextChanged += new System.EventHandler(this.richTextBox_TextChanged); // ... private void richTextBox_TextChanged(object sender, EventArgs e) { UpdateStatusBar(); }

关键细节GetLineFromCharIndex()GetFirstCharIndexOfCurrentLine()RichTextBox提供的精确行定位 API,比简单Split('\n')更可靠(尤其处理\r\n\n混合换行时)。若你发现状态栏行号计算错误,优先检查是否误用了TextBox替代RichTextBox——前者不支持这些高级定位方法。


3. 文件 I/O 与编码处理:OpenFileDialog/SaveFileDialog背后的字节流真相

3.1 打开文件:StreamReader的编码自动探测逻辑

openToolStripMenuItem_Click方法中,文件读取并非简单File.ReadAllText(),而是使用StreamReader并启用编码自动检测:

private void openToolStripMenuItem_Click(object sender, EventArgs e) { OpenFileDialog openFileDialog = new OpenFileDialog(); openFileDialog.Filter = "文本文件|*.txt;*.log|所有文件|*.*"; openFileDialog.Title = "打开文件"; if (openFileDialog.ShowDialog() == DialogResult.OK) { try { // 关键:使用 StreamReader 自动探测编码 using (StreamReader reader = new StreamReader(openFileDialog.FileName, true)) { richTextBox.Text = reader.ReadToEnd(); currentFilePath = openFileDialog.FileName; this.Text = Path.GetFileName(currentFilePath) + " - NotePad++"; UpdateStatusBar(); } } catch (Exception ex) { MessageBox.Show($"无法打开文件:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } }

new StreamReader(fileName, true)的第二个参数true启用编码自动探测(Auto-detect encoding),其原理是读取文件前 3 个字节判断 BOM(Byte Order Mark):EF BB BF→ UTF-8,FF FE→ UTF-16 LE,FE FF→ UTF-16 BE。若无 BOM,则默认按系统 ANSI 编码(如简体中文 Windows 为 GB2312)解码。这是避免中文乱码的第一道防线

实操验证:用记事本另存为 UTF-8(带BOM)和 UTF-8(无BOM)两个文件,用此程序打开——前者正常,后者可能乱码。此时需手动指定编码:new StreamReader(fileName, Encoding.UTF8)

3.2 保存文件:StreamWriter的编码显式指定与 BOM 控制

保存逻辑更需谨慎,因为StreamWriter默认不写入 BOM,可能导致其他编辑器(如 VS Code)误判编码:

private void saveToolStripMenuItem_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(currentFilePath)) { saveAsToolStripMenuItem_Click(sender, e); return; } try { // 显式指定 UTF-8 编码(无 BOM) using (StreamWriter writer = new StreamWriter(currentFilePath, false, Encoding.UTF8)) { writer.Write(richTextBox.Text); } MessageBox.Show("文件已保存。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show($"保存失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } }

Encoding.UTF8构造的StreamWriter默认输出无 BOM 的 UTF-8 字节流。若需兼容旧系统,可改用new UTF8Encoding(true)强制写入 BOM:

using (StreamWriter writer = new StreamWriter(currentFilePath, false, new UTF8Encoding(true))) { writer.Write(richTextBox.Text); }

3.3 文件路径与状态管理:currentFilePath字段的双重角色

currentFilePath字段在NotePadForm.cs中被声明为private string currentFilePath;,它承担两个关键职责:

  1. 标识当前文档来源:当currentFilePath == null时,表示文档为新建未保存状态,saveToolStripMenuItem_Click会跳转至另存为逻辑;
  2. 控制窗体标题显示this.Text = Path.GetFileName(currentFilePath) + " - NotePad++"保证标题栏始终反映真实文件名。

该字段的维护必须严格同步于文件操作:

  • newToolStripMenuItem_ClickcurrentFilePath = null
  • openToolStripMenuItem_ClickcurrentFilePath = openFileDialog.FileName
  • saveToolStripMenuItem_Click→ 保持currentFilePath不变(覆盖保存)
  • saveAsToolStripMenuItem_Click→ 更新currentFilePath为新路径

排错重点:若出现“保存后标题仍显示‘无标题’”,必然是saveAsToolStripMenuItem_Click中遗漏了currentFilePath = saveFileDialog.FileName;这一行。检查saveAsToolStripMenuItem_Click方法末尾是否包含此赋值。


4. 查找与替换功能:正则表达式引擎与 UI 状态同步的协同设计

4.1 查找对话框 (findForm.cs) 的模态交互流程

查找功能由独立窗体findForm.cs实现,其设计遵循 WinForms 模态对话框规范。主窗体调用方式为:

private void findToolStripMenuItem_Click(object sender, EventArgs e) { FindForm findForm = new FindForm(); findForm.Owner = this; // 设置所有者窗体,确保层级关系 findForm.StartPosition = FormStartPosition.CenterParent; findForm.ShowDialog(); // 阻塞式显示 }

findForm.cs内部关键逻辑在于btnFindNext_Click事件:

private void btnFindNext_Click(object sender, EventArgs e) { string searchText = txtFindText.Text; if (string.IsNullOrWhiteSpace(searchText)) return; // 获取主窗体的 RichTextBox 引用 NotePadForm mainForm = Owner as NotePadForm; if (mainForm == null) return; int startIndex = mainForm.richTextBox.SelectionStart + mainForm.richTextBox.SelectionLength; if (startIndex >= mainForm.richTextBox.TextLength) startIndex = 0; int foundIndex = mainForm.richTextBox.Find(searchText, startIndex, RichTextBoxFinds.None); if (foundIndex != -1) { mainForm.richTextBox.Select(foundIndex, searchText.Length); mainForm.richTextBox.ScrollToCaret(); } else { MessageBox.Show("未找到匹配项。", "查找", MessageBoxButtons.OK, MessageBoxIcon.Information); } }

技术要点RichTextBox.Find()方法返回首次匹配位置索引,Select()高亮选中,ScrollToCaret()自动滚动到可视区域。startIndex从当前光标位置开始搜索,实现“查找下一个”的连续性。

4.2 替换对话框 (replaceForm.cs) 的双向状态传递

替换功能比查找复杂,需在replaceForm中同时持有“查找内容”和“替换内容”,并支持“全部替换”。其核心是btnReplaceAll_Click

private void btnReplaceAll_Click(object sender, EventArgs e) { string findText = txtFindText.Text; string replaceText = txtReplaceText.Text; if (string.IsNullOrWhiteSpace(findText)) return; NotePadForm mainForm = Owner as NotePadForm; if (mainForm == null) return; string originalText = mainForm.richTextBox.Text; string replacedText = originalText.Replace(findText, replaceText); // 简单字符串替换 mainForm.richTextBox.Text = replacedText; mainForm.UpdateStatusBar(); // 同步更新状态栏 }

注意边界:此处使用string.Replace()是最简实现,不支持正则或区分大小写。若需增强,应引入Regex.Replace()并根据chkMatchCase.Checked动态构建RegexOptions。但原项目未实现此高级特性,说明其定位是基础功能教学。

4.3 多语言资源文件 (*.resx) 的实际加载路径

项目中存在多个.resx文件(findForm.resx,replaceForm.resx,about.resx),它们并非仅用于设计器本地化,而是被编译进程序集资源。FindForm的构造函数中隐式调用:

public FindForm() { InitializeComponent(); // 此方法内部会从资源中加载本地化文本 }

InitializeComponent()findForm.Designer.cs生成,其中包含:

private void InitializeComponent() { System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FindForm)); this.txtFindText = new System.Windows.Forms.TextBox(); this.txtFindText.AccessibleDescription = resources.GetString("txtFindText.AccessibleDescription"); this.txtFindText.AccessibleName = resources.GetString("txtFindText.AccessibleName"); // ... 其他控件资源加载 }

resources.GetString()从嵌入的findForm.resx中提取键值对。若你新增控件(如CheckBox chkWholeWord),必须手动在findForm.resx中添加对应键(如chkWholeWord.Text),否则运行时该控件文本为空。


5. 编译、调试与定制化改造:从源码包到可执行程序的最后一步

5.1 Visual Studio 版本兼容性与项目加载实操

该工程包含notepad.sln(解决方案文件)和NotePadPlus.csproj(C# 项目文件),支持 Visual Studio 2015 及以上版本。加载步骤:

  1. 解压NotePad++.rar至空目录(如D:\NotePad++);
  2. 双击notepad.sln,VS 自动识别为 .NET Framework 项目;
  3. 若提示“需要升级项目”,选择“稍后升级”——原项目基于 .NET Framework 4.5,无需升级;
  4. 在“解决方案资源管理器”中右键NotePadPlus.csproj→ “设为启动项目”;
  5. F5启动调试,程序将编译并运行。

常见报错:若出现The type or namespace name 'Resources' does not exist in the namespace 'NotePadPlus.Properties',说明Properties\Resources.resx未正确生成 Designer 文件。右键该文件 → “运行自定义工具”,强制生成Resources.Designer.cs

5.2 快速定制:添加“行号显示”功能的三步法

原项目未实现行号栏,但可通过继承RichTextBox轻量扩展。新建类LineNumberRichTextBox.cs

public class LineNumberRichTextBox : RichTextBox { private const int MARGIN_WIDTH = 30; private Brush lineNumberBrush = Brushes.LightGray; protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); DrawLineNumbers(e.Graphics); } private void DrawLineNumbers(Graphics g) { Rectangle clientRect = this.ClientRectangle; int firstLine = this.GetLineFromCharIndex(this.GetFirstCharIndexOfCurrentLine()); int lastLine = this.GetLineFromCharIndex(this.TextLength); for (int i = firstLine; i <= lastLine; i++) { string lineNum = (i + 1).ToString(); SizeF size = g.MeasureString(lineNum, this.Font); float x = clientRect.Left + 5; float y = this.GetPositionFromCharIndex(this.GetFirstCharIndexFromLine(i)).Y; g.DrawString(lineNum, this.Font, lineNumberBrush, x, y); } } }

然后在NotePadForm.Designer.cs中替换richTextBox声明:

// 替换原声明 // private System.Windows.Forms.RichTextBox richTextBox; private LineNumberRichTextBox richTextBox;

并在InitializeComponent()中初始化:

this.richTextBox = new LineNumberRichTextBox(); // ... 其余属性设置不变

5.3 发布部署:生成独立可执行文件(.exe)

要生成无需安装 .NET Framework 的单文件应用,需修改NotePadPlus.csproj

<!-- 在 <PropertyGroup> 中添加 --> <OutputType>WinExe</OutputType> <TargetFramework>net45</TargetFramework> <PublishTrimmed>false</PublishTrimmed> <SelfContained>true</SelfContained> <RuntimeIdentifier>win-x64</RuntimeIdentifier>

然后在 VS 中右键项目 → “发布” → 选择“文件夹”目标 → 完成。生成的publish\目录下将包含NotePadPlus.exe及所有依赖 DLL,可直接拷贝至任意 Windows 机器运行。

验证技巧:用Process Monitor(Sysinternals 工具)监控NotePadPlus.exe启动时的文件访问,确认其未尝试加载缺失的 DLL——这是判断发布包完整性的最直接方法。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询