书籍:《Visual C++ 2017从入门到精通》的4.2 按钮控件
环境:visual studio 2022
内容:[例 4.2]制作图片按钮
说明:以上内容大部分来自腾讯元宝。
新建对话框工程
拷贝图片到res目录
添加自定义资源目录
添加图片资源到
添加图片到PNG资源目录
修改资源ID的方法:
新建类
右键项目 → 添加 → 类
在对话框中增加两个按钮
Button2的Disable属性设置为TRUE,表示该按钮失能。
分别给两个按钮添加变量
分别命名为m_btn1和m_btn2
修改m_btn1和m_btn2的类型为CTranButton
//添加按钮变量后自动生成的代码:
CButton m_btn1;
CButton m_btn2;
//1.在CTest402Dlg中将m_btn1和m_btn2的类型从CButton改为CTranButton
CTranButton m_btn1;
CTranButton m_btn2;
//2.在对话框头文件CTest402Dlg.h中添加头文件。
#include "CTranButton.h"
编写CTranButton.h
定义枚举类型TRA_BTNSTATE和结构体_TRATRAPNGINFO_,声明基于CButton的类CTranButton
#pragma once
#include <afxwin.h>
//状态枚举设计
//定义按钮的四种核心状态,通过状态机机制实现不同视觉表现。
typedef enum TRA_BTNSTATE
{
TRA_BTN_NOR, // 正常状态
TRA_BTN_HOT, // 悬停状态
TRA_BTN_PRE, // 按下前状态
TRA_BTN_DIS // 禁用状态
};
//资源管理结构
//使用结构体封装图片资源信息,支持多分辨率适配。
typedef struct _TRATRAPNGINFO_
{
int nWidth;
int nHeight;
CImage* pImg; // 存储HBITMAP句柄
}TRAPNGINFO;
//继承自MFC的CButton类,通过重写虚函数实现自绘功能,符合MFC控件扩展规范。
class CTranButton :
public CButton
{
public:
CTranButton();
virtual ~CTranButton();
/*资源加载系统
支持多种资源类型(PNG / BMP等),需实现资源解析逻辑:
从资源ID加载图像
自动缩放适配指定尺寸
异常处理(资源不存在等情况)*/
void Load(UINT IDBkGroup, int width = 0, int height = 0, const CString& resourceType = _T("PNG"));
/*自动尺寸适配
根据图片尺寸自动调整按钮大小,需在PreSubclassWindow中处理WM_MEASUREITEM消息。*/
void SetAutoSize(bool bAutoSize);
protected:
/*主绘制函数
重写自绘核心函数,处理:
背景擦除(通过OnEraseBkgnd)
不同状态下的图像绘制
文本渲染(DrawBtnText)*/
virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
public:
/*消息处理
状态转换逻辑
OnMouseMove:检测鼠标进入 / 离开,切换m_bHot状态
OnLButtonDown:记录按下状态m_bPress
OnLButtonUp:触发点击事件并恢复状态*/
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
/*特殊消息处理
通过OnEraseBkgnd实现双缓冲防闪烁,需配合InvalidateRect使用。*/
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
DECLARE_MESSAGE_MAP()
private:
TRAPNGINFO m_btninfoNor;
TRAPNGINFO m_btninfoHot;
TRAPNGINFO m_btninfoPre;
TRAPNGINFO m_btninfoDis;
//通过布尔变量组合实现状态切换(如:m_bHot&& m_bPress表示按下状态)
BOOL m_bHot; // 鼠标悬停:在 OnMouseMove 中更新,释放鼠标时强制重置,避免残留悬停效果。
BOOL m_bPress; // 按下状态:仅在 OnLButtonDown 中设为 TRUE,在 OnLButtonUp 中重置,确保状态一致性。
BOOL m_bAutoSize; //自动尺寸适配标志
private:
/*分层绘制逻辑
分离背景和文本绘制,支持复杂效果(如渐变、阴影)*/
void DrawBK(HDC dc, CImage* img, TRA_BTNSTATE btnstate);
void DrawBtnText(HDC dc, const CString& strText, int nMove, TRA_BTNSTATE btnstate);
/*自绘模式激活
在控件初始化时设置BS_OWNERDRAW风格,激活自绘机制。*/
virtual void PreSubclassWindow();
};
重写CTranButton类中的OnMouseMove(), OnLButtonUp(), OnLButtonDown(), OnEraseBkgnd()
对应的消息分别为WM_MOUSEMOVE、WM_LBUTTONUP、WM_LBUTTONDOWN、WM_ERASEBKGND
编写CTranButton.cpp
#include "pch.h"
#include "CTranButton.h"
CTranButton::CTranButton()
{
//成员变量初始化
m_btninfoNor.pImg = NULL;
m_btninfoHot.pImg = NULL;
m_btninfoPre.pImg = NULL;
m_btninfoDis.pImg = NULL;
m_bHot = false;
m_bPress = false;
m_bAutoSize = true;
}
CTranButton::~CTranButton()
{
/*资源释放:释放按钮四种状态(正常 / 悬停 / 按下 / 禁用)的图像资源
空指针防护:双重检查确保delete操作安全
内存泄漏预防:显式置空指针避免悬挂指针*/
if (m_btninfoNor.pImg != NULL)
{
delete m_btninfoNor.pImg;
m_btninfoNor.pImg = NULL;
}
if (m_btninfoHot.pImg != NULL)
{
delete m_btninfoHot.pImg;
m_btninfoHot.pImg = NULL;
}
if (m_btninfoPre.pImg != NULL)
{
delete m_btninfoPre.pImg;
m_btninfoPre.pImg = NULL;
}
if (m_btninfoDis.pImg != NULL)
{
delete m_btninfoDis.pImg;
m_btninfoDis.pImg = NULL;
}
}
BEGIN_MESSAGE_MAP(CTranButton, CButton)
ON_WM_MOUSEMOVE()
ON_WM_LBUTTONUP()
ON_WM_LBUTTONDOWN()
ON_WM_ERASEBKGND()
END_MESSAGE_MAP()
//核心作用:从资源组中加载按钮状态图像(正常 / 悬停 / 按下 / 禁用),支持 PNG 透明通道处理,并生成统一尺寸的图像资源缓存。
void CTranButton::Load(UINT IDBkGroup, int width, int height, const CString& resourceType)
{
CImage orgImg;
/*资源加载与解码
关键机制:
通过资源 ID 和类型定位资源数据
使用 LoadResource 加载资源到内存
问题发现:
未检查 FreeResource 的返回值(旧版 SDK 需要手动释放)*/
HINSTANCE hInst = AfxGetResourceHandle();
/*
核心作用:在指定模块中定位资源的位置,返回资源信息块的句柄(HRSRC)
参数详解:
hModule:包含资源的模块句柄(如 NULL 表示当前进程)
lpName:资源名称(字符串或整数标识符,需用 MAKEINTRESOURCE 宏转换)
lpType:资源类型(如 "PNG"、"RT_BITMAP" 或自定义类型)
返回值:成功返回资源句柄,失败返回 NULL*/
HRSRC hRsrc = ::FindResource(hInst, MAKEINTRESOURCE(IDBkGroup), resourceType);
if (hRsrc == NULL)
{
return;
}
/*核心作用:获取指定资源的字节大小
参数要求:
hResInfo 必须通过 FindResource 或 FindResourceEx 获取
返回值:资源大小(字节),失败返回 0
资源大小计算规则
对于二进制资源(如 PNG、DLL),返回实际数据大小
对于字符串资源,返回所有字符串的总长度(含计数字段)
对于复合资源(如图标组),返回所有子资源的总大小*/
DWORD len = SizeofResource(hInst, hRsrc);
/*核心作用:将资源加载到全局内存块中
内存管理:
返回的 HGLOBAL 句柄由系统管理,进程结束时自动释放
不可直接调用 GlobalFree 释放(旧版 SDK 需要调用 FreeResource)*/
BYTE* lpRsrc = (BYTE*)LoadResource(hInst, hRsrc);
if (lpRsrc == NULL)
{
return;
}
/*该代码实现了从资源加载数据到内存,并通过流对象解析为图像的完整流程,
涉及内存分配、数据复制、流对象创建和资源释放四个核心阶段。*/
/*内存分配
功能:分配固定内存块(GMEM_FIXED),返回 HGLOBAL 句柄
特性:
直接返回物理地址,无需后续锁定即可操作
内存生命周期与进程绑定,进程退出时自动释放
风险:若 len 过大可能导致分配失败(需检查返回值是否为 NULL)*/
HGLOBAL m_hMem = GlobalAlloc(GMEM_FIXED, len);
/*内存操作
锁定机制:
GlobalLock 增加锁定计数器(对 GMEM_FIXED 无效但需调用)
GlobalUnlock 减少计数器,确保符合 API 规范
数据复制:
将资源数据 lpRsrc 复制到内存块
需确保 lpRsrc 有效性(如通过 LoadResource 获取)*/
BYTE* pmem = (BYTE*)GlobalLock(m_hMem);
memcpy(pmem, lpRsrc, len);
/*流对象创建
COM 流封装:
基于内存句柄创建 IStream 接口
FALSE 参数表示不自动释放内存(需手动调用 GlobalFree)
生命周期:
流对象 pstm 独立于内存句柄存在
流操作(如读取 / 写入)不影响内存句柄状态*/
IStream* pstm;
CreateStreamOnHGlobal(m_hMem, FALSE, &pstm);
/*图像加载
数据解析:
调用图像对象 orgImg 的加载方法
通过流接口读取内存中的图像数据
格式支持:
依赖 orgImg 实现(如 GDI + / WIC 支持的格式)*/
orgImg.Load(pstm);
//解锁
GlobalUnlock(m_hMem);
/*资源释放
释放顺序:
释放内存块(GlobalFree)
释放流对象(Release)
释放原始资源(FreeResource)
关键点:
必须先释放内存再释放流,否则流可能访问已释放内存
FreeResource 仅用于释放 LoadResource 获取的资源*/
GlobalFree(m_hMem);
pstm->Release();
FreeResource(lpRsrc);
/*PNG 透明度处理
技术原理:
解决 GDI 对 PNG 预乘 Alpha 的错误处理(参考搜索结果)
实现真透明效果而非纯白背景*/
if (resourceType == _T("PNG"))
{
if (orgImg.GetBPP() == 32)
{
for (int i = 0; i < orgImg.GetWidth(); i++)
{
for (int j = 0; j < orgImg.GetHeight(); j++)
{
/*PNG 图像预乘 Alpha 处理
作用:将 RGB 通道值按 Alpha 通道进行加权(公式:Component = Component * Alpha / 255)
应用场景:实现图像透明混合效果(如 UI 元素与背景的合成)
技术细节:
仅处理 32 位 PNG 图像(含 Alpha 通道)
逐像素操作实现颜色混合*/
unsigned char* pucColor = reinterpret_cast<unsigned char*>(orgImg.GetPixelAddress(i, j));
pucColor[0] = pucColor[0] * pucColor[3] / 255; // R通道处理
pucColor[1] = pucColor[1] * pucColor[3] / 255; // G通道处理
pucColor[2] = pucColor[2] * pucColor[3] / 255; // B通道处理
}
}
}
}
// 按钮尺寸设置:根据图像尺寸自动适配按钮大小
if (width == 0 && height == 0)
{
width = orgImg.GetWidth(); // 使用图像宽度作为宽度
}
if (height == 0)
{
height = orgImg.GetHeight(); // 使用图像高度作为高度
}
// 统一设置按钮各状态的尺寸
m_btninfoNor.nWidth = width;
m_btninfoNor.nHeight = height;
m_btninfoHot.nWidth = width;
m_btninfoHot.nHeight = height;
m_btninfoPre.nWidth = width;
m_btninfoPre.nHeight = height;
m_btninfoDis.nWidth = width;
m_btninfoDis.nHeight = height;
//图像资源管理
/*指针数组初始化
存储按钮四种状态(正常 / 按下 / 悬停 / 禁用)的图像指针地址
通过地址操作直接修改成员变量,避免值拷贝*/
CImage** imgs[] = {&m_btninfoNor.pImg, &m_btninfoHot.pImg, &m_btninfoPre.pImg, &m_btninfoDis.pImg};
int posX = 0;
for (int i = 0; i < 4 && posX <= (orgImg.GetWidth() - width); i++, posX += width)
{
CImage* pMap = new CImage();
/*内存安全处理
确保每次循环前释放旧图像内存
防止内存泄漏和野指针访问 */
if (*imgs[i] != NULL)
{
delete *imgs[i]; // 释放旧图像资源
*imgs[i] = NULL; // 防止悬挂指针
}
*imgs[i] = pMap;
//动态图像创建
/*PNG 透明通道处理
为 PNG 图像创建 32 位带 Alpha 通道的 DIB
启用透明混合功能(AlphaBlend)*/
if (resourceType == _T("PNG"))
{
BOOL bStat = FALSE;
if (orgImg.GetBPP() == 32)
{
/*位图参数配置
参数 说明
width 目标图像宽度(按钮尺寸)
height 目标图像高度
orgImg.GetBPP() 源图像位深(24 / 32 位)
BI_RGB 无压缩 RGB 格式*/
bStat = pMap->CreateEx(width, height, orgImg.GetBPP(), BI_RGB, NULL, CImage::createAlphaChannel);
}
else
{
bStat = pMap->CreateEx(width, height, orgImg.GetBPP(), BI_RGB, NULL);
}
ASSERT(bStat);
}
else
{
BOOL bStat = pMap->CreateEx(width, height, orgImg.GetBPP(), BI_RGB, NULL);
ASSERT(bStat);
}
//图像绘制操作
/*设备上下文管理
自动锁定图像内存(LockBits)
确保绘制操作的原子性*/
CImageDC imageDC(*pMap);
/*源区域裁剪
从源图像(posX, 0) 处截取 width x height 区域
实现水平滚动式图像拼接效果*/
orgImg.Draw(imageDC, 0, 0, width, height, posX, 0, width, height);
}
}
//控制按钮是否启用自适应大小功能,根据父窗口或内容动态调整自身尺寸。
void CTranButton::SetAutoSize(bool bAutoSize)
{
m_bAutoSize = bAutoSize;
}
//核心作用:根据按钮状态(禁用、按下、悬停、正常)动态绘制背景图像和文本内容,实现完全自定义的按钮外观。
void CTranButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
/*参数验证
功能:确保该函数仅处理按钮控件的绘制请求
设计意图:防止误调用或类型不匹配导致的崩溃*/
ASSERT(lpDrawItemStruct->CtlType == ODT_BUTTON);
/*设备上下文获取
关键对象:
pDC:封装绘图设备上下文(HDC)
rect:按钮客户区矩形范围*/
CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
CRect rect = lpDrawItemStruct->rcItem;
/*状态判断与背景绘制
状态映射表:
按钮状态 触发条件 对应图像
禁用状态 ODS_DISABLED m_btninfoDis
按下 / 选中状态 ODS_SELECTED 或(悬停 + 按下) m_btninfoPre
悬停状态 m_bHot 为真 m_btninfoHot
正常状态 默认 m_btninfoNor*/
if (lpDrawItemStruct->itemState & ODS_DISABLED) // 禁用状态
{
DrawBK(*pDC, m_btninfoDis.pImg, TRA_BTN_DIS);
}
else if (lpDrawItemStruct->itemState & ODS_SELECTED || (m_bHot && m_bPress))
{
DrawBK(*pDC, m_btninfoPre.pImg, TRA_BTN_PRE); // 按下前状态
}
else if (m_bHot)
{
DrawBK(*pDC, m_btninfoHot.pImg, TRA_BTN_HOT); // 悬停状态
}
else
{
DrawBK(*pDC, m_btninfoNor.pImg, TRA_BTN_NOR); // 正常状态
}
/*文本处理与绘制
文本处理逻辑:
获取按钮文本内容
移除所有空格(可能影响多单词显示)
非空文本才进行绘制*/
CString strText = _T("");
GetWindowText(strText);
CString strTemp(strText);
strTemp.Remove(' ');
if (!strTemp.IsEmpty())
{
// 根据状态选择文本绘制参数
if (lpDrawItemStruct->itemState & ODS_DISABLED) // 禁用状态
{
DrawBtnText(*pDC, strText, 0, TRA_BTN_DIS);
}
else if (lpDrawItemStruct->itemState & ODS_SELECTED || (m_bHot && m_bPress)) // 按下前状态
{
DrawBtnText(*pDC, strText, 1, TRA_BTN_PRE);
}
else if (m_bHot)
{
DrawBtnText(*pDC, strText, 0, TRA_BTN_HOT); // 悬停状态
}
else
{
DrawBtnText(*pDC, strText, 0, TRA_BTN_NOR); // 正常状态
}
}
}
//核心作用:处理鼠标在按钮区域内的移动事件,实现以下功能:
//更新按钮的悬停状态(m_bHot)
//管理鼠标捕获(SetCapture / ReleaseCapture)
//协调父窗口的界面更新
/*
A[WM_MOUSEMOVE] -- > B{ Is Mouse in Rect ? }
B-- > | Yes | C[Check Left Button State]
B-- > | No | D[Release Capture]
C-- > E{ Left Button Pressed ? }
E-- > | No | F[Clear m_bPress]
E-- > | Yes | G[Keep Capture]
F-- > H[Update m_bHot]
G-- > H
H-- > I[Invalidate Self]
H-- > J[Update Parent Region]
D-- > K[Invalidate Self]
K-- > L[Update Parent Region]*/
void CTranButton::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
/*获取客户区坐标
功能:获取按钮的客户区矩形范围
MFC特性:GetClientRect返回相对于控件客户区的坐标系统*/
CRect rect;
GetClientRect(rect);
/*鼠标位置检测
关键点:使用PtInRect判断鼠标是否在按钮客户区内*/
if (rect.PtInRect(point)) // 鼠标在按钮内
{
/*左键状态处理
功能:当左键未按下时,清除按下状态
参数解析:MK_LBUTTON表示左键被按下的标志位*/
if (!(nFlags & MK_LBUTTON))
{
/*状态变量 更新条件 关联操作
m_bPress 左键按下 / 释放 影响按钮的视觉反馈*/
m_bPress = FALSE;
}
/*鼠标捕获管理
作用:确保按钮持续接收鼠标事件(即使鼠标移出控件)
风险提示:需在适当时机调用ReleaseCapture()避免死锁*/
if (GetCapture() != this)
{
/*鼠标捕获机制
SetCapture的作用:
即使鼠标移出按钮区域,仍能接收后续鼠标事件
释放时机:应在WM_LBUTTONUP或WM_MOUSELEAVE事件中调用ReleaseCapture*/
SetCapture();
}
/*悬停状态处理
状态变量 更新条件 关联操作
m_bHot 鼠标进入 / 离开按钮区域 触发重绘和父窗口更新*/
if (m_bHot == TRUE)
{
//nothing to do
}
else
{
m_bHot = TRUE; //状态变更:将悬停标志m_bHot设为TRUE
SetFocus(); //焦点设置:通过SetFocus()获取输入焦点
Invalidate(); //界面更新:Invalidate()触发按钮自身重绘
// 坐标转换与父窗口更新:将按钮位置转换为父窗口坐标系,通知父窗口更新相关区域
/*
A[按钮客户区坐标] -- > B[ClientToScreen] -- > C[屏幕坐标]
C-- > D[ScreenToClient(父窗口)] -- > E[父窗口客户区坐标]
E-- > F[重建矩形]*/
/*创建临时矩形
功能:复制原始矩形 rect(按钮的客户区坐标)
参数:rect 通常是按钮的客户区范围(通过 GetClientRect 获取)*/
CRect rcTemp(rect);
/*转换为屏幕坐标
功能:将客户区坐标转换为屏幕坐标
坐标系变化:
原始坐标:相对于按钮客户区左上角(0, 0)
转换后坐标:相对于整个屏幕左上角(0, 0)
示例:
按钮在客户区位置(50, 50),尺寸 100x50
转换后屏幕坐标:左上角(50, 50),右下角(150, 100)*/
ClientToScreen(&rcTemp);
/*提取左上角坐标
功能:获取转换后的屏幕坐标左上角点
用途:作为后续坐标转换的基准点*/
CPoint pointTemp(rcTemp.left, rcTemp.top);
/*转换回父窗口客户区坐标
功能:将屏幕坐标转换为父窗口的客户区坐标
关键点:
GetParent()->GetSafeHwnd() 获取父窗口句柄
转换后坐标相对于父窗口客户区左上角(0, 0)
示例:
父窗口客户区原点在屏幕坐标(100, 100)
点(150, 100) 转换为父窗口坐标(50, 0)*/
::ScreenToClient(GetParent()->GetSafeHwnd(), &pointTemp);
/*重建父窗口坐标系矩形
功能:基于父窗口坐标系重建矩形
参数计算:
左上角:(pointTemp.x, pointTemp.y)
右下角:左上角 + 原始宽度 / 高度
结果:rcTemp 表示按钮在父窗口客户区中的位置和尺寸*/
rcTemp.SetRect(pointTemp.x, pointTemp.y, pointTemp.x + rect.Width(), pointTemp.y + rect.Height());
/*触发父窗口重绘
功能:标记父窗口的 rcTemp 区域为无效,需要重绘
参数:
rcTemp:需要更新的区域
TRUE:使用异步绘制(双缓冲),避免闪烁
效果:父窗口在下一轮消息循环中会调用 OnPaint 重绘该区域*/
GetParent()->InvalidateRect(rcTemp, TRUE);
}
}
else // 鼠标移出按钮
{
/*
鼠标移出处理
资源释放:通过ReleaseCapture()释放鼠标捕获
状态同步:清除悬停标志m_bHot
界面更新逻辑:与悬停状态处理对称*/
ReleaseCapture();
m_bHot = FALSE;
Invalidate();
// 坐标转换与父窗口更新
CRect rcTemp(rect);
ClientToScreen(&rcTemp);
CPoint pointTemp(rcTemp.left, rcTemp.top);
::ScreenToClient(GetParent()->GetSafeHwnd(), &pointTemp);
rcTemp.SetRect(pointTemp.x, pointTemp.y, pointTemp.x + rect.Width(), pointTemp.y + rect.Height());
GetParent()->InvalidateRect(rcTemp, TRUE);
}
/*基类消息处理
消息传递:确保基类CButton的默认处理逻辑执行
重要性:维持MFC消息处理链的完整性*/
CButton::OnMouseMove(nFlags, point);
}
/*核心作用:处理鼠标左键释放事件,实现以下功能:
更新按钮的按下状态(m_bPress)
触发按钮自身重绘(恢复默认样式)
通知父窗口更新相关区域
重置悬停状态(m_bHot)*/
void CTranButton::OnLButtonUp(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
/*检查按下状态
设计意图:确保仅在鼠标左键确实被按下后释放时处理逻辑(避免误触)。
状态跟踪:m_bPress 是自定义成员变量,记录按钮是否被按下(通常在 OnLButtonDown 中设为 TRUE)。*/
if (m_bPress)
{
/*获取客户区矩形并触发重绘
功能:
GetClientRect(rect):获取按钮的客户区矩形(相对于按钮自身的坐标系)。
Invalidate():标记整个客户区为无效,触发 WM_PAINT 消息,重绘按钮外观。
典型场景:当按钮从“按下”状态恢复时,需要重绘以清除按压效果(如凹陷样式)。*/
CRect rect;
GetClientRect(rect);
Invalidate();
/*坐标转换与父窗口更新
坐标转换流程:
客户区 → 屏幕坐标:ClientToScreen 将按钮的客户区坐标转换为屏幕绝对坐标。
屏幕 → 父窗口客户区坐标:ScreenToClient 将屏幕坐标转换为父窗口的客户区坐标。
重建矩形:基于父窗口坐标系,构造与按钮位置对应的矩形。
父窗口更新:InvalidateRect(rcTemp, TRUE) 通知父窗口重绘该区域,确保按钮状态变化在父窗口中同步(如焦点高亮)。*/
CRect rcTemp(rect);
ClientToScreen(&rcTemp);
CPoint pointTemp(rcTemp.left, rcTemp.top);
::ScreenToClient(GetParent()->GetSafeHwnd(), &pointTemp);
rcTemp.SetRect(pointTemp.x, pointTemp.y, pointTemp.x + rect.Width(), pointTemp.y + rect.Height());
GetParent()->InvalidateRect(rcTemp, TRUE);
//表示按钮是否被按下,释放后设为 FALSE。
m_bPress = FALSE;
}
//表示鼠标是否悬停在按钮上,释放后设为 FALSE(即使鼠标仍在按钮内)。
m_bHot = FALSE;
/*调用基类实现
基类 CButton 可能处理点击事件(如发送 BN_CLICKED 通知),跳过会导致功能缺失。*/
CButton::OnLButtonUp(nFlags, point);
}
//核心作用:处理鼠标左键按下事件,实现以下功能:
//更新按钮的按下状态(m_bPress)
//触发按钮自身重绘(显示按压效果)
//通知父窗口更新相关区域
//传递事件到基类处理默认行为
void CTranButton::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
/*获取客户区矩形
功能:获取按钮的客户区矩形(相对于按钮自身的坐标系)
用途:确定按钮的尺寸和位置,为后续坐标转换做准备*/
CRect rect;
GetClientRect(rect);
/*坐标转换与父窗口更新
坐标转换流程:
客户区 → 屏幕坐标:ClientToScreen 将按钮的客户区坐标转换为屏幕绝对坐标
屏幕 → 父窗口客户区坐标:ScreenToClient 将屏幕坐标转换为父窗口的客户区坐标
重建矩形:基于父窗口坐标系,构造与按钮位置对应的矩形
父窗口更新:InvalidateRect(rcTemp, TRUE) 通知父窗口重绘该区域,确保按钮状态变化在父窗口中同步(如焦点高亮)*/
CRect rcTemp(rect);
ClientToScreen(&rcTemp);
CPoint pointTemp(rcTemp.left, rcTemp.top);
::ScreenToClient(GetParent()->GetSafeHwnd(), &pointTemp);
rcTemp.SetRect(pointTemp.x, pointTemp.y, pointTemp.x + rect.Width(), pointTemp.y + rect.Height());
GetParent()->InvalidateRect(rcTemp, TRUE);
/*标记按钮处于按下状态
通过自定义成员变量 m_bPress 记录按钮是否被按下,用于后续状态恢复和绘制逻辑*/
m_bPress = TRUE;
/*调用基类实现
基类 CButton 可能处理点击事件(如发送 BN_CLICKED 通知),跳过会导致功能缺失*/
CButton::OnLButtonDown(nFlags, point);
}
//核心作用:控制按钮背景擦除行为,支持自绘模式下的背景透明化或自定义绘制。
/*自绘模式与系统绘制的区别
特性 自绘模式(BS_OWNERDRAW) 系统默认模式
背景控制权 应用程序完全控制 系统自动处理
绘制触发时机 响应 WM_DRAWITEM 消息 响应 WM_ERASEBKGND 消息
典型应用 自定义外观(如圆角、阴影) 标准按钮样式*/
BOOL CTranButton::OnEraseBkgnd(CDC* pDC)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
/*功能:判断按钮是否启用了 BS_OWNERDRAW 扩展样式
样式含义:
BS_OWNERDRAW:按钮由应用程序完全控制绘制(包括背景和内容)
触发机制:需在按钮创建时通过 ModifyStyle 或资源编辑器设置该样式*/
if (GetButtonStyle() & BS_OWNERDRAW)
{
/*设计意图:告知系统背景已由控件自身处理,无需调用默认擦除逻辑
典型场景:
需要透明背景的按钮(如玻璃效果)
自定义渐变、纹理填充的按钮
需要动态更新背景的交互式控件*/
return TRUE; // 自绘模式下拦截背景擦除
}
/*默认处理
功能:调用基类实现,执行标准背景擦除(通常填充按钮背景色)
适用场景:非自绘按钮,保留系统默认的背景处理*/
return CButton::OnEraseBkgnd(pDC); // 默认处理
}
//核心作用:根据按钮状态和自动尺寸策略,绘制按钮背景图像。
//参数 类型 功能说明
//HDC dc 设备上下文 目标绘制表面
//CImage* img 图像对象 待绘制的按钮状态对应图像
//btnstate 状态枚举 按钮当前状态(正常 / 悬停 / 按下 / 禁用)
void CTranButton::DrawBK(HDC dc, CImage* img, TRA_BTNSTATE btnstate)
{
/*空指针安全校验
功能:防止空指针解引用导致的程序崩溃
设计考量:符合防御性编程原则,确保图像对象有效性*/
if (!img)
{
return;
}
/*客户区坐标获取
功能:获取按钮客户区矩形范围
MFC特性:GetClientRect返回相对于控件客户区的坐标*/
CRect rc;
GetClientRect(&rc);
CRect tmpRect;
int nX = 0;
int nY = 0;
int nW = 0;
int nH = 0;
if (m_bAutoSize == true)
{
/*自动尺寸模式处理
工作原理:
设置临时矩形为全客户区大小
直接绘制完整图像到客户区
适用场景:图像尺寸与按钮尺寸严格匹配时
潜在问题:图像过大可能导致拉伸,过小则留白*/
tmpRect.SetRect(0, 0, rc.Width(), rc.Height());
if (img)
{
img->Draw(dc, tmpRect);
}
}
else
{
/*手动尺寸模式处理
核心逻辑:
状态匹配:根据btnstate选择对应状态的图像尺寸
居中定位:通过计算(rc.Width() - nW) / 2实现水平垂直居中
区域裁剪:通过tmpRect限定绘制区域*/
// 根据状态获取图像尺寸
if (btnstate == TRA_BTN_NOR)
{
nW = m_btninfoNor.nWidth;
nH = m_btninfoNor.nHeight;
}
else if (btnstate == TRA_BTN_HOT)
{
nW = m_btninfoHot.nWidth;
nH = m_btninfoHot.nHeight;
}
else if (btnstate == TRA_BTN_PRE)
{
nW = m_btninfoPre.nWidth;
nH = m_btninfoPre.nHeight;
}
else
{
nW = m_btninfoDis.nWidth;
nH = m_btninfoDis.nHeight;
}
// 居中计算
/*居中算法验证
nX = (rc.Width()−nW) / 2
nY = (rc.Height()−nH) / 2
边界情况:
当nW > rc.Width()时,nX为负值,导致图像左溢出
当nH > rc.Height()时,nY为负值,导致图像上溢出
改进建议:增加尺寸校验
nX = max(0, (rc.Width() - nW) / 2);
nY = max(0, (rc.Height() - nH) / 2);*/
nX = (rc.Width() - nW) / 2;
nY = (rc.Height() - nH) / 2;
tmpRect.SetRect(nX, nY, nW + nX, nH + nY);
if (img)
{
img->Draw(dc, tmpRect);
}
}
}
//核心作用:根据按钮状态(btnstate)动态绘制按钮文本,支持颜色渐变、边距调整和透明背景。
void CTranButton::DrawBtnText(HDC dc, const CString& strText, int nMove, TRA_BTNSTATE btnstate)
{
/*文本尺寸计算
功能:获取文本的实际宽度和高度
用途:为后续布局调整提供依据(如判断文本是否超出按钮范围)*/
CSize sizeText = CDC::FromHandle(dc)->GetTextExtent(strText);
/*客户区矩形处理
设计意图:
保留按钮边缘的空白区域(nMove 控制边距)
保持文本居中显示的基准区域*/
CRect rect;
GetClientRect(&rect);
rect.DeflateRect(nMove, nMove, 0, 0); // 左右各缩减nMove像素
/*透明背景设置
作用:使文本背景透明,避免覆盖按钮原有绘制内容
对比:若设置为 OPAQUE 需手动填充背景色*/
CDC::FromHandle(dc)->SetBkMode(TRANSPARENT);
/*文本颜色控制 */
if (btnstate == TRA_BTN_NOR)
{
CDC::FromHandle(dc)->SetTextColor(RGB(255, 0, 0)); //红
}
else if (btnstate == TRA_BTN_HOT)
{
CDC::FromHandle(dc)->SetTextColor(RGB(0, 255, 0)); //绿
}
else if (btnstate == TRA_BTN_PRE)
{
CDC::FromHandle(dc)->SetTextColor(RGB(0, 0, 255)); //蓝
}
else
{
CDC::FromHandle(dc)->SetTextColor(RGB(100, 100, 100)); //灰
}
/*文本绘制
格式控制:
DT_SINGLELINE:单行显示(防止换行)
DT_CENTER:水平居中
DT_VCENTER:垂直居中
局限性:未处理文本过长导致的截断问题*/
CDC::FromHandle(dc)->DrawText(strText, rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
}
//核心作用:在按钮窗口被子类化前,强制启用自绘模式(BS_OWNERDRAW),确保按钮外观完全由代码控制。
/*
自绘模式实现原理
1. 子类化流程
对话框模板控件:通过 SubclassDlgItem 关联控件时触发 PreSubclassWindow
动态创建控件:调用 Create 函数时触发 PreCreateWindow 和 PreSubclassWindow
样式生效时机:在窗口句柄(HWND)创建后、消息泵启动前生效
2. 自绘模式与系统绘制的对比
特性 自绘模式(BS_OWNERDRAW) 系统默认模式
绘制触发时机 响应 WM_DRAWITEM 消息 响应 WM_ERASEBKGND 消息
控制粒度 完全自定义(背景、文本、图标) 仅能修改有限属性(颜色、字体)
典型应用 圆形按钮、渐变效果、动画按钮 标准按钮样式*/
void CTranButton::PreSubclassWindow()
{
/*修改窗口样式
功能:通过 ModifyStyle 函数动态修改窗口样式
参数解析:
0:移除的样式掩码(此处不移除任何样式)
BS_OWNERDRAW:添加自绘样式
自绘模式特性:
触发 WM_DRAWITEM 消息(需重写 DrawItem 函数)
按钮背景、文本、图标等元素需自行绘制
支持动态视觉效果(如渐变、动画)*/
ModifyStyle(0, BS_OWNERDRAW); // 添加自绘样式
/*调用基类实现
必要性:确保基类 CButton 的初始化逻辑正常执行
执行顺序:先应用自定义样式修改,再执行基类逻辑*/
CButton::PreSubclassWindow(); // 调用基类实现
}
在CTest402Dlg::OnInitDialog()调用
m_btn1.Load(IDR_BTN, 244);
m_btn2.Load(IDR_BTN, 244);
相关内容
详细说明DECLARE_MESSAGE_MAP()和成员函数声明顺序的关系-CSDN博客https://blog.csdn.net/qq_20725221/article/details/147412844?spm=1001.2014.3001.5501详细介绍C++的默认参数使用-CSDN博客
https://blog.csdn.net/qq_20725221/article/details/147418340?spm=1001.2014.3001.5501MFC中,true, false和TRUE, FALSE的差异-CSDN博客
https://blog.csdn.net/qq_20725221/article/details/147419867?spm=1001.2014.3001.5501详细介绍PtInRect()-CSDN博客
https://blog.csdn.net/qq_20725221/article/details/147422283?spm=1001.2014.3001.5501
详细介绍SetFocus()-CSDN博客https://blog.csdn.net/qq_20725221/article/details/147423339 MFC的GetCapture()和SetCapture()-CSDN博客
https://blog.csdn.net/qq_20725221/article/details/147423699
MFC中PreSubclassWindow()什么时候被调用-CSDN博客https://blog.csdn.net/qq_20725221/article/details/147425566
MFC中子类化是什么意思?-CSDN博客https://blog.csdn.net/qq_20725221/article/details/147425847 MFC中SetAutoSize()什么时候被调用-CSDN博客
https://blog.csdn.net/qq_20725221/article/details/147426558
详细介绍CDC::FromHandle()-CSDN博客https://blog.csdn.net/qq_20725221/article/details/147426962
详细介绍AfxGetResourceHandle()-CSDN博客https://blog.csdn.net/qq_20725221/article/details/147443877
详细介绍MFC中FindResource()-CSDN博客https://blog.csdn.net/qq_20725221/article/details/147444383 详细介绍SizeofResource()-CSDN博客
https://blog.csdn.net/qq_20725221/article/details/147445326
详细介绍MFC中的LoadResource()-CSDN博客https://blog.csdn.net/qq_20725221/article/details/147445633 详细介绍GlobalAlloc()-CSDN博客
https://blog.csdn.net/qq_20725221/article/details/147448378
详细介绍GlobalLock()和GlobalUnlock()-CSDN博客https://blog.csdn.net/qq_20725221/article/details/147448911 详细介绍GlobalAlloc()和GlobalFree()-CSDN博客
https://blog.csdn.net/qq_20725221/article/details/147450655
详细介绍CreateStreamOnHGlobal()-CSDN博客https://blog.csdn.net/qq_20725221/article/details/147455312
HGLOBAL m_hMem = GlobalAlloc(GMEM_FIXED, len);中第一个参数为GMEM_FIXED,还需要对m_hMem进行锁定吗?-CSDN博客https://blog.csdn.net/qq_20725221/article/details/147455595 详细介绍一下CImage中的GetBPP()-CSDN博客
https://blog.csdn.net/qq_20725221/article/details/147455900
详细介绍GetPixelAddress()-CSDN博客https://blog.csdn.net/qq_20725221/article/details/147456218 详细介绍CImage中的CreateEx()-CSDN博客
https://blog.csdn.net/qq_20725221/article/details/147456932