目录

1、绘制简单的图形化窗口

2、设置窗口属性

2.1 颜色设置

2.2 刷新

3、基本绘图函数

3.1 绘制直线

3.2 绘制圆

3.3 绘制矩形

4、贴图

4.1 原样贴图

4.1.1 IMAGE变量去表示图片

4.1.2 加载图片 

4.1.3 显示图片

4.2 透明贴图

4.2.1 认识素材

4.3 png贴图

5、按键交互

5.1 小球自由移动

5.2 按键控制小球

6、鼠标交互

1、绘制简单的图形化窗口

在之前,我们一运行程序会显示出来的黑框框是控制台窗口,现在,我们要绘制的是另一个窗口,即图形化窗口

首先需要包含头文件,这里有两个头文件可以使用,任意选择一个即可

1. graphics.h : 包含已经被淘汰的的函数

2. easyx.h : 只包含最新的函数

创建图形化窗口只需要两个函数:

1. 打开图形化窗口:initgraph(int x, int y, int style)

2. 关闭图形化窗口:closegraph()

在initgraph中,x和y是要创建的图形化窗口的大小,第三个参数是创建方式,若传入1则表示创建图形化窗口的同时还要创建控制台窗口,若不传或者传入的不是1,则表示只创建图形化窗口

打开完图形化窗口后一定记得要关闭图形化窗口 

void test_graph()
{
	initgraph(800, 600);
	while (1); // 为了防止一闪而过
	closegraph();
}

图形化窗口的坐标:

2、设置窗口属性

2.1 颜色设置

setbkcolor(颜色)

其中这面的颜色有两种形式传入

1. 颜色宏:既使用颜色英语的大写,这是编译器定义好的,可以在编译器内查看

即可查看可以使用的颜色

2. RGB配置

在电脑自带的画图当中,就可以找到一种颜色对应的RGB

所以我们此时可以写出这样的代码

void test_graph()
{
	initgraph(800, 600);
	setbkcolor(RED);
	while (1); // 为了防止一闪而过
	closegraph();
}

 但运行起来后会发现,图形化窗口仍然是黑色的,并没有变成设置的红色,这是为什么呢?

因为我们没有刷新

2.2 刷新

当我们设置完图形化窗口的颜色后,还需要刷新才能看到我们设置的颜色,否则仍然是initgraph打开的图形化窗口默认的黑色

void test_graph()
{
	initgraph(800, 600);
	setbkcolor(RED);
	cleardevice();
	while (1); // 为了防止一闪而过
	closegraph();
}

这样图形化窗口就变成了红色

3、基本绘图函数

3.1 绘制直线

line(int x, int y, int xx, int yy)

第一、二个参数是起点的坐标,第三、四个参数是终点的坐标

可以使用setlinecolor(颜色)来改变线的颜色

3.2 绘制圆

circle(int x, int y, int r)

第一、二个参数是圆心的坐标,第三个参数是半径

使用circle画出的圆是带线的,带线是指带边框线,并且圆内部的颜色是和图形化窗口的背景颜色相同的,若想要填充圆的颜色,需要使用下面的函数

填充圆,设置填充颜色:setfillcolor(颜色)

带线:fillcircle(int x, int y, int r)

不带线:solidcircle(int x, int y, int r)

注意:要填充圆的颜色只能使用下面这两个函数来画圆,不能使用circle

3.3 绘制矩形

rectangle(int x, int y, int xx, int yy)

第一、二个参数是矩形左上角的点的坐标,第三、四个参数是矩形右下角的点的坐标

填充矩形,设置填充颜色:setfillcolor(颜色)

带线:fillrectangle(int x, int y, int xx, int yy)

不带线:solidrectangle(int x, int y, int xx, int yy)

这里的规则与绘制圆中的规则是类似的,就不过多赘述了

void test_graph()
{
	initgraph(800, 600);
	setbkcolor(RED);
	cleardevice();
	line(0, 0, 800, 600);
	setfillcolor(BLUE);
	fillcircle(100, 100, 50);
	solidrectangle(200, 200, 300, 300);
	while (1); // 为了防止一闪而过
	closegraph();
}

我们可以使用刚刚学习的知识来绘制一个棋盘

void draw()
{
	initgraph(400, 400);
	for (int i = 0; i <= 400; i += 20)
	{
		line(0, i, 400, i);
		line(i, 0, i, 400);
	}
	while (1);
	closegraph();
}

此时画出来的是黑底白线,也可以修饰一下

void draw()
{
	initgraph(400, 400);
	setbkcolor(BLUE);
	cleardevice();
	setlinecolor(BLACK);
	for (int i = 0; i <= 400; i += 20)
	{
		line(0, i, 400, i);
		line(i, 0, i, 400);
	}
	while (1);
	closegraph();
}

此时就变成了蓝底黑线了

4、贴图

4.1 原样贴图

原样贴图就是将一张图片直接原封不动的贴到图形化窗口中

要使用原样贴图有三个步骤:

4.1.1 IMAGE变量去表示图片

图片是一个IMAGE类型的变量,类似于1是一个int类型的变量,所以可以int a = 1,用a来存储1

所以需要先创建一个IMAGE变量来存储图片

4.1.2 加载图片 

使用函数loadimage(IMAGE* img, URL, int width, int height)

第一个参数是指向图片的指针,传参时传的就是图片的地址,第二个参数是图片所在的路径(可以用相对路径,也可以用绝对路径),第三、四个参数可传可不传,不传是就是将图片原本的大小贴到图形化窗口中,若传了则可以修改图片在图形化窗口上的大小

4.1.3 显示图片

putimage(int x, int y, IMAGE* img)

第一、二个参数是图片左上角的坐标,第三个参数是指向图片的指针

此时演示一下。通常,我们会在.cpp的路径下创建一个文件夹,然后将相关资源放到这个文件夹里面,这里我们就可以将图片放到这个文件夹里面

编译器还要修改一下属性,修改成“使用多字节字符集”

void test_maps()
{
	initgraph(800, 600);
	IMAGE img;
	loadimage(&img, "./Res/qiqi.jpg");
	putimage(0, 0, &img);
	while (1); // 为了防止一闪而过
	closegraph();
}

此时会发现,图片太大了,设置的这个图形化窗口太小了,无法完全显示,所以我们可以设置图片的大小。缩放是为了解决图片大小与窗口大小不一致

void test_maps()
{
	initgraph(800, 600);
	IMAGE img;
	loadimage(&img, "./Res/qiqi.jpg", 800, 600);
	putimage(0, 0, &img);
	while (1); // 为了防止一闪而过
	closegraph();
}

此时就可以将图片完全显示了

4.2 透明贴图

当我们需要将两张图片放到同一个图形化窗口中时,其中一张作为背景图,另一张只需要一部分,此时会发现只需要一部分的那一张图片的一部分会遮挡住背景图

让第一张作为背景图,第二种放在其中

void test_maps()
{
	initgraph(800, 600);
	IMAGE img;
	loadimage(&img, "./Res/qiqi.jpg", 800, 600);
	putimage(0, 0, &img);
	IMAGE hz;
	loadimage(&hz, "./Res/hz.jpg",100,100);
	putimage(100, 100, &hz);
	while (1); // 为了防止一闪而过
	closegraph();
}

 

此时第二张图片的背景图也会影响第一张图片,那要如何才能去除第二张图片的背景图呢?

4.2.1 认识素材

首先,我们需要将第二章图片分别做成掩码图和背景图,利用PS

掩码图:想要显示的部分弄成黑色,不想显示的部分弄成白色

背景图:想要显示的部分不动,不想显示的部分弄成黑色

弄好了掩码图和背景图后,按照特定步骤贴图即可

SRCAND  贴掩码图

SRCPAINT  贴背景图   

要先贴掩码图,再贴背景图

void test_maps()
{
	initgraph(800, 600);
	IMAGE img;
	loadimage(&img, "./Res/qiqi.jpg", 800, 600);
	putimage(0, 0, &img);
	IMAGE test[2];
	loadimage(test + 0, "./Res/ym.jpg", 100, 100);
	loadimage(test + 1, "./Res/bj.jpg", 100, 100);
	putimage(100, 100, test + 0, SRCAND);
	putimage(100, 100, test + 1, SRCPAINT);
	while (1); // 为了防止一闪而过
	closegraph();
}

此时运行结果就是正常的

4.3 png贴图

#pragma once
#include<easyx.h>
// 把像素的颜色拆解出来
typedef struct _ARGB
{
	byte a;
	byte r;
	byte g;
	byte b;
}ARGB;
// 颜色拆解
ARGB color2Argb(DWORD c)
{
	ARGB res;
	res.r = (byte)c;
	res.g = (byte)(c >> 8);
	res.b = (byte)(c >> 16);
	res.a = (byte)(c >> 24);
	return res;
}
DWORD argb2Color(ARGB c)
{
	DWORD t = RGB(c.r, c.g, c.b);
	return ((DWORD)c.a) << 24 | t;
}
// 把彩色图转成黑白图
void toGray(IMAGE* src)
{
	DWORD* psrc = GetImageBuffer(src);
	for (int i = 0; i < src->getwidth() * src->getheight(); i++)
	{
		// 获取每一个像素点的颜色值
		ARGB t = color2Argb(psrc[i]);
		// 灰度圈,求三个或者四个颜色值的均值
		byte arv = (t.r + t.g + t.b) / 3;
		ARGB res = { t.a,arv,arv,arv };
		psrc[i] = argb2Color(res);
	}
}
// @png透明贴图
void drawImg(int x, int y, IMAGE* src)
{
	// 变量初始化
	DWORD* pwin = GetImageBuffer();
	DWORD* psrc = GetImageBuffer(src);
	int win_w = getwidth();
	int win_h = getheight();
	int src_w = src->getwidth();
	int src_h = src->getheight();

	// 计算贴图的实际长度
	int real_w = (x + src_w > win_w) ? win_w - x : src_w;       // 处理超出右边界
	int real_h = (y + src_h > win_h) ? win_h - y : src_h;       // 处理超出下边界
	if (x < 0) { psrc += -x;           real_w -= -x; x = 0; }   // 处理超出左边界
	if (y < 0) { psrc += (src_w + -y); real_h -= -y; y = 0; }   // 处理超出上边界

	// 修正贴图起始位置
	pwin += (win_w * y + x);

	// 实现透明贴图
	for (int iy = 0; iy < real_h; iy++)
	{
		for (int ix = 0; ix < real_w; ix++)
		{
			byte a = (byte)(psrc[ix] >> 24);// 计算透明通道的值[0,256) 0为完全透明 255为完全不透明
			if (a > 100) pwin[ix] = psrc[ix];
		}
		// 换到下一行
		pwin += win_w;
		psrc += src_w;
	}
}

5、按键交互

案件交互分为阻塞按键交互和非阻塞按键交互

阻塞按键交互:不按按键时图片不会动

C语言中很多函数都是阻塞型的,如scanf,没有输入时程序就停止在哪里了

非阻塞按键交互:不按按键时图片可以动

接下来我们来实现两个案例

5.1 小球自由移动

struct Ball // 球的结构体
{
	int x;   // 球的横坐标
	int y;   // 球的纵坐标
	int r;   // 球的半径
	int dx;  // 球一次向x轴正方向移动的距离
	int dy;  // 球一次向y轴正方向移动的距离
};
struct Ball ball = { 400,400,15,5,-4 }; // 定义一个球
void DrawBall(struct Ball ball) // 画出球的函数
{
	setfillcolor(RED);
	solidcircle(ball.x, ball.y, ball.r);
}
void MoveBall() // 控制球移动的函数
{
	ball.x += ball.dx;
	ball.y += ball.dy;
}
int main()
{
	initgraph(800, 800);
	while (1)
	{
		DrawBall(ball); // 首先画出一个球
		MoveBall(); // 移动完成后更新了小球的状态,等待下一次进入循环后打印出新状态的小球
		Sleep(20); // 延时20ms
	}
	return 0;
}

但此时小球移动会有轨迹存在,所以在开始画出更新完状态之后的小球时,要先刷新

int main()
{
	initgraph(800, 800);
	while (1)
	{
		cleardevice(); // 刷新
		DrawBall(ball); // 首先画出一个球
		MoveBall(); // 移动完成后更新了小球的状态,等待下一次进入循环后打印出新状态的小球
		Sleep(20); // 延时20ms
	}
	return 0;
}

此时小球只会往一个方向移动,所以我们需要控制一下小球在碰撞到图形化窗口边界时,要反弹

void MoveBall() // 控制球移动的函数
{
	if (ball.x - ball.r <= 0 || ball.x + ball.r >= 800) { ball.dx = -ball.dx; }
	if (ball.y - ball.r <= 0 || ball.y + ball.r >= 800) { ball.dy = -ball.dy; }
	ball.x += ball.dx;
	ball.y += ball.dy;
}

5.2 按键控制小球

当用户按下按键时,也会产生一个值,可以用_getch()来获取,头文件是<conio.h>,然后根据这个值,来对小球做出处理

struct Ball // 球的结构体
{
	int x;   // 球的横坐标
	int y;   // 球的纵坐标
	int r;   // 球的半径
	int dx;  // 球一次向x轴正方向移动的距离
	int dy;  // 球一次向y轴正方向移动的距离
};
struct Ball ball = { 400,400,15,5,-4 }; // 定义一个球
void DrawBall(struct Ball ball) // 画出球的函数
{
	setfillcolor(RED);
	solidcircle(ball.x, ball.y, ball.r);
}
void MoveBall() // 控制球移动的函数
{
	if (ball.x - ball.r <= 0 || ball.x + ball.r >= 800) { ball.dx = -ball.dx; }
	if (ball.y - ball.r <= 0 || ball.y + ball.r >= 800) { ball.dy = -ball.dy; }
	ball.x += ball.dx;
	ball.y += ball.dy;
}
struct Ball myball = { 500,500,15,5,5 }; // 定义一个自己用按键控制的球,不调用MoveBall,而是自己用按键控制小球移动
void KeyDown() // 控制自己用按键控制的球的函数,通过接收按下的按键,来对小球的参数进行修改
{
	int userKey = _getch();
	switch (userKey)
	{
	case 'w':
	case 'W':
	case '72': // 键盘上的方向键
		myball.y -= 5;
		break;
	case 's':
	case 'S':
	case '80': // 键盘上的方向键
		myball.y += 5;
		break;
	case 'a':
	case 'A':
	case '75': // 键盘上的方向键
		myball.x -= 5;
		break;
	case 'd':
	case 'D':
	case '77': // 键盘上的方向键
		myball.x += 5;
		break;
	}
}
void test_myball()
{
	initgraph(800, 800);
	while (1)
	{
		cleardevice(); // 刷新
		DrawBall(ball); // 首先画出一个球
		DrawBall(myball);
		MoveBall(); // 移动完成后更新了小球的状态,等待下一次进入循环后打印出新状态的小球
		KeyDown();
		Sleep(20); // 延时20ms
	}
    closegraph();
}
int main()
{
	test_myball();
	return 0;
}

此时会发现,自己移动的小球也不动了,只有当我们按了按键让按键控制的球动了之后,自己移动的小球也会移动,一旦我们没按按键,自己移动的小球又停了

这是因为KeyDown函数中的_getch函数是阻塞型函数,当我们没有按按键时,程序会停止在这一步,循环无法继续,所以自己动的小球也不会继续走,这时候我们可以使用_kbhit()函数来判断我们是否按了按键,当我们按了按键,才会去走KeyDown

void test_myball()
{
	initgraph(800, 800);
	while (1)
	{
		cleardevice(); // 刷新
		DrawBall(ball); // 首先画出一个球
		DrawBall(myball);
		MoveBall(); // 移动完成后更新了小球的状态,等待下一次进入循环后打印出新状态的小球
		if(_kbhit())
			KeyDown();
		Sleep(20); // 延时20ms
	}
    closegraph();
}

此时就正常了

玩过之后,会发现自己移动的小球移动起来不是很流畅,这是因为Sleep也是阻塞的,通常会使用定时器去控制自由移动的东西,而不使用Sleep

#include<iostream>
#include<graphics.h>
#include<conio.h>
#include<time.h>
using namespace std;
struct Ball // 球的结构体
{
	int x;   // 球的横坐标
	int y;   // 球的纵坐标
	int r;   // 球的半径
	int dx;  // 球一次向x轴正方向移动的距离
	int dy;  // 球一次向y轴正方向移动的距离
};
struct Ball ball = { 400,400,15,5,-4 }; // 定义一个球
void DrawBall(struct Ball ball) // 画出球的函数
{
	setfillcolor(RED);
	solidcircle(ball.x, ball.y, ball.r);
}
void MoveBall() // 控制球移动的函数
{
	if (ball.x - ball.r <= 0 || ball.x + ball.r >= 800) { ball.dx = -ball.dx; }
	if (ball.y - ball.r <= 0 || ball.y + ball.r >= 800) { ball.dy = -ball.dy; }
	ball.x += ball.dx;
	ball.y += ball.dy;
}
struct Ball myball = { 500,500,15,5,5 }; // 定义一个自己用按键控制的球,不调用MoveBall,而是自己用按键控制小球移动
void KeyDown() // 控制自己用按键控制的球的函数,通过接收按下的按键,来对小球的参数进行修改
{
	int userKey = _getch();
	switch (userKey)
	{
	case 'w':
	case 'W':
	case '72': // 键盘上的方向键
		myball.y -= 5;
		break;
	case 's':
	case 'S':
	case '80': // 键盘上的方向键
		myball.y += 5;
		break;
	case 'a':
	case 'A':
	case '75': // 键盘上的方向键
		myball.x -= 5;
		break;
	case 'd':
	case 'D':
	case '77': // 键盘上的方向键
		myball.x += 5;
		break;
	}
}
int Timer(int duration, int id)
{
	static int startTime[10]; // 创建10个定时器
	int endTime = clock();
	if (endTime - startTime[id] > duration)
	{
		startTime[id] = endTime;
		return 1;
	}
	return 0;
}
void test_myball()
{
	initgraph(800, 800);
	while (1)
	{
		cleardevice(); // 刷新
		DrawBall(ball); // 首先画出一个球
		DrawBall(myball);
		if(Timer(20,0))
			MoveBall(); // 移动完成后更新了小球的状态,等待下一次进入循环后打印出新状态的小球
		if(_kbhit())
			KeyDown();
		// Sleep(20); // 延时20ms
	}
	closegraph();
}
int main()
{
	test_myball();
	return 0;
}

定时器的原理就是使用静态数组,因为静态变量只会初始化一次,所以每当函数调用时,数组中储存的还是上一次的值,并且静态数组不需要初始化,因为静态变量会被自动初始化为0。定时器的第一个参数是多少毫秒就需要让自己移动的小球移动一次,第二个参数是使用下标为id的定时器。clock()函数是基类程序运行到这里用了多长的时间,头文件是time.h。当程序运行到这里的时间减去选中的那个定时器的值大于给定的时间duration时,就表示自己移动的小球需要移动了,将下标为id的那个定时器的值改成clock()的结果,并返回1,表示需要移动。

为了不让用按键控制的小球一闪一闪的,还可以使用双缓冲绘图

开始双缓冲:BeginBatchDraw()

结束双缓冲:EndBatchDraw()

显示一帧:FlushBatchDraw()

void test_myball()
{
	initgraph(800, 800);
	BeginBatchDraw();
	while (1)
	{
		cleardevice(); // 刷新
		DrawBall(ball); // 首先画出一个球
		DrawBall(myball);
		if(Timer(20,0))
			MoveBall(); // 移动完成后更新了小球的状态,等待下一次进入循环后打印出新状态的小球
		if(_kbhit())
			KeyDown();
		// Sleep(20); // 延时20ms
		FlushBatchDraw();
	}
	EndBatchDraw();
	closegraph();
}

因为_getch()是阻塞型的函数,所以在移动自己控制的小球时,会一卡一卡的,为了让小球更流畅,可以使用非阻塞型函数来接收按键的值
 

struct Ball // 球的结构体
{
	int x;   // 球的横坐标
	int y;   // 球的纵坐标
	int r;   // 球的半径
	int dx;  // 球一次向x轴正方向移动的距离
	int dy;  // 球一次向y轴正方向移动的距离
};
struct Ball ball = { 400,400,15,5,-4 }; // 定义一个球
void DrawBall(struct Ball ball) // 画出球的函数
{
	setfillcolor(RED);
	solidcircle(ball.x, ball.y, ball.r);
}
void MoveBall() // 控制球移动的函数
{
	if (ball.x - ball.r <= 0 || ball.x + ball.r >= 800) { ball.dx = -ball.dx; }
	if (ball.y - ball.r <= 0 || ball.y + ball.r >= 800) { ball.dy = -ball.dy; }
	ball.x += ball.dx;
	ball.y += ball.dy;
}
struct Ball myball = { 500,500,15,5,5 }; // 定义一个自己用按键控制的球,不调用MoveBall,而是自己用按键控制小球移动
void KeyDown() // 控制自己用按键控制的球的函数,通过接收按下的按键,来对小球的参数进行修改
{
	int userKey = _getch();
	switch (userKey)
	{
	case 'w':
	case 'W':
	case '72': // 键盘上的方向键
		myball.y -= 5;
		break;
	case 's':
	case 'S':
	case '80': // 键盘上的方向键
		myball.y += 5;
		break;
	case 'a':
	case 'A':
	case '75': // 键盘上的方向键
		myball.x -= 5;
		break;
	case 'd':
	case 'D':
	case '77': // 键盘上的方向键
		myball.x += 5;
		break;
	}
}
void KeyDown2()
{
	if (GetAsyncKeyState(VK_UP))    { myball.y -= 5; }
	if (GetAsyncKeyState(VK_DOWN))  { myball.y += 5; }
	if (GetAsyncKeyState(VK_LEFT))  { myball.x -= 5; }
	if (GetAsyncKeyState(VK_RIGHT)) { myball.x += 5; }
}
int Timer(int duration, int id)
{
	static int startTime[10]; // 创建10个定时器
	int endTime = clock();
	if (endTime - startTime[id] > duration)
	{
		startTime[id] = endTime;
		return 1;
	}
	return 0;
}
void test_myball()
{
	initgraph(800, 800);
	BeginBatchDraw();
	while (1)
	{
		cleardevice(); // 刷新
		DrawBall(ball); // 首先画出一个球
		DrawBall(myball);
		if(Timer(20,0))
			MoveBall(); // 移动完成后更新了小球的状态,等待下一次进入循环后打印出新状态的小球
		/*if(_kbhit())
			KeyDown();*/
		KeyDown2();
		// Sleep(20); // 延时20ms
		FlushBatchDraw();
	}
	EndBatchDraw();
	closegraph();
}
int main()
{
	test_myball();
	return 0;
}

用KeyDown2来替代KeyDown,并且因为是非阻塞型的,所以可以不需要if(_kbhit())来判断了

但此时自己控制的小球会移动的非常快,为了让小球速度正常,可以使用定时器

void test_myball()
{
	initgraph(800, 800);
	BeginBatchDraw();
	while (1)
	{
		cleardevice(); // 刷新
		DrawBall(ball); // 首先画出一个球
		DrawBall(myball);
		if(Timer(20,0))
			MoveBall(); // 移动完成后更新了小球的状态,等待下一次进入循环后打印出新状态的小球
		/*if(_kbhit())
			KeyDown();*/
		if(Timer(20,1))
			KeyDown2();
		// Sleep(20); // 延时20ms
		FlushBatchDraw();
	}
	EndBatchDraw();
	closegraph();
}

此时使用的是下标为1的定时器

6、鼠标交互

当用户点击鼠标的按键时,就会产生一个值,类型是ExMessage,称为鼠标消息,鼠标消息实际上是一个结构体

获取鼠标消息peekmessage(&变量)

讨论鼠标消息 

msg.message:区分鼠标消息的类型,用户按下的值存储在结构体变量msg的message中

msg.x   msg.y: 鼠标当前的坐标

 

void test_mouse()
{
	initgraph(800, 800);
	ExMessage msg;
	// 按左键画圆,按右键画矩形
	while (1)
	{
		while (peekmessage(&msg))
		{
			switch(msg.message) // 根据用户按下的按键,判断在当前鼠标位置画哪一个
			{
			case WM_LBUTTONDOWN:
				circle(msg.x, msg.y, 30);
				break;
			case WM_RBUTTONDOWN:
				rectangle(msg.x - 10, msg.y - 10, msg.x + 10, msg.y + 10);
				break;
			}
		}
	}
	closegraph();
}
int main()
{
	test_mouse();
	return 0;
}

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部