命名空间:CConUI
继承:System.Windows.Forms.Control
可见性:public class RadioButton
默认尺寸:130 × 20
默认事件:CheckedChanged(设计器双击控件即生成该事件处理器)
定位:自绘单选按钮——左侧圆环 + 选中内部实心圆点 + 右侧文字;同一父容器内自动互斥(选中一个时自动取消其它 RadioButton)。
一、作用
一个自绘的单选按钮,具备如下特性:
圆环自适应高度:圆的直径 = Height - CirclePadding × 2,控件变高圆跟着变大;
选中圆点:选中时在圆环内画一个直径 ≈ 外圆 45% 的实心圆点;
状态颜色:圆环边框支持三态颜色——常态 BorderColor / 悬停 HoverColor / 按下 PressedColor;
自动互斥:同父容器内选中一个时,自动把其它 RadioButton 的 Checked 置为 false;
文字:圆环右侧自动偏移 6px 绘制 Text,ForeColor / Font 可控,超长自动省略号(EllipsisCharacter)。
状态与视觉对照
| 状态 | 圆环颜色 | 内部圆点 |
|---|
| 常态(未选中、未悬停) | BorderColor | 无 |
| 悬停 | HoverColor | 无 |
| 按下 | PressedColor | 无 |
| 选中 | 按上面的悬停/按下/常态规则 | 画一个直径 ≈ 外圆 45% 的 CheckedColor 实心圆点 |
二、快速上手
using System.Drawing;
using System.Windows.Forms;
using CConUI;
// 1. 创建控件
var rb = new RadioButton();
// 2. 放入容器(自动互斥发生在此父容器内)
rb.Parent = panel1;
rb.Location = new Point(20, 20);
rb.Text = "选项 A";
// 3. 初始选中
rb.Checked = true;
// 4. 订阅选中状态变化
rb.CheckedChanged += (s, e) =>
{
var r = (RadioButton)s;
Console.WriteLine((r.Text) + " → " + r.Checked);
};
// 5. 外观自定义(可选)
rb.BorderColor = Color.FromArgb(139, 148, 158);
rb.HoverColor = Color.FromArgb(173, 186, 199);
rb.PressedColor = Color.FromArgb(88, 166, 255);
rb.CheckedColor = Color.FromArgb(88, 166, 255);
rb.RingThickness = 3f;
rb.CirclePadding = 0;
rb.ForeColor = Color.White;
rb.Font = new Font("Microsoft YaHei", 9F);
三、构造函数
public RadioButton()
| 项目 | 说明 |
|---|
| 参数 | 无 |
| 作用 | 初始化自绘单选按钮 |
| 实际行为 | ① 启用 UserPaint | AllPaintingInWmPaint | OptimizedDoubleBuffer | ResizeRedraw 样式(自绘 + 双缓冲 + 尺寸变化自动重绘); ② 默认尺寸 Size = (130, 20); ③ 默认前景色 Color.FromArgb(238, 238, 238); ④ 默认光标 Cursors.Hand |
| 默认尺寸 | 130 × 20 |
| 默认状态 | Checked = false、RingThickness = 3f、CirclePadding = 0 |
var rb = new RadioButton
{
Parent = panel1,
Text = "选项 A"
};
四、公开属性
4.1 public bool Checked
| 项目 | 说明 |
|---|
| 类型 | bool |
| 默认 | false |
| Category | CCon |
| DefaultValue | false |
| 作用 | 控件的选中状态 |
| 副作用(赋值时值有变化) | ① 触发 CheckedChanging(此时 Checked 还是旧值); ② 写入新值; ③ 若新值为 true 且 Parent != null,遍历父容器内所有控件,把其它 RadioButton 的 Checked 置为 false(自动互斥); ④ 触发 CheckedChanged; ⑤ Invalidate() 重绘 |
| 幂等 | 赋相同值直接返回,不触发任何事件、不重绘 |
| 互斥范围 | 仅限同一 Parent.Controls。若多个 RadioButton 位于不同容器(如不同 Panel),则不会自动互斥 |
rb1.Checked = true;
rb2.Checked = true; // rb1 会自动被置为 false
rb2.Checked = true; // 已经是 true,无事发生
4.2 public int CirclePadding
| 项目 | 说明 |
|---|
| 类型 | int(像素) |
| 默认 | 0 |
| Category | CCon |
| DefaultValue | 0 |
| 作用 | 圆环与控件上下左右边缘之间的内边距。
0:圆直径 = Height,圆环紧贴控件上下边缘;
> 0:圆按此值向内缩,圆的直径 = Height - CirclePadding × 2 |
| 取值范围 | Math.Max(0, value)——自动 clamp 到 ≥0 |
| 副作用 | 赋值后 Invalidate() 重绘 |
| 约束 | 建议 CirclePadding < Height / 2;若过大导致直径 ≤4,绘制时兜底为 4 |
rb.CirclePadding = 0; // 圆直径 = 控件高度(默认)
rb.CirclePadding = 2; // 圆直径 = 高度 - 4
rb.CirclePadding = 4; // 圆直径 = 高度 - 8
4.3 public float RingThickness
| 项目 | 说明 |
|---|
| 类型 | float(像素) |
| 默认 | 3f |
| Category | CCon |
| DefaultValue | 3f |
| 作用 | 圆环边框的线宽 |
| 取值范围 | Math.Max(0.5f, value)——自动 clamp 到 ≥0.5 |
| 副作用 | 赋值后 Invalidate() 重绘 |
| 绘制处理 | 绘制时 ringRect 向内缩 RingThickness / 2,避免画笔居中描边时溢出外接矩形 |
rb.RingThickness = 3f;
rb.RingThickness = 2f; // 更细的圆环
rb.RingThickness = 4f; // 更粗的圆环
4.4 颜色属性
public Color BorderColor
| 项目 | 说明 |
|---|
| 类型 | Color |
| 默认 | Color.FromArgb(139, 148, 158)(中灰) |
| Category | CCon |
| 作用 | 圆环在常态(未悬停、未按下)下的边框颜色 |
public Color HoverColor
| 项目 | 说明 |
|---|
| 类型 | Color |
| 默认 | Color.FromArgb(173, 186, 199)(浅灰) |
| Category | CCon |
| 作用 | 鼠标悬停时圆环的颜色 |
public Color PressedColor
| 项目 | 说明 |
|---|
| 类型 | Color |
| 默认 | Color.FromArgb(88, 166, 255)(亮蓝) |
| Category | CCon |
| 作用 | 鼠标按下时圆环的颜色 |
public Color CheckedColor
| 项目 | 说明 |
|---|
| 类型 | Color |
| 默认 | Color.FromArgb(88, 166, 255)(亮蓝) |
| Category | CCon |
| 作用 | 选中时内部实心圆点的填充色 |
rb.BorderColor = Color.FromArgb(139, 148, 158);
rb.HoverColor = Color.FromArgb(173, 186, 199);
rb.PressedColor = Color.FromArgb(88, 166, 255);
rb.CheckedColor = Color.FromArgb(88, 166, 255);
4.5 重写的外观属性
public override string Text
| 项目 | 说明 |
|---|
| 类型 | string |
| Category | CCon |
| 作用 | 圆环右侧显示的文字;空串时不绘制 |
| 副作用 | 赋值后 Invalidate() |
| 绘制 | 起始 X = 圆右边缘 + 6px;LineAlignment = Center 垂直居中;超长自动省略号(EllipsisCharacter) |
public override Color ForeColor
| 项目 | 说明 |
|---|
| 类型 | Color |
| 默认 | Color.FromArgb(238, 238, 238)(浅白) |
| Category | CCon |
| 作用 | 文字颜色 |
public override Font Font
| 项目 | 说明 |
|---|
| 类型 | Font |
| Category | CCon |
| 作用 | 文字字体 |
五、公开事件
public event EventHandler CheckedChanged
| 项目 | 说明 |
|---|
| 参数 | EventHandler:sender 为当前 RadioButton,e 为 EventArgs.Empty |
| Category | CCon |
| 默认事件 | 是([DefaultEvent("CheckedChanged")]) |
| 触发时机 | Checked 值改变后;若是“被其它 RadioButton 自动置为 false”,也会触发此事件 |
| 不触发情况 | 赋相同值不触发 |
| 触发顺序 | CheckedChanging → 更新 Checked → 自动互斥其它项 → CheckedChanged |
rb.CheckedChanged += (s, e) =>
{
var r = (RadioButton)s;
Console.WriteLine(r.Text + " 状态:" + r.Checked);
};public event EventHandler CheckedChanging
| 项目 | 说明 |
|---|
| 参数 | EventHandler |
| Category | CCon |
| 触发时机 | Checked 值即将改变前——此时 Checked 仍为旧值 |
| 用途 | 记录切换前的状态;由于 EventArgs 为空,不支持取消操作 |
| 不触发情况 | 赋相同值不触发 |
bool _lastState = false;
rb.CheckedChanging += (s, e) =>
{
_lastState = ((RadioButton)s).Checked;
};
六、公开方法
RadioButton 没有新增 public 方法。所有公开方法继承自 Control(如 Invalidate()、Focus()、BringToFront() 等)。
内部重写方法(protected override,外部不可直接调用)
| 方法 | 可见性 | 作用 |
|---|
OnPaint(PaintEventArgs e) | protected override | 绘制:外圆环 → 选中圆点 → 文字 |
OnMouseEnter(EventArgs e) | protected override | 设置 _isHover = true 并重绘 |
OnMouseLeave(EventArgs e) | protected override | 清除悬停与按下状态并重绘 |
OnMouseDown(MouseEventArgs e) | protected override | 左键按下时设置 _isPressed = true 并重绘 |
OnMouseUp(MouseEventArgs e) | protected override | 左键抬起时清 _isPressed;若抬起点在客户区内 → Checked = true |
七、公开成员速查表
| 成员 | 来源 | 类型 | 说明 |
|---|
RadioButton() | 本类 | 构造函数 | 默认尺寸 130×20 |
Checked | 本类 | 属性(bool) | 选中状态;true 时自动互斥同容器内其它 RadioButton |
CirclePadding | 本类 | 属性(int) | 圆环与控件边缘的内边距(默认 0) |
RingThickness | 本类 | 属性(float) | 圆环线宽(默认 3f,≥0.5f) |
BorderColor | 本类 | 属性(Color) | 常态圆环颜色 |
HoverColor | 本类 | 属性(Color) | 悬停圆环颜色 |
PressedColor | 本类 | 属性(Color) | 按下圆环颜色 |
CheckedColor | 本类 | 属性(Color) | 选中内部圆点颜色 |
Text(重写) | Control | 属性(string) | 圆环右侧文字 |
ForeColor(重写) | Control | 属性(Color) | 文字颜色 |
Font(重写) | Control | 属性(Font) | 文字字体 |
Size / ClientSize | Control | 属性(Size) | 控件尺寸;圆直径 = 高度 - CirclePadding × 2 |
Cursor | Control | 属性 | 默认 Hand(手型) |
CheckedChanged | 本类 | 事件(默认事件) | 状态改变后触发 |
CheckedChanging | 本类 | 事件 | 状态改变前触发(Checked 仍是旧值) |
Invalidate() | Control | 方法 | 触发重绘 |
OnPaint(e) | 本类(override) | 方法(protected) | 绘制逻辑,外部不可直接调用 |
OnMouseEnter / OnMouseLeave / OnMouseDown / OnMouseUp | 本类(override) | 方法(protected) | 鼠标交互,外部不可直接调用 |
八、绘制原理(深入理解)
8.1 计算圆环尺寸
float diameter = this.Height - (this._circlePadding * 2f);
if (diameter <= 4f) diameter = 4f;
float circleX = this._circlePadding;
float circleY = this._circlePadding;
RectangleF outerCircle = new RectangleF(circleX, circleY, diameter, diameter);
| 要点 | 说明 |
|---|
diameter = Height - CirclePadding × 2 | 上下各留 CirclePadding 像素,因此减去 × 2。
CirclePadding=0 时直径 = 控件高度,圆环紧贴上下边缘 |
≤ 4f 兜底 | 控件过小或 padding 过大时,强制直径 4,避免 0 或负值导致绘制异常 |
circleX = circleY = CirclePadding | 圆心从 (CirclePadding, CirclePadding) 开始,左上角贴合 |
8.2 决定圆环颜色
Color ringColor;
if (this._isPressed)
ringColor = this._pressedColor;
else if (this._isHover)
ringColor = this._hoverColor;
else
ringColor = this._borderColor;
优先级:按下 > 悬停 > 常态。
8.3 绘制外圆环
float halfThickness = this._ringThickness / 2f;
RectangleF ringRect = RectangleF.Inflate(outerCircle, -halfThickness, -halfThickness);
if (ringRect.Width > 0 && ringRect.Height > 0)
{
using (var pen = new Pen(ringColor, this._ringThickness))
g.DrawEllipse(pen, ringRect);
}| 要点 | 说明 |
|---|
ringRect 内缩 halfThickness | Pen 居中描边时,外边缘会超出路径 RingThickness / 2。内缩半个线宽后,外边缘正好贴到 outerCircle,不会被控件裁剪 |
ringRect.Width ≤ 0 保护 | 当 RingThickness ≥ 直径时,圆环退化成点,不绘制 |
8.4 绘制选中圆点
if (this._checked)
{
float innerDiameter = diameter * 0.45f;
float innerX = circleX + (diameter - innerDiameter) / 2f;
float innerY = circleY + (diameter - innerDiameter) / 2f;
RectangleF innerCircle = new RectangleF(innerX, innerY, innerDiameter, innerDiameter);
using (var brush = new SolidBrush(this._checkedColor))
g.FillEllipse(brush, innerCircle);
}| 要点 | 说明 |
|---|
innerDiameter = diameter × 0.45 | 内点直径约为外圆的 45%,视觉上内外留空,看起来像是“圆环 + 实心圆点” |
innerX = circleX + (diameter - innerDiameter) / 2 | 水平居中:(diameter - innerDiameter) / 2 是左右各留的空隙 |
innerY 同理 | 垂直居中 |
8.5 绘制文字
float textX = circleX + diameter + 6f;
RectangleF textRect = new RectangleF(textX, 0, this.Width - textX, this.Height);
using (var brush = new SolidBrush(this.ForeColor))
using (var sf = new StringFormat
{
Alignment = StringAlignment.Near,
LineAlignment = StringAlignment.Center,
Trimming = StringTrimming.EllipsisCharacter
})
{
g.DrawString(this.Text, this.Font, brush, textRect, sf);
}| 要点 | 说明 |
|---|
textX = circleX + diameter + 6 | 文字起点 = 圆右边缘 + 6px 间距 |
LineAlignment = Center | 文字垂直居中于控件高度 |
Trimming = EllipsisCharacter | 文字过长时自动省略号,避免溢出 |
| 宽度计算 | this.Width - textX,即圆右侧到控件右边缘的剩余宽度 |
九、交互一览
| 操作 | 效果 |
|---|
| 鼠标进入控件 | _isHover = true,圆环变 HoverColor |
| 鼠标离开控件 | _isHover = false、_isPressed = false,圆环恢复正常或按状态 |
| 左键按下 | _isPressed = true,圆环变 PressedColor |
| 左键抬起(在控件内) | Checked = true(走 setter:触发事件 + 互斥 + 重绘) |
| 左键抬起(移出控件外) | 不选中,仅清除 _isPressed |
| 右键 / 中键 | 不响应 |
| 键盘 | 本类未重写键盘处理:Space / 方向键不会切换;如需键盘支持请自行扩展 |
十、完整使用示例
示例 1:三个选项互斥
using System.Drawing;
using System.Windows.Forms;
using CConUI;
var rb1 = new RadioButton { Parent = panel1, Text = "选项 A", Location = new Point(20, 20) };
var rb2 = new RadioButton { Parent = panel1, Text = "选项 B", Location = new Point(20, 50) };
var rb3 = new RadioButton { Parent = panel1, Text = "选项 C", Location = new Point(20, 80) };
rb1.Checked = true; // 默认 A
rb2.CheckedChanged += (s, e) =>
{
if (rb2.Checked) Console.WriteLine("选中 B");
};
rb3.CheckedChanged += (s, e) =>
{
if (rb3.Checked) Console.WriteLine("选中 C");
};示例 2:自定义配色(深色主题)
var rb = new RadioButton
{
Parent = panel1,
Text = "深色主题选项",
Location = new Point(20, 20),
ForeColor = Color.White,
BorderColor = Color.FromArgb(90, 90, 95),
HoverColor = Color.FromArgb(140, 140, 150),
PressedColor = Color.FromArgb(0, 200, 255),
CheckedColor = Color.FromArgb(0, 200, 255),
RingThickness = 2.5f,
CirclePadding = 2
};示例 3:缩小圆环(增加内边距)
var rb = new RadioButton
{
Parent = this,
Size = new Size(150, 24),
Text = "缩小的圆环",
CirclePadding = 4, // 圆直径 = 24 - 8 = 16
RingThickness = 2f
};示例 4:加粗圆环
var rb = new RadioButton
{
Parent = this,
Size = new Size(150, 30),
Text = "粗圆环",
CirclePadding = 2,
RingThickness = 4.5f,
BorderColor = Color.Gray,
CheckedColor = Color.Lime
};示例 5:用事件处理做“选项确认”
var rb1 = new RadioButton { Parent = this, Text = "男", Location = new Point(20, 20) };
var rb2 = new RadioButton { Parent = this, Text = "女", Location = new Point(20, 50) };
rb1.CheckedChanged += OnGenderChanged;
rb2.CheckedChanged += OnGenderChanged;
void OnGenderChanged(object sender, EventArgs e)
{
var r = (RadioButton)sender;
if (!r.Checked) return;
Console.WriteLine("当前选择:" + r.Text);
}