SwitchButton 切换开关按钮调用文档
命名空间:CConUI
继承:System.Windows.Forms.Control
可见性:public class SwitchButton
默认尺寸:60 × 30
默认事件:CheckedChanged(设计器双击控件即生成该事件处理器)
定位:仿移动端 Switch 开关按钮——自绘跑道 + 圆滑块,单击切换开/关,支持悬停高亮与全套颜色自定义;随控件尺寸自动缩放,无需手动设置滑块大小。
一、作用
一个交互简洁、自绘实现的开关控件,用于表达“开 / 关”、“启用 / 禁用”类二元状态:
跑道背景:开启时显示
OnBackColor,关闭时显示OffBackColor;圆滑块:开启时靠右,关闭时靠左;
悬停高亮:鼠标移入时滑块变为
HoverThumbColor;单击切换:左键单击即切换
Checked状态,并依次触发CheckedChanging→CheckedChanged;尺寸自适应:跑道、滑块直径、滑块位置都按控件宽高按比例计算,改
Size即可整体缩放。
视觉示意
| Checked | 跑道颜色 | 滑块位置 | 滑块颜色 |
|---|---|---|---|
false(关闭) | OffBackColor(默认深灰) | 左侧 | OffThumbColor(默认浅灰) |
true(开启) | OnBackColor(默认蓝) | 右侧 | OnThumbColor(默认白) |
| 鼠标悬停(任意状态) | 不变 | 不变 | HoverThumbColor(默认浅白) |
二、快速上手
using System.Drawing;
using System.Windows.Forms;
using CConUI;
// 1. 创建控件
var sw = new SwitchButton();
// 2. 放入容器
sw.Parent = panel1;
sw.Location = new Point(20, 20);
// 尺寸默认 60×30;可按需放大,例如:
sw.Size = new Size(80, 40);
// 3. 设置初始状态
sw.Checked = true;
// 4. 订阅状态变化事件
sw.CheckedChanged += (s, e) =>
{
var ctrl = (SwitchButton)s;
Console.WriteLine("当前状态:" + ctrl.Checked);
};
sw.CheckedChanging += (s, e) =>
{
// 状态即将改变(此时 Checked 还是旧值)
var ctrl = (SwitchButton)s;
Console.WriteLine("即将从 " + ctrl.Checked + " 切换");
};
// 5. 自定义外观(可选)
sw.OnBackColor = Color.FromArgb(0, 180, 120);
sw.OffBackColor = Color.FromArgb(70, 70, 70);
sw.OnThumbColor = Color.White;
sw.OffThumbColor = Color.FromArgb(200, 200, 200);
sw.HoverThumbColor = Color.LightYellow;
sw.ThumbMargin = 4;三、构造函数
public SwitchButton()
| 项目 | 说明 |
|---|---|
| 参数 | 无 |
| 作用 | 初始化开关控件 |
| 实际行为 | ① 启用 UserPaint | AllPaintingInWmPaint | OptimizedDoubleBuffer | ResizeRedraw 样式(自绘 + 双缓冲 + 尺寸变化自动重绘);② 设置 Size = (60, 30);③ 设置 Font = "微软雅黑", 10pt;④ 设置 Cursor = Cursors.Hand(悬停显示手型光标) |
| 默认尺寸 | 60 × 30(宽高比 2:1,视觉为标准开关比例) |
| 默认状态 | Checked = false、ThumbMargin = 4 |
var sw = new SwitchButton(); sw.Parent = panel1;
四、公开属性
4.1 public bool Checked
| 项目 | 说明 |
|---|---|
| 类型 | bool |
| 默认 | false |
| Category | CCon |
| DefaultValue | false |
| 作用 | 控件的选中状态:true → 开启(滑块靠右,跑道用 OnBackColor);false → 关闭(滑块靠左,跑道用 OffBackColor) |
| 副作用 | 赋值时若值有变化: ① 先触发 CheckedChanging(此时 Checked 还是旧值);② 再写入新值; ③ 触发 CheckedChanged(此时 Checked 已经是新值);④ Invalidate() 重绘 |
| 幂等 | 赋相同值时直接返回,不触发任何事件,也不重绘 |
sw.Checked = true; // 开启,触发 CheckedChanging + CheckedChanged sw.Checked = true; // 已是 true,无事发生 sw.Checked = false; // 关闭,触发事件
4.2 颜色属性
public Color OnBackColor
| 项目 | 说明 |
|---|---|
| 类型 | Color |
| 默认 | Color.FromArgb(88, 166, 255)(亮蓝) |
| Category | CCon |
| 作用 | 控件开启时跑道的填充色 |
| 副作用 | 赋值后 Invalidate() 重绘 |
public Color OffBackColor
| 项目 | 说明 |
|---|---|
| 类型 | Color |
| 默认 | Color.FromArgb(60, 60, 60)(深灰) |
| Category | CCon |
| 作用 | 控件关闭时跑道的填充色 |
| 副作用 | 赋值后 Invalidate() 重绘 |
public Color OnThumbColor
| 项目 | 说明 |
|---|---|
| 类型 | Color |
| 默认 | Color.White |
| Category | CCon |
| 作用 | 控件开启状态下、鼠标未悬停时滑块的颜色 |
| 副作用 | 赋值后 Invalidate() 重绘 |
public Color OffThumbColor
| 项目 | 说明 |
|---|---|
| 类型 | Color |
| 默认 | Color.FromArgb(180, 180, 180)(浅灰) |
| Category | CCon |
| 作用 | 控件关闭状态下、鼠标未悬停时滑块的颜色 |
| 副作用 | 赋值后 Invalidate() 重绘 |
public Color HoverThumbColor
| 项目 | 说明 |
|---|---|
| 类型 | Color |
| 默认 | Color.FromArgb(220, 220, 220) |
| Category | CCon |
| 作用 | 鼠标悬停在控件上时滑块的高亮颜色。 注意:悬停时滑块颜色统一用此色,不再区分 On / Off |
| 副作用 | 赋值后 Invalidate() 重绘 |
sw.OnBackColor = Color.FromArgb(0, 180, 120); // 开启 → 绿色跑道 sw.OffBackColor = Color.FromArgb(70, 70, 70); // 关闭 → 深灰跑道 sw.OnThumbColor = Color.White; // 开启滑块白 sw.OffThumbColor = Color.FromArgb(200, 200, 200); // 关闭滑块浅灰 sw.HoverThumbColor = Color.LightYellow; // 悬停滑块浅黄
4.3 public int ThumbMargin
| 项目 | 说明 |
|---|---|
| 类型 | int(像素) |
| 默认 | 4 |
| Category | CCon |
| DefaultValue | 4 |
| 作用 | 滑块与跑道边缘之间的内边距(上下左右对称)。数值越大,滑块相对跑道越小,视觉上跑道越“细” |
| 取值范围 | Math.Max(0, value)——自动 clamp 到 ≥0 |
| 约束 | 应小于 Height / 2,否则滑块直径为 ≤0,绘制时会直接 return(看不到滑块) |
| 副作用 | 赋值后 Invalidate() 重绘 |
| 联动 | 滑块直径 = Height - ThumbMargin × 2。改 ThumbMargin 会同时改滑块大小 |
sw.ThumbMargin = 4; // 默认 sw.ThumbMargin = 2; // 滑块更大(跑道更"满") sw.ThumbMargin = 6; // 滑块更小(跑道更"粗")
五、公开事件
public event EventHandler CheckedChanged
| 项目 | 说明 |
|---|---|
| 参数 | EventHandler:sender 为当前 SwitchButton,e 为 EventArgs.Empty |
| Category | CCon |
| 默认事件 | 是([DefaultEvent("CheckedChanged")])——设计器双击即生成此事件处理器 |
| 触发时机 | Checked 值发生改变后,且在 Invalidate() 之前触发 |
| 不触发情况 | 赋相同值不触发 |
| 触发顺序 | CheckedChanging → 更新 Checked 值 → CheckedChanged |
sw.CheckedChanged += (s, e) =>
{
var ctrl = (SwitchButton)s;
// 此时 ctrl.Checked 已经是新值
lblStatus.Text = ctrl.Checked ? "已开启" : "已关闭";
};public event EventHandler CheckedChanging
| 项目 | 说明 |
|---|---|
| 参数 | EventHandler |
| Category | CCon |
| 触发时机 | Checked 值即将改变前触发——此时 Checked 仍为旧值 |
| 不触发情况 | 赋相同值不触发 |
| 用途 | 可在此事件中记录旧状态;由于 EventArgs 为空,若需“取消”切换,需在 CheckedChanged 里再还原(本类未提供“取消”机制,只能在 changed 后自行回滚) |
bool _lastChecked = false;
sw.CheckedChanging += (s, e) =>
{
var ctrl = (SwitchButton)s;
_lastChecked = ctrl.Checked; // 记录旧值(此时 Checked 还是旧状态)
Console.WriteLine("旧状态:" + _lastChecked);
};
sw.CheckedChanged += (s, e) =>
{
var ctrl = (SwitchButton)s;
Console.WriteLine("新状态:" + ctrl.Checked);
// 如果需要“取消切换”,可以:
// if (someCondition) ctrl.Checked = _lastChecked;
};六、公开方法
SwitchButton没有新增 public 方法。所有公开方法继承自Control(如Invalidate()、Focus()、BringToFront()等)。
内部重写方法(protected override,外部不可直接调用)
| 方法 | 可见性 | 作用 |
|---|---|---|
OnPaint(PaintEventArgs e) | protected override | 整体绘制:跑道背景 → 滑块(含悬停高亮) |
OnMouseEnter(EventArgs e) | protected override | 标记 _isHover = true 并重绘 |
OnMouseLeave(EventArgs e) | protected override | 标记 _isHover = false 并重绘 |
OnMouseDown(MouseEventArgs e) | protected override | 左键按下时切换 Checked = !Checked |
七、公开成员速查表
| 成员 | 来源 | 类型 | 说明 |
|---|---|---|---|
SwitchButton() | 本类 | 构造函数 | 默认尺寸 60×30,手型光标 |
Checked | 本类 | 属性(bool) | 选中状态(默认 false);赋值触发事件 + 重绘 |
OnBackColor | 本类 | 属性(Color) | 开启时跑道颜色 |
OffBackColor | 本类 | 属性(Color) | 关闭时跑道颜色 |
OnThumbColor | 本类 | 属性(Color) | 开启时滑块颜色(非悬停) |
OffThumbColor | 本类 | 属性(Color) | 关闭时滑块颜色(非悬停) |
HoverThumbColor | 本类 | 属性(Color) | 悬停时滑块颜色 |
ThumbMargin | 本类 | 属性(int) | 滑块与跑道边距(默认 4,≥0) |
CheckedChanged | 本类 | 事件(默认事件) | 状态改变后触发 |
CheckedChanging | 本类 | 事件 | 状态改变前触发(Checked 仍是旧值) |
Text / Font / ForeColor / BackColor | Control | 属性 | 继承但对本类绘制无影响(本类不使用这些来渲染) |
Size / ClientSize | Control | 属性(Size) | 设置控件尺寸;跑道与滑块按比例自适应 |
Cursor | Control | 属性(Cursor) | 默认为 Hand;改为 Default 可去掉手型提示 |
Invalidate() | Control | 方法 | 触发重绘 |
OnPaint(e) | 本类(override) | 方法(protected) | 绘制逻辑,外部不可直接调用 |
OnMouseEnter / OnMouseLeave / OnMouseDown | 本类(override) | 方法(protected) | 悬停 / 点击交互,外部不可直接调用 |
八、绘制原理(深入理解)
8.1 基础矩形与半径
float w = this.Width; float h = this.Height; RectangleF bgRect = new RectangleF(0.5f, 0.5f, w - 1f, h - 1f); float radius = bgRect.Height / 2f;
| 要点 | 说明 |
|---|---|
0.5f, 0.5f, w-1, h-1 | 对称内缩 0.5 像素: · 防止抗锯齿边缘被控件边缘物理裁剪; · 保证跑道精确居中; · 与滑块也用 0.5 偏移,三者像素级对齐 |
radius = Height / 2 | 跑道左右两端是半圆,半径 = 控件高的一半,这样跑道就是一个完美的“胶囊形” |
8.2 绘制跑道(背景)
using (GraphicsPath path = new GraphicsPath())
{
path.AddArc(bgRect.X, bgRect.Y, radius * 2f, radius * 2f, 90, 180);
path.AddArc(bgRect.Right - radius * 2f, bgRect.Y, radius * 2f, radius * 2f, -90, 180);
path.CloseFigure();
Color backColor = this._checked ? this._onBackColor : this._offBackColor;
using (Brush brush = new SolidBrush(backColor))
g.FillPath(brush, path);
}| 要点 | 说明 |
|---|---|
左弧 (x, y, 2r, 2r, 90, 180) | 从 90°(正下方)开始顺时针扫 180° 到 -90°(正上方)。 原因:GDI+ 0° 在 3 点方向、顺时针为正;从 90° 扫到 270° 正好是左半圆(从底到顶)。 |
右弧 (Right - 2r, y, 2r, 2r, -90, 180) | 从 -90°(正上方)开始顺时针扫 180° 到 90°(正下方)。 正好是右半圆(从顶到底)。 |
| 两弧拼接 | 左弧终点在顶部,右弧起点也在顶部;左弧起点在底部,右弧终点也在底部;CloseFigure 自动用直线连接。结果就是左右两个半圆 + 上下两条直线组成的胶囊形 |
| 颜色 | Checked ? OnBackColor : OffBackColor |
8.3 计算滑块
float thumbDiameter = h - (this._thumbMargin * 2f); if (thumbDiameter <= 0) return; float thumbX = this._checked ? (w - thumbDiameter - this._thumbMargin - 0.5f) : this._thumbMargin + 0.5f; float thumbY = this._thumbMargin + 0.5f; RectangleF thumbRect = new RectangleF(thumbX, thumbY, thumbDiameter, thumbDiameter);
| 要点 | 说明 |
|---|---|
thumbDiameter = h - ThumbMargin × 2 | 滑块直径 = 控件高 − 上下两个边距。 为什么减 2×:上下各留一个 ThumbMargin |
thumbDiameter ≤ 0 | 如果边距大到滑块直径变成 0 或负数,直接 return,不绘制滑块(只剩跑道) |
开启状态 X:w - diameter - margin - 0.5 | 右对齐。 · w - diameter 是让滑块右边缘贴到控件右边缘;· 再 - margin 留出右边距;· 再 - 0.5 与跑道 0.5 偏移对齐 |
关闭状态 X:margin + 0.5 | 左对齐。 · margin 是左边距;· 再 + 0.5 与跑道 0.5 偏移对齐 |
thumbY = margin + 0.5 | 垂直居中,与上下边距一致 |
8.4 决定滑块颜色
Color thumbColor; if (this._isHover) thumbColor = this._hoverThumbColor; else thumbColor = this._checked ? this._onThumbColor : this._offThumbColor;
| 状态 | 滑块颜色 |
|---|---|
| 鼠标悬停(任意 Checked) | HoverThumbColor(悬停色统一) |
| 未悬停 + Checked=true | OnThumbColor |
| 未悬停 + Checked=false | OffThumbColor |
8.5 绘制滑块
using (Brush brush = new SolidBrush(thumbColor)) g.FillEllipse(brush, thumbRect);
简单用 FillEllipse 画圆。因 thumbRect 宽高相等(直径),画出来就是正圆。
九、交互一览
| 操作 | 效果 |
|---|---|
| 鼠标进入控件 | _isHover = true,滑块变为 HoverThumbColor |
| 鼠标离开控件 | _isHover = false,滑块恢复 OnThumbColor / OffThumbColor |
| 左键按下 | Checked = !Checked,走 setter:触发 CheckedChanging → 更新 → CheckedChanged → 重绘 |
| 右键 / 中键按下 | 不响应(OnMouseDown 里只判断 MouseButtons.Left) |
| 鼠标悬停光标 | 默认 Hand(手型),提示可点击 |
| 键盘 | 本类未重写键盘处理:Space / Enter 不会切换状态;如需键盘支持,请自行外部处理或重写 |
十、完整使用示例
示例 1:基础开关
using System.Drawing;
using System.Windows.Forms;
using CConUI;
var sw = new SwitchButton
{
Parent = panel1,
Location = new Point(20, 20),
Checked = true
};
sw.CheckedChanged += (s, e) =>
{
var ctrl = (SwitchButton)s;
MessageBox.Show(ctrl.Checked ? "开启了" : "关闭了");
};示例 2:自定义颜色
var sw = new SwitchButton
{
Parent = this,
Location = new Point(20, 20),
OnBackColor = Color.FromArgb(0, 180, 120), // 绿跑道
OffBackColor = Color.FromArgb(80, 80, 80), // 深灰跑道
OnThumbColor = Color.White,
OffThumbColor = Color.FromArgb(200, 200, 200),
HoverThumbColor = Color.FromArgb(255, 240, 180),
ThumbMargin = 3,
Checked = false
};示例 3:放大尺寸(自适应)
// 跑道、滑块、位置都会自动按比例缩放
var sw = new SwitchButton
{
Parent = this,
Size = new Size(100, 50),
Location = new Point(20, 20),
ThumbMargin = 6, // 大控件时边距可适当加大
OnBackColor = Color.DodgerBlue,
OffBackColor = Color.Gray
};示例 4:用 CheckedChanging 做“确认取消”
bool _previousChecked = false;
var sw = new SwitchButton { Parent = this, Location = new Point(20, 20) };
sw.CheckedChanging += (s, e) =>
{
// 此时 Checked 还是旧值,记录一下
_previousChecked = sw.Checked;
};
sw.CheckedChanged += (s, e) =>
{
// 此时 Checked 已经是新值
if (sw.Checked)
{
var result = MessageBox.Show("确认开启?", "确认",
MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (result == DialogResult.No)
{
// 用户在确认框里选了 No:还原到切换前的状态
sw.Checked = _previousChecked;
}
}
};注意:
sw.Checked = _previousChecked会再次触发CheckedChanging和CheckedChanged——这是“回滚”时的固有副作用。为避免无限递归,建议在回滚时加标志位:bool _reverting = false; sw.CheckedChanged += (s, e) => { if (_reverting) return; if (sw.Checked) { if (MessageBox.Show("确认开启?", "确认", MessageBoxButtons.YesNo) == DialogResult.No) { _reverting = true; try { sw.Checked = _previousChecked; } finally { _reverting = false; } } } };
示例 5:批量同步多个开关
var master = new SwitchButton { Parent = this, Location = new Point(20, 20) };
var slave1 = new SwitchButton { Parent = this, Location = new Point(20, 60) };
var slave2 = new SwitchButton { Parent = this, Location = new Point(20, 100) };
master.CheckedChanged += (s, e) =>
{
slave1.Checked = master.Checked;
slave2.Checked = master.Checked;
};转载请注明出处。