资讯详情

WinForms自定义下划线输入框:继承TextBox自绘实现与避坑指南

📅 2026/10/18 1:44:27 | 华诺云谱 👁 阅读
WinForms自定义下划线输入框:继承TextBox自绘实现与避坑指南
简介在窗体应用开发中简洁现代的界面设计常需要自定义控件。此资源提供一款仅显示底部一条直线的文本框控件源码通过继承基础文本框类并重写绘画方法先以窗体同色填充背景再用细笔绘制底边实现单线条输入框适合轻量化表单或极简风格界面避免默认边框的突兀感。资源包共35个文件以9个C#源代码文件为核心另含4个动态库、调试符号、工程文件及说明文档压缩后仅53KB便于学习阅读。除核心控件实现外还附带示例程序可直接运行查看效果并参考其中对鼠标悬停、焦点切换等事件的重写方式掌握自定义控件从外观到交互的完整写法。已有516人学习浏览是窗体自绘控件入门的实用参考也可直接提取源码改造复用。1. 一个只显示一条直线的 TextBox从需求到控件的完整落地接手过一个内部系统的登录界面改造需求只有一句把输入框做成只显示底部一条线的极简风格。一开始以为把 BorderStyle 改成 None 再画条线就行真正动手才知道 WinForms 的 TextBox 没那么听话——系统边框、焦点态、提示文字、DPI 缩放任何一个环节不处理那条直线都会以各种姿势翻车。这份 TextBox控件一条直线源码就是为这个场景准备的一个继承自 TextBox 的自定义控件重写绘制逻辑让输入框干净地只留下一条横线同时保留光标、选中、右键菜单这些原生能力。适合正在做 WinForms 现代化界面的开发者或者想把自绘控件机制搞懂的人拿过去改改就能用在登录页、搜索框、设置表单上。2. 整体设计与技术选型为什么这个控件值得用继承而不是从零画2.1 继承 TextBox 还是从 Control 重写一条线的两种实现路线要做只显示一条直线的输入框表面看有两条路一是继承 Control 从零画一个文本框二是继承现成的 TextBox 做绘制层改造。前者自由度最高想画成什么样都行但代价是光标、输入法、选中态、右键菜单全部要自己实现工程量翻倍后者保留了 WinForms 文本框几十年积累的原生交互只需要在绘制层动手脚。这条源码选的是继承 TextBox这是成熟项目里最常见的做法。原因很实际登录框也好、搜索框也好用户对输入交互的预期是和普通输入框一致谁也不想遇到中文输入法候选框位置不对、光标不闪烁这类基础问题。继承 TextBox 等于把这些行为全部白嫖到手要改的只是画出来长什么样这一层。另一个设计决策是关闭系统边框。TextBox 自带的 BorderStyle 有 None、FixedSingle、Fixed3D 三种要让下划线完全可控必须设成 None否则系统边框会先画一层自己再画一条线两层叠在一起就是重影。源码的构造函数里对这个做了显式处理后面会讲。2.2 源码目录结构一个类文件干完所有事这份源码的工程结构很精简核心只有一个控件类其余是承载演示的窗体骨架。UnderlineTextBox/ ├── UnderlineTextBox.cs // 自定义控件主类所有绘制和交互逻辑都在这里 ├── UnderlineTextBoxDemo/ // 演示工程 │ ├── Form1.cs // 演示窗体放置控件并展示效果 │ ├── Form1.Designer.cs // 窗体设计器代码 │ ├── Program.cs // 入口 │ └── UnderlineTextBoxDemo.csproj控件类单独拧出来放在上层意味着你可以把 UnderlineTextBox.cs 直接拖进任意一个 WinForms 工程不需要引用整个解决方案。演示工程的作用不是展示复杂用法而是验证一个最简单的场景拖进去、改几行属性、跑起来看到那条线。整个控件不依赖任何第三方包纯 .NET Framework / .NET Core WinForms 自带 API 实现。这对想改造成自己项目一部分的人来说很重要——没有包版本冲突没有 NuGet 依赖地狱拷过去就能编。2.3 绘制架构三层内容叠加的 OnPaint 策略控件最终呈现出来的效果由三层绘制叠加而成背景层、文字层、下划线层。背景层直接清屏填充 BackColor文字层用 TextRenderer 画真实文本没输入时画占位提示文字下划线层在最底部用 Pen 画一条横线按焦点状态切换颜色和粗细。这里有个 WinForms 的老玄学必须点出来TextBox 默认不是自绘控件你单纯重写 OnPaint 不会生效因为系统窗口接管了绘制。要让自绘代码跑起来必须在构造函数调用 SetStyle 打开 UserPaint 标志同时配合 AllPaintingInWmPaint 和 OptimizedDoubleBuffer 减少闪烁。源码里正是这样处理的第一次写这种控件的人最容易在这个点卡住。3. 核心代码拆解从构造到消息处理的完整链路3.1 构造与属性定义暴露给使用者的可配置项先看构造函数和属性这块。控件的公共属性决定了使用者在设计器里能调什么源码里把颜色、提示文字、线条粗细全部做成了可配置项。public partial class UnderlineTextBox : TextBox { private Color _underlineColor Color.FromArgb(160, 160, 160); private Color _focusUnderlineColor Color.FromArgb(64, 158, 255); private string _placeholderText string.Empty; private Color _placeholderColor Color.FromArgb(180, 180, 180); private int _underlineThickness 2; public UnderlineTextBox() { // 关键打开自绘标志否则 OnPaint 永远不会被调用 SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true); // 关掉系统边框避免重影 BorderStyle BorderStyle.None; // 初始高度预留绘制区域 Height 28; } [Category(Appearance)] public Color UnderlineColor { get _underlineColor; set { _underlineColor value; Invalidate(); } } [Category(Appearance)] public Color FocusUnderlineColor { get _focusUnderlineColor; set { _focusUnderlineColor value; Invalidate(); } } [Category(Appearance)] public string PlaceholderText { get _placeholderText; set { _placeholderText value; Invalidate(); } } [Category(Appearance)] public Color PlaceholderColor { get _placeholderColor; set { _placeholderColor value; Invalidate(); } } [Category(Appearance)] public int UnderlineThickness { get _underlineThickness; set { _underlineThickness Math.Max(1, value); Invalidate(); } } }构造函数里 SetStyle 的三行是核心。UserPaint 告诉系统这个控件由托管代码绘制OnPaint 才会被调用AllPaintingInWmPaint 让所有绘制消息统一走 OnPaint避免系统先画一遍又让 OnPaint 画一遍造成闪烁OptimizedDoubleBuffer 是双缓冲绘制频繁刷新时不会肉眼可见地闪。属性全部用 Category 标记在 Visual Studio 设计器里会自动归类到外观分组。每个 setter 里都调 Invalidate()强制触发重绘这样在设计器里改属性的同时设计视图会实时刷新不用重新编译就能看到效果。UnderlineThickness 的 setter 还做了下限保护Math.Max(1, value) 防止有人填 0 把线画没。3.2 OnPaint 重写文字、提示、下划线的三合一绘制绘制主体在 OnPaint 里这一段是整套控件的核心。它把前面说的三层内容一次画完。protected override void OnPaint(PaintEventArgs e) { Graphics g e.Graphics; g.Clear(BackColor); Rectangle textRect ClientRectangle; textRect.Height - _underlineThickness; // 真实输入文字 if (Text.Length 0) { TextRenderer.DrawText(g, Text, Font, textRect, ForeColor, TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); } // 提示文字仅当没有输入且设置了占位符时出现 else if (!string.IsNullOrEmpty(_placeholderText)) { TextRenderer.DrawText(g, _placeholderText, Font, textRect, _placeholderColor, TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); } // 下划线焦点用高亮色失焦用普通色 Color lineColor Focused ? _focusUnderlineColor : _underlineColor; using (Pen pen new Pen(lineColor, _underlineThickness)) { int y Height - _underlineThickness; g.DrawLine(pen, 0, y, Width, y); } base.OnPaint(e); }textRect 的高度先减掉下划线的厚度否则文字会跟线贴在一起。TextRenderer.DrawText 和 Graphics.DrawString 的区别值得留意DrawText 用的是 GDI 的文本渲染对中文字体渲染更锐利而且 TextFormatFlags.VerticalCenter 能让文字在矩形内垂直居中手动算坐标的麻烦全省了。焦点判断直接用 TextBox 自带的 Focused 属性。获取焦点画高亮蓝、失去焦点画灰色这是极简输入框常见的交互反馈。Pen 对象放在 using 里每帧绘制用完就释放这个习惯能避免 GDI 对象泄漏——WinForms 程序跑久了出现创建图形对象失败的异常十有八九是 Pen 和 Brush 没释放。最后一个细节是 base.OnPaint(e)它在所有自绘完成后调用。有人习惯把 base.OnPaint 放最前面在这个控件里会导致系统先画一层造成闪动所以放在尾部确保自绘内容完整覆盖。3.3 消息处理拦截 WM_PAINT 之外的关键消息OnPaint 负责静态画面的呈现但控件的交互行为还需要在消息层兜底。源码里重写了 WndProc 处理一组和输入体验相关的 Windows 消息。protected override void WndProc(ref Message m) { const int WM_PAINT 0x000F; const int WM_ERASEBKGND 0x0014; const int WM_NCPAINT 0x0085; switch (m.Msg) { // 拦截背景擦除避免绘制前先清一次屏造成闪动 case WM_ERASEBKGND: return; // 非客户区绘制消息直接吃掉改为由 OnPaint 统一处理 case WM_NCPAINT: return; default: base.WndProc(ref m); break; } }WM_ERASEBKGND 是闪屏的罪魁祸首。系统默认在重绘前先擦除背景擦一次、画一次视觉上就是闪。直接 return 掉这个消息配合构造里的 OptimizedDoubleBuffer闪烁问题从根上断掉。WM_NCPAINT 在设置了 Self-Driving 绘制的控件里会干扰整体效果吃掉后让它走 OnPaint 统一绘制能避免下划线边缘出现像素级别的残影。这两个消息是最常见的坑网上搜WinForms 自绘控件闪烁八成帖子都在讨论它。需要注意WndProc 里没有处理 WM_PAINT因为 UserPaint 模式会把刷新统一调度到 OnPaint再拦截一次反而多余。只拦 ERASEBKGND 和 NCPAINT 足够。4. 使用与验证在真实窗体上跑出那条线4.1 最小运行配置三步跑起 Demo拿到代码后想最快看到效果按三步操作用 Visual Studio 打开演示工程直接按 F5 编译运行主窗体上就会出现一个已经放好的 UnderlineTextBox点击输入框时下划线从灰色变蓝色空白时显示提示文字。如果你想在现有项目里用这个控件更典型的路径是打开你自己的窗体设计器把 UnderlineTextBox.cs 拖进工程编译一次控件就会出现在工具箱顶部的组件栏里然后像拖普通 TextBox 一样拖到窗体上即可。拖上去之后有个容易忽略的点控件的默认高度是 28 像素如果你把 Anchor 或 Dock 设成自动拉伸下划线会跟着变。登录表单里一般期望固定高度建议在设计器里把 Anchor 设置为 Left | Right锁住垂直方向。4.2 属性对照表每个公开属性管什么控件在属性面板里暴露的内容不多但每项都能直接影响观感对照关系如下属性类型作用建议取值UnderlineColorColor失焦时下划线颜色灰色系如 160,160,160FocusUnderlineColorColor聚焦时下划线颜色品牌高亮色如 64,158,255PlaceholderTextstring无输入时显示的提示文字如请输入用户名PlaceholderColorColor提示文字颜色比正文浅一到两阶UnderlineThicknessint下划线粗细像素1 到 3 之间FontFont输入文字和提示文字的字体与全局字体一致ForeColorColor输入文字颜色深灰或近黑色属性面板里 Font 和 ForeColor 是 TextBox 自带的但在这个控件里它们同时影响输入文字和提示文字的显示改的时候要意识到这一点。设计器里实时预览是这项源码体验做得比较好的地方——因为每个 setter 都触发了 Invalidate改完属性窗口的内容设计界面马上刷新。4.3 行为验证从输入到失焦的完整测试路径控件跑起来以后验证的重点不是能不能画出线而是自绘模式下交互有没有被破坏。我一般按下面这条路径过一遍第一步初始状态检查。窗体加载后控件无输入显示浅灰色提示文字下划线为普通灰。第二步聚焦检查。用鼠标点击或 Tab 键进入控件下划线切换为高亮色文字光标正常闪烁提示文字消失。第三步输入检查。输入中文、英文、数字混合内容确认字符显示清晰无重绘残影输入法候选框定位正常。第四步复制粘贴检查。CtrlC、CtrlV 操作可用右键菜单弹得出来。第五步失焦检查。点击窗体其他区域下划线变回灰色但输入的文字保留。这套路径能覆盖 90% 的自绘控件翻车点。如果其中有任何一步不对问题基本都出在消息处理那层回到 3.3 节的 WndProc 里排查。5. 避坑排查自绘 TextBox 最容易翻车的五个场景5.1 下划线被系统边框重影覆盖现象下划线画出来了但上面叠着一层系统默认的立体边框看起来又脏又乱。原因BorderStyle 没有设为 None。TextBox 的默认边框是 Fixed3D 或 FixedSingle系统在绘制边框时占了底部几个像素和自绘的下划线叠加。解决在构造里显式设置 BorderStyle BorderStyle.None。如果是在设计器里拖的控件还要在设计器属性面板里把 BorderStyle 改掉因为设计器序列化出来的赋值发生在构造之后会覆盖构造里的设置。5.2 提示文字在输入后仍然显示现象已经输入了字符占位提示文字还压在输入内容后面两层文字重叠。原因判断是否显示提示文字时漏掉了对焦点状态的组合判断或者只判断了 Text 是否为空。解决OnPaint 里的提示分支严格按Text 长度为 0 且设置了 PlaceholderText来走。代码里用的是Text.Length 0先判断真实文本else if 才轮到提示文字这个顺序保证不会重叠。5.3 DPI 缩放后下划线偏离底部现象100% 缩放下线位置正确系统缩放到 125% 或 150% 时下划线要么跑到控件外面要么离文字越来越近。原因WinForms 默认的 AutoScaleMode 是 FontDPI 变化时控件高度被缩放但 OnPaint 里用的是物理像素坐标Height 属性此时返回的是缩放后的值叠加机制乱了。解决一种做法是让 OnPaint 里所有坐标都从 ClientRectangle 推导不要写死数字。更彻底的方案是在窗体级别设置 AutoScaleMode Dpi并把控件的 MinimumSize 设为一个跟随 DPI 换算的值。实践中建议至少把 y 坐标写成ClientRectangle.Bottom - _underlineThickness而不是Height - _underlineThickness后者在缩放时偶尔会差一两像素。5.4 快速输入时下划线闪动现象连续打字时整条线轻微闪动同时文字区域偶发残影。原因WM_ERASEBKGND 没拦掉系统在每次重绘时先擦背景再画前景双缓冲设置没有完全生效。解决确认 WndProc 里 WM_ERASEBKGND 是直接 return不要再调用 base。同时检查构造里的 SetStyle 是否包含 OptimizedDoubleBuffer。这两个条件同时满足闪烁基本能压干净。5.5 设计器里控件显示异常运行时却正常现象设计器里控件一团乱文字不显示下划线位置也不对F5 运行起来一切正常。原因设计器环境下 OnPaint 的时机和运行时不同有些属性在设计器构造阶段还是默认值导致绘制分支走了错误路径。解决给控件类加[DesignerCategory(code)]特性并在属性 setter 里用 DesignMode 属性做一次判断。虽然不是所有情况都能完全规避但能显著减少设计器里红叉或者空白控件的概率。遇到这种情况最省心的办法是先编译一次再打开设计器让属性序列化充分。6. 二次开发把它升级成带浮动标签的输入框拿到这份源码最常见的改造需求就是下划线输入框 浮动标签——输入框为空且失焦时标签显示在输入框内部聚焦或输入后标签平滑移到上边框位置。这个效果在 Web 端很多组件库里都有WinForms 里自己实现也不算复杂。改动集中在 OnPaint 和新增一个属性上。新增PlaceholderLabelText属性逻辑改成有内容或聚焦时提示文字用更小的字号画到顶部区域无内容且未聚焦时提示文字按正常字号画在中间。protected override void OnPaint(PaintEventArgs e) { Graphics g e.Graphics; g.Clear(BackColor); bool hasText Text.Length 0; float ratio hasText || Focused ? 1f : 0f; // 简化版不处理动画过程 Rectangle textRect ClientRectangle; textRect.Height - _underlineThickness; if (hasText || Focused) { // 浮动态小号字体画在顶部 Font smallFont new Font(Font.FontFamily, Font.Size * 0.7f, Font.Style); TextRenderer.DrawText(g, _placeholderText, smallFont, new Rectangle(textRect.Left, 2, textRect.Width, 18), _focusUnderlineColor, TextFormatFlags.Left | TextFormatFlags.NoPadding); smallFont.Dispose(); } if (hasText) { TextRenderer.DrawText(g, Text, Font, textRect, ForeColor, TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); } else if (!Focused) { TextRenderer.DrawText(g, _placeholderText, Font, textRect, _placeholderColor, TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); } using (Pen pen new Pen(Focused ? _focusUnderlineColor : _underlineColor, _underlineThickness)) { g.DrawLine(pen, 0, Height - _underlineThickness, Width, Height - _underlineThickness); } base.OnPaint(e); }这里把浮动逻辑拆成了两步有内容或聚焦时画顶部小号标签再单独处理正文或占位符。为了让移动看起来更顺滑可以引入一个 float 类型的_labelProgress字段在 Enter 和 Leave 事件里用 Timer 驱动从 0 到 1 的插值控制标签的 Y 坐标和字号。验证这套改造是否成功和前面验证基础控件的方法一致聚焦、输入、失焦三种状态各看一遍确认标签位置正确、文字不重叠、失焦后有内容的输入框标签仍然保持浮动。再验证一次 DPI 125% 和 150% 下的显示因为浮动标签涉及绝对坐标是最容易在缩放下越界的地方。改完这个控件再回头看那次做登录框的经历让我学到一个习惯遇到 WinForms 自定义控件永远先确认它是自绘还是系统绘制再动手改代码。绕开这个前提后面所有调整都是空中楼阁。从那时起我每次接到自绘控件相关的需求都会强制走一遍关边框、开自绘、拦消息、测 DPI这四步踩坑的概率低了大半。希望这些内容帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑