简介:这是一份面向C#初学者与桌面应用开发者的登录界面源码示例,基于.NET平台,采用Windows Forms或WPF实现,帮助解决GUI入门阶段界面设计与交互逻辑无从下手的问题。压缩包共42个文件,约2.06MB,包含6个cs源码文件、2个resx与5个resources资源文件、9个png界面素材、1个ico图标,以及sln解决方案、csproj项目文件和exe、dll等编译产物,结构完整可直接在Visual Studio中打开运行。源码覆盖用户名与密码输入框、登录按钮、记住我选项、忘记密码与注册链接、错误提示等典型元素,并通过Click事件处理程序演示输入校验与登录逻辑的衔接方式。目前已有1616人学习下载,适合希望快速理解窗体控件布局、事件响应与简单验证机制的读者参考借鉴,也可作为改进自身登录界面设计的灵感来源。
1. 登录界面这件小事,为什么值得单独拆一个 C# 项目
做 WinForm 或 WPF 项目的人都有个共识:登录页是整个系统里最容易被低估、又最容易被用户第一眼嫌弃的模块。业务功能做得再扎实,登录框要是灰扑扑一片、按钮还是 90 年代那种凸起立体感,用户还没进主界面心里就先打了折扣。这次拆的这份 C# 登录界面源码,就是专门解决这个问题的——它不绑定任何具体业务,纯粹把「登录窗口」当成一个独立 UI 组件来做,包含账号密码输入、验证码、记住密码、窗口拖动、无边框圆角、渐变背景这些常见元素。适合两类人:一类是刚学完 C# 基础语法、想找个完整小项目练手的开发者;另一类是手头有 WinForm/WPF 老项目、想快速换一套登录皮肤的熟手。下面我按「它长什么样 → 怎么跑起来 → 代码怎么组织 → 怎么改成自己的」这条线,把这份源码从头到尾过一遍。
2. 技术选型与运行环境:WinForm 还是 WPF,先定下来
2.1 这份源码大概率用的是 WinForm + GDI+ 自绘
拿到一份 C# 登录界面源码,第一件事是判断它属于 WinForm 还是 WPF。从「好看的登录界面」这个描述和常见实现方式来看,这类源码绝大多数走的是 WinForm 路线,原因很直接:WinForm 上手门槛低,拖控件就能出界面,而「好看」的部分靠 GDI+ 自绘和无边框窗体来补。WPF 当然也能做,XAML 写渐变和圆角更优雅,但 WPF 项目文件结构复杂,对新手不够友好。
WinForm 实现「好看」的核心手段有三个:
- 无边框窗体:把
FormBorderStyle设为None,去掉系统自带的标题栏和边框,自己画。 - 圆角与阴影:用
GraphicsPath画圆角矩形,配合Region属性裁剪窗体形状。 - 渐变背景:用
LinearGradientBrush在OnPaint里刷背景,或者直接放一个PictureBox铺背景图。
提示:无边框窗体去掉系统标题栏后,窗口拖动、最小化、关闭这些功能都得自己实现,这是后面代码里一定会出现的部分。
2.2 运行环境与依赖确认
这份源码的运行环境不复杂,但有几个点必须确认,否则跑不起来会以为是代码问题:
| 项目 | 要求 | 说明 |
|---|---|---|
| 操作系统 | Windows 7 及以上 | WinForm 只能在 Windows 上跑 |
| 开发工具 | Visual Studio 2019/2022 | 社区版免费,够用 |
| .NET 版本 | .NET Framework 4.5+ 或 .NET 6/8 | 看项目文件里的 TargetFramework |
| 额外依赖 | 通常无 | 纯 GDI+ 自绘,不依赖第三方 UI 库 |
打开项目后先看.csproj文件里的<TargetFramework>标签。如果是net6.0-windows或net8.0-windows,说明用的是新版 .NET,需要装对应 SDK;如果是v4.7.2这类,就是 .NET Framework,Windows 自带运行时,基本不用额外装东西。
常见做法是:先双击.sln文件用 VS 打开,VS 会自动提示缺什么组件,按提示装就行。如果提示「找不到目标框架」,去项目属性里把目标框架改成你机器上已有的版本,重新生成即可。
2.3 第一次运行要改的两个地方
源码直接 F5 运行,大概率能起来,但有两个地方我一般会先检查:
第一,窗体启动位置。很多登录界面源码把StartPosition设成CenterScreen,这没问题;但有些会设成Manual并写死坐标,换台分辨率不同的机器就跑到屏幕外了。检查Program.cs或窗体构造函数里的StartPosition设置。
第二,资源文件路径。如果登录界面用了背景图、图标,这些资源通常放在Resources文件夹或嵌入为资源。如果运行后背景是白的、图标不显示,多半是资源路径写成了绝对路径。改成相对路径或嵌入资源即可。
// Program.cs 里典型的启动配置 static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); // 确保登录窗体居中显示,避免换分辨率后跑偏 Application.Run(new LoginForm()); }这段代码是 WinForm 程序的标准入口。EnableVisualStyles()开启视觉样式,让控件跟随系统主题;SetCompatibleTextRenderingDefault(false)用 GDI 渲染文本,兼容性更好。最后一行Application.Run启动登录窗体,整个程序的主消息循环从这里开始。参数就是你那个登录窗体的实例,改成自己的类名即可。
3. 目录结构与核心模块:一个登录窗口拆成几块
3.1 典型目录结构长什么样
这类登录界面源码的目录结构通常不复杂,但分工清晰。我拆过的类似项目,结构大致是这样:
LoginDemo/ ├── Properties/ │ ├── AssemblyInfo.cs // 程序集信息 │ ├── Resources.resx // 资源定义 │ └── Settings.settings // 配置项 ├── Resources/ // 图片、图标等素材 │ ├── bg_login.png // 登录背景图 │ ├── logo.png // 系统 Logo │ └── btn_close_hover.png // 关闭按钮悬停态 ├── Controls/ // 自定义控件 │ ├── RoundButton.cs // 圆角按钮 │ └── GradientPanel.cs // 渐变面板 ├── Forms/ │ ├── LoginForm.cs // 登录窗体逻辑 │ ├── LoginForm.Designer.cs // 界面布局代码 │ └── LoginForm.resx // 窗体资源 ├── Utils/ │ ├── VerifyCodeHelper.cs // 验证码生成 │ └── WindowDragHelper.cs // 窗口拖动辅助 ├── App.config // 应用配置 └── LoginDemo.csproj // 项目文件这个结构里,Forms放窗体,Controls放自定义控件,Utils放工具类,Resources放素材。新手容易把所有代码都堆在LoginForm.cs里,几百行挤在一起,改起来很痛苦。这份源码如果分了Controls和Utils,说明作者是有意做模块拆分的,二次开发会舒服很多。
3.2 无边框窗体的拖动与关闭实现
无边框窗体最大的坑就是「拖不动」。系统标题栏没了,鼠标按住窗体不会移动,必须自己监听鼠标事件。常见做法是在窗体上记录鼠标按下时的坐标,移动时计算偏移量,再设置窗体位置。
// 窗口拖动辅助类:让无边框窗体支持鼠标拖拽 public static class WindowDragHelper { private static Point _mouseOffset; private static bool _isDragging; // 在窗体上挂载拖动逻辑 public static void EnableDrag(Form form) { form.MouseDown += (s, e) => { if (e.Button == MouseButtons.Left) { _isDragging = true; // 记录鼠标相对窗体左上角的偏移 _mouseOffset = new Point(-e.X, -e.Y); } }; form.MouseMove += (s, e) => { if (_isDragging) { // 用屏幕坐标减去偏移量,得到窗体新位置 Point newPos = form.PointToScreen(e.Location); newPos.Offset(_mouseOffset); form.Location = newPos; } }; form.MouseUp += (s, e) => { _isDragging = false; }; } }这段代码的关键在_mouseOffset的计算。e.X和e.Y是鼠标相对于窗体客户区的坐标,取负值后加上屏幕坐标,就能让窗体跟着鼠标走而不跳变。_isDragging标志位防止鼠标没按下时窗体乱跑。参数方面,MouseButtons.Left限定只有左键能拖,右键留给其他操作。如果拖动时窗体闪烁,可以在窗体构造函数里开启双缓冲:this.DoubleBuffered = true;。
3.3 圆角与渐变背景的绘制
「好看」的核心在绘制。WinForm 默认的方形窗体和灰色背景是没法看的,必须自己画圆角和渐变。
// 在窗体的 OnPaint 里绘制圆角渐变背景 protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); Graphics g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; // 抗锯齿,圆角才平滑 // 1. 画渐变背景 Rectangle rect = new Rectangle(0, 0, this.Width, this.Height); using (LinearGradientBrush brush = new LinearGradientBrush( rect, Color.FromArgb(64, 158, 255), // 起始色:蓝色 Color.FromArgb(45, 90, 180), // 结束色:深蓝 LinearGradientMode.Vertical)) // 垂直渐变 { g.FillRectangle(brush, rect); } // 2. 裁剪成圆角 int radius = 12; // 圆角半径 GraphicsPath path = new GraphicsPath(); path.AddArc(0, 0, radius, radius, 180, 90); path.AddArc(this.Width - radius, 0, radius, radius, 270, 90); path.AddArc(this.Width - radius, this.Height - radius, radius, radius, 0, 90); path.AddArc(0, this.Height - radius, radius, radius, 90, 90); path.CloseFigure(); this.Region = new Region(path); // 把窗体裁剪成圆角 }SmoothingMode.AntiAlias是圆角平滑的关键,不开的话圆角会有锯齿。LinearGradientBrush的四个参数分别是绘制区域、起始颜色、结束颜色、渐变方向。颜色值Color.FromArgb(64, 158, 255)里的三个数字是 RGB 分量,改这三个数就能换主题色。GraphicsPath用四段圆弧拼出圆角矩形,AddArc的参数依次是左上角坐标、宽高、起始角度、扫过角度。最后把Region赋给窗体,超出圆角的部分就被裁掉了。
注意:
Region裁剪后窗体的四个角是透明的,但 WinForm 不支持真正的透明,实际显示的是父窗口或桌面的颜色。如果发现圆角处有黑边,检查是否开启了TransparencyKey或用了分层窗口。
4. 登录逻辑与验证码:从输入到校验的完整链路
4.1 账号密码输入框的细节处理
登录界面的输入框不是拖个TextBox就完事。好看的设计里,输入框通常有占位提示文字、聚焦时边框变色、密码框显示圆点或星号。WinForm 原生TextBox不支持占位符,得自己实现。
// 给 TextBox 加占位提示文字的简单实现 private void SetupPlaceholder(TextBox txt, string placeholder) { txt.Text = placeholder; txt.ForeColor = Color.Gray; txt.Enter += (s, e) => { // 聚焦时如果还是占位文字,清空并变黑 if (txt.Text == placeholder) { txt.Text = ""; txt.ForeColor = Color.Black; } }; txt.Leave += (s, e) => { // 失焦时如果为空,恢复占位文字 if (string.IsNullOrWhiteSpace(txt.Text)) { txt.Text = placeholder; txt.ForeColor = Color.Gray; } }; }这段逻辑用Enter和Leave两个事件模拟占位符。Enter是获得焦点时触发,Leave是失去焦点时触发。参数placeholder就是提示文字,比如「请输入账号」。密码框的话,把txt.PasswordChar设为'●'或'*',输入的内容就会显示成圆点。注意占位文字和真实输入要区分开,否则用户输入的内容可能被当成占位符清掉——这也是很多新手翻车的地方。
4.2 验证码生成与校验
验证码是登录界面的标配。WinForm 里生成验证码靠Bitmap和Graphics,把随机字符画到图片上,再加几条干扰线。
// 生成四位数字验证码图片 public static Bitmap CreateVerifyCode(out string code) { code = string.Empty; Bitmap bmp = new Bitmap(100, 36); Graphics g = Graphics.FromImage(bmp); g.Clear(Color.White); g.SmoothingMode = SmoothingMode.AntiAlias; Random rand = new Random(); string chars = "0123456789"; Font font = new Font("Arial", 18, FontStyle.Bold); // 逐位画字符,每个字符随机偏移和旋转 for (int i = 0; i < 4; i++) { char c = chars[rand.Next(chars.Length)]; code += c; // 随机颜色,避免纯黑太死板 Brush brush = new SolidBrush(Color.FromArgb( rand.Next(50, 150), rand.Next(50, 150), rand.Next(50, 150))); // 每个字符位置加随机偏移 g.DrawString(c.ToString(), font, brush, 10 + i * 22 + rand.Next(-3, 3), rand.Next(2, 8)); } // 画干扰线 for (int i = 0; i < 5; i++) { Pen pen = new Pen(Color.FromArgb(rand.Next(150, 220), rand.Next(150, 220), rand.Next(150, 220)), 1); g.DrawLine(pen, rand.Next(100), rand.Next(36), rand.Next(100), rand.Next(36)); } g.Dispose(); return bmp; }out string code把生成的验证码传出去,用于后续比对。Bitmap尺寸 100x36 是常见验证码大小,改这两个数能调整图片尺寸。字符颜色用rand.Next(50, 150)限制在深色范围,太浅会看不清。干扰线颜色用rand.Next(150, 220)限制在浅色范围,避免盖住字符。校验时把用户输入和code做不区分大小写的比较即可。
4.3 登录按钮的异步校验与反馈
点击登录按钮后,通常要请求后端接口验证账号密码。如果同步请求,界面会卡住,用户体验很差。正确做法是用async/await异步处理。
// 登录按钮点击事件:异步校验,避免界面卡死 private async void btnLogin_Click(object sender, EventArgs e) { string user = txtUser.Text.Trim(); string pass = txtPwd.Text.Trim(); string inputCode = txtCode.Text.Trim(); // 1. 本地校验:非空和验证码 if (string.IsNullOrEmpty(user) || string.IsNullOrEmpty(pass)) { ShowTip("账号和密码不能为空"); return; } if (!string.Equals(inputCode, _currentCode, StringComparison.OrdinalIgnoreCase)) { ShowTip("验证码错误"); RefreshVerifyCode(); // 刷新验证码 return; } // 2. 禁用按钮,防止重复点击 btnLogin.Enabled = false; btnLogin.Text = "登录中..."; try { // 3. 异步请求后端(这里用 Task.Delay 模拟网络请求) bool success = await Task.Run(() => CheckLogin(user, pass)); if (success) { // 登录成功,打开主窗体 this.Hide(); new MainForm().ShowDialog(); this.Close(); } else { ShowTip("账号或密码错误"); RefreshVerifyCode(); } } finally { // 4. 无论成功失败都恢复按钮 btnLogin.Enabled = true; btnLogin.Text = "登 录"; } }这段代码有几个关键点。async void是事件处理器的标准写法,因为事件处理器不能有返回值。Task.Run把耗时的网络请求放到线程池,不阻塞 UI 线程。btnLogin.Enabled = false防止用户狂点按钮导致重复请求,这是血泪经验——不加这行,用户手快能给你发出去七八个登录请求。finally块保证无论成功失败按钮都会恢复,避免请求异常后按钮永久禁用。CheckLogin是实际请求后端的方法,替换成你自己的 HTTP 调用即可。
5. 避坑与常见问题:跑不起来先看这几条
5.1 窗体显示为白块或黑块
现象:运行后登录窗体一片白或一片黑,看不到背景和控件。
原因:多半是OnPaint里绘制逻辑有问题,或者背景图资源没加载成功。WinForm 的OnPaint如果没调base.OnPaint(e),控件不会正常绘制;如果背景图路径写错,Image.FromFile会抛异常但被吞掉。
解决:先确认OnPaint第一行调了base.OnPaint(e)。再检查资源加载代码,把图片改成嵌入资源(在项目属性里把图片的「生成操作」设为「嵌入的资源」),用Assembly.GetManifestResourceStream读取,避免路径问题。
5.2 无边框窗体无法拖动或拖动跳变
现象:鼠标按住窗体拖不动,或者一拖窗体就跳到屏幕角落。
原因:拖动逻辑里坐标计算用错了参照系。e.Location是相对于控件的坐标,直接赋给form.Location会错位。另外如果窗体上放了Panel覆盖整个客户区,鼠标事件被Panel截获,窗体收不到。
解决:用PointToScreen把鼠标坐标转成屏幕坐标再计算偏移。如果事件被Panel截获,把拖动逻辑挂到Panel上,或者给Panel也挂同样的MouseDown/Move/Up事件。
5.3 验证码图片不刷新
现象:点击验证码图片,图片没变化,还是原来那张。
原因:PictureBox的Image属性赋了新Bitmap,但旧的没释放,或者刷新方法没被调用。
解决:刷新时先pictureBox.Image?.Dispose()释放旧图,再赋新图。同时确认点击事件绑定的是pictureBox_Click而不是窗体点击。如果验证码是画在Panel上的,需要调panel.Invalidate()触发重绘。
5.4 登录成功后主窗体一闪而过
现象:登录成功,主窗体打开后立刻关闭,程序退出。
原因:LoginForm是Application.Run的启动窗体,this.Close()后消息循环结束,整个程序退出。如果主窗体用Show()而不是ShowDialog(),它不会阻塞消息循环。
解决:用ShowDialog()模态打开主窗体,或者在Program.cs里改用Application.Run(new MainForm())并把登录做成前置对话框。常见做法是登录窗体用ShowDialog()返回DialogResult.OK,主程序根据结果决定是否启动主窗体。
5.5 高 DPI 下界面模糊或错位
现象:在 4K 屏或缩放 125% 的屏幕上,登录界面文字模糊、控件错位。
原因:WinForm 默认不感知 DPI,系统会拉伸整个窗口,导致模糊。
解决:在app.manifest里开启 DPI 感知,或者在Program.cs里调Application.SetHighDpiMode(HighDpiMode.SystemAware)(.NET Core 及以上)。同时布局尽量用Anchor和Dock,少用绝对坐标。
6. 二次开发:把这份登录界面改成你自己的
拿到这份源码,最终目的是改成自己项目能用的样子。我一般按「换皮 → 换逻辑 → 加功能」三步走。
第一步换皮,改主题色和背景图。主题色在OnPaint的LinearGradientBrush里,把Color.FromArgb(64, 158, 255)和Color.FromArgb(45, 90, 180)换成你项目的品牌色。背景图替换Resources文件夹里的bg_login.png,尺寸建议和窗体设计尺寸一致,避免拉伸变形。Logo 同理。这一步改完,界面就完全是你的了。
第二步换逻辑,把CheckLogin方法里的模拟校验换成真实接口调用。常见做法是用HttpClient发 POST 请求:
// 替换模拟校验为真实接口调用 private async Task<bool> CheckLogin(string user, string pass) { using (HttpClient client = new HttpClient()) { client.BaseAddress = new Uri("https://your-api.com/"); var content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("username", user), new KeyValuePair<string, string>("password", pass) }); var response = await client.PostAsync("api/login", content); if (!response.IsSuccessStatusCode) return false; string result = await response.Content.ReadAsStringAsync(); // 根据返回的 JSON 判断是否登录成功 return result.Contains("\"code\":0"); } }BaseAddress换成你的后端地址,api/login换成你的登录接口路径。FormUrlEncodedContent是表单提交格式,如果后端要 JSON,改用StringContent并设置Content-Type: application/json。返回结果解析建议用Newtonsoft.Json或System.Text.Json,别用字符串Contains判断,容易误判。
第三步加功能,按需扩展。常见的扩展点有:记住密码(把账号密码加密存到本地配置文件)、自动登录(启动时读配置直接跳过登录)、多语言(把界面文字抽到资源文件)、主题切换(准备几套颜色配置,运行时切换)。记住密码别明文存,用ProtectedData.Protect加密后再写文件,这是基本的安全习惯。
| 改造点 | 改动位置 | 难度 |
|---|---|---|
| 换主题色 | OnPaint 里的颜色值 | 低 |
| 换背景图 | Resources 文件夹 | 低 |
| 接真实接口 | CheckLogin 方法 | 中 |
| 记住密码 | 新增配置读写类 | 中 |
| 自动登录 | Program.cs 启动逻辑 | 中 |
| 多语言 | 资源文件 + 切换逻辑 | 高 |
最后说个我自己的习惯:每次改完登录界面,我都会在 100%、125%、150% 三种缩放比例下各跑一遍,再换一台 1366x768 的老机器试一次。登录界面是用户看到的第一屏,这里翻车最不划算。从那以后我每次交付前都强制走一遍这个检查流程,希望帮到你。
本文还有配套的精品资源,点击获取