简介面向Winform开发者的文本框水印实现资源针对原生TextBox缺少placeholder占位提示的痛点提供基于自定义控件与重写TextBox类的完整解决方案。资源通过继承System.Windows.Forms.TextBox重写OnPaint方法并配合GotFocus、LostFocus与TextChanged事件实现输入为空时显示灰色提示文字、获得焦点或开始输入后自动隐藏的效果。压缩包共25个文件整体仅44KB包含C#源码文件、项目配置文件、resx/resources资源文件以及编译好的exe可执行程序目录结构清晰既可直接运行查看效果也可打开工程逐行学习。已有254人学习下载适合有一定Winform基础、希望掌握控件封装技巧的初中级开发者。内容涵盖水印绘制原理、文本变化刷新逻辑、焦点状态处理等核心知识点并附完整示例与可运行程序。开发者可参照源码自行调整水印文本、颜色和绘制样式还能进一步扩展多语言支持、自定义字体等高级功能便于在实际项目中快速集成。1. 给 WinForms 的 TextBox 加水印先分清“提示文字”和“防截图水印”在 WinForms 项目里给 TextBox 加一行浅灰色的“请输入账号”提示看起来是几分钟的小事但原生控件并没有 Watermark 属性。很多老项目里产品和 UI 把这类提示统称为“水印”于是 textbox 水印、winform 水印的搜索隔三差五就会出现一次。严格说文本框水印要分成两种一种是在输入框为空且未聚焦时显示的引导文案专业叫法 Placeholder另一种是平铺在整个窗口上的防截图水印。前者用第 3 章的自绘控件解决后者用第 6 章的最小方案兜底。适合往下读的人是正在维护原生 WinForms 客户端、需要在界面美化中交付一个不闪烁、能改颜色、能适配密码框的水印文本框的开发者。如果你已经在用第三方控件库先看库自带的 NullText 或 Placeholder 属性不必重复造轮子。2. WinForm 文本框水印的三条实现路线SendMessage、透明 Label 和自绘控件别选错2.1 先回答“你口中的水印是哪一种”需求来源不同技术方案完全不同。第一类是引导文案也就是 TextBox 空着没聚焦时浮现的浅色提示属于 Placeholder第二类是安全水印用多行多列文字盖住主界面用来防截图泄密。常见做法是先问一句这个水印是给谁看的如果目标是引导用户输入账号密码那要求是 30 分钟内做完、不闪烁、样式可调如果目标是防截图那要求是拍照时能看清、平时不干扰操作。这两条路没有任何共享代码选错一次就要推倒重来。我在实际项目里见过把 Placeholder 做成全局防截图水印的翻车现场也见过给安全水印加了“输入后消失”逻辑的尴尬版本。所以动手前先落一句话需求什么条件下显示、什么条件下消失、谁可见、颜色是否固定。这句话定了路线也就定了。2.2 路线一EM_SETCUEBANNER5 行 API 的能行与不行第一类需求里最简单的方式是给原生 EDIT 控件发 EM_SETCUEBANNER 消息。WinForms 的 TextBox 本质包装了系统 EDIT而 Windows 从 Vista 开始ComCtl32 v6给 EDIT 提供了 cue banner 能力也就是系统搜索框里见到的淡灰色提示文字。using System; using System.Runtime.InteropServices; using System.Windows.Forms; public static class TextBoxWatermark { private const int EM_SETCUEBANNER 0x1501; [DllImport(user32.dll, CharSet CharSet.Unicode)] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, string lParam); public static void SetWatermark(TextBox textBox, string watermarkText) { // wParam 传 1 表示显示 cue banner传 0 表示移除 SendMessage(textBox.Handle, EM_SETCUEBANNER, (IntPtr)1, watermarkText); } }这段代码的逻辑很简单把控件句柄、消息编号、显示标志和提示文案依次传给系统。参数里唯一要留意的是第一个参数必须是 TextBox 已经创建好的 Handle。你如果在 Form_Load 里直接调用此时访问 Handle 属性会强制创建句柄一般能跑通更稳的做法是重写 TextBox 的 OnHandleCreated。这条路线能解决 80% 的登录框需求原生渲染、无闪烁、默认就是灰色、字号自动跟随控件。但它有三个边界颜色不支持自定义样式被系统锁定PasswordChar 场景在部分 Windows 版本上显示异常RichTextBox 完全没有 cue banner 能力。内部工具和原型验证用它很合适对外交付的界面美化项目就得继续往下看。2.3 路线二透明 Label看起来快做起来全是坑第二种做法是把一个 Label 叠在 TextBox 上设置 AutoSize、ForeColor 为浅灰初始 Visible 为 trueTextChanged 或 Enter 事件里切换 Visible。这种做法胜在零代码量新手最容易踩进去。坑在三个地方WinForms 的 Label 透明是假透明它把父窗体背景“画”到自己身上一旦 TextBox 背景色和父窗体不一致立刻出现一块色斑点击事件会落在 Label 上必须给 Label 挂 Click 事件把焦点转给 TextBox多行 TextBox 或滚动条出现后Label 的位置和大小很难与内部内容区对齐。我一般只拿它做界面原型演示不放进交付代码。真正要长期维护的 winform 项目案例里这种叠加方案过两个月就会被“水印不见了”或“背景色盖不住”的 bug 找上门。2.4 路线三自绘控件WinForms 界面美化的正解第三种路线是继承 TextBox 自己画提示文字这也是 WinForms 界面美化里水印控件的主流做法。实现成本比 API 多 40 到 60 行换来的是全套控制权WatermarkText、WatermarkColor、字体、对齐方式、多行、密码框、聚焦时是否显示全部自己定义。自绘的难点在于绘制的时机和图层。TextBox 是原生 EDIT 控件直接重写 OnPaint 需要打开 UserPaint 风格而 UserPaint 会破坏原生编辑光标和选中状态。正确的常见做法是重写 WndProc在原生绘制完成之后再叠一层水印文字。第 3 章给完整实现。顺带说一句如果你新项目已经落在 WPF 或 .NET MAUI 上WPF 可以用 Style 加 Trigger 实现水印MAUI 的 Entry/Editor 更是内置 Placeholder 属性只有原生 WinForms 用户才需要纠结这个自定义控件。2.5 选型结论场景推荐路线理由内部工具、原型验证EM_SETCUEBANNER代码最少原生渲染对外演示、临时方案EM_SETCUEBANNER 或 Label快速能看到效果正式交付、界面美化自绘 WndProc 控件颜色、字体、密码框、多行都可控多行或 RichTextBox自绘控件API 和 Label 都覆盖不了如果项目里已经在用 DevExpress、Telerik 这类控件库优先看库自带属性不要和厂商的渲染管线抢绘制权。3. 自绘 WatermarkTextBox 控件可直接抄的完整代码与参数说明3.1 推荐写法重写 WndProc 而不是 OnPaint先给一个可以直接放进项目的完整控件。核心思路是在 WndProc 里先调用 base.WndProc 让原生 EDIT 完成输入、光标、选中状态的绘制然后再用 TextRenderer 把水印文字叠在最上层。using System.ComponentModel; using System.Drawing; using System.Windows.Forms; namespace WinFormWatermark.Controls { public class WatermarkTextBox : TextBox { private const int WM_PAINT 0x000F; private string _watermarkText 请输入内容; private Color _watermarkColor Color.FromArgb(160, 160, 160); private bool _showWhenFocused; private TextFormatFlags _textFormat TextFormatFlags.Left | TextFormatFlags.VerticalCenter; [Browsable(true)] [Category(水印)] [Description(文本框为空且未聚焦时显示的文字)] public string WatermarkText { get _watermarkText; set { _watermarkText value; Invalidate(); } } [Browsable(true)] [Category(水印)] public Color WatermarkColor { get _watermarkColor; set { _watermarkColor value; Invalidate(); } } [Browsable(true)] [Category(水印)] [Description(聚焦时仍然显示水印默认 false)] public bool ShowWhenFocused { get _showWhenFocused; set { _showWhenFocused value; Invalidate(); } } [Browsable(true)] [Category(水印)] [Description(水印对齐方式多行请改用 Top | Left)] public TextFormatFlags WatermarkTextFormat { get _textFormat; set { _textFormat value; Invalidate(); } } public WatermarkTextBox() { // 不要设置 UserPaint保持原生 EDIT 自绘 this.Font new Font(微软雅黑, 9F); } protected override void WndProc(ref Message m) { base.WndProc(ref m); if (m.Msg WM_PAINT) { bool showByFocus ShowWhenFocused || !Focused; if (string.IsNullOrEmpty(Text) showByFocus) { using (Graphics g CreateGraphics()) { Rectangle rect new Rectangle(2, 2, ClientSize.Width - 4, ClientSize.Height - 4); TextRenderer.DrawText( g, _watermarkText, Font, rect, _watermarkColor, BackColor, WatermarkTextFormat); } } } } } }这段代码有四个关键点。第一WndProc 里先 base 后绘制保证水印永远在原生内容之上不会盖住输入光标。第二WM_PAINT 是最低优先级消息窗口拖拽、内容变化、焦点切换都会触发重绘水印消失和出现的响应点是正确的。第三WatermarkTextFormat默认是居左垂直居中适合单行 TextBox多行时客户端区域变高垂直居中会把文字画到整个控件中间需要改成TextFormatFlags.Left | TextFormatFlags.Top。第四绘制矩形比 ClientRectangle 内缩了 2 像素避免水印和系统边框贴在一起。你还可能注意到代码里没有打开ControlStyles.UserPaint。这是有意为之SetStyle(UserPaint) 会让 EDIT 控件进入不完全自绘状态光标、选中背景、系统边框全部可能出现异常这是 WinForms 水印控件最容易翻车的点。3.2 另一种写法UserPaint OnPaint 的代价网上还有不少方案是重写 OnPaint为了让 OnPaint 被触发先调用 SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true)然后在 OnPaint 里 base.OnPaint 再叠文字。public class PaintWatermarkTextBox : TextBox { public PaintWatermarkTextBox() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); if (string.IsNullOrEmpty(Text) !Focused) { TextRenderer.DrawText( e.Graphics, 请输入内容, Font, ClientRectangle, Color.Gray, BackColor, TextFormatFlags.Left | TextFormatFlags.VerticalCenter); } } }这段代码在不少博客里能搜到但真机表现会暴露问题光标变成扁块、选中背景颜色异常、边框绘制不均匀。原因就是 UserPaint 让原生 EDIT 放弃了部分自绘职责而 base.OnPaint 并不会完整补回。我一般不建议对 TextBox 使用这条路线除非你已经准备好自己处理边框和光标。3.3 接入现有 WinForm 项目的两个小细节接入方式很简单在窗体里声明控件实例或直接拖入工具箱都行。this.txtAccount.WatermarkText 请输入账号; this.txtAccount.WatermarkColor Color.FromArgb(180, 180, 180); this.txtAccount.Font new Font(微软雅黑, 10F); this.txtAccount.WatermarkTextFormat TextFormatFlags.Left | TextFormatFlags.VerticalCenter;两个细节值得注意。第一属性修改后已经调用了 Invalidate不需要额外刷新但如果控件 Handle 还没创建Invalidate 是安全的它会记录一个无效区域等句柄创建后的下一次绘制自动按新值画。第二不想在代码里写死文案时可以在设计器属性面板里直接编辑 WatermarkText因为上面加了 Browsable 和 Category 特性属性面板能正常显示“水印”分组。4. 把水印控件做成交付级密码框、多行、RichTextBox 和 5 个可调参数4.1 密码框水印判空逻辑别用 IsNullOrWhiteSpace密码框的水印表现和普通文本框没有区别Text 为空时显示“请输入密码”输入后显示掩码圆点。真正要注意的是判空逻辑。第 3 章的控件用的是string.IsNullOrEmpty(Text)只要用户敲了任意字符哪怕是一个空格水印都会消失这是符合预期交互的。如果你顺手改成string.IsNullOrWhiteSpace(Text)用户输入空格时水印仍然挂在那里视觉上像是密码框坏了。这个差别很隐蔽自测时不容易发现。另外PasswordChar 和 UserPaint 有冲突如果某份代码里同时设置了 PasswordChar 又开了 UserPaint掩码圆点可能和水印同时出现这属于 5.2 说的光标绘制异常的同源问题。4.2 多行文本框水印VerticalCenter 会让文字漂到中间多行 TextBox 的 ClientRectangle 高度会被内容撑开默认的 VerticalCenter 会把水印画在整个控件的垂直中心。一个三行高的输入框水印会出现在第二行位置看起来像是提示文字错位。this.txtRemark.Multiline true; this.txtRemark.Height 120; this.txtRemark.WatermarkText 请输入备注最多 200 字; this.txtRemark.WatermarkTextFormat TextFormatFlags.Left | TextFormatFlags.Top;对齐改成左上之后还要处理滚动条多行加滚动条再滚动内容时水印的位置不会跟着内容动但绘制区域本身不变滚动后如果不重绘水印会和文字重叠。常见做法是拦截 WM_VSCROLL 和 WM_HSCROLL 后调用 Invalidate让滚动结束后强制重绘一次。这个处理统一放在 WndProc 里即可。4.3 RichTextBox 水印EM_GETRECT 决定绘制区域RichTextBox 没有 EM_SETCUEBANNER 支持自绘时它的内容区也不是整个 ClientRectangle而是由 EM_GETRECT 返回的一个内边距区域。直接用 ClientRectangle 画水印会踩在边框和滚动条附近。using System; using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms; public class WatermarkRichTextBox : RichTextBox { private const int EM_GETRECT 0x00B2; private const int WM_PAINT 0x000F; [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; } [DllImport(user32.dll)] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, ref RECT lParam); public string WatermarkText { get; set; } 请输入内容; protected override void WndProc(ref Message m) { base.WndProc(ref m); if (m.Msg WM_PAINT string.IsNullOrEmpty(Text) !Focused) { RECT rect new RECT(); SendMessage(Handle, EM_GETRECT, IntPtr.Zero, ref rect); Rectangle bounds new Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top); using (Graphics g CreateGraphics()) { TextRenderer.DrawText(g, WatermarkText, Font, bounds, Color.Gray, BackColor, TextFormatFlags.Left | TextFormatFlags.Top); } } } }这里 WndProc 的总体结构和 TextBox 版本一致区别只在取绘制矩形的方式。EM_GETRECT 返回的是控件工作区里真正用于排版文字的区域水印画进去正好落在第一行文字的位置。注意 RichTextBox 的边框样式如果是 FixedSingle这个矩形会比 ClientRectangle 小一圈别再把 2 像素内缩叠加进去了。4.4 BorderStyleNone 后水印与自定义圆弧边框怎么共存WinForms 界面美化里经常把 TextBox 的 BorderStyle 设为 None再自己画圆角边框。这时第 3 章的控件完全兼容水印绘制用的是 ClientRectangle去除系统边框不影响它。常见做法是先让 UI 层控件把背景画成圆角矩形再叠加一个 WatermarkTextBox 作为输入层。圆角裁剪可以用 Region 或 GraphicsPath 实现水印文字照常按 WatermarkTextFormat 绘制两者互不干扰。如果你看过“winform 自定义圆弧文本框 带图片”这类案例本质就是把背景图或图片画到底层 Panel输入框只负责透明背景加水印文字。4.5 水印控件的 5 个必调参数参数类型默认值说明WatermarkTextstring“请输入内容”水印文案为空则不显示WatermarkColorColorColor.FromArgb(160,160,160)建议用灰色系和正常输入文字拉开对比WatermarkTextFormatTextFormatFlagsLeft | VerticalCenter单行用 VerticalCenter多行用 Top | LeftShowWhenFocusedboolfalse聚焦后隐藏密码框如需要提示可设 trueFontFont微软雅黑 9F继承 TextBox.Font修改时水印同步变化这五个参数基本覆盖了日常需求。实际项目里我通常只改 WatermarkText、WatermarkColor 和 TextFormat剩下的保持默认。如果产品要求水印右侧对齐比如金额输入框把 WatermarkTextFormat 改成 Right | VerticalCenter 即可。5. 文本框水印避坑指南残影、光标遮挡、输入法闪烁与滚动漂移5.1 水印有残影或重影先说现象窗口被拖拽或 TextBox 频繁重绘时水印文字边缘出现淡淡的重影好像上一帧没有擦干净。根因是 WM_PAINT 里 base 先画了内容随后用 CreateGraphics 又画一遍而第二次绘制使用的 DC 与系统擦除背景的时机对不上旧的像素残留。解决方式是在 WndProc 里对 WM_ERASEBKGND 做处理控制擦除行为。我一般用 BufferedGraphics 把两段绘制合并或者直接在 WM_ERASEBKGND 时返回 1让系统不要额外擦背景。private const int WM_ERASEBKGND 0x0014; protected override void WndProc(ref Message m) { if (m.Msg WM_ERASEBKGND) { // 返回 1 表示背景已由我们处理避免重复擦除产生残影 m.Result (IntPtr)1; return; } base.WndProc(ref m); }这段处理放在水印控件里不影响原生输入绘制。加了之后如果背景没有正确更新再把 Drawing2D 的 GraphicsPath 绘制逻辑检查一遍。5.2 光标被水印盖住或消失这是把 UserPaint 打开后最常见的翻车现场水印确实出来了但输入时光标变成一个扁块甚至完全看不见选中文字的底色也异常。根因是 UserPaint 让原生 EDIT 不再完整自绘base.OnPaint 又不负责编辑光标。解决不要对 TextBox 打开 UserPaint回到第 3 章的 WndProc 方案。如果你已经在一份现成代码里看到了 SetStyle(UserPaint)优先怀疑是同一份代码里的水印绘制问题。真要在 UserPaint 模式下工作必须自己在 OnPaint 里补画边框和光标维护成本会明显上升。5.3 中文输入法打字时水印闪烁中文输入法在打字时会触发 WM_IME_STARTCOMPOSITION 和 WM_IME_ENDCOMPOSITION如果水印绘制逻辑只看 Focused 和 Text 长度组合窗口出现时水印会闪几下甚至把输入候选框顶到奇怪位置。解决在 WndProc 里拦截这两条消息调用 Invalidate同时把 ImeMode 设为 On让输入法状态可控。WinForms 在这个点上很像黑匣子修完记得在微软拼音和搜狗输入法下各测一轮。private const int WM_IME_STARTCOMPOSITION 0x010D; private const int WM_IME_ENDCOMPOSITION 0x010E; protected override void WndProc(ref Message m) { if (m.Msg WM_IME_STARTCOMPOSITION || m.Msg WM_IME_ENDCOMPOSITION) { Invalidate(); } base.WndProc(ref m); }Invalidate 本身开销很低不会有性能问题放心调用。5.4 多行加滚动条后水印从第一行漂到中间多行 TextBox 文本超过可视区域出现滚动条后水印固定在 ClientRectangle 的某个位置但如果你沿用了 VerticalCenter它会停在视觉中线看起来就是漂到了第二三行附近。解决多行时把 WatermarkTextFormat 改为 Left | Top并在 WM_VSCROLL、WM_HSCROLL 后 Invalidate让滚动后强制重绘一次。真机表现是滚动停下瞬间水印回到左上角不会出现跟随内容滚动的错觉。5.5 EM_SETCUEBANNER 在某些环境不显示如果选了 API 路线最隐蔽的坑是 Win7 或某些精简系统上PasswordChar 文本框怎么发消息都不出现提示文字RichTextBox 更是完全没有 cue banner。解决遇到密码框直接换自绘控件不要在同一个控件里混用两个方案。API 路线可以作为快速原型一旦出现环境差异立刻切到第 3 章的代码不要花时间查注册表或系统补丁那是一条死路。6. 进阶从输入框水印到多行多列文字水印窗口防截图方案怎么落地6.1 透明置顶水印窗体的最小写法如果你的真实需求是防截图比如“内部资料 工号 时间”那要的不是 Placeholder 而是覆盖在主界面上层的多行多列文字。常见做法是一个置顶透明窗体设置 WS_EX_TRANSPARENT 让鼠标点击穿透。public partial class WatermarkOverlay : Form { public WatermarkOverlay() { FormBorderStyle FormBorderStyle.None; ShowInTaskbar false; TopMost true; BackColor Color.Magenta; TransparencyKey Color.Magenta; DoubleBuffered true; this.ShowWithoutActivation true; } protected override CreateParams CreateParams { get { CreateParams cp base.CreateParams; cp.ExStyle | 0x00000020; // WS_EX_TRANSPARENT cp.ExStyle | 0x00000080; // WS_EX_TOOLWINDOW return cp; } } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); e.Graphics.TranslateTransform(20, 40); e.Graphics.RotateTransform(-30); Font font new Font(微软雅黑, 12F); // 平铺多行多列文字水印 for (int y 0; y Height; y 120) { for (int x 0; x Width; x 320) { e.Graphics.DrawString( 内部资料 · Environment.UserName · DateTime.Now.ToString(MM-dd HH:mm), font, Brushes.Gray, x, y); } } } }要说明的边界TransparencyKey 方案是 0/1 透明水印文字颜色选纯灰背景全部透明做不到半透明灰度效果。想控制透明度就用 UpdateLayeredWindow那是另一套代码。RotateTransform 前先 TranslateTransform 是为了让旋转中心偏移否则以左上角为轴转平铺位置会乱。6.2 怎么验证窗口水印真的生效验证不能只靠肉眼看屏幕。做法是用系统截图工具截全屏然后把图片放大看水印文字是否进入截图如果屏幕上能看到、截图里没有说明它只是覆盖层不符合防截图预期。实际项目中我一般把水印文字间距调到 120 像素一行、320 像素一列字号 12 号灰度 128。太密会干扰录入太疏拍照时又读不出来。我在这块也翻过车把 Placeholder 当防截图水印交付在客户现场被指出输入框没有提示文字。后来每次听到“加个水印”都会先确认一句它是给谁看、什么时候消失。把这两类需求分开之后WinForms 水印方案其实都很薄难的是判断入口。希望帮到你。本文还有配套的精品资源点击获取