ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Winform仿QQ登录界面源码解析:无边框窗体与验证码实战

Winform仿QQ登录界面源码解析:无边框窗体与验证码实战 简介一份基于Winform的QQ登录界面仿制源码面向初学C#.NET桌面开发的读者或需要快速搭建登录窗口的设计人员。项目完全使用Winform内置控件如Label、TextBox、PictureBox和Button不引入任何第三方组件高度模拟QQ2013版登录界面的布局与视觉风格。通过学习这份源码能够掌握控件拖放设计、属性配置、Button点击事件处理、密码框隐藏显示等基础技能并理解如何利用Graphics类绘制边框阴影等视觉效果。压缩包共包含129个文件整体体积约1.37MB其中93个png图片为主要界面素材此外还有cs源文件、config配置文件、exe可执行程序、dll动态链接库以及解决方案与项目文件结构清晰完整既便于直接运行体验也适合按需修改扩展。该资源在CSDN已有1349人学习浏览说明其实用性得到一定认可。开发者可从登录逻辑、界面美化和异常处理几个层面进行二次开发例如增加记住密码、验证码校验、自动登录等常见功能从而快速构建自己的桌面应用登录模块。1. 这也是一份登录界面源码Winform 仿 QQ 登录到底在考什么如果你手头已经下到了这份《Winform QQ登录界面源码》打开工程的第一眼可能会觉得平平无奇一个窗体几个控件一段验证码绘制好像没有多少东西。但真正把它跑起来、再想改成自己产品的登录页你就会发现里面塞满了 Winform 界面美化的细节——无边框窗体拖动、自绘按钮、输入框占位符、验证码刷新、记住密码每一块都是在别的管理系统源码里很难一次集齐的。这个资源适合两类人一类是课程设计需要交一个像样桌面程序的在校生登录页好看能直接拉高印象分另一类是接外包要把现有工具加上账号体系的开发者直接拿这套壳子改业务比自己从零布局省半天时间。我拆这类登录源码的习惯是先跑再改搞清楚每个控件背后的属性再动手否则很容易改一个毁一个。2. 无边框窗体拖动、关闭与阴影的取舍2.1 为什么 QQ 风格登录框必须去掉标题栏QQ 登录窗和普通 Winform 业务窗最大的差异不是那个头像而是整个窗体没有系统标题栏。系统标题栏会带出一个矩形边框、一组固定样式的按钮还会把窗体的圆角截断视觉上立刻变“老”。要做成 QQ 那种圆角卡片第一步就是关掉系统边框this.FormBorderStyle FormBorderStyle.None;这一步做完标题栏和玻璃效果都没了但同时也带来三个问题窗口不能拖动、没有系统关闭按钮、没有阴影。所以源码里通常紧接着会写一套自定义标题栏逻辑左上角放 Logo右上角放最小化和关闭按钮窗体下面留出圆角区域。我在拆这套源码时建议你把FormBorderStyle这一行先注释掉跑一遍对比一下有边框和无边框的控件坐标变化这样能理解为什么无边框窗体的坐标起点是(0,0)而不是系统标题栏下方的区域。这里有个取舍一旦去掉了系统边框真·阴影也没了。你看到 QQ 登录窗边缘那层淡淡的阴影并不是DropShadow属性而是用一张半透明背景图或者自绘边框实现的。这套源码里如果只做了Region圆角没做阴影视觉效果会打折后面第三节我会说圆角的坑。2.2 两种拖动方案MouseMove 与 SendMessage许多初学者在无边框窗体上写拖动第一版都是这个思路记录鼠标按下的位置然后跟着 MouseMove 改窗体 Location。代码很直观我拆过的源码里也有不少是这种写法private bool _dragging false; private Point _offset; private void LoginForm_MouseDown(object sender, MouseEventArgs e) { if (e.Button MouseButtons.Left) { _dragging true; _offset e.Location; } } private void LoginForm_MouseMove(object sender, MouseEventArgs e) { if (_dragging) { Point p Control.MousePosition; this.Location new Point(p.X - _offset.X, p.Y - _offset.Y); } } private void LoginForm_MouseUp(object sender, MouseEventArgs e) { _dragging false; }这个方案的逻辑是MouseDown 时把鼠标相对窗体的偏移存到_offsetMouseMove 时取屏幕坐标Control.MousePosition减去偏移量得到窗体新位置。参数说明_offset必须是鼠标按下时的相对坐标不能是e.X this.Location.X否则鼠标一旦移出窗体移动就会断。实际使用中这套方法在鼠标快速移动时会有轻微抖动更稳的是把事件交给系统处理[DllImport(user32.dll)] public static extern bool ReleaseCapture(); [DllImport(user32.dll)] public static extern IntPtr SendMessage(IntPtr hwnd, int Msg, IntPtr wParam, IntPtr lParam); private void LoginForm_MouseDown(object sender, MouseEventArgs e) { if (e.Button MouseButtons.Left) { ReleaseCapture(); SendMessage(this.Handle, 0x00A1, new IntPtr(2), IntPtr.Zero); } }这里的核心是WM_NCLBUTTONDOWN消息值0x00A1告诉窗口“鼠标按在了非客户区”wParam传2表示按在标题栏区域系统会接管整个拖动过程。副作用是鼠标按下后如果快速移动窗口会跟随系统标题栏的动画再配合DoubleBuffered true能明显减少残影。我一般优先用 SendMessage 方案因为它在高分屏下缩放更稳定手工算 Location 在 125% 缩放下容易计算出非整数坐标造成窗体边缘抖动。2.3 圆角和阴影Region 的正确姿势仿 QQ 登录窗还有一个标志性处理四角是圆的。Winform 里最常见的圆角做法是设置Region但直接new Region(new RectangleF(...))只能切掉正圆角边缘锯齿非常明显。要做出平滑圆角必须用GraphicsPath画弧线using System.Drawing.Drawing2D; private void SetRoundedRegion(Form form, int radius) { int w Math.Max(form.Width, 10); int h Math.Max(form.Height, 10); radius Math.Min(radius, Math.Min(w, h) / 2); using (GraphicsPath path new GraphicsPath()) { path.AddArc(0, 0, radius, radius, 180, 90); path.AddArc(w - radius - 1, 0, radius, radius, 270, 90); path.AddArc(w - radius - 1, h - radius - 1, radius, radius, 0, 90); path.AddArc(0, h - radius - 1, radius, radius, 90, 90); path.CloseFigure(); form.Region new Region(path); } }参数说明radius是圆角半径QQ 风格一般取16~24AddArc的四个参数分别是矩形区域、起始角度和扫过角度四条弧连起来正好是四个圆角。注意我把w - radius - 1和h - radius - 1都减了 1因为最后一个弧的终点要闭合不减 1 会出现右下角缺一条细线。这个方法要放在Load事件里调用同时订阅Resize事件重新生成否则窗口最大化或拖动后圆角会错位private void LoginForm_Load(object sender, EventArgs e) { SetRoundedRegion(this, 18); } private void LoginForm_Resize(object sender, EventArgs e) { SetRoundedRegion(this, 18); }关于阴影Region 只能裁剪不能画阴影。如果源码里只是纯白底 Region效果会比较“寡”。我的做法是在窗体后面再放一个半透明的Form作为阴影层或者干脆用一张四角带透明灰的 PNG 背景图替代纯色背景。这两种方式都不复杂但阴影层要注意跟随主窗体移动、最小化时同步隐藏否则会出现一个灰色“尾巴”这也是常见翻车点。3. 控件布局从 Logo 到登录按钮的坐标工程3.1 布局策略固定尺寸绝对定位最省心登录界面不像主业务窗口需要大量自适应缩放窗口就这么大放不下的东西也不应该硬塞。所以我一般在 320x480 的固定尺寸下用绝对定位AutoScaleMode设置为None避免高分屏把字体放大后控件错位。下面的坐标表是我拆同类源码时最常用的基础布局可以直接对应到设计器里控件XY宽高关键属性Logo 头像11028100100SizeModeZoom账号输入框4016024032BorderStyleFixedSingle密码输入框4020024032PasswordChar●验证码图片4024212036CursorHand验证码输入框17024211032ImeModeOff记住密码勾选4029010020Text记住密码登录按钮4033024040FlatStyleFlat注册链接2402908020LinkLabel这个布局的关键是把纵向间距控制在 40~50 像素之间太近显得挤太远又撑不起 480 的高度。如果你用的是 320x480 固定窗体最底部还要留 20 像素给圆角区域所以按钮不要贴到Height - 10的位置否则圆角会切掉按钮下边缘。3.2 账号密码框占位符和 ImeMode 一起设QQ 风格登录框里的输入框是没有 Label 的直接在框里显示灰色提示文字也就是占位符。Winform 的 TextBox 没有原生 Placeholder 属性常见做法是用 Enter/Leave 事件模拟private void txtAccount_Enter(object sender, EventArgs e) { if (txtAccount.Text QQ号码) { txtAccount.Text ; txtAccount.ForeColor Color.Black; } } private void txtAccount_Leave(object sender, EventArgs e) { if (txtAccount.Text.Trim() ) { txtAccount.Text QQ号码; txtAccount.ForeColor Color.Gray; } }逻辑说明Enter 时如果当前内容是默认提示就清空并恢复正常颜色Leave 时如果用户啥都没填就把提示文字填回去。这个做法有个隐患用户真的输入了“QQ号码”这四个字也会被当成提示清掉所以判断条件建议改成用一个私有字段_isPlaceholder标记而不是直接比较文本。源码里如果只判断文本二次开发时经常会在这里踩坑。密码框相对简单但它有两个容易混淆的属性txtPassword.PasswordChar ●; txtPassword.UseSystemPasswordChar false; txtPassword.MaxLength 20; txtPassword.ImeMode System.Windows.Forms.ImeMode.Off;参数说明PasswordChar和UseSystemPasswordChar不能同时设成true如果UseSystemPasswordChar true系统会把PasswordChar忽略掉显示成默认圆点。ImeMode Off是为了防止中文输入法在密码框里弹拼音这个问题在第 5 章会专门展开。MaxLength建议设成 20QQ 密码不会太长设了还能阻止用户误输一大串。这里可以顺手对着一下winform 控件属性大全占位符用到的ForeColor、提示语判断用到的Text、布局用到的Anchor都是属性面板里最常改的几项。不要每次都用代码去设设计器里改完属性后代码会同步生成到InitializeComponent可读性更好。3.3 登录按钮FlatStyle 自绘与可用状态默认的 Button 带灰色边框和渐变背景和 QQ 登录那种纯色圆角按钮气场完全不合。所以登录按钮基本都会改成 FlatStylebtnLogin.FlatStyle FlatStyle.Flat; btnLogin.FlatAppearance.BorderSize 0; btnLogin.FlatAppearance.MouseOverBackColor Color.FromArgb(74, 144, 255); btnLogin.FlatAppearance.MouseDownBackColor Color.FromArgb(28, 108, 255); btnLogin.BackColor Color.FromArgb(16, 128, 255); btnLogin.ForeColor Color.White; btnLogin.Font new Font(Microsoft YaHei, 11F, FontStyle.Bold);参数说明BorderSize 0去掉按钮默认边框MouseOverBackColor是鼠标悬停时的颜色MouseDownBackColor是按下时的颜色。这里有几个细节BackColor必须设置成有意义的颜色否则 FlatStyle 下会显示控件默认的灰色文字字体要用微软雅黑宋体在 125% 缩放下会比较糊。这样自绘出来的按钮和 QQ 登录主按钮观感非常接近。更进阶一点的是登录按钮的可用状态联动。账号或密码为空时按钮应该置灰避免用户点击后弹“请输入账号”。实现方式是订阅两个 TextBox 的TextChanged事件private void txtAccount_TextChanged(object sender, EventArgs e) { UpdateLoginButtonState(); } private void UpdateLoginButtonState() { bool enable !string.IsNullOrWhiteSpace(txtAccount.Text) txtAccount.Text ! QQ号码 !string.IsNullOrEmpty(txtPassword.Text); btnLogin.Enabled enable; btnLogin.BackColor enable ? Color.FromArgb(16, 128, 255) : Color.FromArgb(180, 200, 220); }可以注意逻辑置灰时不要只改Enabled不改背景色否则灰了一点不明显用户会以为按钮坏掉了。很多时候大家从网上下载的登录源码只写了账号密码为空弹提示没有做前置禁用体验就差一截。4. 登录逻辑验证码、记住密码与回车触发4.1 验证码GDI 绘制与点击刷新QQ 登录早期是有验证码的后续登录频繁也会出滑块和验证码。这套源码里如果带了验证码生成那十有八九是用 GDI 现画的。它的核心不是验证码本身而是“每次刷新必须释放旧图形资源”。private Bitmap CreateCaptcha(int width, int height, string code) { Bitmap bmp new Bitmap(width, height); Random rand new Random(); using (Graphics g Graphics.FromImage(bmp)) { g.Clear(Color.White); for (int i 0; i 6; i) { using (Pen pen new Pen(Color.FromArgb(rand.Next(256), rand.Next(256), rand.Next(256)), 1)) { g.DrawLine(pen, rand.Next(0, width), rand.Next(0, height), rand.Next(0, width), rand.Next(0, height)); } } using (Font font new Font(Arial, 18, FontStyle.Bold | FontStyle.Italic)) { for (int i 0; i code.Length; i) { using (Matrix matrix new Matrix()) { matrix.RotateAt(rand.Next(-18, 18), new PointF(i * 22 12, height / 2 2)); g.Transform matrix; g.DrawString(code[i].ToString(), font, Brushes.DarkSlateBlue, i * 22 8, height / 2 - 12); g.ResetTransform(); } } } } return bmp; }逻辑说明先画背景再画干扰线最后逐字符画带随机旋转角度的验证码。参数说明干扰线数量6、旋转角度-18~18、字符间距22这三个值共同决定验证码的可读性。角度太大字符会叠在一起角度太小又起不到干扰作用。using包裹了Graphics、Pen、Font、Matrix这些对象不释放会导致 GDI 句柄耗尽。点击刷新的事件里还要注意释放旧图private void pictureBoxCaptcha_Click(object sender, EventArgs e) { if (pictureBoxCaptcha.Image ! null) { pictureBoxCaptcha.Image.Dispose(); } pictureBoxCaptcha.Image CreateCaptcha(120, 36, GenerateCode(4)); }GenerateCode(4)是生成 4 位随机字符串的函数这里只展示调用。刷新时先把pictureBoxCaptcha.Image释放再赋新图否则旧位图一直占着内存刷新十几次就会触发OutOfMemoryException这不是真正内存不够而是 GDI 对象没释放干净。4.2 记住密码设置文件与加密存储登录源码里一般会放一个“记住密码”的 CheckBox背后对应一套持久化逻辑。最简单的做法是写进Properties.Settingsprivate void SaveLoginSettings() { Properties.Settings.Default.Account txtAccount.Text.Trim(); if (chkRemember.Checked) { Properties.Settings.Default.Password EncryptPassword(txtPassword.Text); Properties.Settings.Default.Remember true; } else { Properties.Settings.Default.Password ; Properties.Settings.Default.Remember false; } Properties.Settings.Default.Save(); }注意这里我没有把明文密码直接存进设置文件。过去很多课程设计源码都是把密码字符串直接放进user.config那就等于把账号密码明文送给本机所有进程读。常见做法是用 DPAPI 加密using System.Security.Cryptography; using System.Text; private string EncryptPassword(string password) { byte[] plain Encoding.UTF8.GetBytes(password); byte[] encrypted ProtectedData.Protect(plain, null, DataProtectionScope.CurrentUser); return Convert.ToBase64String(encrypted); }参数说明ProtectedData.Protect的第一个参数是明文字节第二个参数是可选附加熵传null就行DataProtectionScope.CurrentUser表示只有当前 Windows 用户能解密。这个接口依赖System.Security.Cryptography.ProtectedData程序集需要在项目引用里加一下否则编译会报“找不到类型”。解密用Unprotect过程刚好反过来。如果源码里用的是自写 XOR 或者 Base64建议直接替换成 DPAPI安全性差距很大。4.3 回车触发登录与状态栏进度条登录窗口上按回车应该触发登录按钮这是桌面软件最基本的交互。Winform 里最省事的做法是设置窗体的AcceptButtonthis.AcceptButton btnLogin;设置之后不管焦点在哪个控件上按回车都会走btnLogin_Click。如果登录过程不是瞬间完成的还需要给用户反馈。很多新手在论坛上问“C# winform 如何更新状态栏与进度条”其实关键不是改 Value而是别让 UI 线程被阻塞private async void btnLogin_Click(object sender, EventArgs e) { btnLogin.Enabled false; btnLogin.Text 登录中…; toolStripProgressBar1.Value 20; bool success await Task.Run(() CheckLogin(txtAccount.Text, txtPassword.Text)); if (success) { toolStripProgressBar1.Value 100; MessageBox.Show(登录成功); } else { toolStripProgressBar1.Value 0; MessageBox.Show(账号或密码错误); } btnLogin.Enabled true; btnLogin.Text 登 录; }这段代码里await Task.Run会把耗时的登录检查放到线程池await之后的代码自动回到 UI 线程所以toolStripProgressBar1.Value可以安全赋值不需要额外写Invoke。async void只建议用在事件处理器里普通方法不要这么写否则异常会直接抛到 UI 同步上下文里。进度条如果不想只显示 0/20/100 三档可以在Task.Run的回调里用IProgressint逐步上报但前提是底层登录方法能被拆成多步。5. 实战避坑五个 Winform 登录界面常见问题5.1 中文输入法把密码框带偏现象用户用搜狗或微软拼音输入账号后直接 Tab 切到密码框拼音候选框会悬浮在密码框上甚至把拼音字母打进去了。原因Winform 的 TextBox 默认继承窗体的ImeMode很多窗体没有显式设置系统继承默认的NoControl而密码框本应是纯英文输入。解决单独给密码框设置ImeMode Off账号框如果只允许数字也设置成Disable如果账号允许中文昵称则保持On。还可以在窗体Load里给所有密码输入框统一设置foreach (Control ctl in this.Controls) { if (ctl is TextBox txt txt.PasswordChar ! \0) { txt.ImeMode System.Windows.Forms.ImeMode.Off; } }这段代码的优点是后续新增密码框时不用一个个设属性缺点是不能覆盖嵌套在 Panel 里的控件需要改成递归遍历。如果你发现设置ImeMode后在某些输入法上仍然弹出拼音那就是输入法自身的兼容问题暂时没有通用解法可以在窗体标题栏加一个“中/英”状态提示引导用户手动切换。5.2 Region 圆角毛边和黑色残留现象窗体设置圆角后边缘有明显的锯齿把背景色改成深色时更明显有时候窗口缩放后四个角会露出黑色矩形。原因Region本质是按位图裁剪裁剪边界不做抗锯齿另外Resize后没有重新设置 Region旧 Region 还保留原来的大小。解决使用GraphicsPath重建 Region并在Resize事件里重新执行。同时给窗体开启DoubleBuffered true能有效减少裁剪边界的闪烁protected override void OnLoad(EventArgs e) { base.OnLoad(e); this.DoubleBuffered true; SetRoundedRegion(this, 18); }注意第一次设置 Region 的时机不要太早如果在InitializeComponent里执行窗体实际大小可能还没有应用圆角会按默认尺寸计算。放到Load或Shown事件里最稳妥。还有一点圆角区域外是透明的如果主窗体背景色和父容器颜色不一致会出现一圆角外一圈杂色这时把TransparencyKey设成一个不常用的颜色比如Magenta可以部分缓解。5.3 鼠标拖动时窗体跑飞现象按住窗体顶部拖动鼠标还没松开窗体就突然跳到屏幕左上角或者卡在某个位置不动。原因使用MouseMove方案时鼠标移动过快会离开窗体客户区MouseMove事件不再触发_dragging状态一直是true等鼠标回到窗体上e.Location已经变了导致坐标计算错乱。解决改用ReleaseCapture()SendMessage()方案把拖动交给系统处理。如果坚持用鼠标事件需要在MouseMove里用Control.MousePosition取屏幕坐标并保证_dragging在MouseUp外也能被重置。更稳的办法是重写WndProc拦截WM_NCHITTEST把客户区顶部区域伪装成标题栏protected override void WndProc(ref Message m) { if (m.Msg 0x0084) // WM_NCHITTEST { Point pos PointToClient(Cursor.Position); if (pos.Y 30) { m.Result new IntPtr(2); // HTCAPTION return; } } base.WndProc(ref m); }这段代码的意思是鼠标在窗体上部 30 像素内按下时系统认为你点中了标题栏会自动处理拖动。好处是完全不需要管理鼠标事件坏处是这块区域里的按钮如果正好在顶部点击效果会被系统优先截获所以自绘按钮不要放在pos.Y 30的范围内。5.4 密码框内容能被复制出来现象用户在密码框里输入一串密码显示成圆点但他按Ctrl C或者右键复制得到的却是明文。原因WinformTextBox内部存的就是真实字符串PasswordChar只是显示层的遮盖剪贴板读写不经过显示层。这个问题在无边框自绘登录窗里特别隐蔽因为没有系统右键菜单遮挡用户直接按快捷键就能复制。解决禁用快捷键和系统菜单txtPassword.ShortcutsEnabled false; txtPassword.ContextMenuStrip new ContextMenuStrip();ShortcutsEnabled false会让CtrlC、CtrlV、CtrlA全部失效ContextMenuStrip赋一个空的菜单对象让右键点击不出菜单。要注意禁用之后密码框也不能粘贴密码了如果产品要求“忘记密码”的二次确认输入也得能粘贴就需要自己写粘贴逻辑从剪贴板读取文本时判断长度和字符集是否合理。这个坑在登录源码里基本不会暴露但一旦软件用来做内部系统安全测试人员肯定会测这一条。5.5 验证码刷新几次就内存不足现象登录界面连续点验证码图片十几次程序开始卡顿最后弹OutOfMemoryException强制重启后恢复正常。原因每次刷新都new Bitmap旧的Bitmap没有 DisposeGDI 资源句柄被耗尽。报错信息虽然写着内存不足其实真正耗尽的是 GDIPlus 内部句柄不是托管堆内存。解决先释放旧图片再赋值新图片。绘制验证码时所有Graphics、Pen、Font都要using包裹。这里有一个非常容易漏的点给PictureBox.Image赋新图前一定要判断pictureBoxCaptcha.Image ! null否则第一次点击就会把上一次的空引用直接 Dispose 掉。正确写法private void pictureBoxCaptcha_Click(object sender, EventArgs e) { if (pictureBoxCaptcha.Image ! null) { pictureBoxCaptcha.Image.Dispose(); } pictureBoxCaptcha.Image CreateCaptcha(120, 36, GenerateCode(4)); }窗体关闭时同样要释放private void LoginForm_FormClosing(object sender, FormClosingEventArgs e) { if (pictureBoxCaptcha.Image ! null) { pictureBoxCaptcha.Image.Dispose(); } }这是我拆源码时反复遇到的一类问题很多下载下来的验证码模块表面上看没问题但运行时间一长就崩。遇到这种问题不要怀疑系统内存不够先把所有Image相关的赋值点找出来在换图和关闭两个时机做释放症状立刻消失。6. 进阶用 UserControl 把登录界面变成可复用组件前面五章讲的是怎么把这份 QQ 登录界面源码跑起来、改好细节。如果你只是交个课程设计到第五章已经够了。但如果你要拿它接自己的业务系统我建议再做一步重构把整个登录窗体里的 UI 和逻辑拆开封装成UserControl。这样以后任何项目需要登录时直接拖一个控件进去而不是复制一堆窗体代码。拆法并不复杂把原来 Form 上的控件挪到 UserControl 里把按钮点击事件改成事件外抛public partial class LoginControl : UserControl { public event EventHandlerLoginEventArgs LoginAccepted; public event EventHandler LoginCancelled; public string Account txtAccount.Text.Trim(); public string Password txtPassword.Text; private void btnLogin_Click(object sender, EventArgs e) { if (ValidateInput()) { LoginAccepted?.Invoke(this, new LoginEventArgs(Account, Password)); } } private bool ValidateInput() { return !string.IsNullOrWhiteSpace(Account) Account ! QQ号码 !string.IsNullOrEmpty(Password); } }关键点是把验证码校验、网络请求、记住密码这些业务都移到 UserControl 外面。LoginControl只负责收集输入、做基础校验、通过事件把数据交出去。主窗体拿到LoginEventArgs后再调你自己的 API、弹提示、更新状态栏。验证这个重构是否成功的标准有两个第一新建一个空白 Winform把这个LoginControl从工具箱拖进设计器编译不报错第二在Program.cs里把启动窗体换成另一个只包含LoginControl的测试窗体登录逻辑一行都不用改。如果这两步都能过说明控件没有依赖原窗体的私有属性和全局变量复制到其他工程时不会“缺胳膊少腿”。如果你想让它更通用可以给LoginControl加一个主题方法public void ApplyTheme(Color primaryColor) { btnLogin.BackColor primaryColor; btnLogin.FlatAppearance.MouseOverBackColor ControlPaint.Light(primaryColor, 0.2f); txtAccount.ForeColor Color.Black; }这样不同项目可以传不同主色QQ 蓝、微信绿、企业橙都只改一个参数。从那以后我每拆一套登录窗体源码都会先看一眼事件是写在按钮里还是写在控件接口里如果是前者我会顺手做一层事件封装再交给业务组。这个习惯帮我少改了很多重复登录代码。希望帮到你。本文还有配套的精品资源点击获取
返回列表