ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C#代码高亮控件:基于RichTextBox的WinForm实现

C#代码高亮控件:基于RichTextBox的WinForm实现 简介面向Windows窗体应用开发者这份资源提供了用开源ICSharpCode.TextEditor控件替代只支持纯文本的标准文本框的完整示例能够对C#代码进行语法高亮解决代码阅读与编辑时无法区分关键字、注释和字符串的问题提升代码可读性。压缩包共206个文件以C#源码、xshd语法高亮定义、DLL类库以及Visual Studio工程与解决方案文件为主另含可直接运行的exe演示程序、光标、样式图片和调试符号其中cs文件可直接编译xshd文件保存高亮规则dll与exe便于运行验证整体体积仅380KB。目前已有610人学习下载适合WinForms开发者、.NET初学者以及需要为编辑器增加代码高亮功能的技术人员也可作为控件二次开发的入门模板。工程内展示了如何创建TextEditorControl、设置“C#”高亮策略并嵌入窗体配合xshd规则文件和示例资源可帮助理解高亮规则配置、控件集成和界面扩展细节便于二次开发和教学参考。1. C#代码高亮的WinForm TEXTBOX控件先分清“TextBox”和“高亮控件”WinForm 项目里放一个 TEXTBOX 给用户输入 C# 代码原生 TextBox 只负责收文本不支持按字符改前景色。输入十几行脚本后字符串、注释和关键字全挤成一个颜色可读性比记事本还差。标题里这个组合词实际要做的不是给 TextBox 加一个开关而是用一个可承载格式的文本编辑控件去替换输入区在 WinForm 生态里最常见的底座是 RichTextBox再配合正则词法把 C# 语句分段染色。下面按这个思路给出一套可运行的方案覆盖选型、着色实现、性能优化和验证方法适合要在上位机脚本编辑页或内部工具里嵌入代码编辑区的 C# 开发者。2. 选 RichTextBox 还是自绘 TextBoxC#代码高亮控件的实现路径很多人以为代码高亮控件就是给 TextBox 加属性第一反应是去找 TextBox 的 AllowColor 之类开关。其实原生 TextBox 底层是 Win32 Edit 控件能用的重绘消息只有 WM_CTLCOLOR 这类整块换色能力不能按字符位置设置颜色。要在原版 TextBox 上做到语法高亮只能走自绘也就是接管 WM_PAINT自己测量字符串、自己画选区还要处理 IME 和光标闪烁。对于一个以编辑 C# 代码为核心目标的控件这条路投入太高。2.1 三条实现路径的成本边界实现方式开发成本长文本表现适用场景自绘 TextBox高取决于绘制实现完全定制外观的极简编辑器RichTextBox 二次封装中100KB 内文本可接受上位机脚本区、日志查看器ScintillaNET中高高完整代码编辑器、大文件高亮ScintillaNET 是 WinForm 封装了 Scintilla 编辑组件的第三方库词法解析和滚动性能都比 RichTextBox 好但它会引入额外的原生依赖而且默认行为像一个编辑器而不是一个输入框。大部分 C# 上位机或工具软件里只是需要一个“能写几十行脚本”的输入区用 RichTextBox 就够了如果目标是做一个类似代码编辑器的独立工具可以直接上 ScintillaNET。2.2 RichTextBox 的最小控件骨架我一般会先写一个继承 RichTextBox 的类把会干扰高亮的默认行为关掉public class CSharpCodeBox : RichTextBox { public CSharpCodeBox() { Font new Font(Consolas, 10f); BackColor Color.FromArgb(30, 30, 30); ForeColor Color.FromArgb(220, 220, 220); DetectUrls false; WordWrap false; AcceptsTab true; ScrollBars RichTextBoxScrollBars.Both; HideSelection false; SelectionTabs new int[] { 32 }; } }这里把 Font 设为 Consolas是为了等宽对齐和行号对齐WordWrapfalse 防止自动换行把行索引打乱AcceptsTabtrue 让 Tab 键入代码而不是切换焦点DetectUrlsfalse 很重要否则 RichTextBox 会把代码里的点斜杠识别成链接染成蓝色。SelectionTabs 定义 Tab 宽度32 像素对应 4 个等宽字符。背景和前景用深色是为了后续高亮颜色在深色背景下有对比度也是 WinForm 界面美化里比较省事的做法。这里有一个容易被坑的点不要在构造函数里调用基于 Handle 的重绘逻辑因为此时控件句柄还没创建Handle 为 0。需要在 HandleCreated 事件里做一次性初始化。2.3 用派生类而不是 UserControl 组合用 UserControl 包一个 RichTextBox 也可以但派生类能省掉一堆“把内部控件暴露出来”的转发代码。外部代码拿到 CSharpCodeBox.Text、SelectionStart、SelectionLength 时语义和 TextBox 基本一致。另一个常见误操作是全量清掉 Text 再重写比如执行 Text text 或构造新的 Rtf这种赋值会重置编辑历史导致撤销序列失效。正确做法是只通过 Select 和 SelectionColor 改颜色不要动 Text 或 Rtf。RichTextBox 的 SelectionColor 只对当前选中的区域生效这个机制正好用来做逐段上色但颜色属性会记住上一次的值所以每次上色前要先全选设为 ForeColor否则上一次残留颜色会蔓延到新输入的文本。3. 用 RichTextBox 把 C# 代码着色跑起来关键字表与正则词法3.1 C# 词法里最少要处理四类 token类别示例高亮目的字符串nt, c:\x避免误配关键字注释// 单行 /* 多行 */让说明与代码逻辑分离关键字if、for、class、return主语法层数字/常量12、3.14、0xFF区别于普通标识符着色顺序有一个关键约定字符串和注释必须最先匹配。原因很直接string s if;里的 if 不能染成关键字// class里的 class 也不能染成关键字。如果按先关键字再字符串的顺序就会把注释里的内容染脏。多行注释还要考虑跨行正则里用内联(?s)让.能匹配换行否则/*到*/中间一旦有换行就匹配不到。3.2 颜色表和高频规则下面这组颜色取自常见的深色主题分别对应字符串、注释、关键字和数字类型RGB示例色值字符串206, 145, 120暖橙色注释106, 153, 85绿色关键字86, 156, 214蓝色数字181, 206, 168浅绿默认文本220, 220, 220浅灰代码里保存成(Pattern, Color)元组数组后续增删规则都很方便private static readonly (string Pattern, Color Color)[] CodeRules { (?(?:[^\\]|\\.)*, Color.FromArgb(206, 145, 120)), (//[^\n]*, Color.FromArgb(106, 153, 85)), ((?s)/\*.*?\*/, Color.FromArgb(106, 153, 85)), (\b(?:abstract|as|base|bool|break|case|catch|if|for|foreach|class|return|void|static|public|private)\b, Color.FromArgb(86, 156, 214)), (\b\d[\d_]*(?:\.\d)?(?:[eE][-]?\d)?[fFdDmM]?\b, Color.FromArgb(181, 206, 168)) };上面字符串正则用 C# 逐字字符串写每两个双引号表示正则里的一个双引号\b是单词边界避免把classify里的 class 当成关键字。关键字列表这里只列了常用的几个完整 C# 关键字可以直接按 7.0 之后的列表补在竖线后面。数字正则覆盖了整数、小数、科学计数法和f/d/m后缀。3.3 全量着色最小实现public void ApplyHighlight() { SuspendLayout(); string source Text; int cursor SelectionStart; var hits new List(int Start, int Length, Color Color)(); foreach (var rule in CodeRules) { foreach (Match m in Regex.Matches(source, rule.Pattern, RegexOptions.Compiled | RegexOptions.Multiline)) { hits.Add((m.Index, m.Length, rule.Color)); } } hits.Sort((a, b) { int c a.Start.CompareTo(b.Start); return c ! 0 ? c : b.Length.CompareTo(a.Length); }); Select(0, source.Length); SelectionColor ForeColor; foreach (var hit in hits) { Select(hit.Start, hit.Length); SelectionColor hit.Color; } Select(cursor, 0); ScrollToCaret(); ResumeLayout(); }逻辑说明先把 Text 和光标位置存下来正则匹配只做一次把所有命中的位置、长度和颜色放进一个集合按 Start 升序排好相同 Start 时 Length 更长的排在前面这样当字符串和关键字命中同一段时字符串规则先上色最后全选恢复默认色再逐段 Select SelectionColor 上色。因为改 SelectionColor 不会改 Text所以不会递归触发 TextChanged。恢复光标位置用 Select(cursor, 0)ScrollToCaret 保证光标可见。3.4 用 Timer 防抖挂上 TextChangedprivate Timer _highlightTimer; private void InitHighlightTimer() { _highlightTimer new Timer { Interval 200 }; _highlightTimer.Tick (s, e) { _highlightTimer.Stop(); ApplyHighlight(); }; TextChanged (s, e) { _highlightTimer.Stop(); _highlightTimer.Start(); }; }Timer 每次触发都会先 Stop再在 200ms 后重新执行高亮。这样用户连续打字时不会每个字符都跑一次全量着色只在停顿后执行。200ms 是一个平衡值如果脚本行数很多可以放宽到 300ms。对实时性要求更高的场景直接进入下一章的增量着色。4. 性能与闪烁优化C#代码高亮控件的增量着色和 3 个必调参数4.1 全量着色为什么会在长文本下卡顿用第 3 章的 ApplyHighlight 处理 300 行脚本时每次按键都要把所有文本重新过正则再执行几百次 Select 和 SelectionColor。RichTextBox 每执行一次 Select 和 SelectionColor 都会触发内部布局和重绘因此整体复杂度不只是 O(n)而是 O(n × hits)。在 WinForm 上位机里如果一边做循环数据采集一边刷新 UI再叠加这个高亮操作界面就会明显卡顿。这是“循环数据采集和 UI 刷新卡顿”问题里常见的叠加因素。4.2 增量着色只重画发生变化的行public void HighlightChangedLine(int changedIndex) { int line GetLineFromCharIndex(changedIndex); int start GetFirstCharIndexFromLine(line); int end GetFirstCharIndexFromLine(line 1); if (end 0) end TextLength; BeginUpdate(); Select(start, end - start); SelectionColor ForeColor; string lineText Text.Substring(start, end - start); if (lineText.Contains(/*) || lineText.Contains(*/)) { ApplyHighlight(); EndUpdate(); return; } foreach (var rule in CodeRules) { foreach (Match m in Regex.Matches(lineText, rule.Pattern, RegexOptions.Multiline)) { Select(start m.Index, m.Length); SelectionColor rule.Color; } } Select(changedIndex, 0); ScrollToCaret(); EndUpdate(); }GetLineFromCharIndex 用字符位置换行号因为 WordWrapfalse行号和换行符一一对应。BeginUpdate/EndUpdate 用来挂起重绘下面会讲。这里对跨行注释做了简单退化处理如果当前行出现/*或*/就回退到全量着色否则多行注释开始行着色后后续行颜色会错。这个版本只处理了当前行适合结合 Timer 防抖一起用每 200ms 只重画光标停在一行性能压力小很多。4.3 三个必调参数挂起重绘、禁用换行、保留选择参数/方法设定解决什么问题WM_SETREDRAW 挂起重绘BeginUpdate/EndUpdate批量着色时避免闪烁WordWrapfalse保证行索引和行号一致HideSelectionfalse失焦后高亮不消失另外两个隐藏开关是 DetectUrlsfalse 和 SelectionTabs32第 2 章已经说明。WM_SETREDRAW 通过 Win32 消息实现ListBox 有 BeginUpdateRichTextBox 没有需要自己补[DllImport(user32.dll)] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); private const int WM_SETREDRAW 0x000B; public void BeginUpdate() { if (!IsHandleCreated) return; SendMessage(Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero); } public void EndUpdate() { if (!IsHandleCreated) return; SendMessage(Handle, WM_SETREDRAW, new IntPtr(1), IntPtr.Zero); Invalidate(); }BeginUpdate 让控件停止绘制EndUpdate 恢复并强制重绘一次。注意 IsHandleCreated 判断Handle 没创建时调用 SendMessage 会报错。把这两个方法放到控件公开接口里批量着色时包一层闪烁基本就解决了。4.4 把正则匹配放到后台线程对于几千行的文本正则本身也可能成为瓶颈。常见做法是把匹配计算挪到 Task.Run只把上色放回 UI 线程private int _version; public void QueueHighlight() { int version _version; string snapshot Text; Task.Run(() { var hits MatchInBackground(snapshot); BeginInvoke(new Action(() { if (version ! _version) return; ApplyHits(hits); })); }); }MatchInBackground 就是把第 3 章的 Regex.Matches 循环抽出去ApplyHits 就是把 Select/SelectionColor 循环放回来。_version 是防过期标记后台任务完成时如果用户已经改过文本version 不一致直接丢弃结果。BeginInvoke 保证颜色操作只在 UI 线程执行这是 WinForms 线程模型的硬性要求。需要提醒的是 Text 属性只能在 UI 线程读所以先取 snapshot 再开 Task。5. 行号、跳转与高亮验证把 C# 代码高亮控件做成可交付的上位机脚本区5.1 左侧行号最小绘制行号一般放在左边一个 Panel 里监听 TextChanged 和 VScroll 后重绘。最小实现只画可视区private void LineNumberPanel_Paint(object sender, PaintEventArgs e) { e.Graphics.Clear(Color.FromArgb(37, 37, 38)); int lineHeight Font.Height 1; int visibleRows Height / lineHeight; for (int i 0; i visibleRows; i) { e.Graphics.DrawString((i 1).ToString(), new Font(Consolas, 9f), Brushes.Gray, new PointF(4, i * lineHeight 2)); } }这里只画了从第 1 行开始的可见行真实滚动场景要用 GetCharIndexFromPosition 算出当前首行再偏移行号否则滚屏时行号不动。5.2 跳转到指定行跳转是脚本编辑区常用功能配合查找结果定位代码位置public void GoToLine(int lineNumber) { if (lineNumber 1) lineNumber 1; int index GetFirstCharIndexFromLine(lineNumber - 1); if (index 0) return; Select(index, 0); ScrollToCaret(); }GetFirstCharIndexFromLine 的入参是 0 基行号所以传 lineNumber - 1无效行返回 -1需要先判断。Select(index, 0) 把光标移到该行开头ScrollToCaret 滚动到可见区域。5.3 验证着色规则按位置取颜色而不是凭眼睛看全量高亮后可以用一段自检代码验证字符串里的关键字没有被染色public Color GetColorAt(int index) { int start SelectionStart; int length SelectionLength; Select(index, 1); Color c SelectionColor; Select(start, length); return c; }验证代码可以放在单元测试或临时按钮里CSharpCodeBox box new CSharpCodeBox(); box.Text var s \if\;; box.ApplyHighlight(); int pos box.Text.IndexOf(if, StringComparison.Ordinal); Color c box.GetColorAt(pos); Debug.Assert(c CodeRules[0].Color);如果断言通过说明字符串规则在关键字规则之前生效如果不通过检查 CodeRules 数组顺序。最后的交付细节是在 FormClosing 里把 _version 加一停掉 Timer后台任务即使还在跑BeginInvoke 时发现版本过期也会直接丢弃不会在窗体关闭后抛 ObjectDisposedException。整理好之后CSharpCodeBox 就是一个可以直接拖到其他窗口复用的 C# 代码高亮 TEXTBOX 控件。本文还有配套的精品资源点击获取
返回列表