制作图片按钮

书籍:《Visual C++ 2017从入门到精通》的4.2 按钮控件

环境:visual studio 2022

内容:[例 4.2]制作图片按钮

说明:以上内容大部分来自腾讯元宝。

新建对话框工程

一个简单的对话框程序-CSDN博客https://blog.csdn.net/qq_20725221/article/details/146396703?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522d04c513b850b06d55102b36c83a4515f%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=d04c513b850b06d55102b36c83a4515f&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-146396703-null-null.nonecase&utm_term=%E5%AF%B9%E8%AF%9D%E6%A1%86&spm=1018.2226.3001.4450

拷贝图片到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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值