ButtonB 图片自绘按钮控件调用文档
命名空间:CConUI
继承:System.Windows.Forms.Control
可见性:public class ButtonB
默认尺寸:110 × 32
默认事件:Click(继承自 Control;[DefaultEvent("Click")],设计器双击控件即生成该事件处理器)
定位:图片自绘按钮——用图片作为按钮外观,支持常态 / 悬停 / 按下三种图片切换,图片等比缩放居中绘制;支持鼠标左键点击、键盘 Space 触发 Click;禁用时文字自动变灰。
一、作用
一个完全自绘的图片按钮控件,具备如下特性:
三态图片切换:常态
ImageBack/ 悬停ImageEnter/ 按下ImageDown,带回退机制(未设置的图片自动回落到上一级);图片等比缩放居中:图片按原始宽高比等比缩放,不拉伸变形,居中绘制在控件内;
高质量缩放:使用
InterpolationMode.HighQualityBicubic平滑缩放;文字叠加:
Text居中绘制在图片上层,超长自动省略号;鼠标点击:左键按下 → 松开(松开时仍在控件内)触发
Click;键盘支持:控件获得焦点后,Space 按下 / 松开触发
Click;手型光标:默认
Cursors.Hand;禁用视觉:
Enabled=false时文字变Color.Gray,图片仍按ImageBack绘制(不自动置灰)。
状态与图片回退规则
| 状态 | 使用的图片 | 文字颜色 |
|---|---|---|
禁用(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 ButtonB();
// 2. 放入容器
btn.Parent = panel1;
btn.Location = new Point(20, 20);
btn.Size = new Size(120, 40);
btn.Text = "确定";
// 3. 设置图片(只需 ImageBack 也能工作)
btn.ImageBack = Image.FromFile("btn_normal.png");
btn.ImageEnter = Image.FromFile("btn_hover.png");
btn.ImageDown = Image.FromFile("btn_pressed.png");
// 4. 订阅点击
btn.Click += (s, e) =>
{
MessageBox.Show("点击了按钮!");
};三、构造函数
public ButtonB()
| 项目 | 说明 |
|---|---|
| 参数 | 无 |
| 作用 | 初始化图片自绘按钮 |
| 实际行为 | ① 启用 UserPaint | AllPaintingInWmPaint | OptimizedDoubleBuffer | ResizeRedraw | Selectable 样式(自绘 + 双缓冲 + 尺寸变化自动重绘 + 可获取焦点);② 设置 Size = (110, 32);③ 设置 Font = new Font("微软雅黑", 10);④ 设置 Cursor = Cursors.Hand;⑤ 设置 ForeColor = Color.FromArgb(250, 250, 250) |
| 默认尺寸 | 110 × 32 |
| 默认图片 | 三张图片均为 null(不绘制任何图片,仅显示文字) |
var btn = new ButtonB
{
Parent = panel1,
Text = "确定"
};四、公开属性
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 也可以工作(悬停/按下会自动用 ImageBack)
btn.ImageEnter = null;
btn.ImageDown = null;4.2 文本属性(重写)
public override string Text
| 项目 | 说明 |
|---|---|
| 类型 | string |
| Category | CCon |
| 作用 | 图片上方居中显示的文字;空串时不绘制 |
| 副作用 | 赋值后 Invalidate() 重绘 |
| 绘制 | 居中(Center / Center),超长自动省略号(EllipsisCharacter) |
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(属性面板中隐藏) |
| 作用 | 对按钮外观无影响——按钮外观由图片控制。 此属性仅为兼容 Control 基类而保留,读写不会改变绘制效果 |
五、公开事件
public event EventHandler Click(继承自 Control)
| 项目 | 说明 |
|---|---|
| 参数 | EventHandler:sender 为当前 ButtonB,e 为 EventArgs.Empty |
| 默认事件 | 是([DefaultEvent("Click")])——设计器双击即生成 |
| 触发时机 | ① 鼠标左键:按下时在控件内 → 松开时仍在控件内(ClientRectangle.Contains(e.Location));② 键盘 Space: OnKeyUp 里按 Space 松开时触发 |
| 不触发情况 | ① 鼠标左键按下后移出控件再松开; ② 右键 / 中键; ③ 控件禁用( Enabled = false)——Control 基类不派发鼠标 / 键盘事件 |
| 备注 | 内部通过 base.OnClick(EventArgs.Empty) 触发;本类未重写 OnClick,Click 由基类直接派发 |
btn.Click += (s, e) =>
{
MessageBox.Show("点击了!");
};六、公开方法
ButtonB没有新增 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) |
DrawImageStretchedAndCentered(Graphics, Image, Rectangle) | private | 将图片按等比缩放并居中绘制到目标区域的辅助方法 |
七、公开成员速查表
| 成员 | 来源 | 类型 | 说明 |
|---|---|---|---|
ButtonB() | 本类 | 构造函数 | 默认尺寸 110×32,默认字体微软雅黑 10pt |
ImageBack | 本类 | 属性(Image) | 常态图片 / 回退图片(默认 null) |
ImageEnter | 本类 | 属性(Image) | 悬停图片(默认 null,回退到 ImageBack) |
ImageDown | 本类 | 属性(Image) | 按下图片(默认 null,回退到 ImageEnter / ImageBack) |
Text(重写) | Control | 属性(string) | 文字(居中绘制在图片上方) |
ForeColor(重写) | Control | 属性(Color) | 文字颜色(禁用时变 Gray) |
Font(重写) | Control | 属性(Font) | 文字字体 |
BackColor(隐藏) | Control | 属性(Color) | Browsable(false);对按钮外观无影响 |
Enabled | Control | 属性(bool) | 是否启用;禁用时文字变灰,图片固定用 ImageBack |
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) | 键盘交互,外部不可直接调用 |
DrawImageStretchedAndCentered | 本类 | 方法(private) | 等比缩放居中绘制辅助方法,外部不可直接调用 |
八、绘制原理(深入理解)
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 |
按下但未设
ImageDown:会落到“悬停”分支,如果设置了ImageEnter就显示悬停图;否则显示ImageBack。这是合理的“按下回退”。
8.2 等比缩放居中绘制
float imgRatio = (float)img.Width / img.Height;
float rectRatio = (float)destRect.Width / destRect.Height;
int drawWidth, drawHeight;
if (imgRatio > rectRatio)
{
drawWidth = destRect.Width;
drawHeight = (int)(drawWidth / imgRatio);
}
else
{
drawHeight = destRect.Height;
drawWidth = (int)(drawHeight * imgRatio);
}
int drawX = destRect.X + (destRect.Width - drawWidth) / 2;
int drawY = destRect.Y + (destRect.Height - drawHeight) / 2;| 要点 | 说明 |
|---|---|
imgRatio / rectRatio | 图片原始宽高比 / 目标区域宽高比 |
imgRatio > rectRatio | 图片相对更“扁”,以宽度为基准缩放;高度按比例算 |
imgRatio <= rectRatio | 图片相对更“高”,以高度为基准缩放;宽度按比例算 |
| 效果 | 始终在区域内“完整显示”图片(类似 PictureBoxSizeMode.Zoom),不拉伸不变形 |
drawX / drawY | 居中偏移:(destRect.Width - drawWidth) / 2 是左右多余半宽 |
| 缩放质量 | InterpolationMode.HighQualityBicubic,比默认的低质量缩放平滑很多 |
8.3 绘制文字
if (!string.IsNullOrEmpty(this.Text))
{
Color textColor = this.Enabled ? this.ForeColor : Color.Gray;
using (var brush = new SolidBrush(textColor))
using (var sf = new StringFormat
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center,
Trimming = StringTrimming.EllipsisCharacter
})
{
g.DrawString(this.Text, this.Font, brush, this.ClientRectangle, sf);
}
}| 要点 | 说明 |
|---|---|
| 禁用时文字变灰 | 用 Color.Gray 替代 ForeColor |
| 居中 + 省略号 | Center / Center 居中,超长自动 EllipsisCharacter |
绘制在整个 ClientRectangle 上 | 不需要考虑图片的内缩;文字浮在图片上方居中 |
九、交互一览
| 操作 | 效果 |
|---|---|
| 鼠标进入控件 | _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 ButtonB
{
Parent = this,
Location = new Point(20, 20),
Size = new Size(120, 40),
Text = "确定",
ImageBack = Image.FromFile("btn.png")
};
btn.Click += (s, e) => MessageBox.Show("点击了确定!");示例 2:三态图片按钮(推荐)
var btn = new ButtonB
{
Parent = this,
Location = new Point(20, 20),
Size = new Size(140, 44),
Text = "提交",
ImageBack = Image.FromFile("btn_normal.png"),
ImageEnter = Image.FromFile("btn_hover.png"),
ImageDown = Image.FromFile("btn_pressed.png")
};
btn.Click += (s, e) =>
{
MessageBox.Show("提交成功!");
};示例 3:只设置悬停图(按下时回退到悬停图)
var btn = new ButtonB
{
Parent = this,
Text = "只设置悬停图",
ImageBack = Image.FromFile("btn_normal.png"),
ImageEnter = Image.FromFile("btn_hover.png")
// ImageDown 不设置 → 按下时也会显示 ImageEnter
};示例 4:图标按钮(小尺寸)
var btn = new ButtonB
{
Parent = this,
Size = new Size(32, 32), // 正方形,适合图标
Text = "", // 不需要文字
ImageBack = Image.FromFile("icon_normal.png"),
ImageEnter = Image.FromFile("icon_hover.png"),
ImageDown = Image.FromFile("icon_pressed.png")
};示例 5:禁用按钮
var btn = new ButtonB
{
Parent = this,
Text = "禁用按钮",
ImageBack = Image.FromFile("btn.png"),
Enabled = false // 禁用:固定显示 ImageBack,文字变 Gray
};
// 业务需要时启用
btn.Enabled = true;示例 6:键盘操作(Tab 聚焦 + Space 点击)
var btn = new ButtonB
{
Parent = this,
Text = "Tab 聚焦 + Space 点击",
ImageBack = Image.FromFile("btn.png"),
TabStop = true // 允许 Tab 键聚焦
};
btn.Click += (s, e) => MessageBox.Show("Space 触发");示例 7:动态切换主题图片
void ApplyDarkTheme(ButtonB btn)
{
btn.ImageBack = Image.FromFile("btn_dark_normal.png");
btn.ImageEnter = Image.FromFile("btn_dark_hover.png");
btn.ImageDown = Image.FromFile("btn_dark_pressed.png");
btn.ForeColor = Color.White;
}
void ApplyLightTheme(ButtonB btn)
{
btn.ImageBack = Image.FromFile("btn_light_normal.png");
btn.ImageEnter = Image.FromFile("btn_light_hover.png");
btn.ImageDown = Image.FromFile("btn_light_pressed.png");
btn.ForeColor = Color.Black;
}
ApplyDarkTheme(btnSave);
ApplyLightTheme(btnCancel);示例 8:异步提交按钮(加载中禁用)
async void BtnSubmit_Click(object sender, EventArgs e)
{
btnSubmit.Enabled = false;
btnSubmit.Text = "提交中…";
try
{
await DoSubmitAsync();
btnSubmit.Text = "已提交";
}
finally
{
btnSubmit.Enabled = true;
btnSubmit.Text = "提交";
}
}十一、注意事项
Click需要“按下与松开都在控件内”:用户按下左键后把鼠标拖到控件外再松开不会触发Click。这是本类的设计,避免误点击。右键 / 中键不触发
Click:OnMouseDown / OnMouseUp都只判断MouseButtons.Left。Space 松开时触发,Space 按下时只是视觉反馈:
OnKeyDown只改_isPressed,OnKeyUp才派发Click。不支持 Enter 键触发:本类只处理
Keys.Space。如需 Enter 触发,需要自行扩展OnKeyDown/OnKeyUp。Tab 聚焦需要
TabStop = true:Control.TabStop默认是false,本类未显式设置。要让键盘 Space 在无鼠标点击的情况下生效,请在创建时设置TabStop = true。鼠标按下会自动
Focus():点击按钮后按钮获取焦点,即可继续用 Space 模拟点击。图片资源需要调用方自行管理:控件不负责
Dispose传入的 Image。更换图片时,请自行释放旧图片;否则会内存泄漏。ImageDown/ImageEnter为null时自动回退:因此你可以只设置ImageBack,也能作为一个可点击的图片按钮使用。按下但未设
ImageDown:落到悬停分支(ImageEnter)或ImageBack;悬停但未设
ImageEnter:ImageBack。BackColor对按钮无效:已被Browsable(false)隐藏,代码设置也不会改变绘制。按钮外观完全由图片控制。图片按等比缩放居中,不拉伸填满:若图片宽高比与控件宽高比不一致,会出现留白(顶部/底部 或 左侧/右侧)。
如果需要“拉伸铺满”,需要修改源码DrawImageStretchedAndCentered用g.DrawImage(img, destRect)。禁用状态不会自动置灰图片:本类在禁用时仍按
ImageBack原色绘制图片,只把文字变Color.Gray。若需要“禁用图片变灰”,需要在设计阶段准备一张“禁用态”图片,并在外部手动切换ImageBack。文字始终叠加在图片之上:无论图片是否设置,
Text都会在控件中心绘制。若图片本身已包含文字,建议将Text设为空串。控件尺寸极小(如 < 8×8)时图片可能缩到不足 1px:绘制时用
int计算drawWidth / drawHeight,若原图比区域更“扁”,小尺寸可能让drawHeight被截断为 0。本类未做最小 1px 保护,建议Size不要设得太小。DrawImageStretchedAndCentered未使用ImageAttributes:如果图片是带 Alpha 通道的 PNG,透明部分会透出Control默认背景(通常是SystemColors.Control)。
如需真正的透明按钮背景,需要把BackColor设为父控件颜色,或开启SupportsTransparentBackColor(本类未开启)。与原生
Button的差异:无
FlatStyle/FlatAppearance/DialogResult/TextAlign/ImageAlign等属性;无
ImageList/ImageIndex/ImageKey(图片需要直接赋Image对象);文字始终居中,不支持图片 + 文字左右排布。
Cursor默认为Hand:如果不需要“可点击”提示,可改为Cursors.Default。禁用时Control仍会显示Hand,如需在禁用时显示默认光标,请自行在EnabledChanged事件中处理。图片加载方式:
Image.FromFile(...)加载的图片会锁住文件句柄,建议改用using (var fs = File.OpenRead(...)) { image = Image.FromStream(fs); }或new Bitmap(path),避免后续无法修改文件。默认字体 “微软雅黑” 通过代码指定:若目标系统没有该字体,WinForms 会自动回退到默认字体,不会抛异常;但对字体有严格要求时建议改换。
本类未重写
Dispose:其持有的Image对象不会在控件销毁时自动释放(因为图片资源可能被其它地方共享),需要调用方自行管理。不支持作为容器:本类是
Control的直接子类,不建议在其上Controls.Add(...)叠加子控件,绘制会被遮挡或布局混乱。
转载请注明出处。