资讯详情

资讯详情

WinForm TextBox水印实现:重写WndProc打造可复用控件

简介本资源面向Winform桌面开发初学者与需要优化表单交互体验的开发者提供一套通过继承TextBox并重写绘制逻辑来实现水印placeholder效果的完整示例工程。资源包共25个文件包含8个cs源码、4个cache缓存、3个exe可执行程序、2个resources资源文件、2个pdb调试符号、2个resx资源定义、1个csproj项目文件、1个settings配置、1个txt说明及1个manifest清单压缩包约44KB结构紧凑便于直接编译运行。核心代码围绕WaterTextBox自定义控件展开涵盖OnPaint绘制水印、TextChanged与焦点事件联动刷新等关键实现并配有Form1窗体演示与Properties配置方便读者对照理解控件生命周期。目前已有254人学习下载适合希望掌握自定义控件绘制、事件重写与Winform界面细节打磨的开发者参考借鉴。1. WinForm 里给 TextBox 加水印从一句需求到可复用的控件方案做 WinForm 项目的人多半遇到过这个场景登录框里孤零零一个 TextBox用户盯着它不知道填账号还是手机号产品经理一句“加个提示文字吧”你就得动手。这就是 TextBox 水印也叫占位符提示本质是在输入框为空且未聚焦时显示一段灰色提示文字聚焦或输入后自动消失。它看着简单但真做起来有人用 Label 叠上去有人重写 WndProc有人干脆在 TextChanged 里手动清空——每种做法都有各自的翻车点。这篇笔记面向正在做 WinForm 界面、需要给输入框加提示的开发者把水印控件的实现路径、参数设置和踩坑经验讲透让你能直接抄进项目里用。2. TextBox 水印的三种实现路线为什么我最终选了重写 WndProc2.1 三种常见做法的对比与选型理由在 WinForm 里给 TextBox 加水印市面上流传的做法大致三类我按可靠性和维护成本排个序。第一种是 Label 叠加法。拖一个 Label 放到 TextBox 上面设置灰色字体在 TextBox 的 Enter、Leave、TextChanged 事件里控制 Label 的 Visible。这种做法上手最快新手十分钟能搞定但问题也最多Label 和 TextBox 的坐标要手动对齐窗口缩放时容易错位Label 会挡住鼠标点击得额外处理如果 TextBox 有圆角或自定义边框Label 的定位更麻烦。我早期项目里用过后来全部替换掉了。第二种是 SendMessage 发送 EM_SETCUEBANNER 消息。这是 Windows 系统原生支持的水印机制一行 P/Invoke 就能调用效果最自然系统自己管理显示和隐藏。但它有两个硬限制只支持单行 TextBox多行 TextBox 无效水印文字颜色和字体由系统主题决定你改不了。如果你的需求就是单行输入框加个灰色提示这是最省事的方案。第三种是重写 WndProc 自绘。继承 TextBox拦截 WM_PAINT 消息在控件绘制完成后用 Graphics.DrawString 画上提示文字。这种做法完全可控单行多行都支持颜色字体随便调还能加图标。代价是要理解 Windows 消息机制代码量比前两种多。我最终选它是因为项目里有多行备注框需要水印而且设计要求水印颜色跟主题走前两种都满足不了。方案多行支持样式可控代码量适用场景Label 叠加支持高少临时原型、要求不高的场景EM_SETCUEBANNER不支持低极少单行输入框、追求原生效果重写 WndProc支持高中等正式项目、多行、自定义样式选型建议很直接单行且不在意样式用 EM_SETCUEBANNER多行或要自定义颜色字体重写 WndProcLabel 叠加只适合临时验证别带进正式代码。2.2 重写 WndProc 的核心代码与参数说明下面是我项目里在用的水印 TextBox 完整实现基于 .NET Framework 4.7.2 和 VS2022 验证过.NET 6/8 的 WinForm 同样适用。using System; using System.Drawing; using System.Windows.Forms; public class WatermarkTextBox : TextBox { private string _watermarkText 请输入内容; private Font _watermarkFont; private Color _watermarkColor Color.Gray; private bool _showWatermark true; // 水印文字修改后触发重绘 public string WatermarkText { get _watermarkText; set { _watermarkText value; Invalidate(); } } // 水印颜色默认灰色 public Color WatermarkColor { get _watermarkColor; set { _watermarkColor value; Invalidate(); } } public WatermarkTextBox() { // 默认用水印字体跟随控件字体避免字体不一致导致错位 _watermarkFont this.Font; this.TextChanged (s, e) UpdateWatermarkState(); this.Enter (s, e) UpdateWatermarkState(); this.Leave (s, e) UpdateWatermarkState(); } private void UpdateWatermarkState() { // 有内容时不显示水印聚焦时也不显示 bool shouldShow string.IsNullOrEmpty(this.Text) !this.Focused; if (_showWatermark ! shouldShow) { _showWatermark shouldShow; Invalidate(); // 只重绘不改变 Text 属性 } } protected override void WndProc(ref Message m) { base.WndProc(ref m); // WM_PAINT 0x000F在系统绘制完成后叠加水印 if (m.Msg 0x000F _showWatermark !string.IsNullOrEmpty(_watermarkText)) { using (Graphics g this.CreateGraphics()) { // 文字起点留 3 像素内边距避免贴边 TextRenderer.DrawText( g, _watermarkText, _watermarkFont, new Point(3, 3), _watermarkColor, TextFormatFlags.Left | TextFormatFlags.Top); } } } }逻辑说明关键在于不修改 Text 属性。很多新手做法是在 TextChanged 里把水印文字写进 Text聚焦时再清空这样虽然能显示但会污染业务逻辑——你判断用户是否输入时得先排除水印文字绑定数据时也得过滤后患无穷。这里用 _showWatermark 标志位控制绘制Text 始终是用户真实输入干净。参数说明WatermarkText 是提示文字支持中文WatermarkColor 默认 Gray深色主题下可以改成 Color.FromArgb(120, 120, 120)绘制起点 (3, 3) 是经验值如果你的 TextBox 有自定义 Padding需要相应调整。TextRenderer.DrawText 比 Graphics.DrawString 更贴近系统文本渲染中文显示更清晰推荐用前者。2.3 多行 TextBox 水印的适配要点多行 TextBox 用上面的代码基本能跑但有两个细节要处理。第一多行 TextBox 默认有垂直滚动条水印文字如果太长会跟滚动条区域重叠建议水印文字控制在 20 字以内或者设置 TextFormatFlags.WordBreak 让它自动换行。第二多行 TextBox 的 Text 属性里可能包含 \r\n判断是否为空时用 string.IsNullOrEmpty 就够了但如果用户输入了空格再删掉Text 会变成空字符串水印能正常恢复这点没问题。如果你的多行框需要水印文字换行显示把绘制那行改成TextRenderer.DrawText( g, _watermarkText, _watermarkFont, new Rectangle(3, 3, this.Width - 6, this.Height - 6), _watermarkColor, TextFormatFlags.Left | TextFormatFlags.Top | TextFormatFlags.WordBreak);用 Rectangle 限定绘制区域加 WordBreak 标志长文字会自动折行。注意宽度减 6 是留出左右边距高度减 6 同理避免文字被边框裁掉。3. 水印控件的属性设计与设计器支持让同事拖控件就能用3.1 在属性面板暴露可调参数上面那个类已经能在代码里用了但团队协作时同事更希望从 VS 设计器的属性面板直接调。要做到这点需要给属性加 Category 和 Description 特性让它们出现在属性窗口的合适分组里。using System.ComponentModel; [Category(水印设置)] [Description(输入框为空时显示的提示文字)] public string WatermarkText { get _watermarkText; set { _watermarkText value; Invalidate(); } } [Category(水印设置)] [Description(水印文字的颜色)] public Color WatermarkColor { get _watermarkColor; set { _watermarkColor value; Invalidate(); } }加上 Category 后属性面板里会出现“水印设置”分组WatermarkText 和 WatermarkColor 都在里面同事拖一个 WatermarkTextBox 到窗体上直接在属性面板填提示文字就行不用看代码。Description 里的文字会显示在属性面板底部相当于内置文档。这里有个坑要注意WatermarkText 的 setter 里调用了 Invalidate()但如果你在设计器里改属性Invalidate 可能不会立即触发重绘因为设计器里的控件还没真正显示。这不影响运行效果只是设计器预览时水印可能不刷新编译运行后就正常了。3.2 处理字体变化与 DPI 缩放水印字体默认跟随控件字体但有个隐患如果运行时动态改了 this.Font_watermarkFont 还是旧字体水印会跟输入文字字体不一致。解决办法是重写 OnFontChanged。protected override void OnFontChanged(EventArgs e) { base.OnFontChanged(e); _watermarkFont this.Font; // 字体变化时同步更新水印字体 Invalidate(); }DPI 缩放是另一个容易翻车的地方。在高分屏上如果项目没开 PerMonitorV2绘制起点 (3, 3) 在 150% 缩放下会显得偏小水印文字贴着边框。稳妥做法是用 this.Font.Height 来算垂直偏移或者直接读取 DeviceDpi。protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); // 根据 DPI 调整绘制起点96 是标准 DPI int offset (int)(3 * (this.DeviceDpi / 96.0)); _drawOffset new Point(offset, offset); }把 _drawOffset 存成字段WndProc 里用 _drawOffset 替代硬编码的 (3, 3)。这样在 125%、150% 缩放的高分屏上水印位置都能保持合理边距。我吃过这个亏客户用 Surface 笔记本演示时水印贴边现场改代码很尴尬。3.3 与数据绑定和验证控件的配合WinForm 项目里 TextBox 经常跟 ErrorProvider 或数据绑定一起用。水印控件跟它们配合时核心原则还是那条不污染 Text。ErrorProvider 校验的是 Text 属性水印不写进 Text校验逻辑就不用改。数据绑定同理绑定的也是 Text水印不影响。但有一个场景要留意如果你用了 TextBox 的 PlaceholderText 属性.NET Core 3.0 之后 TextBox 原生支持那就不需要自己实现了。原生 PlaceholderText 只支持单行且样式不可调但胜在零代码。判断标准很简单.NET Framework 项目用自定义控件.NET 6 单行场景优先试原生 PlaceholderText多行或要自定义样式再上自定义控件。4. 水印控件的避坑与排查那些我踩过的坑4.1 水印文字被输入内容覆盖或闪烁现象快速输入时水印偶尔闪一下才消失或者输入第一个字符后水印还残留。原因UpdateWatermarkState 在 TextChanged 里调用但 Invalidate 是异步的系统可能先绘制了旧状态再处理重绘。另外如果 Enter 事件和 TextChanged 触发顺序有偏差状态判断会短暂不一致。解决在 WndProc 里绘制前再判断一次状态而不是只依赖 _showWatermark 字段。把绘制条件改成string.IsNullOrEmpty(this.Text) !this.Focused双保险。闪烁问题还可以开双缓冲在构造函数里加this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);不过 TextBox 对双缓冲支持有限实测能减轻但不能完全消除属于 WinForm 原生控件的玄学范畴。4.2 水印在只读或禁用状态下显示异常现象把 TextBox 设为 ReadOnly 或 Enabledfalse 后水印颜色跟背景混在一起看不清或者禁用时水印不显示。原因ReadOnly 状态下 TextBox 背景变灰默认的 Gray 水印跟灰背景对比度不够。禁用状态下系统绘制逻辑不同WM_PAINT 的处理可能有差异。解决在 UpdateWatermarkState 里根据 ReadOnly 和 Enabled 动态调水印颜色。ReadOnly 时用 Color.FromArgb(160, 160, 160)禁用时用 SystemColors.GrayText。代码里加个判断private Color GetEffectiveWatermarkColor() { if (!this.Enabled) return SystemColors.GrayText; if (this.ReadOnly) return Color.FromArgb(160, 160, 160); return _watermarkColor; }绘制时用 GetEffectiveWatermarkColor() 替代 _watermarkColor。4.3 多行 TextBox 水印与滚动条冲突现象多行 TextBox 有垂直滚动条时水印文字画到了滚动条下面或者滚动内容时水印跟着滚。原因CreateGraphics 拿到的是整个控件区域包括滚动条。水印绘制在 WM_PAINT 之后滚动时系统重绘内容区水印位置没跟着调整。解决绘制区域要排除滚动条宽度。用 this.ClientRectangle 减去 SystemInformation.VerticalScrollBarWidth。如果滚动条是自动出现的还得判断 this.ScrollBars 属性。更简单的办法是给多行 TextBox 设一个固定的右边距水印文字区域宽度设为 this.ClientSize.Width - SystemInformation.VerticalScrollBarWidth - 6这样即使滚动条出现也不会重叠。4.4 设计器里水印不显示或报错现象把 WatermarkTextBox 拖到窗体上设计器里看不到水印或者属性面板改 WatermarkText 时报错。原因设计器模式下控件的 Handle 可能还没创建CreateGraphics 会抛异常。另外设计器里的 Invalidate 调用可能被忽略。解决在 WndProc 里加判断if (this.DesignMode) return;设计器模式下不绘制水印。或者用 LicenseManager.UsageMode 判断运行时。这样设计器里看到的是普通 TextBox运行时才显示水印虽然设计器预览不完美但至少不报错。我一般还会在构造函数里加if (DesignMode) { _watermarkText ; }让设计器里干净些。4.5 水印文字包含特殊字符时的绘制异常现象水印文字里有 符号时显示不全或者有换行符时布局错乱。原因TextRenderer.DrawText 默认会把 当作助记符前缀单个 后面的字符会带下划线。换行符 \n 在单行绘制模式下不生效。解决绘制时加 TextFormatFlags.NoPrefix 标志禁用助记符解析。水印文字里如果有换行需求用多行绘制模式前面 2.3 节的 Rectangle 版本并且把 \n 替换成 Environment.NewLine。水印文字建议只放纯中文、英文、数字特殊符号能避则避。5. 进阶把水印做成可复用的基础控件库5.1 从单个控件到项目级控件库一个项目里往往有十几个输入框要加水印每个都拖 WatermarkTextBox 虽然能用但样式不统一——有人设灰色有人设浅蓝产品看了要打人。更好的做法是建一个控件库项目把水印样式收敛成几个预设。具体做法新建一个 WinForm 控件库项目.NET Framework 选“Windows 窗体控件库”.NET 6 选“Windows 窗体控件库”模板把 WatermarkTextBox 放进去再加一个 WatermarkStyle 枚举和对应的颜色映射。public enum WatermarkStyle { Normal, // 普通输入框灰色水印 Search, // 搜索框浅灰水印 放大镜图标位 Required // 必填项浅红水印 } // 在控件里加一个 Style 属性 [Category(水印设置)] [Description(水印预设样式)] public WatermarkStyle Style { get _style; set { _style value; _watermarkColor value switch { WatermarkStyle.Search Color.FromArgb(180, 180, 180), WatermarkStyle.Required Color.FromArgb(220, 150, 150), _ Color.Gray }; Invalidate(); } }这样同事用的时候只需要设一个 Style 属性颜色自动匹配整个项目的输入框提示风格就统一了。控件库编译成 DLL 后其他项目引用即可不用每个项目复制一遍代码。5.2 验证水印控件是否真的可用写完控件别急着交付按这个清单过一遍单行输入框聚焦时水印消失、失焦且为空时水印恢复多行输入框输入文字后水印消失、清空后恢复ReadOnly 和 Enabledfalse 状态下水印颜色可辨高分屏 150% 缩放下水印不贴边设计器里拖控件不报错数据绑定和 ErrorProvider 校验不受影响。这六条过了基本就能进生产环境。我自己的习惯是建一个测试窗体把这几种状态各放一个控件跑一遍截图存档。后面改代码时对照截图能快速发现回归问题。水印这东西看着小但用户天天看做扎实了省心做糙了天天有人提 bug。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →