简介:这是一份面向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跑耗时任务,通过IProgress<int>把进度回传到 UI 线程更新进度条。这样做的原因是:如果在 UI 线程直接跑初始化,进度条根本不会动,界面会假死。用IProgress<T>而不是直接跨线程操作控件,是因为Progress<T>会自动把回调 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里模拟了加载配置、检查更新、初始化数据库等步骤,每一步都通过IProgress<int>上报进度:
// AppInitializer.cs public static async Task RunAsync(IProgress<int> 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 Progress<int>(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,通过IProgress<T>回传进度。切记不要在后台线程直接操作控件属性,必须走Progress<T>或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 界面的那几个小时。
本文还有配套的精品资源,点击获取