CircularButtonB 圆形图片自绘按钮控件调用文档
命名空间:CConUI
继承:System.Windows.Forms.Control
可见性:public class CircularButtonB
默认尺寸:60 × 60(正方形)
默认事件:Click(继承自 Control;[DefaultEvent("Click")],设计器双击控件即生成该事件处理器)
定位:圆形图片自绘按钮——用图片作为按钮外观,自动将图片居中裁剪为正圆;支持常态 / 悬停 / 按下三态图片切换、可自定义边框、抗锯齿开关、键盘 Space 触发 Click;背景透明,图片边缘可平滑抗锯齿。
一、作用
完全自绘的圆形图片按钮,特点:
自适应正圆:以
min(宽, 高)作为直径居中绘制正圆,非正方形控件也不会变形;图片自动裁剪为圆:先把原图按中心正方形(Center-Crop)裁剪,再通过
GraphicsPath.AddEllipse+TextureBrush填充成圆形——图片铺满整圆不拉伸变形,也不会溢出到圆外;三态图片切换:常态
ImageBack/ 悬停ImageEnter/ 按下ImageDown,带回退机制(未设置的自动降级);抗锯齿开关:
SmoothImage = true(默认)使用临时位图 +TextureBrush高质抗锯齿;false使用SetClip硬裁剪(更快但有轻微锯齿);可自定义边框:
BorderSize/BorderColor,圆边框绘制在图片边缘,可进一步掩盖边缘瑕疵;透明背景:启用
SupportsTransparentBackColor,圆外四角区域透明;鼠标点击:左键按下 → 松开(松开时仍在控件内)触发
Click;键盘支持:控件获得焦点后,Space 按下 / 松开触发
Click;文字居中:
Text在圆中央绘制,使用TextRenderer完美居中,超长自动省略号。
状态与图片回退规则
| 状态 | 使用的图片 | 文字颜色 |
|---|---|---|
禁用(Enabled=false) | ImageBack(忽略悬停 / 按下) | Color.Gray |
按下 + 设置了 ImageDown | ImageDown | ForeColor |
悬停 + 设置了 ImageEnter | ImageEnter | ForeColor |
其它情况(按下未设 ImageDown、悬停未设 ImageEnter) | ImageBack | ForeColor |
回退机制说明:
ImageDown或ImageEnter为null时,会自动降级使用ImageBack。只设置ImageBack也能正常工作。
二、快速上手
using System.Drawing;
using System.Windows.Forms;
using CConUI;
// 1. 创建控件
var btn = new CircularButtonB();
// 2. 放入容器
btn.Parent = panel1;
btn.Location = new Point(20, 20);
// 默认 60×60;可放大,建议保持正方形
btn.Size = new Size(72, 72);
btn.Text = "OK";
// 3. 设置三态图片(只需 ImageBack 也能用)
btn.ImageBack = Image.FromFile("avatar_normal.png");
btn.ImageEnter = Image.FromFile("avatar_hover.png");
btn.ImageDown = Image.FromFile("avatar_pressed.png");
// 4. 订阅点击
btn.Click += (s, e) =>
{
MessageBox.Show("点击了圆形头像按钮!");
};
// 5. 可选外观
btn.BorderSize = 2;
btn.BorderColor = Color.FromArgb(139, 148, 158);
btn.SmoothImage = true; // 抗锯齿(默认)三、构造函数
public CircularButtonB()
| 项目 | 说明 |
|---|---|
| 参数 | 无 |
| 作用 | 初始化圆形图片按钮 |
| 实际行为 | ① 启用 UserPaint | AllPaintingInWmPaint | OptimizedDoubleBuffer | ResizeRedraw | Selectable | SupportsTransparentBackColor 样式(自绘 + 双缓冲 + 尺寸变化自动重绘 + 可获取焦点 + 透明背景);② 设置 Size = (60, 60)(正方形,默认正圆);③ 设置 Font = new Font("微软雅黑", 10);④ 设置 Cursor = Cursors.Hand;⑤ 设置 ForeColor = Color.FromArgb(250, 250, 250);⑥ 设置 BackColor = Color.Transparent |
| 默认尺寸 | 60 × 60(正方形) |
| 默认外观 | SmoothImage = true、BorderSize = 0、BorderColor = (139, 148, 158) |
| 默认图片 | 三张图片均为 null(不绘制任何图片,只显示文字) |
var btn = new CircularButtonB
{
Parent = panel1,
Text = "OK"
};四、公开属性
4.1 图片属性
public Image ImageBack
| 项目 | 说明 |
|---|---|
| 类型 | System.Drawing.Image |
| 默认 | null |
| Category | CCon |
| 作用 | 按钮常态下的图片;同时也是禁用状态、以及未设置悬停 / 按下图片时的回退图片 |
| 副作用 | 赋值后 Invalidate() 重绘 |
| 备注 | 控件不负责 Dispose 传入的 Image;图片资源由调用方管理 |
public Image ImageEnter
| 项目 | 说明 |
|---|---|
| 类型 | Image |
| 默认 | null |
| Category | CCon |
| 作用 | 按钮悬停下的图片;null 时回退到 ImageBack |
| 副作用 | 赋值后 Invalidate() 重绘 |
public Image ImageDown
| 项目 | 说明 |
|---|---|
| 类型 | Image |
| 默认 | null |
| Category | CCon |
| 作用 | 按钮按下下的图片;null 时回退到 ImageEnter(若也未设置则到 ImageBack) |
| 副作用 | 赋值后 Invalidate() 重绘 |
btn.ImageBack = Image.FromFile("normal.png");
btn.ImageEnter = Image.FromFile("hover.png");
btn.ImageDown = Image.FromFile("pressed.png");
// 只设置 ImageBack 也可以工作
btn.ImageEnter = null;
btn.ImageDown = null;4.2 抗锯齿与边框
public bool SmoothImage
| 项目 | 说明 |
|---|---|
| 类型 | bool |
| 默认 | true |
| Category | CCon |
| DefaultValue | true |
| 作用 | 是否对圆形图片边缘开启抗锯齿:true(默认):创建临时 Bitmap,用 TextureBrush + GraphicsPath 填充,边缘极其平滑;false:使用 Graphics.SetClip 硬裁剪,速度更快但圆边可能有轻微锯齿 |
| 副作用 | 赋值后 Invalidate() 重绘 |
| 性能影响 | true 每帧会创建临时 Bitmap + Graphics(图片大时有一定开销);false 更快 |
btn.SmoothImage = true; // 高质抗锯齿(默认) btn.SmoothImage = false; // 快速硬裁剪
public int BorderSize
| 项目 | 说明 |
|---|---|
| 类型 | int(像素) |
| 默认 | 0 |
| Category | CCon |
| DefaultValue | 0 |
| 作用 | 圆形边框粗细:0 → 无边框;> 0 → 用 BorderColor 绘制圆形边框,图片会被内缩半个边框宽度,让边框外边缘贴到圆的外接正方形 |
| 取值范围 | Math.Max(0, value)——不能为负 |
| 副作用 | 赋值后 Invalidate() 重绘 |
btn.BorderSize = 0; // 无边框(默认) btn.BorderSize = 2; // 2px 圆形边框
public Color BorderColor
| 项目 | 说明 |
|---|---|
| 类型 | Color |
| 默认 | Color.FromArgb(139, 148, 158)(浅灰) |
| Category | CCon |
| 作用 | 圆形边框颜色;仅在 BorderSize > 0 时可见 |
| 副作用 | 赋值后 Invalidate() 重绘 |
btn.BorderColor = Color.FromArgb(0, 120, 215);
4.3 文本属性(重写)
public override string Text
| 项目 | 说明 |
|---|---|
| 类型 | string |
| Category | CCon |
| 作用 | 圆中央显示的文字;空串时不绘制 |
| 副作用 | 赋值后 Invalidate() 重绘 |
| 绘制 | 使用 TextRenderer.DrawText 在圆的外接正方形内水平 + 垂直居中;NoPadding 保证绝对居中;超长自动省略号(EndEllipsis) |
public override Color ForeColor
| 项目 | 说明 |
|---|---|
| 类型 | Color |
| 默认 | Color.FromArgb(250, 250, 250)(构造时设置) |
| Category | CCon |
| 作用 | 文字颜色;禁用时自动变为 Color.Gray |
public override Font Font
| 项目 | 说明 |
|---|---|
| 类型 | Font |
| 默认 | "微软雅黑", 10(构造时设置) |
| Category | CCon |
| 作用 | 文字字体 |
public override Color BackColor(隐藏)
| 项目 | 说明 |
|---|---|
| 类型 | Color |
| Browsable | false(属性面板中隐藏) |
| 作用 | 对按钮外观无影响——按钮外观由图片控制。 构造时已设置为 Color.Transparent,让圆外四角区域透明(透出父容器) |
五、公开事件
public event EventHandler Click(继承自 Control)
| 项目 | 说明 |
|---|---|
| 参数 | EventHandler:sender 为当前 CircularButtonB,e 为 EventArgs.Empty |
| 默认事件 | 是([DefaultEvent("Click")])——设计器双击即生成 |
| 触发时机 | ① 鼠标左键:按下时在控件内 → 松开时仍在控件内; ② 键盘 Space: OnKeyUp 里按 Space 松开时触发 |
| 不触发情况 | ① 鼠标左键按下后移出控件再松开; ② 右键 / 中键; ③ 控件禁用( Enabled = false)——Control 基类不派发鼠标 / 键盘事件 |
| 备注 | 内部通过 base.OnClick(EventArgs.Empty) 触发;本类未重写 OnClick,Click 由基类直接派发 |
btn.Click += (s, e) =>
{
MessageBox.Show("点击了!");
};六、公开方法
CircularButtonB没有新增 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 与 _isPressed 并重绘 |
OnMouseDown(MouseEventArgs e) | protected override | 左键按下时 _isPressed = true,并调用 Focus() |
OnMouseUp(MouseEventArgs e) | protected override | 左键松开时清 _isPressed;若松开点在客户区内则调用 OnClick(EventArgs.Empty) |
OnKeyDown(KeyEventArgs e) | protected override | Space 按下 → _isPressed = true(显示按下图) |
OnKeyUp(KeyEventArgs e) | protected override | Space 松开 → _isPressed = false + 触发 OnClick(EventArgs.Empty) |
七、公开成员速查表
| 成员 | 来源 | 类型 | 说明 |
|---|---|---|---|
CircularButtonB() | 本类 | 构造函数 | 默认尺寸 60×60,默认字体微软雅黑 10pt,背景透明 |
ImageBack | 本类 | 属性(Image) | 常态图片 / 回退图片(默认 null) |
ImageEnter | 本类 | 属性(Image) | 悬停图片(默认 null,回退到 ImageBack) |
ImageDown | 本类 | 属性(Image) | 按下图片(默认 null,回退到 ImageEnter / ImageBack) |
SmoothImage | 本类 | 属性(bool) | 是否抗锯齿(默认 true) |
BorderSize | 本类 | 属性(int) | 圆形边框粗细(默认 0,≥0) |
BorderColor | 本类 | 属性(Color) | 边框颜色 |
Text(重写) | Control | 属性(string) | 圆中央文字 |
ForeColor(重写) | Control | 属性(Color) | 文字颜色(禁用时变 Gray) |
Font(重写) | Control | 属性(Font) | 文字字体 |
BackColor(隐藏) | Control | 属性(Color) | Browsable(false);构造时设为 Transparent,圆外四角透明 |
Enabled | Control | 属性(bool) | 是否启用;禁用时图片固定用 ImageBack,文字变 Gray |
Cursor | Control | 属性 | 默认 Hand;可自行改为 Default |
TabStop | Control | 属性(bool) | 默认 false;要支持 Tab 聚焦 + Space 触发需设为 true |
Size / ClientSize | Control | 属性(Size) | 控件尺寸;建议保持正方形,非正方形时圆取短边尺寸 |
Click | Control | 事件(默认事件) | 鼠标左键或 Space 松开时触发 |
Invalidate() | Control | 方法 | 触发重绘 |
Focus() | Control | 方法 | 让按钮获取焦点(鼠标按下时已自动调用) |
OnPaint(e) | 本类(override) | 方法(protected) | 绘制逻辑,外部不可直接调用 |
OnMouseEnter / OnMouseLeave / OnMouseDown / OnMouseUp | 本类(override) | 方法(protected) | 鼠标交互,外部不可直接调用 |
OnKeyDown / OnKeyUp | 本类(override) | 方法(protected) | 键盘交互,外部不可直接调用 |
八、绘制原理(深入理解)
8.1 选择当前图片(多级回退)
Image currentImage = null; if (!this.Enabled) currentImage = this._imageBack; else if (this._isPressed && this._imageDown != null) currentImage = this._imageDown; else if (this._isHover && this._imageEnter != null) currentImage = this._imageEnter; else currentImage = this._imageBack;
| 优先级 | 条件 | 使用的图片 |
|---|---|---|
| 1(最高) | 禁用(!Enabled) | ImageBack(忽略悬停/按下) |
| 2 | 按下 + 设置了 ImageDown | ImageDown |
| 3 | 悬停 + 设置了 ImageEnter | ImageEnter |
| 4(兜底) | 其它情况 | ImageBack |
8.2 计算正圆区域
float side = Math.Min(ClientSize.Width, ClientSize.Height); if (side <= 4f) return; float ox = (ClientSize.Width - side) / 2f; float oy = (ClientSize.Height - side) / 2f; RectangleF outerRect = new RectangleF(ox, oy, side, side); float halfBorder = this._borderSize / 2f; RectangleF imageRect = RectangleF.Inflate(outerRect, -halfBorder, -halfBorder); if (imageRect.Width <= 0 || imageRect.Height <= 0) return;
| 要点 | 说明 |
|---|---|
side = min(宽, 高) | 取短边作为圆的直径,不论控件是什么形状都画正圆,多余空间留白 |
ox / oy | 水平 / 垂直居中偏移 |
side ≤ 4 | 控件过小时直接不绘制 |
imageRect 内缩 halfBorder | 边框 Pen 居中描边,图片需要内缩半个边框宽度,让边框外边缘正好贴到圆的外接正方形 outerRect |
imageRect 无效 | 边框过大导致内圆为空时直接返回 |
8.3 中心裁剪(Center-Crop)
float minSide = Math.Min(currentImage.Width, currentImage.Height); float sx = (currentImage.Width - minSide) / 2f; float sy = (currentImage.Height - minSide) / 2f; RectangleF srcRect = new RectangleF(sx, sy, minSide, minSide);
| 要点 | 说明 |
|---|---|
minSide = min(宽, 高) | 取原图短边作为正方形边长 |
sx / sy | 从原图中心裁出 minSide × minSide 的正方形;(W - minSide) / 2 = 左侧需裁掉的宽度,保证裁剪后正方形居中于原图 |
| 效果 | 不管原图是横图、竖图、还是方图,都以中心正方形作为后续圆形填充的源区域,不变形、不拉伸 |
8.4 抗锯齿路径(SmoothImage=true)
int bmpWidth = Math.Max(1, (int)Math.Ceiling(imageRect.Width));
int bmpHeight = Math.Max(1, (int)Math.Ceiling(imageRect.Height));
using (var tempBmp = new Bitmap(bmpWidth, bmpHeight))
using (var tempG = Graphics.FromImage(tempBmp))
{
tempG.InterpolationMode = InterpolationMode.HighQualityBicubic;
tempG.PixelOffsetMode = PixelOffsetMode.Half;
tempG.DrawImage(currentImage, new Rectangle(0, 0, bmpWidth, bmpHeight), srcRect, GraphicsUnit.Pixel);
using (var brush = new TextureBrush(tempBmp))
{
brush.TranslateTransform(imageRect.X, imageRect.Y);
g.FillPath(brush, clipPath);
}
}| 要点 | 说明 |
|---|---|
Math.Max(1, Ceiling(...)) | 临时位图宽高必须 ≥1,否则 new Bitmap(0, 0) 会抛异常;用 Ceiling 保证非整数尺寸也能完全覆盖目标区域 |
tempG.DrawImage(..., srcRect, ...) | 把中心裁剪的正方形区域拉伸到整张临时位图;临时位图 = 目标圆的直径,所以拉伸后的图铺满圆 |
brush.TranslateTransform | 核心修复:TextureBrush 默认从 (0,0) 开始平铺——而圆的起点在 imageRect.X, imageRect.Y,如果不用 TranslateTransform,纹理会错位偏移 |
g.FillPath(brush, clipPath) | clipPath 是 AddEllipse(imageRect) 生成的圆形路径;用 TextureBrush 填充,GDI+ 会自动抗锯齿边缘——比 SetClip 硬裁剪更平滑 |
| 性能 | 每帧创建临时 Bitmap + Graphics;图片较大或控件频繁重绘时有 CPU / 内存开销 |
8.5 硬裁剪路径(SmoothImage=false)
GraphicsState state = g.Save(); g.SetClip(clipPath, CombineMode.Replace); g.DrawImage(currentImage, imageRect, srcRect, GraphicsUnit.Pixel); g.Restore(state);
| 要点 | 说明 |
|---|---|
SetClip(clipPath, Replace) | 把绘制区域限制在圆形路径内,超出的部分被丢弃 |
| 边缘质量 | 锯齿明显:GDI+ 的 SetClip 使用硬裁剪,SmoothingMode.AntiAlias 对裁剪边缘无效 |
| 性能 | 比抗锯齿方案快(无需创建临时 Bitmap) |
8.6 绘制边框
using (var pen = new Pen(this._borderColor, this._borderSize))
{
g.DrawEllipse(pen, imageRect);
}| 要点 | 说明 |
|---|---|
边框绘制在 imageRect 上 | 与图片裁剪区域相同,边框内侧贴合图片边缘,可以掩盖图片边缘的瑕疵 |
| Pen 居中描边 | imageRect 已内缩 halfBorder,所以边框外边缘正好贴到 outerRect |
| 绘制在图片之后 | 边框覆盖图片边缘,视觉更干净 |
8.7 绘制文字
TextRenderer.DrawText( g, this.Text, this.Font, Rectangle.Round(outerRect), textColor, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPadding );
| 要点 | 说明 |
|---|---|
TextRenderer 而非 DrawString | 基于 GDI 绘制,字体渲染更清晰;NoPadding 保证垂直完美居中(避免 DrawString 偏上问题) |
绘制区域为 outerRect | 即圆的外接正方形;文字在正方形内居中 → 视觉上在圆中央 |
| 禁用时文字变灰 | 用 Color.Gray 替代 ForeColor |
九、交互一览
| 操作 | 效果 |
|---|---|
| 鼠标进入控件 | _isHover = true,显示 ImageEnter(若设置),否则 ImageBack |
| 鼠标离开控件 | _isHover = false、_isPressed = false,恢复 ImageBack |
| 左键按下 | _isPressed = true,Focus() 获取焦点,显示 ImageDown(若设置),否则回退 |
| 左键松开(在控件内) | _isPressed = false,触发 Click,显示悬停图或常态图 |
| 左键松开(移出控件) | _isPressed = false,不触发 Click |
| Space 按下(有焦点) | _isPressed = true,显示 ImageDown |
| Space 松开(有焦点) | _isPressed = false,触发 Click,显示悬停 / 常态图 |
| Enter / 其它键 | 不响应(本类只处理 Space) |
| 右键 / 中键 | 不响应,不触发 Click,也不改变图片 |
禁用(Enabled=false) | 固定显示 ImageBack;文字变 Color.Gray;鼠标键盘事件不派发 |
十、完整使用示例
示例 1:基础圆形图片按钮
using System.Drawing;
using System.Windows.Forms;
using CConUI;
var btn = new CircularButtonB
{
Parent = this,
Location = new Point(20, 20),
Size = new Size(60, 60),
Text = "",
ImageBack = Image.FromFile("avatar.png")
};
btn.Click += (s, e) => MessageBox.Show("点击了头像!");示例 2:三态图片 + 边框(推荐)
var btn = new CircularButtonB
{
Parent = this,
Location = new Point(20, 20),
Size = new Size(72, 72),
Text = "",
ImageBack = Image.FromFile("avatar_normal.png"),
ImageEnter = Image.FromFile("avatar_hover.png"),
ImageDown = Image.FromFile("avatar_pressed.png"),
BorderSize = 2,
BorderColor = Color.FromArgb(139, 148, 158),
SmoothImage = true
};
btn.Click += (s, e) =>
{
// 打开用户资料
ShowUserProfile();
};示例 3:只设置常态图(无悬停/按下切换)
var btn = new CircularButtonB
{
Parent = this,
Text = "",
Size = new Size(48, 48),
ImageBack = Image.FromFile("icon.png"),
BorderSize = 1,
BorderColor = Color.LightGray
};示例 4:禁用抗锯齿(性能优化)
// 图片较大或按钮数量很多时,可关闭 SmoothImage 提升性能
var btn = new CircularButtonB
{
Parent = this,
Size = new Size(80, 80),
ImageBack = Image.FromFile("bigavatar.jpg"),
SmoothImage = false, // 硬裁剪(圆边轻微锯齿)
BorderSize = 2 // 边框可以部分掩盖锯齿
};示例 5:禁用状态
var btn = new CircularButtonB
{
Parent = this,
Size = new Size(60, 60),
ImageBack = Image.FromFile("avatar.png"),
BorderSize = 2,
Enabled = false // 禁用:图片固定 ImageBack,文字变 Gray
};
// 业务满足条件后启用
btn.Enabled = true;示例 6:键盘操作(Tab 聚焦 + Space 点击)
var btn = new CircularButtonB
{
Parent = this,
Size = new Size(60, 60),
ImageBack = Image.FromFile("avatar.png"),
TabStop = true // 允许 Tab 键聚焦
};
btn.Click += (s, e) => MessageBox.Show("Space 触发");示例 7:带文字叠加的图片按钮
var btn = new CircularButtonB
{
Parent = this,
Size = new Size(80, 80),
Text = "点击",
ForeColor = Color.White,
Font = new Font("Microsoft YaHei", 9F, FontStyle.Bold),
ImageBack = Image.FromFile("darkbg.png"),
BorderSize = 2,
BorderColor = Color.FromArgb(0, 120, 215)
};示例 8:动态切换头像
void UpdateAvatar(Image newAvatar)
{
var old = btn.ImageBack;
btn.ImageBack = newAvatar;
// 释放旧图片(若不与其它地方共享)
if (old != null && old != newAvatar)
old.Dispose();
}转载请注明出处。