目录
一、导言
EasyX 是针对 C/C++ 的图形库,可以帮助使用C/C++语言的程序员快速上手图形编程。如果对于图形界面开发还比较陌生的朋友,可以通过这个图形库快速上手,之后不管是继续学习界面开发还是工作学习中需要用到相关内容都能有一定帮助。
阅读完本文之后你可以了解EasyX图形库的基本概念,并通过学习例程来实现简单的界面开发。
二、环境配置
1.配置说明
EasyX Graphics Library 是针对 Visual C++ 的免费绘图库,支持 VC6.0 ~ VC2022,这意味着使用常用的能够编译C++的环境即可,无需额外的配置。
EasyX 采用静态编译,不依赖任何 dll,同传统程序的发布方式没有区别,程序不会因为引入 EasyX 而增添任何发布负担。
静态链接的 EasyX,会使编译后的 exe 体积增加 70KB 左右。对于绝大多数应用而言,增加的体积是可以忽略的。若将 Visual C++ 的运行时库改为静态链接模式,编译后的 exe 可以单文件运行。
2.开发环境
本文中使用到的开发环境和工具如下:
EasyX Graphics Library for C++(2022-9-1)
Visual Studio Community (17.3.4)
3.安装步骤
由于只是简单的库文件,不需要复杂的配置,按照需要根据提示安装即可。
三、基础概念
1.设备
“设备”,是指绘图表面。在 EasyX 中,设备分两种,一种是默认的绘图窗口,另一种是 IMAGE 对象。通过 SetWorkingImage 函数可以设置当前用于绘图的设备。设置当前用于绘图的设备后,所有的绘图函数都会绘制在该设备上。
一开始对于这个概念不是很理解可以暂时搁置,简单的说法就是,你使用这个图形库进行绘图,那么需要一块画布来作画,设备就是你作画的画桌。
2.坐标
在 EasyX 中,坐标分两种:物理坐标和逻辑坐标。物理坐标是描述设备的坐标体系。坐标原点在设备的左上角,X 轴向右为正,Y 轴向下为正,度量单位是像素。
逻辑坐标是在程序中用于绘图的坐标体系。坐标默认的原点在窗口的左上角,X 轴向右为正,Y 轴向下为正,度量单位是点。
默认情况下,逻辑坐标与物理坐标是一一对应的,一个逻辑点等于一个物理像素。我们可以用图2来理解:
即红色坐标系为物理坐标,而蓝色坐标系为逻辑坐标,大部分函数中使用的是逻辑坐标。
3.颜色
用RGB宏合成颜色,实际上合成出来的颜色是一个十六进制的的整数。如下图的调色盘所示:
这个属性就相当于画笔的色彩,你可以用颜色定义不同的背景和线条来呈现各种效果。
四、设计界面
1.准备工作
- 想要调用EasyX库里的函数,首先调用头文件: <graphics.h>。
- 我们想要画图,首先要有一张画布,创建画布的语法也非常简单,我们需要调用EasyX库里的一个函数: initgraph(int width, int height);
- 使用完画布要将画布关闭,需要调用函数:closegraph( );
#include<graphics.h>
#include<conio.h>//需要调用_getch()函数使程序暂停
int main()
{
const int width = 640;
const int height = 480;
initgraph(width, height);//初始化画布,640是画布宽度,480是画布高度
_getch();//暂停,等待键盘按键
closegraph();//关闭当前画布
return 0;
}
2.绘制函数
作为一个图形库最基础的功能就是画图,EasyX包括了许多绘制函数,如下所示
总的来说都是围绕着坐标来展开的,以点成线,以最简单的画直线为例子,你只需要设置好给定的四个参数(即起点和终点的逻辑坐标),那么就会以起始坐标开始画一条直线到终点坐标
那么以此类推,画圆形就是设定原点坐标和半径,那么根据各种不同的图案组合,你就能得到期望的图形或者效果。
如下例程分别绘制了直线、圆形、矩形和多边形,效果如图所示:
#include<graphics.h>
#include<conio.h>//需要调用_getch()函数使程序暂停
int main()
{
const int width = 640;
const int height = 480;
initgraph(width, height);//初始化画布,640是画布宽度,480是画布高度
setbkcolor(0xFFFFFF);//设置背景色为白色
cleardevice(); //调用清屏cleardevice用背景色刷新背景
setlinecolor(0xFFAAAA);
line(width / 2 - 100, height / 2, width / 2, height / 2 - 100);//画线1
line(width / 2 + 100, height / 2, width / 2, height / 2 + 100);//画线2
line(width / 2, height / 2 - 100, width / 2 + 100, height / 2);//画线3
line(width / 2, height / 2 + 100, width / 2 - 100, height / 2);//画线3
circle(width / 2, height / 2, 100);//画布中心半径100的圆
circle(width / 2, height / 2, 68);//画布中心半径68的圆
rectangle(width / 2 - 100, height / 2 - 100, width / 2 + 100, height / 2 + 100);//圆的外接正方形
POINT pts1[] = { {width / 2, height / 2 - 68}, {width / 2 - 59, height / 2 + 34}, {width / 2 + 59, height / 2 + 34} }; //圆的两个内接三角形
POINT pts2[] = { {width / 2, height / 2 + 68}, {width / 2 - 59, height / 2 - 34}, {width / 2 + 59, height / 2 - 34} }; //坐标信息
polygon(pts1, 3); //画一个无填充多边形
polygon(pts2, 3); //参数1:坐标;参数2:多边形顶点数。
_getch();//暂停,等待键盘按键
closegraph();//关闭当前画布
return 0;
}
这里就同时用到了上一章说的三个基础概念,即设备、坐标、颜色,有了这些理解之后我们可以实现更复杂功能。
五、例程
大部分的函数都可以在文档中找到具体的参数和使用方式,有了各种各样的工具之后我们可以实现什么样的效果是需要发挥一些创造力的。
为了更好的帮助理解和使用好这个图形库,我设计了两个简单的例程,希望看完能对你有所启发。
1.迷宫算法可视化
使用图形库设计一个可以便遍历迷宫(二维数组)找到通路并将寻路过程可视化的程序。
算法部分不是本文探讨的重点,做简单描述,以链栈储存当前路径,在每个节点以上右下左的顺序寻找通路,发现通路入栈并做标记,遇到死路出栈至拥有通路的上一个节点,直到找到出口
界面的设计我们就采用简单的色块,以白色色块填充地图网格(图6),青色作为通路,蓝色作为墙体,红色作为死路,设计好之后效果如图7所示:
int main()
{
// 初始化窗口
initgraph(64*r, 64*r);
setbkcolor(RGB(241, 250, 238));//设置背景色,此时采用RGB宏的方式定义
cleardevice(); //调用清屏cleardevice用背景色刷新背景
setlinecolor(0xFFAAAA); //设置网格线的颜色
for (int y = 0; y <= 64*r; y += 64)
{
// 画线
line(0, y, 64*r, y);
}
for (int x = 0; x <= 64*r; x += 64)
{
// 画线
line(x, 0, x, 64*r);
}
setfillcolor(RGB(69, 123, 157)); //以蓝色方块填充起点终点
fillrectangle(64, 64, 128, 128); //填充起点矩形
fillrectangle(64 * (r - 2), 64 * (r - 2), 64 * (r - 1), 64 * (r - 1)); //填充终点矩形
map(); //初始化地图
MazePath(mazeStatus, startPos, endPos); //循环迷宫算法更新地图
getchar();// 暂停窗口,不让其退出
closegraph(); //关闭画布
return 0;
}
初始化成功后调用迷宫算法循环各个节点,同时在每一个判断分支后更新地图,达到动画的效果,图8展示了实际的运行过程。
int MazePath(int maze[][r], position start, position end) //参数1;迷宫二维数组指针;参数2:起点坐标;参数3:终点坐标。
{
SqStack S;//用于保存路径的栈结构
InitStack(S);
position curPos = start;
int curStep = 1;
do {
if (mazeStatus[curPos.x][curPos.y] == 1)//1表示可通且未访问过
{
mazeStatus[curPos.x][curPos.y] = 2;//2表示访问过
map(); //更新迷宫UI
Sleep(300);
SElemType e = { curStep, curPos, 1 };
Push(S, e); //入栈
if (curPos == end) //抵达终点判断
{
return true;
}
curPos = NextPos(curPos, 1);
curStep++;
}
else {
if (!StackEmpty(S))
{
SElemType t;
Pop(S, t);
curStep--;
while (t.di == 4 && !StackEmpty(S)) //将遍历过四个方向的点标记
{
mazeStatus[t.pos.x][t.pos.y] = 3;//3不可通过
map(); //更新迷宫UI
Sleep(100);
Pop(S, t); //出栈
curStep--;
}
if (t.di < 4) //未完全遍历
{
t.di++;
Push(S, t);
curStep++;
curPos = NextPos(t.pos, t.di);
}
}
}
} while (!StackEmpty(S));
return false;
}
void map() //更新地图
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < r; j++)
{
if(mazeStatus[i][j]==0) //画墙
{
setfillcolor(RGB(29, 53, 87));
fillrectangle(j * 64, i * 64, (j + 1) * 64, (i + 1) * 64);
}
if (mazeStatus[i][j] == 2) //通路
{
setfillcolor(RGB(168, 218, 220));
fillrectangle(j * 64, i * 64, (j + 1) * 64, (i + 1) * 64);
}
if (mazeStatus[i][j] == 3) //死路
{
setfillcolor(RGB(230, 57, 70));
fillrectangle(j * 64, i * 64, (j + 1) * 64, (i + 1) * 64);
}
}
}
}
通过上面这个例子可以看到,我们在画重复图形,且排列之间有一定关系时,可以用循环配合变量进行批量绘制,而不是每个图形都需要常量的坐标值,同样在每次调用之前记得三个要素中的坐标和颜色这两个重点。
如果绘制的图形不能恰好覆盖上一个图形,记得对区域进行清除再上绘制,这样就不会出现预期外图案重叠的情况(除非你本来就想设计这种情况)。
2.打印机任务
基于EasyX实现的模拟8台打印机进程,模拟时间片轮转调度,8台打印机依次占用资源完成打印任务。
动画的设计上我们使用进度条的方式,也就意味着以实心矩形逐步填充空心矩形从而达到进度条的效果,同时能够增加相应的打印任务。
程序逻辑实现采用队列结构,符合FIFO原则,由于这不是本文探讨的重点不做详细论述,重点了解界面的部分。
这个例程加入了响应处理鼠标点击事件和用户输入的交互,这让这个图形库拥有了和大部分GUI相同的交互效果。
你可以通过这个程序学习设计简单的上位机来展示设备的状态以及简单的控制操作。我们直接关注鼠标交互和对话窗口的部分代码:
while (true)
{
HWND hwnd = 0;
POINT pt; GetCursorPos(&pt);//取得鼠标在屏幕中的位置信息传递给pt
ScreenToClient(hwnd, &pt);//将pt的位置信息转换为在hwnd对应窗口的相对位置
int x = pt.x; int y = pt.y;
// 获取一条鼠标消息
if (y > 512) //当鼠标停留在操作区域时
{
m = GetMouseMsg();
switch (m.uMsg)
{
case WM_LBUTTONDOWN: //左键单击来创建任务
input(q, t); //输入事件处理
break;
case WM_RBUTTONUP:
return 0; // 按鼠标右键退出程序
}
}
if (y < 512)
show(q, t); //打印程序运行
FlushMouseMsgBuffer();
}
通过注释我们可以看到,本质上这还是三个基本概念中的坐标要素,通过API获取坐标信息之后,设计相应的响应事件,这就像在Qt界面开发中对应的信号与槽机制,
我们把操作区域想象成一个button,当鼠标停留时,我们停止当前进行的任务,判断鼠标下一步的点击事件,对应不同的点击事件(左键、右键、双击、长按等),设置期望的事件处理函数。
在这个例子中就是当鼠标停留在操作区域,暂停打印机的任务,并等待创建新任务,待用户输入完成后继续完成打印任务,当然这种简单的处理会引发一些问题,
如果用户长时间停留在操作区域而不操作,那么程序将停止不前,但主要是理解事件的响应流程,由于这只是一个图形库,对于较为完善的界面开发的事件响应机制,可以抽象理解为一种中断处理,
点击按钮这个事件(信号)就是触发中断请求,之后会响应中断请求进入中断服务,那么处理这个事件对应的函数(在本例中就是下文的input函数)就是中断处理的代码了。
通过这个例子希望对你以后的界面开发有所帮助。
void input(queue<int> q[], struct txt& t) //输入任务信息
{
char s[10];
InputBox((LPTSTR)s, 10, choose); //用户输入对话框
float i;
sscanf_s(s, "%f", &i); //接收打印机编号
if (i < 8 && i >= 0)
InputBox((LPTSTR)s, 10, enter); //对话框显示输入打印任务
int r;
sscanf_s(s, "%d", &r); //接收打印任务
if (r <= 50 && r > 0)
q[(int)i].push(r * 10); //进入队列
}
整体实现的效果如图10所示,打印机打印完预设的任务后,可以手动添加新的打印任务。
图中进度条的处理相信你通过之前的了解也能轻松的实现,由于灵活性极强,可以设计用来展示下位机的一些状态或是整合数据成为动态的图表。
3.进阶
当然它能做到的远不止如此,由于是一个图形库,可以用于制作各类图形相关的算法可视化或是小工具、小游戏。
(1)车牌定位及分割算法可视化
(2)一个可以拧动和观察的魔方
(3)2048小游戏
六、总结
本文介绍了EasyX图形库的基本概念,以及一些简单的应用,其最大的优势就是环境的安装和配置简单,不需要繁琐的配置,同时对于任何没有接触过界面开发的新手较为友好,
你可以通过简单的学习快速上手,而且编译速度较快,你可以每设计一部分图形就编译运行查看效果,反馈较快,出现错误可以马上调整,虽然没有很复杂很高级的进阶功能,但是仅仅依靠简单的设计就能实现各种界面效果。
当然他的缺点也比较明显,没有可视化的界面设计视图以及较为完善的开发环境,对于需要多种控件和响应事件的应用场景,不如WinForm、QT等开发工具来的便捷,对于更多可能的应用场景欢迎讨论。