
简介这是一份面向C#桌面开发初学者与进阶者的启动界面实战示例围绕Windows Forms与WPF两种UI框架演示如何打造兼具视觉美感与加载逻辑的程序启动窗口。资源包共38个文件约525KB以cs源码、resx与resources资源文件、exe可执行程序、png与jpg图片素材为主另含sln解决方案、csproj工程文件、pdb调试符号及xml配置等结构完整可直接编译运行。内容涵盖界面控件布局、背景图与进度条设计、Timer配合AlphaBlend实现渐显渐隐、BackgroundWorker后台加载与进度更新、启动完成自动关闭等关键环节并附有文档说明与多套启动窗体示例。目前已有414人学习下载适合希望提升桌面应用第一印象、系统掌握启动界面设计与动画过渡技巧的开发者参考借鉴。1. 从一张“不像WinForm”的启动页说起很多人第一次看到 C# 自制精美程序启动界面的截图第一反应是“这怕不是 WPF 做的吧”。实际上WinForm 只要肯在 GDI 和双缓冲上花心思照样能做出带渐变、阴影、进度动画的启动页。这个项目的定位很明确给桌面端 WinForm 程序配一个能拿得出手的 Splash 启动界面解决原生启动画面“灰底白字、进度条卡顿、关闭时闪屏”这三个老毛病。它适合两类人一是手里有 WinForm 项目、想低成本提升第一印象的开发者二是想学 GDI 自绘、双缓冲、异步加载这套组合拳的 C# 新手。整个项目不依赖第三方 UI 库纯原生实现拿到源码改改图片和文字就能用。2. 项目结构与技术选型为什么不用 WPF 也能做出质感2.1 目录结构一览拿到源码后先别急着 F5。花两分钟把目录扫一遍后面改起来会顺很多。这个项目的结构非常扁平没有过度分层适合作为“拿来即改”的模板SplashDemo/ ├── Forms/ │ ├── SplashForm.cs # 启动界面主窗体 │ └── MainForm.cs # 主程序窗体启动页关闭后显示 ├── Controls/ │ └── RoundProgressBar.cs # 自绘圆角进度条控件 ├── Utils/ │ ├── UiHelper.cs # 双缓冲、圆角、阴影等绘制辅助 │ └── AppInitializer.cs # 模拟初始化任务可替换为真实业务 ├── Resources/ │ ├── splash_bg.png # 背景图可选也可纯代码绘制 │ └── logo.png # 程序 Logo ├── Program.cs # 入口控制启动顺序 └── SplashDemo.csprojForms放窗体Controls放自绘控件Utils放绘制辅助和初始化逻辑Resources放图片资源。这种分法不是强制的但好处是你想换背景图只动Resources想改进度条样式只动Controls互不干扰。2.2 技术选型WinForm GDI 双缓冲项目没有引入任何 NuGet 包全部基于 .NET Framework 自带的System.Drawing。核心思路是把启动窗体设置成无边框FormBorderStyle.None然后用 GDI 在OnPaint里手动绘制背景渐变、Logo、文字和进度条。为了防止闪烁必须开启双缓冲。常见做法是在窗体构造函数里设置// SplashForm.cs 构造函数片段 public SplashForm() { InitializeComponent(); // 关键三行无边框 双缓冲 不显示在任务栏 this.FormBorderStyle FormBorderStyle.None; this.StartPosition FormStartPosition.CenterScreen; this.ShowInTaskbar false; // 开启双缓冲消除绘制闪烁 this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.ResizeRedraw, true); this.UpdateStyles(); // 让窗体支持透明背景用于圆角和阴影 this.BackColor Color.FromArgb(30, 30, 30); this.TransparencyKey Color.FromArgb(30, 30, 30); }这里有几个参数值得说清楚。AllPaintingInWmPaint表示所有绘制都在WM_PAINT消息里完成避免背景擦除导致的闪烁UserPaint表示由控件自己绘制而不是系统绘制DoubleBuffer就是双缓冲先把内容画到内存位图再一次性贴到屏幕。TransparencyKey设成和背景色一样是为了让圆角外的区域透明做出“异形窗体”的效果。注意TransparencyKey和BackColor必须一致否则透明区域会变成一块突兀的色块。2.3 启动流程Program.cs 里的顺序控制启动页最容易翻车的地方不是绘制而是“启动页还没画完主窗体就弹出来了”。正确的顺序应该是先显示启动页 → 在后台线程执行初始化 → 初始化完成后关闭启动页 → 显示主窗体。// Program.cs [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); // 1. 先创建并显示启动页 using (var splash new SplashForm()) { splash.Show(); splash.Refresh(); // 强制立即绘制避免白屏 // 2. 在启动页里执行初始化任务内部用异步 进度回调 splash.RunInitialization(); // 3. 初始化完成后隐藏启动页 splash.Hide(); } // 4. 再启动主窗体 Application.Run(new MainForm()); }RunInitialization是启动页暴露给外部的入口内部用Task.Run跑耗时任务通过IProgressint把进度回传到 UI 线程更新进度条。这样做的原因是如果在 UI 线程直接跑初始化进度条根本不会动界面会假死。用IProgressT而不是直接跨线程操作控件是因为ProgressT会自动把回调 marshal 回创建它的线程这里是 UI 线程省去了手写Invoke的麻烦。3. 核心绘制逻辑渐变背景、圆角进度条与动画3.1 渐变背景与 Logo 绘制启动页的质感八成来自背景。纯色背景显得廉价线性渐变立刻就不一样了。在OnPaint里用LinearGradientBrush从左上到右下铺一层深色渐变// SplashForm.OnPaint 片段 protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); Graphics g e.Graphics; g.SmoothingMode SmoothingMode.AntiAlias; // 抗锯齿 g.InterpolationMode InterpolationMode.HighQualityBicubic; // 图片高质量缩放 // 1. 绘制渐变背景 using (var brush new LinearGradientBrush( this.ClientRectangle, Color.FromArgb(45, 45, 60), // 起始色深蓝灰 Color.FromArgb(20, 20, 30), // 结束色近黑 LinearGradientMode.ForwardDiagonal)) { g.FillRectangle(brush, this.ClientRectangle); } // 2. 绘制 Logo居中偏上 if (logoImage ! null) { int logoX (this.Width - logoImage.Width) / 2; int logoY this.Height / 4; g.DrawImage(logoImage, logoX, logoY, logoImage.Width, logoImage.Height); } // 3. 绘制标题文字 using (var font new Font(微软雅黑, 18f, FontStyle.Bold)) using (var textBrush new SolidBrush(Color.WhiteSmoke)) { var text 我的应用程序; var size g.MeasureString(text, font); g.DrawString(text, font, textBrush, (this.Width - size.Width) / 2, this.Height / 2); } }SmoothingMode.AntiAlias让圆角和曲线边缘不出现锯齿InterpolationMode.HighQualityBicubic在缩放 Logo 时保持清晰。这两个设置是“精美”二字的底层保障少了它们再好的设计稿也会显得毛糙。LinearGradientMode.ForwardDiagonal表示从左上角到右下角渐变你也可以换成Vertical做上下渐变看设计稿定。3.2 圆角进度条控件原生ProgressBar是直角方块放在圆角窗体里很违和。项目里用RoundProgressBar自绘了一个。核心是画两个圆角矩形底层是轨道上层是进度。// RoundProgressBar.OnPaint 片段 protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); Graphics g e.Graphics; g.SmoothingMode SmoothingMode.AntiAlias; int radius this.Height / 2; // 圆角半径 高度一半形成胶囊形 var trackRect new Rectangle(0, 0, this.Width - 1, this.Height - 1); // 1. 绘制轨道半透明白 using (var trackPath UiHelper.CreateRoundedRect(trackRect, radius)) using (var trackBrush new SolidBrush(Color.FromArgb(60, 255, 255, 255))) { g.FillPath(trackBrush, trackPath); } // 2. 绘制进度亮色渐变 int progressWidth (int)(this.Width * (this.Value / (float)this.Maximum)); if (progressWidth 0) { var progressRect new Rectangle(0, 0, progressWidth, this.Height - 1); using (var progressPath UiHelper.CreateRoundedRect(progressRect, radius)) using (var progressBrush new LinearGradientBrush( progressRect, Color.FromArgb(0, 180, 255), Color.FromArgb(0, 120, 215), LinearGradientMode.Horizontal)) { g.FillPath(progressBrush, progressPath); } } }UiHelper.CreateRoundedRect是封装好的圆角矩形路径生成方法用GraphicsPath的AddArc画四个角。进度条的Value和Maximum属性暴露出来外部通过progressBar.Value percent更新。注意每次更新Value后要调用Invalidate()触发重绘否则界面不会刷新。3.3 异步初始化与进度回调启动页不能只是个“花瓶”它得真的在后台干活。AppInitializer里模拟了加载配置、检查更新、初始化数据库等步骤每一步都通过IProgressint上报进度// AppInitializer.cs public static async Task RunAsync(IProgressint progress, CancellationToken token) { // 模拟 5 个初始化步骤每步耗时不同 var steps new[] { 加载配置..., 检查更新..., 初始化数据库..., 加载资源..., 准备就绪 }; for (int i 0; i steps.Length; i) { token.ThrowIfCancellationRequested(); await Task.Delay(400, token); // 模拟耗时操作 int percent (i 1) * 100 / steps.Length; progress?.Report(percent); // 回调到 UI 线程更新进度条 } }Task.Delay(400)只是模拟实际项目里换成你的真实初始化逻辑。progress?.Report(percent)会触发 UI 线程上的回调把进度条的值更新掉。CancellationToken是为了支持用户中途取消比如点了关闭按钮避免后台任务还在跑但窗体已经销毁。在SplashForm里这样调用public void RunInitialization() { var progress new Progressint(percent { roundProgressBar.Value percent; roundProgressBar.Invalidate(); // 触发重绘 }); // 同步等待异步任务完成启动页场景下可以接受 AppInitializer.RunAsync(progress, CancellationToken.None) .GetAwaiter().GetResult(); }这里用.GetAwaiter().GetResult()而不是.Wait()是因为前者不会把异常包装成AggregateException调试时能看到真实堆栈。启动页场景下阻塞 UI 线程是可以接受的因为此时主窗体还没出来用户也不会去点别的。4. 避坑与排查启动页最容易翻车的 5 个地方4.1 启动页一闪而过或白屏现象程序启动后启动页只闪了一下就没了或者干脆显示一片白。原因Show()之后没有强制刷新绘制消息还没处理完初始化就已经跑完了。解决在Show()之后加Refresh()并且确保初始化任务至少有一个最小显示时长比如 800ms否则用户根本看不清。4.2 进度条更新时界面卡死现象进度条不动窗口拖拽也没反应。原因初始化任务跑在 UI 线程上阻塞了消息循环。解决把耗时逻辑放进Task.Run通过IProgressT回传进度。切记不要在后台线程直接操作控件属性必须走ProgressT或Invoke。4.3 圆角边缘出现黑边或锯齿现象窗体圆角处有一圈黑色像素或者边缘毛刺。原因TransparencyKey设置的颜色和实际背景色有细微差异或者没有开启抗锯齿。解决确保TransparencyKey和BackColor完全一致包括 Alpha 值并在OnPaint里设置SmoothingMode.AntiAlias。如果还有黑边检查是否在OnPaintBackground里做了额外绘制。4.4 主窗体在启动页关闭前就显示了现象启动页还在主窗体已经弹出来了两个窗口叠在一起。原因Application.Run(new MainForm())写在了splash.Show()之前或者初始化没等待完成就继续执行了。解决严格按照Program.cs里的顺序先Show启动页 → 执行初始化 →Hide启动页 → 再Application.Run主窗体。4.5 高 DPI 下界面模糊或错位现象在 4K 屏或缩放 150% 的显示器上启动页文字模糊、控件位置偏移。原因WinForm 默认不感知 DPI系统会直接拉伸位图。解决在app.manifest里开启 DPI 感知或者在Program.cs里调用Application.SetHighDpiMode(HighDpiMode.SystemAware).NET Core 及以上。同时绘制时用Graphics.DpiX动态计算尺寸不要写死像素值。5. 二次开发换皮、加功能与性能验证拿到这个启动页模板后最常做的改造就是“换皮”。把Resources里的splash_bg.png和logo.png替换成你自己的图然后在OnPaint里调整渐变颜色和文字内容。如果你想做全图片背景可以把渐变绘制那段注释掉直接g.DrawImage(bgImage, this.ClientRectangle)。但要注意图片尺寸和窗体尺寸匹配否则拉伸会糊。加功能方面最常见的需求是“启动页显示版本号和版权信息”。在OnPaint里加两行DrawString就行位置放在窗体底部字体用 9pt 灰色。另一个需求是“启动页支持淡入淡出”这需要用到Timer逐步调整Opacity属性但注意Opacity和TransparencyKey同时使用会有冲突二选一。性能验证有个简单办法在OnPaint开头记一个Stopwatch结尾打印耗时。正常情况下一帧绘制应该在 5ms 以内。如果超过 20ms检查是不是在OnPaint里创建了太多Brush、Font或GraphicsPath对象——这些都应该用using包起来或者缓存成字段避免每次重绘都重新分配。// 缓存绘制资源避免每帧重复创建 private Font titleFont; private SolidBrush titleBrush; protected override void OnPaint(PaintEventArgs e) { // 懒加载只创建一次 if (titleFont null) { titleFont new Font(微软雅黑, 18f, FontStyle.Bold); titleBrush new SolidBrush(Color.WhiteSmoke); } // ... 后续绘制逻辑 }这个习惯是我踩过坑之后养成的早期版本每次OnPaint都new Font结果启动页动画一卡一卡的用性能分析器一看GC 频繁触发。从那以后我每次写自绘控件都强制把Font、Brush、Pen这类 GDI 对象缓存成字段并在Dispose里释放。希望这个启动页模板能帮你省下折腾 WinForm 界面的那几个小时。本文还有配套的精品资源点击获取