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;
}
你可以像这样通过判断按钮返回值作出响应操作(这里是分别调用EventBtnDown和EventBtnUp函数)。
如下,也可以做成一个单输入参数函数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