EGE基础:鼠标消息篇

EGE专栏:EGE专栏

一、鼠标

1. 鼠标按键

  鼠标一般都有左键右键滚轮,通常滚轮除了可以前后滚动之外,还可以被作为按键使用,称为 中键,不过中键很少使用,远不如左键和右键使用频繁。鼠标上的DPI键用于调节鼠标的灵敏度,对应到系统上可以改变指针的移动速度。有些鼠标还会有两个 侧键,一般是作为浏览网页、应用时的前进和后退使用。

  有些鼠标可以进行宏编程,改变鼠标按键按下时电脑接收到的动作信息,从而改变按键功能。

在这里插入图片描述

2. 鼠标动作产生鼠标消息

  鼠标移动、点击,滚轮滚动等动作都会产生鼠标消息,由鼠标内部MCU向电脑发送数据,发送数据的频率和鼠标的回报率有关,普通鼠标一般是每秒向电脑发送100多次数据,回报率越高,延迟越低。而信息发送给系统,经过处理后,系统再将消息转发到对应的窗口,交由窗口对应的程序进行处理。
  那么在EGE中,操作鼠标时窗口都会接收到哪些消息呢?

在线鼠标按键测试

  

鼠标动作涉及按键发送消息数描述
按键按下左键、中键和右键1按键按下只会发送一条鼠标消息,长按也不会再次发送
按键抬起左键、中键和右键1按键抬起时会发送一条鼠标消息
鼠标移动N发送速度一般为每秒百条以上,鼠标芯片轮询,检测到鼠标移动足够距离会发送一条鼠标移动消息,具体精度由DPI和系统设置
滚轮前滚滚轮1滚轮每滚动一小格发送一条消息
滚轮后滚滚轮1滚轮每滚动一小格发送一条消息

二、鼠标消息

1. 鼠标消息所包含的信息

  在EGE中,鼠标消息被统一封装到 mouse_msg 结构体中。我们可以通过结构体中成员值来区分出是消息的类型:是属于按键按下抬起、鼠标移动还是滚轮滚动消息。结构体中还包含触发此次消息的按键,鼠标的位置信息等。同时,还会附加一个信息:触发鼠标消息时,相应的辅助键是否被按下。辅助键和鼠标状态组合,可以产生更多的操作。

在这里插入图片描述

2. 鼠标消息的读取

  系统发送给窗口的鼠标消息,会被EGE处理后保存到自己的消息队列中,用户可以使用 getmouse() 函数从消息队列中读取鼠标消息。鼠标消息的类型为 mouse_msg

  需要注意的是,如果队列中没有鼠标消息,那么getmouse() 函数会一直等待直到获取到鼠标消息再返回。

在这里插入图片描述

  当消息队列为空时, getmouse() 会一直等待,直到有鼠标消息产生,这就有一个很大的问题,如果用户没有动鼠标,那么程序就会一直在 getmouse() 里面循环,无法执行后面的代码。
  所以 应该在读取鼠标消息之前,先用 mousemsg() 函数判断消息队列里有没有鼠标消息,如果没有,就直接跳过读取环节,进行其它的操作。如果消息队列里有鼠标鼠标消息,则说明用户使用鼠标进行了操作,这时就可以将鼠标消息队列中的消息一一取出进行处理。

//当消息队列中有鼠标时,读取鼠标消息并进行处理
while (mousemsg()) {
	//使用 getmouse()读取鼠标消息
	mouse_msg mouseMsg = getmouse();

	//这里对读取到的鼠标消息进行处理
}

  注意了,这里使用了while 循环,而不是使用 if ,这是因为鼠标消息产生的速度是很快的。如果使用if ,那么每次只能处理一条消息,比鼠标消息产生的速度慢上不少,鼠标1秒内产生的消息,程序要两三秒才能处理完,用户会感觉得到明显的鼠标操作延迟。并且这样会导致鼠标消息堆积,队列满后新产生的消息因无法存储而丢失。

  读取到鼠标消息后,就可以从消息中判断出用户进行的鼠标操作,程序就可以对这些操作进行响应。

3. 鼠标操作的判断

  getmouse() 返回的鼠标消息被保存在 mouse_msg 结构体中,现在就要从 mouse_msg 得到鼠标操作的相关信息。

3.1 鼠标消息结构体 mouse_msg

  mouse_msg 定义在 <ege.h> 头文件中,

//鼠标消息结构体
typedef struct mouse_msg {
	int             x;		// 鼠标指针在窗口中的位置x
	int             y;		// 鼠标指针在窗口中的位置y
	mouse_msg_e     msg;	// 消息类型
	unsigned int    flags;	// 键位标志,鼠标按键及辅助键
	int             wheel;	// 滚轮滚动值
	
	// 触发的按键
	bool is_left()  { return (flags & mouse_flag_left) != 0; }
	bool is_right() { return (flags & mouse_flag_right) != 0; }
	bool is_mid()   { return (flags & mouse_flag_mid) != 0; }

	// 消息类型
	bool is_down()  { return msg == mouse_msg_down; }
	bool is_up()    { return msg == mouse_msg_up; }
	bool is_move()  { return msg == mouse_msg_move; }
	bool is_wheel() { return msg == mouse_msg_wheel; }
} mouse_msg;

  以下是一些成员用到的枚举

// 按键对应位
typedef enum mouse_flag_e {
	mouse_flag_left     = 0x01,
	mouse_flag_right    = 0x02,
	mouse_flag_mid      = 0x04,
	mouse_flag_shift    = 0x100,
	mouse_flag_ctrl     = 0x200,
}mouse_flag_e;

// 消息类型枚举
typedef enum mouse_msg_e {
	mouse_msg_down      = 0x10,
	mouse_msg_up        = 0x20,
	mouse_msg_move      = 0x40,
	mouse_msg_wheel     = 0x80,
}mouse_msg_e;

  mouse_msg 结构体中保存的内容如下:

成员变量保存的内容
说明
x, y鼠标指针位置鼠标指针在窗口坐标系中的坐标
msg消息类型类型有按键按下、按键抬起、鼠标移动和滚轮滚动四种
flags触发消息的鼠标按键以及辅助键状态消息最多只能由鼠标左键、中键和右键三键中的其中一个触发,或者没有触发按键。同时保存产生消息时辅助键有没有按下
wheel滚轮滚动值当类型是滚轮消息时,可以通过此值来判断鼠标滚动方向,前滚为正,后滚为负

  为了方便使用,结构体增加了成员函数,用于识别消息类型和触发按键。当获取到鼠标消息后,就可以调用成员函数对消息进行判断,同时获取鼠标指针位置等信息,辅助键状态等信息。
  下面是判别是否是鼠标左键抬起的消息,

mouse_msg msg = getmouse();

// 鼠标左键抬起
if (msg.is_left() && msg.is_up()) {
	
	//获取鼠标位置
	int x = msg.x;
	int y = msg.y;
	
	//检查辅助键是否按下
	bool shiftKeyIsDown = (msg.flags & mouse_flag_shift) != 0;
	bool ctrlKeyIsDown  = (msg.flags & mouse_flag_ctrl ) != 0;
}
从鼠标消息中提取信息

  下面的例程是不断从队列中读取鼠标消息,并从记录的最后一条鼠标消息中提取出相关信息显示在窗口上.

在这里插入图片描述

#include <graphics.h>

int main()
{
	initgraph(640, 480, INIT_RENDERMANUAL);			//初始化窗口
	setcaption("EGE鼠标消息获取");	//设置窗口标题

	setbkcolor(WHITE);
	setcolor(BLACK);
	setfont(18, 0, "黑体");

	//记录读取到的最后一条鼠标消息
	mouse_msg msgRecord = { 0 };
	bool redraw = true;
	int leftMargin = 80;
	for (; is_run(); delay_fps(60))
	{
		//检查是否有鼠标消息,没有则跳过(避免程序阻塞)
		//有则处理每一条鼠标消息
		while (mousemsg())
		{
			//getmouse 获取鼠标消息
			msgRecord = getmouse();
			redraw = true;
		}

		//msg和flag常数请参考文档或者mouse_msg_e, mouse_flag_e的声明
		if (redraw) {
			redraw = false;
			cleardevice();
			xyprintf(leftMargin, 20, "鼠标位置:  x   = %4d     y = %4d",
				msgRecord.x, msgRecord.y);
			xyprintf(leftMargin, 40, "消息类型:move  = %d   down  = %d    up  = %d  wheel = %d",
				msgRecord.is_move(),
				msgRecord.is_down(),
				msgRecord.is_up(),
				msgRecord.is_wheel());
			xyprintf(leftMargin, 60, "按键:    left  = %d    mid  = %d  right = %d",
				msgRecord.is_left(),
				msgRecord.is_mid(),
				msgRecord.is_right());
			xyprintf(leftMargin, 80, "滚轮值:  wheel rotate = %d",
				msgRecord.wheel);
			xyprintf(leftMargin, 100, "辅助键:   shift = %d    ctrl = %d",
				(msgRecord.flags & mouse_flag_shift) != 0,
				(msgRecord.flags & mouse_flag_ctrl) != 0);
		}
	}

	closegraph();

	return 0;
}

3.2 鼠标按键消息的判断

  鼠标的按键消息一共有按键按下按键抬起两种类型的消息。
在这里插入图片描述
  对于一个mouse_msg类型的鼠标消息 msg,如果是鼠标按键消息,那么 is_down()和 is_up() 这两个判断按键是按下还是抬起的函数会有其中一个返回 true,所以可以使用或来判断是否是按键消息。

//是否是按键消息
if (msg.is_down() || msg.is_up()) {
}

  同时,还可以使用 is_left() is_mid()is_rigth() 函数确认具体触发按键消息的按键,和前面判断按键是按下还是抬起的函数组合起来就能识别出是哪个按键进行的操作。
  例如: msg.is_down() && msg.is_left()判断的是鼠标左键按下的动作。

while (mousemsg()) {
	mouse_msg msg = getmouse();
	if (msg.is_left() && msg.is_down())
		// 鼠标左键按下
	}
}
鼠标动作is_left()is_mid()is_right()
is_down()左键按下中键按下右键按下
is_up()左键抬起中键抬起右键抬起
示例:根据鼠标按键消息识别鼠标按键状态

  通过读取鼠标消息,得到鼠标按键状态,然后根据鼠标按键状态在上方或在下方绘制圆。

在这里插入图片描述

#include <graphics.h>

#define MOUSE_KEY_NUM    3

// 枚举:鼠标按键索引
typedef enum KeyIndex
{
	KeyIndex_LEFT  = 0,  // 鼠标左键
	KeyIndex_MID   = 1,  // 鼠标中键
	KeyIndex_RIGHT = 2,  // 
} KeyIndex;

// 枚举:鼠标按键状态
typedef enum KeyState
{
	KeyState_UP   = 0,
	KeyState_DOWN = 1,
} KeyState;

int main()
{
	const int winWidth = 640, winHeight = winWidth * 3 / 4;

	initgraph(winWidth, winHeight, INIT_RENDERMANUAL);
	ege_enable_aa(true);
	setbkcolor(WHITE);

	bool redraw = true;
	int leftMargin = 80;

	//颜色表
	color_t colorTable[MOUSE_KEY_NUM] = { EGERGB(0x30, 0xC9, 0XF7), EGERGB(0xFC, 0xF3, 0x84), EGERGB(0xA8, 0xE3, 0xA9), };

	//记录按键状态
	KeyState mouseKeyState[MOUSE_KEY_NUM];

	for (int i = 0; i < MOUSE_KEY_NUM; i++) {
		mouseKeyState[i] = KeyState_UP;
	}

	setlinewidth(2);
	setcolor(BLACK);

	for (; is_run(); delay_fps(60)) {
	
		// 处理所有鼠标消息
		while (mousemsg()) {
			mouse_msg msg = getmouse();

			// 当鼠标消息是按键消息时进行处理
			if (msg.is_down() || msg.is_up()) {
				//确定是按键按下还是按键抬起
				KeyState keyState = msg.is_down() ? KeyState_DOWN : KeyState_UP;

				//根据触发的按键,改变对应对应按键的状态
				if (msg.is_left())
					mouseKeyState[KeyIndex_LEFT] = keyState;
				else if (msg.is_mid())
					mouseKeyState[KeyIndex_MID] = keyState;
				else
					mouseKeyState[KeyIndex_RIGHT] = keyState;
				redraw = true;
			}
		}

		// 绘图
		if (redraw) {
			cleardevice();

			for (int i = 0; i < MOUSE_KEY_NUM; i++) {
				//计算圆心位置及半径
				const float radius = winHeight * 2 / 11;
				const int cx = (3 + i * 5) * winWidth / 16;
				const int cy = (mouseKeyState[i] == KeyState_UP) ? (3 * winHeight / 11) : (8 * winHeight / 11);

				setfillcolor(colorTable[i]);
				ege_fillellipse(cx - radius, cy - radius, 2 * radius, 2 * radius);
				ege_ellipse(cx - radius, cy - radius, 2 * radius, 2 * radius);
			}
		}
	}

	closegraph();

	return 0;
}

3.3 鼠标移动消息的判断

  鼠标消息的类型如果是移动消息,那么成员函数 is_move() 将返回 true,由此可以辨别出鼠标移动消息并进行处理。

  鼠标的移动消息产生的速度比鼠标的其它消息快得多,移动时速度一般会达到每秒百条以上,所以可以判断是否是移动消息。

mouse_msg msg = getmouse();

if (msg.is_move()) {
	//鼠标移动消息
}

  利用 is_move() 函数判断是否是鼠标移动消息,主要是在处理鼠标拖拽(鼠标按键按住不放,然后移动鼠标)操作的时候使用。

  ① 鼠标按键按下时记录按下时的位置。
  ② 检测到鼠标移动消息时,如果鼠标按键处于按下状态,那么此时就是在进行鼠标拖拽操作,可以根据鼠标指针在按键按下时的位置和当前位置做相应的操作。

  每个鼠标消息产生时都会带有鼠标指针当前的位置,如果仅仅是想获取鼠标指针的位置,并不需要判断消息类型是否是移动消息,直接读取即可,如下所示:

int mousePosX = -1, mousePosY = -1;

while (mousemsg()) {
	mouse_msg msg = getmouse();
	
	//记录鼠标位置
	mousePosX = msg.x;
	mousePosY = msg.y;
}
示例:绘制跟随鼠标的圆

  示例为处理鼠标消息时记录下最后一次产生消息时的鼠标位置,然后在这个位置上绘制一个圆。

  由于在EGE20.08及之前的版本中,如果鼠标没有产生消息,那么获取的鼠标初始位置错误,因此可以使用 win API 中的 GetCursorPos() 和 ScreenToClient() 函数获取初始鼠标在窗口中的位置。

在这里插入图片描述

#include <graphics.h>
#include <Windows.h>

int main()
{
	timeBeginPeriod(1);
	const int winWidth = 640, winHeight = 480;
	initgraph(winWidth, winHeight, INIT_RENDERMANUAL);
	ege_enable_aa(true);
	setbkcolor(WHITE);

	POINT mousePos;

	//获取鼠标初始位置
	GetCursorPos(&mousePos);
	ScreenToClient(getHWnd(), &mousePos);

	setlinewidth(2);
	setcolor(BLACK);
	setfillcolor( EGERGB(0x30, 0xC9, 0XF7));
	const int radius = 64;
	bool redraw = true;

	for (; is_run(); delay_jfps(120)) {
		while (mousemsg()) {
			mouse_msg msg = getmouse();
			// 读取鼠标指针位置
			mousePos.x = msg.x;
			mousePos.y = msg.y;

			redraw = true;
		}

		if (redraw) {
			redraw = false;

			cleardevice();
			ege_fillellipse(mousePos.x - radius, mousePos.y - radius, 2 * radius, 2 * radius);
			ege_ellipse(mousePos.x - radius, mousePos.y - radius, 2 * radius, 2 * radius);
		}
	}

	timeEndPeriod(1);
	closegraph();

	return 0;
}

3.4 鼠标滚轮消息的判断

  滚动滚轮时,可以感觉得到滚轮是一格一格地转动的,并非平滑连续。滚轮每转动一小格产生一次脉冲信号,触发一次滚轮消息。
在这里插入图片描述
  滚轮消息可以使用成员函数 is_wheel() 判断,如果是滚轮消息,可以通过成员 wheel 的值(一般是120的倍数或约数)来判断滚轮滚动的方向,如果是值大于0,鼠标向前滚动,值小于0则是鼠标向后滚动。

while (mousemsg()) {
	mouse_msg msg = getmouse();
	
	//鼠标滚轮消息处理
	if (msg.is_wheel()) {
		if (msg.wheel > 0) {
			// 滚轮前滚
		}else {
			// 滚轮后滚
		}
	}
}
示例:滚轮滚动

  在示例中,程序会读取鼠标的滚轮消息,并根据滚轮滚动方向不断调节目标位置,之后小球会绕着圈旋转至目标位置。
在这里插入图片描述

#include <graphics.h>
#include <math.h>

struct Point
{
	float x;
	float y;
};

struct Transition
{
	double cur;
	double end;
};

struct TrackCircle
{
	Point trackCenter;	// 轨道中心
	float trackRadius;	// 轨道半径
	float circleRadius;	// 圆半径
	float radian;		// 弧度
};


void transform(Transition* transition);
void updateTargetPos(Transition* transition, double targetPosOffset);
void drawTrackCicle(const TrackCircle* trackCircle);

int main()
{
	const int winWidth = 640, winHeight = 480;
	initgraph(winWidth, winHeight, INIT_RENDERMANUAL);
	ege_enable_aa(true);
	setbkcolor(WHITE);

	setlinewidth(2);
	setcolor(BLACK);
	setfillcolor( EGEARGB(0xE0, 0x30, 0xC9, 0XF7));

	const double trackMaxPos = 12.0;

	Transition transition = { 0.0, 0.0 };

	TrackCircle trakcCircle =
	{
		{winWidth/2.0f, winHeight/2.0f},
		156.0f,
		64.0f,
		0.0f,
	};

	for (; is_run(); delay_jfps(60)) {
		int targetPosOffset = 0;

		//处理鼠标消息
		while (mousemsg()) {
			mouse_msg msg = getmouse();

			//处理鼠标滚轮消息,根据滚动值累计位置差值
			if (msg.is_wheel()) {
				targetPosOffset += (msg.wheel > 0) ? 1 : -1;
			}
		}

		// 更新目标位置
		if (targetPosOffset != 0) {
			updateTargetPos(&transition, targetPosOffset);
		}

		transform(&transition);

		trakcCircle.radian = (float)(2.0 * PI * transition.cur / trackMaxPos);

		cleardevice();
		drawTrackCicle(&trakcCircle);
	}
	
	closegraph();
	return 0;
}

void updateTargetPos(Transition* transition, double targetPosOffset)
{
	transition->end += targetPosOffset;
}

void transform(Transition* transition)
{
	const double MIN_FORWARD_DISTANCE = 1.0 / 32.0;

	// 按照固定比例缩短当前位置与目标位置的距离
	if (transition->cur != transition->end) {
		double forwardDistance = 1.0 / 32.0 * (transition->end - transition->cur);

		// 控制最低前进距离,避免前进过慢
		if (fabs(forwardDistance) < MIN_FORWARD_DISTANCE) {
			forwardDistance = (forwardDistance >= 0) ? MIN_FORWARD_DISTANCE : -MIN_FORWARD_DISTANCE;
		}

		// 位置修正,避免越过目标位置
		if (forwardDistance / (transition->end - transition->cur) > (1.0 - 1E-3))
			transition->cur = transition->end;
		else
			transition->cur = transition->cur + forwardDistance;
	}
}

void drawTrackCicle(const TrackCircle* trackCircle)
{
	const Point* trackCenter = &trackCircle->trackCenter;
	const Point circleCenter = {
		trackCenter->x + trackCircle->trackRadius * cosf(trackCircle->radian),
		trackCenter->y + trackCircle->trackRadius * sinf(trackCircle->radian)
	};

	float left = trackCenter->x - trackCircle->trackRadius;
	float top  = trackCenter->y - trackCircle->trackRadius;
	float width = 2 * trackCircle->trackRadius;

	ege_ellipse(left, top, width, width);

	left = circleCenter.x - trackCircle->circleRadius;
	top  = circleCenter.y - trackCircle->circleRadius;
	width = 2 * trackCircle->circleRadius;

	ege_fillellipse(left, top, width, width);
	ege_ellipse(left, top, width, width);
}

三、鼠标事件

1. 鼠标消息处理循环

  从上面的示例可以看出,处理鼠标消息的方式先用 mousemsg() 判断消息队列中有没有鼠标消息,如果有就使用 getmouse() 从队列中读取消息,直至队列中的鼠标消息都读取完毕。

while (mousemsg()) {
	mouse_msg msg = getmouse();
}

  为什么要使用循环一次性将队列中的鼠标消息处理完毕呢?因为当你检测到时队列中存在鼠标消息时,那已经是之前移动鼠标、点击鼠标按键产生的,如果这次不处理完毕,这些消息又会被遗留下来,等待下次处理,这样消息就存在滞后性,用户会感觉到明显的延迟。鼠标消息可能会一直快速产生,如果每次只处理一条,处理的速度跟不上消息产生的速度,那么消息就会堆积得越来越多,所以每次检测到时,及时处理完消息列队中停留的鼠标消息。

1.1 处理不及时所带来的消息滞后

  接下来看看实际情况如何,将上面的示例:跟随鼠标的圆while 循环 改为 if,每次只处理一条鼠标消息。运行示例,可以看到,不断移动鼠标,最后鼠标指针和圆偏离得越来越远,圆无法及时跟随鼠标指针。
  因为帧循环设定的是60FPS,比鼠标消息产生的速度要慢一些,产生的消息总是要过一会才处理到,但这时鼠标指针已经移动到另一个位置了。

  那可不可以通过提高帧率来提高处理速度呢,比如从60FPS提升到120FPS?可以,但是帧率过高会导致其它操作重复进行,耗费CPU资源,并且鼠标消息产生的速度是可以调节的,有些好一点的鼠标每秒能发送1000条消息,这样120FPS仍然无法满足要求,要提高到多少帧率就难以确定,并不是一个好的办法。

在这里插入图片描述

#include <graphics.h>
#include <Windows.h>

int main()
{
	const int winWidth = 640, winHeight = 480;
	initgraph(winWidth, winHeight, INIT_RENDERMANUAL);
	ege_enable_aa(true);
	setbkcolor(WHITE);

	POINT mousePos;

	//获取鼠标初始位置
	GetCursorPos(&mousePos);
	ScreenToClient(getHWnd(), &mousePos);

	setlinewidth(2);
	setcolor(BLACK);
	setfillcolor(EGERGB(0x30, 0xC9, 0XF7));
	const int radius = 64;
	bool redraw = true;

	for (; is_run(); delay_jfps(120)) {
		while (mousemsg()) {
			mouse_msg msg = getmouse();
			// 读取鼠标位置
			mousePos.x = msg.x;
			mousePos.y = msg.y;

			redraw = true;
		}

		if (redraw) {
			redraw = false;

			cleardevice();
			ege_fillellipse(mousePos.x - radius, mousePos.y - radius, 2 * radius, 2 * radius);
			ege_ellipse(mousePos.x - radius, mousePos.y - radius, 2 * radius, 2 * radius);
		}
	}

	closegraph();

	return 0;
}

1.2 消息处理循环中如何对消息进行处理

1.2.1 不应在消息处理循环外进行检测

  我们每次从消息队列中读取鼠标消息时,队列中存放的消息数量可能会有0 至数条不等的消息,这就意味着 不能 按下述代码先读取完所有消息,再在循环外进行检测:

mouse_msg msg = {0};

while (mousemsg()) {
	msg = getmouse();
}

// 错误做法:在循环外检测鼠标消息
if (msg.is_left() && msg.is_down()) {

}

  因为当队列中有多条消息时,循环对同一个变量 msg 赋值,会将之前的值覆盖,只保存最后一条鼠标消息,这样就会有鼠标消息被漏掉。

  比如在鼠标移动时按下鼠标按键,这时消息队列中可能会有多条鼠标消息,因为移动消息发送的速度比较快,所以很可能在最后面的是移动消息,这样按键消息就会被漏掉。

1.2.2 在消息处理循环内对每一条消息进行检测

  我们需要在消息处理循环内对每一条消息进行检测,这样才不会遗漏掉消息。
  当然,因为鼠标移动消息产生的速度过快,可能达每条上千条,因此尽量避免检测到鼠标移动消息时进行复杂绘图等耗时操作,只记录处理数据,循环结束后再统一进行绘图操作。而其他如按下、抬起、滚轮之类的消息一般只有每秒几条,可以加入绘图操作,也可以只记录修改数据,等到后面统一进行绘图。

  鼠标消息中大多数是移动消息,在判断消息类型时,可以优先判断是否是鼠标移动消息,这样可以提高效率。

while (mousemsg())
{
	mouse_msg msg = getmouse();
	if (msg.is_move()) {
		//移动消息
	} else if (msg.is_down())) {
		//按键按下消息
	} else if (msg.is_up()) {
		//按键抬起消息
	} else  {
		//滚轮消息
	}
}

2. 获取鼠标指针位置

2.1 通过鼠标消息获取鼠标指针位置

  鼠标指针位置可以通过读取鼠标消息结构体 mouse_msg中的成员x, y来获取,里面记录的坐标是鼠标消息产生时,鼠标指针在窗口绘图区域中的坐标。

  需要注意的是,当鼠标指针移动到窗口外边缘的位置时,此时程序中所读取到的鼠标指针坐标是会出现超出创建窗口时所设定的大小范围的情况,如出现负值、超出最大值等。这是因为窗口外边缘实际上有个阴影边框,在这里产生的鼠标消息也能被窗口接收到。
  如果在程序中需要根据坐标进行计算,需要先检查坐标值的有效性。

在这里插入图片描述

while (mousemsg()) {
	mouse_msg msg = getmouse();
	int x = msg.x;
	int y = msg.y;
}

  那么有个问题,在没有接收到鼠标消息时,怎么确定鼠标的位置?如果创建窗口后不操作鼠标,窗口是接收不到鼠标消息,此时无法通过鼠标消息得到鼠标指针的位置,这个可以通过winAPI解决

2.2 win API 获取鼠标指针实时位置

  win API 中的 GetCursorPos() 函数可以得到当前鼠标指针在屏幕上的位置,而ScreenToClient() 函数可以将屏幕坐标转换为窗口客户区域坐标,这样就得到了鼠标指针的实时位置。

#include <Windows.h>

//获取鼠标初始位置
POINT mousePos;
GetCursorPos(&mousePos);
ScreenToClient(getHWnd(), &mousePos);

2.3 鼠标指针初始位置问题

  EGE20.08及之前的版本中,在窗口没有接收到鼠标消息之前,EGE的接口是读取不到准确的鼠标指针位置的。可以使用 win API 中的 GetCursorPos() 和 ScreenToClient() 函数读取鼠标指针的实时位置,作为位置初始值。

  读取到初始位置后,后续应该使用EGE的鼠标消息来获取鼠标指针位置。由于消息处理的滞后性,等到你进行处理时,距离鼠标消息产生可能已经过去了十几毫秒的时间。如果鼠标在这个过程中不断移动,鼠标指针位置可能已经有了极大的变化。这时候你获取的鼠标实时位置就就与消息产生时的位置不一样,从而造成误判。

2.4 mousepos() 函数

  mousepos()函数可以获取到EGE窗口所接收到的最后一次鼠标消息时鼠标指针的位置。两个参数都是 int* 型指针,用于返回鼠标指针的位置。

int x, y;
mousepos(&x, &y);

  因为 mousepos()函数是根据窗口接收到的鼠标消息来确定鼠标指针的位置的,这就意味着如果鼠标指针超出窗口范围,返回的位置将是错误的。
  同时,在EGE20.08以及之前的版本,如果窗口创建后鼠标没有动作,那么 mousepos()将会因没有接收到鼠标消息而无法确定鼠标指针的位置,返回一个错误值,在新版本中已完成修复。


3. 鼠标的点击判断

  鼠标按键有左键、中键和右键三个,按键消息只有两种类型:按下和抬起。这就有两种触发点击事件的方式,一种是鼠标按键按下时触发,另一个种是鼠标按键抬起时触发。

鼠标点击事件触发方式条件适用场景
按键按下时触发检测到鼠标消息类型为 按键按下快速响应
按键抬起时触发检测到鼠标消息类型为 按键抬起延迟响应,用于点击按钮、鼠标拖拽、框选等

  因为鼠标按键只会在按下抬起时各发送一条鼠标消息,长按并不会再次发送,所以检测到有按键按下抬起的消息时就可以直接判定为点击。

3.1 鼠标按下时触发点击

  当检测到鼠标消息类型是按键按下时判定为鼠标点击。

  鼠标按下时触发点击可以达到快速响应的效果。当检测到有按键按下时立刻触发点击,把按下时的鼠标指针位置作为点击位置,这时再由按下的按键进行相应的操作即可。

while (mousemsg()) {
	mouse_msg msg = getmouse();
	//以按键按下作为触发鼠标点击条件
	if (msg.is_down()) {
		// 用is_left(), is_mid() 和is_right()判断按键
		// msg.x, msg.y 是按键按下时鼠标的位置
	}
}
示例:鼠标按键按下时绘制圆

  检测到鼠标按键按下后,在指针点击处绘制一个圆,圆的颜色由按下的按键决定。
在这里插入图片描述

#include <graphics.h>

int main()
{
	const int winWidth = 640, winHeight = winWidth * 3 / 4;

	initgraph(winWidth, winHeight, INIT_RENDERMANUAL);
	ege_enable_aa(true);
	setbkcolor(WHITE);

	const float radius = 48.0f;

	setlinewidth(2);
	setcolor(BLACK);

	for (; is_run(); delay_fps(60)) {
		// 处理所有鼠标消息
		while (mousemsg()) {
			mouse_msg msg = getmouse();

			// 当鼠标消息是按键按下时进行处理
			if (msg.is_down()) {
				// 根据触发按键设置不同的颜色
				color_t circleColor;
				if (msg.is_left())
					circleColor = EGERGB(0x30, 0xC9, 0XF7);
				else if (msg.is_right())
					circleColor = EGERGB(0xA8, 0xE3, 0xA9);
				else
					circleColor = EGERGB(0xFC, 0xF3, 0x84);

				// 以鼠标点击处为圆心绘制圆
				setfillcolor(circleColor);
				ege_fillellipse(msg.x - radius, msg.y - radius, 2 * radius, 2 * radius);
				ege_ellipse(msg.x - radius, msg.y - radius, 2 * radius, 2 * radius);
			}
		}
	}

	closegraph();

	return 0;
}

3.2 鼠标按键抬起时触发点击

  检测到鼠标消息类型为按键抬起时判定为鼠标点击。

  当把鼠标按键抬起作为鼠标点击的触发条件时,鼠标按下和抬起并非是独立的,一般是把按键按下时指针的位置作为点击位置,只是点击操作会延迟到按键抬起时才触发。
  在鼠标按键按下时,就可以根据按下的位置对按钮等进行检测,如果有需要,可以记录下此时的位置,。等到按键抬起时,根据之前的检测结果执行相应的点击操作。
在这里插入图片描述

// 记录
int xLeftPress = -1, yLeftClick = -1;

while (mousemsg())
{
	mouse_msg msg = getmouse();
	
	// 检测鼠标左键点击操作
	if (msg.is_left()) {
		if (msg.is_down()) {
			// 按下:记录左键按下位置
			xClick = msg.x;
			yClick = msg.y;
		} else {
			// 抬起:执行点击操作	
		}
	}
}
示例:鼠标点击按钮

  鼠标点击按钮,按钮会改变颜色样式。
在这里插入图片描述

#include <graphics.h>

typedef struct ColorStyle
{
	color_t normal;
	color_t press;
} ColorStyle;

typedef struct Button
{
	float x;
	float y;
	float radius;
	int styleIndex;
	bool press;
} Button;

// 改变按钮样式
void changeButtonStyle(Button* button);

// 获取颜色样式
const ColorStyle* getColorStyle(int index);

// 绘制按钮
void drawButton(const Button* button);

// 判断点击位置是否在按钮区域
bool isPressOnButton(const Button* button, int x, int y);

#define COLOR_STYLE_NUM  3
const ColorStyle colorStyleTable[COLOR_STYLE_NUM] = {
	{EGEACOLOR(0xFF, 0xA0D8EF), EGEACOLOR(0xFF, 0x2CA9E0)},
	{EGEACOLOR(0xFF, 0xFCF384), EGEACOLOR(0xFF, 0xF9CA31)},
	{EGEACOLOR(0xFF, 0xEBC4DB), EGEACOLOR(0xFF, 0xCC7EB0)},
};

int main()
{
	const int winWidth = 640, winHeight = winWidth * 3 / 4;

	initgraph(winWidth, winHeight, INIT_RENDERMANUAL);
	ege_enable_aa(true);
	setbkcolor(WHITE);

	Button button = {winWidth / 2.0f, winHeight / 2.0f, 64.0f, 0, false};

	int xLeftPress = -1, yLeftPress = -1;
	bool flag_pressButton = false;

	for (; is_run(); delay_fps(60)) {
		// 处理所有鼠标消息
		while (mousemsg()) {
			mouse_msg msg = getmouse();

			// 处理鼠标左键点击
			if (msg.is_left()) {
				if (msg.is_down()) {
					xLeftPress = msg.x;
					yLeftPress = msg.y;

					// 如果鼠标在按钮区域按下,设置鼠标为按下状态,标记鼠标按下按钮
					if (isPressOnButton(&button, xLeftPress, yLeftPress)) {
						flag_pressButton = true;
						button.press = true;
					}
				} else {
					//鼠标按下按钮,抬起时触发点击事件
					if (flag_pressButton) {
						flag_pressButton = false;

						// 恢复按钮为非按下状态,并改变按钮样式
						button.press = false;
						changeButtonStyle(&button);
					}
				}
			}
		}
		cleardevice();
		drawButton(&button);
	}

	closegraph();
	return 0;
}

// 改变按钮样式
void changeButtonStyle(Button* button)
{
	// 切换到样式表中下一个样式
	button->styleIndex++;
	if (button->styleIndex >= COLOR_STYLE_NUM) {
		button->styleIndex = 0;
	}
}

// 获取颜色样式
const ColorStyle* getColorStyle(int index)
{
	return &colorStyleTable[index];
}

bool isPressOnButton(const Button* button, int x, int y)
{
	float dx = x - button->x;
	float dy = y - button->y;
	return (dx * dx + dy * dy) <= (button->radius * button->radius);
}

void drawButton(const Button* button)
{
	// 获取按钮当前的颜色样式
	const ColorStyle* colorStyle = getColorStyle(button->styleIndex);

	// 根据按钮是否被按下设置颜色
	if (button->press)
		setfillcolor(colorStyle->press);
	else
		setfillcolor(colorStyle->normal);

	setlinewidth(2);
	setcolor(BLACK);

	ege_fillellipse(button->x - button->radius,button->y - button->radius, 2 * button->radius, 2 * button->radius);
	ege_ellipse(button->x - button->radius,button->y - button->radius, 2 * button->radius, 2 * button->radius);
}

4. 鼠标按键状态

  鼠标按键有按下松开两种状态,按键状态切换时,会发送相应的鼠标消息。

状态切换发送鼠标消息
松开 → \rightarrow 按下按键按下(mouse_msg_down)
按下 → \rightarrow 松开按键抬起(mouse_msg_up)

4.1 读取鼠标消息记录按键状态

  因此,我们可以通过读取鼠标消息来判断鼠标按键的状态:当接收到一条按键按下的鼠标消息时,标记对应按键的状态为按下;当接收到一条按键抬起的鼠标消息时,标记对应按键状态为松开。

// 鼠标左键是否被按下
bool leftKeyIsPressed = false;

while (mousemsg()) {
	mouse_msg msg = getmouse()
	
	if (msg.is_left()) {
		leftKeyIsPressed = msg.is_down();
	}
}

4.2 keystate()函数

  EGE中还有个 keystate() 函数可以获取按键当前的状态,如果按键是按下的,则返回 1,否则返回 0。如果只想知道当前按键的状态而不想知道鼠标做按下按键、移动、松开按键等操作的顺序,那么就可以使用 keystate()。如果交互的功能与鼠标操作的顺序有关(鼠标拖拽等),那就只能使用鼠标消息来进行处理。

  鼠标左中右三个键的键码如下:

	key_mouse_l     = 0x01,		//左键
	key_mouse_r     = 0x02,		//右键
	key_mouse_m     = 0x04,		//中键

  检测鼠标左键是否是按下状态

if (keystate(key_mouse_l)) {
	// 鼠标左键为按下状态
}

示例:根据鼠标按键消息识别鼠标按键状态

  通过读取鼠标消息,得到鼠标按键状态,然后根据鼠标按键状态在上方或在下方绘制圆。

在这里插入图片描述

#include <graphics.h>

#define MOUSE_KEY_NUM    3

// 枚举:鼠标按键索引
typedef enum KeyIndex
{
	KeyIndex_LEFT  = 0,  // 鼠标左键
	KeyIndex_MID   = 1,  // 鼠标中键
	KeyIndex_RIGHT = 2,  // 
} KeyIndex;

// 枚举:鼠标按键状态
typedef enum KeyState
{
	KeyState_UP   = 0,
	KeyState_DOWN = 1,
} KeyState;

int main()
{
	const int winWidth = 640, winHeight = winWidth * 3 / 4;

	initgraph(winWidth, winHeight, INIT_RENDERMANUAL);
	ege_enable_aa(true);
	setbkcolor(WHITE);

	bool redraw = true;
	int leftMargin = 80;

	//颜色表
	color_t colorTable[MOUSE_KEY_NUM] = { EGERGB(0x30, 0xC9, 0XF7), EGERGB(0xFC, 0xF3, 0x84), EGERGB(0xA8, 0xE3, 0xA9), };

	//记录按键状态
	KeyState mouseKeyState[MOUSE_KEY_NUM];

	for (int i = 0; i < MOUSE_KEY_NUM; i++) {
		mouseKeyState[i] = KeyState_UP;
	}

	setlinewidth(2);
	setcolor(BLACK);

	for (; is_run(); delay_fps(60)) {
	
		// 处理所有鼠标消息
		while (mousemsg()) {
			mouse_msg msg = getmouse();

			// 当鼠标消息是按键消息时进行处理
			if (msg.is_down() || msg.is_up()) {
				//确定是按键按下还是按键抬起
				KeyState keyState = msg.is_down() ? KeyState_DOWN : KeyState_UP;

				//根据触发的按键,改变对应对应按键的状态
				if (msg.is_left())
					mouseKeyState[KeyIndex_LEFT] = keyState;
				else if (msg.is_mid())
					mouseKeyState[KeyIndex_MID] = keyState;
				else
					mouseKeyState[KeyIndex_RIGHT] = keyState;
				redraw = true;
			}
		}

		// 绘图
		if (redraw) {
			cleardevice();

			for (int i = 0; i < MOUSE_KEY_NUM; i++) {
				//计算圆心位置及半径
				const float radius = winHeight * 2 / 11;
				const int cx = (3 + i * 5) * winWidth / 16;
				const int cy = (mouseKeyState[i] == KeyState_UP) ? (3 * winHeight / 11) : (8 * winHeight / 11);

				setfillcolor(colorTable[i]);
				ege_fillellipse(cx - radius, cy - radius, 2 * radius, 2 * radius);
				ege_ellipse(cx - radius, cy - radius, 2 * radius, 2 * radius);
			}
		}
	}

	closegraph();

	return 0;
}

5. 鼠标拖拽

  鼠标拖曳即鼠标按键按住不放,然后移动鼠标。

  鼠标的拖拽功能,大致处理如下:
  ① 当鼠标左键按下时,根据鼠标指针位置确定选中的目标,并记录选中的目标和按下的位置。
  ② 鼠标移动时,如果此时按键处于按下状态,那么对目标进行拖曳判断,如果目标可以被拖拽,就根据鼠标指针当前位置和之前按下时的位置执行相应的操作。

  如果总体拖拽的功能数量较少,也可以在按键按下时就确定目标和相应的功能,在鼠标拖拽时就可以对目标进行相应的拖拽处理。
在这里插入图片描述

#include <graphics.h>
#include <math.h>
#include <ShellScalingApi.h>

struct Circle
{
	int x, y;
	float radius;
	int borderWidth;
};

enum Action
{
	Action_NONE = 0,
	Action_MOVE,
	Action_SCALE,
};

bool PointIsInCircle(const Circle* circle, int x, int y, bool containBorder);
double distance(int a, int b);

void moveCircle(Circle* circle, int x, int y);
void initAllCircle(Circle circleArray[], int num);
void setCircleRadius(Circle* circle, int radius);
void drawCircle(const Circle* circle);

int main()
{
	SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
	initgraph(640, 480, INIT_RENDERMANUAL);
	setbkcolor(WHITE);
	setcolor(BLACK);
	setfont(20, 0, "楷体");
	setbkmode(TRANSPARENT);
	ege_enable_aa(true);

	const int CIRCLE_NUM = 3;
	Circle circleArray[CIRCLE_NUM];
	initAllCircle(circleArray, CIRCLE_NUM);

	int xPressPos = -1, yPressPos = -1;
	int circleIndex = -1;

	Circle initialCircle = { 0 };

	Action action = Action_NONE;
	bool redraw = true;

	for (; is_run(); delay_fps(60)) {	
		while (mousemsg()) {
			mouse_msg msg = getmouse();

			//鼠标左键点击位置记录
			if (msg.is_move() ) {
				if (action == Action_MOVE) {
					//根据按下位置到当前位置的偏移量计算目标位置
					int xTarget = initialCircle.x + msg.x - xPressPos;
					int yTarget = initialCircle.y + msg.y - yPressPos;

					moveCircle(&circleArray[circleIndex], xTarget, yTarget);
				}
				else if (action == Action_SCALE) {
					//根据基点和当前位置分别到圆心的距离,计算圆半径
					int xCircle = circleArray[circleIndex].x;
					int yCircle = circleArray[circleIndex].y;
					float radiusDiff = distance(msg.x - xCircle, msg.y - yCircle) - distance(xPressPos - xCircle, yPressPos - yCircle);
					float radius = initialCircle.radius + radiusDiff;

					setCircleRadius(&circleArray[circleIndex], radius);
				}
				redraw = true;
			}
			else if (msg.is_left()) {
				if (msg.is_down()) {
					//设置基点为点击位置
					xPressPos = msg.x;
					yPressPos = msg.y;

					action = Action_NONE;

					//对每个物体进行检测,
					for (int i = 0; i < CIRCLE_NUM; i++) {
						if (PointIsInCircle(&circleArray[i], xPressPos, yPressPos, true)) {
							// 根据左键按下位置是在圆内还是边框,设置拖拽
							if (PointIsInCircle(&circleArray[i], xPressPos, yPressPos, false))
								action = Action_MOVE;
							else
								action = Action_SCALE;

							initialCircle = circleArray[i];
							circleIndex = i;
							break;
						}
					}
				}
				else {
					// 动作复位
					action = Action_NONE;
				}
			}
		}

		//空格键重置
		while (kbmsg()) {
			key_msg msg = getkey();
			if ((msg.key == key_space) && (msg.msg == key_msg_up)) {
				initAllCircle(circleArray, CIRCLE_NUM);
				action = Action_NONE;
				redraw = true;
			}
		}

		//重绘
		if (redraw) {
			redraw = false;
			cleardevice();

			for (int i = CIRCLE_NUM - 1 ; i >= 0; i--)
				drawCircle(&circleArray[i]);

			setcolor(BLACK);
			outtextxy(300, 0, "拖动内圆移动,拖动外环调整大小");
			outtextxy(300, 22, "按空格键重置位置及大小");
		}
	}

	closegraph();

	return 0;
}

bool PointIsInCircle(const Circle* circle , int x, int y, bool containBorder)
{
	double dx = x - circle->x;
	double dy = y - circle->y;
	double radius = circle->radius - circle->borderWidth / 2.0f;

	if (containBorder)
		radius += circle->borderWidth;

	return dx * dx + dy * dy <= radius * radius;
}

double distance(int a, int b)
{
	return sqrt(a * a + b * b);
}

void moveCircle(Circle* circle, int x, int y)
{
	circle->x = x;
	circle->y = y;
}

void initAllCircle(Circle circleArray[], int num)
{
	for (int i = 0; i < num; i++) {
		circleArray[i].x = 100 + i * 200;
		circleArray[i].y = 240;
		circleArray[i].radius = 80.0;
		circleArray[i].borderWidth = 10;
	}
}

void setCircleRadius(Circle* circle, float radius)
{
	if (radius < circle->borderWidth)
		radius = circle->borderWidth;

	circle->radius = radius;
}

void drawCircle(const Circle* circle)
{
	float x = circle->x - circle->radius, y = circle->y - circle->radius;
	float width = circle->radius * 2.0f, height = width;

	//绘制填充圆
	setfillcolor(EGEARGB(0xFF, 0xFF, 0x33, 0xFF));
	ege_fillellipse(x, y, width, height);

	//绘制边框
	setlinewidth(circle->borderWidth);
	setcolor(EGEARGB(0xFF, 0xA0, 0x00, 0xA0));
	ege_ellipse(x, y, width, height);
}

EGE专栏:EGE专栏

  • 65
    点赞
  • 201
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

依稀_yixy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值