c/c++游戏编程之用Easyx封装按钮

c/c++游戏编程之Easyx图形库基础(一) EasyX基础
c/c++游戏编程之Easyx图形库基础(二) 绘制图片
c/c++游戏编程之Easyx图形库基础(三) 用Easyx封装按钮

新建button头文件和源文件

阅读本节内容建议掌握的前置知识:多文件编程。
这节内容我们介绍如何利用easyx来实现一个Button(按钮)类。

如图所示,在上一节的项目中分别新建头文件button.h,源文件button.cpp。
在这里插入图片描述
button.h:

//button.h

#pragma once //#pragma once是一个比较常用的C/C++预处理指令,只要在头文件的最开始加入这条预处理指令,使头文件不会被重复包含。

#include <Windows.h>

class IMAGE; //声明IMAGE类
struct ExMessage; //声明MOUSEMSG结构体

namespace MyUI {
	
	enum class BUTTON_MSG : short {
		NOMSG = 0, //鼠标与按钮无关联
		MOUSE_IN, //鼠标位于按钮内
		MOUSE_LDOWN, //鼠标左键按下
		MOUSE_LUP, //鼠标左键抬起
		MOUSE_RDOWN, //鼠标右键按下
		MOUSE_RUP, //鼠标右键抬起
	};
	
	//Button描述结构体
	struct BUTTON_DESC {
		LPCTSTR pImgFile; //图片路径
		int posx; //图片初始x坐标
		int posy; //图片初始y坐标
		unsigned int width; //图片宽度
		unsigned int height; //图片高度
		unsigned int transparency; //图片透明度
	};

	class Button {
	public:

		Button(const BUTTON_DESC& _btnDesc);
		Button(const Button& _btn) = delete; //禁用默认拷贝构造函数
		Button& operator = (const Button& _btn) = delete; //禁用默认赋值拷贝运算符
		~Button();

	protected:

		IMAGE* pImg_; //指向图像对象的指针
		BUTTON_DESC btnDesc_;

	};
}


#pragma once 的作用相当于:

#ifndef BUTTON_H_
#define BUTTON_H_
/*
class xxx {
...
}
*/
#endif

两者的作用都是防止头文件被重复包含。

创建命名空间MyUI

使用namespace关键字将我们自定义的控件类放在命名空间MyUI里。
(Button这个类名可能会与其他库定义的Button冲突,可以选择不这样做,这里主要是为了让不懂的同学学习命名空间的用法)

禁用拷贝构造函数和赋值拷贝运算符

因为我们有数据成员 IMAGE* pImg_ ,我们将用它指向一块动态内存。所以需要先禁用Button类的默认拷贝构造函数和默认拷贝赋值运算符,防止隐式的拷贝操作导致内存管理出现问题。

button.cpp:

//button.cpp

#include "button.h"
#include <graphics.h>

namespace MyUI {
	Button::Button(const BUTTON_DESC& _btnDesc) : btnDesc_(_btnDesc) {
		pImg_ = new IMAGE;
		loadimage(pImg_, _btnDesc.pImgFile, _btnDesc.width, _btnDesc.height);
	}

	Button::~Button() {
		delete pImg_;
	}
}

我们在构造函数用参数初始化列表初始化btnDesc_,并为pImg_分配动态内存,在析构函数里释放它。

定义Draw函数

好了,现在的Button空空如也,我们需要定义一个将按钮绘制出来的成员函数。还记得上一节的透明贴图函数吗?我们只需要将它改一改,变成Button类的成员函数即可。
(如果你要封装更多的控件,可以将此段贴图代码做成一个接口,放在公共文件里,供实现控件绘图方法调用)。

//在button.h的声明
BUTTON_MSG Draw(const ExMessage& _msMsg);

//在button.cpp的定义
BUTTON_MSG Button::Draw(const ExMessage& _msMsg) {
		HDC imgDC = GetImageHDC(pImg_); //获取图像设备上下文句柄

		BUTTON_MSG rt = BUTTON_MSG::NOMSG;

		//通过坐标值判断鼠标是否在按钮内
		if (_msMsg.x >= btnDesc_.posx && _msMsg.x <= (btnDesc_.posx + btnDesc_.width) &&
			_msMsg.y >= btnDesc_.posy && _msMsg.y <= (btnDesc_.posy + btnDesc_.height)) 
		{
			if (WM_LBUTTONDOWN == _msMsg.message) {
				rt = BUTTON_MSG::MOUSE_LDOWN;
			} 
			else if (WM_LBUTTONUP == _msMsg.message) {
				rt = BUTTON_MSG::MOUSE_LUP;
			}
			else {
				rt = BUTTON_MSG::MOUSE_IN;
			}
		}

		// 结构体的第三个成员表示额外的透明度,0 表示全透明,255 表示不透明。
		BLENDFUNCTION bf = { AC_SRC_OVER, 0, btnDesc_.transparency, AC_SRC_ALPHA };

		// 使用AlphaBlend函数实现半透明贴图
		AlphaBlend(
			GetImageHDC(NULL), //设备上下文句柄
			btnDesc_.posx, //绘制的x坐标
			btnDesc_.posy,  //绘制的y坐标
			btnDesc_.width, //在所选设备中绘制图像的宽度
			btnDesc_.height, //在所选设备中绘制图像的高度
			imgDC, //图像设备上下文句柄
			0, 0, //绘制图像起点坐标
			btnDesc_.width, //绘制图像的宽度
			btnDesc_.height, //绘制图像的高度
			bf
		);

		return rt;
	}

我们使用枚举类BUTTON_MSG的枚举值作为Draw函数的返回值,目的是在于告诉Draw的调用者鼠标与按钮产生了什么交互。

制作一张简单的按钮图片

先制作了一张简单的按钮图片,老规矩,将它放在images文件夹:
(由于我们是使用图片大小作为按钮大小,所以图片最好不要有太多没有内容的地方,不然会影响判定范围,当然你也可以自定义一个判定范围,或者不使用图片而使用画矩形的方式实现按钮类)
在这里插入图片描述

绘制按钮,验证返回值

main.cpp

#include <Windows.h>
#include <iostream>
#include <graphics.h>
#include <conio.h>
#include <map>
#include "button.h"

constexpr int SCREEN_WIDTH = 640;
constexpr int SCREEN_HEIGHT = 640;

void EventBtnDown() {
	std::cout << "鼠标在按钮内按下了左键" << std::endl;
}

void EventBtnUp() {
	std::cout << "鼠标在按钮内释放了左键" << std::endl;
}

int main() {
	initgraph(SCREEN_WIDTH, SCREEN_HEIGHT, EW_SHOWCONSOLE); //初始化窗口

	MyUI::BUTTON_DESC btnDesc = { "images/button1.png", 200, 200, 200, 60, 255 }; //按钮描述结构体
	MyUI::Button btn1(btnDesc); //构造按钮对象
	MyUI::BUTTON_MSG btnMsg; //用于接收按键消息的结构体对象

	BeginBatchDraw();

	while (1) {
		ExMessage msMsg;
		
		//easyx非阻塞获取鼠标消息,如果peekmessage返回值为false,说明没有消息
		//如果此时不清空msMsg,msMsg的值会是之前的消息值
		if (!peekmessage(&msMsg, EM_MOUSE, true)) {
			msMsg = { 0 }; //没消息就清空msMsg
		}

		btnMsg = btn1.Draw(msMsg);

		if (MyUI::BUTTON_MSG::MOUSE_LDOWN == btnMsg) {
			EventBtnDown();
		}
		else if (MyUI::BUTTON_MSG::MOUSE_LUP == btnMsg) {
			EventBtnUp();
		}

		FlushBatchDraw();

		Sleep(16); //程序休眠16毫秒
		cleardevice(); //16毫秒后清空窗口中的内容
	}

	EndBatchDraw();

	_getch();
	closegraph(); //关闭窗口

	return 0;
}

你可以像这样通过判断按钮返回值作出响应操作(这里是分别调用EventBtnDownEventBtnUp函数)。

如下,也可以做成一个单输入参数函数EventBtn

#include <Windows.h>
#include <iostream>
#include <graphics.h>
#include <conio.h>
#include <map>
#include "button.h"

constexpr int SCREEN_WIDTH = 640;
constexpr int SCREEN_HEIGHT = 640;

void EventBtn(MyUI::BUTTON_MSG _btnMsg) {
	if (MyUI::BUTTON_MSG::MOUSE_LDOWN == _btnMsg) {
			std::cout << "鼠标在按钮内按下了左键" << std::endl;
	}
	else if (MyUI::BUTTON_MSG::MOUSE_LUP == _btnMsg) {
			std::cout << "鼠标在按钮内释放了左键" << std::endl;
	}
}

int main() {
	initgraph(SCREEN_WIDTH, SCREEN_HEIGHT, EW_SHOWCONSOLE); //初始化窗口

	MyUI::BUTTON_DESC btnDesc = { "images/button1.png", 200, 200, 200, 60, 255 }; //按钮描述结构体
	MyUI::Button btn1(btnDesc); //构造按钮对象
	MyUI::BUTTON_MSG btnMsg; //用于接收按键消息的结构体对象

	BeginBatchDraw();

	while (1) {
		ExMessage msMsg;
		
		//easyx非阻塞获取鼠标消息,如果peekmessage返回值为false,说明没有消息
		//如果此时不清空msMsg,msMsg的值会是之前的消息值
		if (!peekmessage(&msMsg, EM_MOUSE, true)) {
			msMsg = { 0 }; //没消息就清空msMsg
		}

		btnMsg = btn1.Draw(msMsg);

		FlushBatchDraw();

		Sleep(16); //程序休眠16毫秒
		cleardevice(); //16毫秒后清空窗口中的内容
	}

	EndBatchDraw();

	_getch();
	closegraph(); //关闭窗口

	return 0;
}

结果都一样:
在这里插入图片描述
截至目前,我们将Button类进行了简单的实现,它已经可以派上用场了。

提示:接下来的内容会涉及到函数指针map容器,不懂这些知识的同学可以选择跳过。

更加细节的做法

接下来我们将在Button类添加一个c++的map容器对象成员eventMap_

std::map<BUTTON_MSG, void(*)()> eventMap_;

它用来为每个按钮对象的每个消息提供一个映射,这个映射是void(*)()类型的函数指针。
(原本可以使用一些例如模板的技术,但秉承由浅入深的原则,就限定为单一类型),也就是说,我们使用返回类型为void,参数列表为的函数作为这个消息的响应函数。

button.h

//button.h

#pragma once //#pragma once是一个比较常用的C/C++预处理指令,只要在头文件的最开始加入这条预处理指令,使头文件不会被重复包含。

#include <Windows.h>
#include <map>

class IMAGE; //声明IMAGE类
struct ExMessage; //声明MOUSEMSG结构体

namespace MyUI {
	
	enum class BUTTON_MSG : short {
		NOMSG = 0, //鼠标与按钮无关联
		MOUSE_IN, //鼠标位于按钮内
		MOUSE_LDOWN, //鼠标左键按下
		MOUSE_LUP, //鼠标左键抬起
		MOUSE_RDOWN, //鼠标右键按下
		MOUSE_RUP, //鼠标右键抬起
	};
	
	//Button描述结构体
	struct BUTTON_DESC {
		LPCTSTR pImgFile; //图片路径
		int posx; //图片初始x坐标
		int posy; //图片初始y坐标
		unsigned int width; //图片宽度
		unsigned int height; //图片高度
		unsigned int transparency; //图片透明度
	};

	class Button {
	public:

		Button(const BUTTON_DESC& _btnDesc);
		Button(const Button& _btn) = delete; //禁用默认拷贝构造函数
		Button& operator = (const Button& _btn) = delete; //禁用默认赋值拷贝运算符
		~Button();

		void Draw(const ExMessage& _msMsg); //绘制
		bool BindEvent(BUTTON_MSG _btnMsg, void(*_event)()); //为按钮消息绑定处理函数

	private:
		bool DoEvent(BUTTON_MSG _btnMsg); //执行事件

	protected:
		IMAGE* pImg_; //指向图像对象的指针
		BUTTON_DESC btnDesc_; //按钮属性
		std::map<BUTTON_MSG, void(*)()> eventMap_; //消息映射表

	};
}

button.cpp

//button.cpp

#include "button.h"
#include <graphics.h>
#include <stdexcept>
#pragma comment(lib, "MSIMG32.lib")

namespace MyUI {
	Button::Button(const BUTTON_DESC& _btnDesc) : btnDesc_(_btnDesc) {
		pImg_ = new IMAGE;
		loadimage(pImg_, _btnDesc.pImgFile, _btnDesc.width, _btnDesc.height);
	}

	Button::~Button() {
		delete pImg_;
	}

	void Button::Draw(const ExMessage& _msMsg) {
		HDC imgDC = GetImageHDC(pImg_); //获取图像设备上下文句柄

		BUTTON_MSG bm = BUTTON_MSG::NOMSG;

		//通过坐标值判断鼠标是否在按钮内
		if (_msMsg.x >= btnDesc_.posx && _msMsg.x <= (btnDesc_.posx + btnDesc_.width) &&
			_msMsg.y >= btnDesc_.posy && _msMsg.y <= (btnDesc_.posy + btnDesc_.height)) 
		{
			if (WM_LBUTTONDOWN == _msMsg.message) {
				bm = BUTTON_MSG::MOUSE_LDOWN;
			} 
			else if (WM_LBUTTONUP == _msMsg.message) {
				bm = BUTTON_MSG::MOUSE_LUP;
			}
			else {
				bm = BUTTON_MSG::MOUSE_IN;
			}
		}

		DoEvent(bm);

		// 结构体的第三个成员表示额外的透明度,0 表示全透明,255 表示不透明。
		BLENDFUNCTION bf = { AC_SRC_OVER, 0, btnDesc_.transparency, AC_SRC_ALPHA };

		// 使用AlphaBlend函数实现半透明贴图
		AlphaBlend(
			GetImageHDC(NULL), //设备上下文句柄
			btnDesc_.posx, //绘制的x坐标
			btnDesc_.posy,  //绘制的y坐标
			btnDesc_.width, //在所选设备中绘制图像的宽度
			btnDesc_.height, //在所选设备中绘制图像的高度
			imgDC, //图像设备上下文句柄
			0, 0, //绘制图像起点坐标
			btnDesc_.width, //绘制图像的宽度
			btnDesc_.height, //绘制图像的高度
			bf
		);
	}

	bool Button::BindEvent(BUTTON_MSG _btnMsg, void(*_event)()) {
		//检查_btnMsg是否是BUTTON_MSG的枚举值
		if (_btnMsg < BUTTON_MSG::NOMSG || _btnMsg > BUTTON_MSG::MOUSE_RUP) {
			return false;
		}
		
		eventMap_[_btnMsg] = _event;
	}

	bool Button::DoEvent(BUTTON_MSG _btnMsg) {
		try {
			eventMap_.at(_btnMsg)(); //如果不存在_btnMsg这个key,抛出out_of_range异常
			return true;
		}
		catch (std::out_of_range e) {
			return false;
		}
	}
}

BindEvent方法用来绑定消息处理函数,DoEvent方法是私有成员,在Draw方法里被调用。

main.cpp

#include <Windows.h>
#include <iostream>
#include <graphics.h>
#include <conio.h>
#include <map>
#include "button.h"

constexpr int SCREEN_WIDTH = 640;
constexpr int SCREEN_HEIGHT = 640;

void EventLBtnDown() {
	std::cout << "左键按下" << std::endl;
}

void EventLBtnUp() {
	std::cout << "左键抬起" << std::endl;
}

int main() {
	initgraph(SCREEN_WIDTH, SCREEN_HEIGHT, EW_SHOWCONSOLE); //初始化窗口

	MyUI::BUTTON_DESC btnDesc = { "images/button1.png", 200, 200, 200, 60, 255 }; //按钮描述结构体
	MyUI::Button btn1(btnDesc); //构造按钮对象
	MyUI::BUTTON_MSG btnMsg; //用于接收按键消息的结构体对象

	//绑定消息处理函数
	btn1.BindEvent(MyUI::BUTTON_MSG::MOUSE_LDOWN, EventLBtnDown);
	btn1.BindEvent(MyUI::BUTTON_MSG::MOUSE_LUP, EventLBtnUp);

	BeginBatchDraw();

	while (1) {
		ExMessage msMsg;

		//easyx非阻塞获取鼠标消息,如果peekmessage返回值为false,说明没有消息
		//如果此时不清空msMsg,msMsg的值会是之前的消息值
		if (!peekmessage(&msMsg, EM_MOUSE, true)) {
			msMsg = { 0 }; //没消息就清空msMsg
		}

		btn1.Draw(msMsg);

		FlushBatchDraw();

		Sleep(16); //程序休眠16毫秒
		cleardevice(); //16毫秒后清空窗口中的内容
	}

	EndBatchDraw();

	_getch();
	closegraph(); //关闭窗口

	return 0;
}

可以看到,我们为按钮消息绑定了我们自定义的处理函数。运行结果截图:
在这里插入图片描述
至此,此Button类可以基本投入使用。
还有一些效果如动态按钮(鼠标移入、点击都会产生效果)的实现会在以后的内容中为大家介绍。

文章持续更新中!
求点赞、收藏!欢迎在评论区留言,有问必答!
作者水平有限,如果有误,欢迎指正!
编译环境:Visual Studio 2019、Easyx_20220116

  • 9
    点赞
  • 93
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值