Margoo

...?

使用 EasyX 实现 UI 原理教程(章三 基础 UI 程序的结构与基础按钮)

[返回:本文目录](/margoo/easyx-control-menu)

## 写在前面 本篇文章是最后一篇适合普通大一大二学生(指需要使用 EasyX 构建普通小界面来完成小作业)阅读的文章,在阅读完本文以后,你们已经具备了构造一个适合自己作业中的 UI 库的能力,而往后的所有章节,难度将会提高,所学习的知识对于你用 EasyX 来实现小作业(甚至是部分大作业)没有任何帮助,只会徒增学习成本,特此做出提醒。

正文

在开始第三章的学习之前,我们先来理解几个概念:

  1. 父与子

窗口

如上图所示,Form 1 窗口下有一 LineText 3 控件与 Button 2 控件,假设 LineText 3 与 Button 2 是 Form1 下一控件,那么我们便说 Form 1 是 LineText 3 与 Button 2 的“父亲”,相对的 LineText 3 与 Button 2 我们叫做“儿子”,专业点说 LineText 3 与 Button 2 是 Form 1 的子控件,Form 1 是 Button 2 与 LineText 3 的父窗口。

  1. 事件(Event)

如果你有略微接触过 Win32 Api 的话,你肯定知道“事件”的概念,事件,简单来说就是用户的任意交互(例如:点击鼠标,移动鼠标,按下键盘),或者是一些提示消息,你可以把你的程序想象成人体的神经系统,你的函数 / 类就是神经元细胞,函数 / 类之间的通信就是基于事件(Event)。

在前两章中,我们已经学会了触发器以及绘图单元的相关知识,所以今天我们来正式进入 UI 的大门,首先我们要了解的是 UI 的基础结构。

我们需要了解 UI 是如何处理鼠标消息并且将它传递给控件的,不知道读者是否还记得我们第一章里边是如何实现触发器的 is_trigger 函数与 main 函数的,忘记了也没关系我把代码放在下面。

// rectangle_trigger 的 is_trigger 函数
bool is_trigger(ExMessage message)
{
	// 判断鼠标是否在矩形范围内
	return (message.x >= x && message.x <= x + width &&
		 	message.y >= y && message.y <= y + height);
}
int main()
{
	initgraph(640, 480, SHOWCONSOLE);

	setfillcolor(WHITE);

	// 绘制矩形和圆
	fillrectangle(40, 40, 90, 90);
	solidellipse(120, 40, 160, 80);

	// 测试触发器
	rectangle_trigger  rect_trigger(40, 40, 50, 50);
	geometry_trigger   geom_trigger(shape_geometry::CIRCLE, {120 - 20, 40 - 20, 40, 40, 20});

	ExMessage message;

	while (true)
	{
		getmessage(&message);

		if (message.message == WM_LBUTTONDOWN)
		{
			if (rect_trigger.is_trigger(message) == true)
			{
				printf("■");
			}
			if (geom_trigger.is_trigger(message) == true)
			{
				printf("●");
			}
		}
	}

	_getch();

	return 0;
}

当时在第一章我并没有解释为什么传参要是 ExMessage 与为什么要这样子用 getmessage 去写 main,其实这就是我们今天的内容:UI 的结构。

其实对于鼠标事件的获取,应该是使用 getmessage 一直去等消息,如果 main 获得了消息,后消息类型的判断然后传递给控件,整个流程从原理上来讲是可以用单线程来实现的。

接下来我们以实现一个基础控件:按钮,为例子来实现一个基础的 UI,这里我分成几部分供读者阅读。

★一切的基石 - Object

一般来说,因为 UI 库里的各种控件,大多数时候他们都是殊途同归的,所以为了方便我们在继承派升上下文章,我们需要所有类(无论控件还是判断器还是绘图单元)都要继承一个基础的类:base_object,base_object 要求需要拥有几个虚函数 mouse_pressed_event(),mouse_move_event(),mouse_released_event()负责用户或默认触发方法,且需要一个 process_event 处理 main 传入的 ExMessage,除此之外还需要一些属性如父子关系等,这里我给大家绘制了一个结构体供大家参考。

base_object的结构示意图

有了上面这一个大概的梳理,我们能很轻易地将 base_object 写出来,并且将它封装于 object.h 中。

注意:base_object 应继承于 geometry_trigger,所以图中的 x, y,height,width 不用在代码中重复定义。

////////////////////////////////////
//      	  object.h
// <创 建 时 间> : 2021/11/13
// <最后修改时间> : 2021/11/13
// <作      者>  : Margoo
// <邮      箱>  : 1683691371@qq.com
//
#pragma once

#include "trigger.h"

#include <vector>

// 一切基石 base_object
class base_object :
	public geometry_trigger
{
private:
	// 焦点是否曾经在控件上
	bool used_to_on = false;

	// 父对象
	base_object*			  parent = nullptr;
	// 子对象
	std::vector<base_object*> child_objects;

public:
	base_object(base_object* init_parent = nullptr)
		: parent(init_parent)
	{

	}

public:
	// 处理事件消息 (一般来说没有必要修改)
	virtual void event_process(ExMessage message)
	{
		if (is_trigger(message) == true)
		{
			switch (message.message)
			{
			case WM_LBUTTONDOWN:
			{
				used_to_on = true;

				mouse_pressed_event();
			}
			
			default:
			{
				mouse_move_event();
			}
			}
		}
		else
		{
			if (used_to_on == true)
			{
				mouse_released_event();
			}
		}
	}

	// 用户自定消息处理函数
	virtual void mouse_pressed_event()
	{

	}
	virtual void mouse_released_event()
	{

	}
	virtual void mouse_move_event()
	{

	}
};

利用先前已经写好了的触发器,我们非常快速地就完成了 base_object ,接下来进入下一小节的内容

★一些细节上的调整

如果我们直接开始写按钮的话,现在我们的代码还是无法正常运行,我们需要做如下几个调整:

  1. 在 <cell.h> 中将 image_cell 改为继承自 base_object
#include "object.h"

class image_cell :
	public base_object
{
// ...
}

为什么?因为到时候我们写按钮控件的时候将需要继承自 image_cell 并重写 draw 函数。

  1. 在 <trigger.h> 中,最上方加入如下的代码:
// Anti easyx _WINVER def
#ifndef WINVER
#	define WINVER 0x0500
#endif

这句代码是什么意思呢?主要的意思是设置当前程序的最低兼容版本,为什么要这么干呢?主要原因是因为 EasyX 为了兼容老旧系统会将 WINVER 定义为 0x0400 而这样的结果会导致部分将来要用到的功能无法正常使用。

★正式开始写 pushbutton 控件

万事俱备,经过前面缕清过逻辑以及写完了 trigger 还有绘图单元相关代码,相信对于你来说写一个 pushbutton 已经不是一艰难事了,这里我也不多言,直接贴我的代码:

#pragma once

#include "object.h"
#include "cell.h"

#include <functional>

// 自动创建矩形的掩码图
IMAGE* get_fillrectangle_mask(int width, int height)
{
	IMAGE* result = new IMAGE(width, height);

	SetWorkingImage(result);

	setfillcolor(BLACK);

	fillrectangle(0, 0, width, height);

	SetWorkingImage();

	return result;
}

class pushbutton :
	public image_cell
{
public:
	std::function<void()> mouse_on_clicked;

public:
	void draw()
	{
		mask = get_fillrectangle_mask(width, height);

		setfillcolor(WHITE);

		fillrectangle(x, y, width + x, height + y);
	}

public:
	pushbutton(base_object* init_parent = nullptr)
	{
		parent = init_parent;
	}

public:
	void mouse_pressed_event()
	{
		mouse_on_clicked();
	}

	void mouse_move_event()
	{
		// 设置鼠标样式 有代入感
		SetClassLongPtr(GetHWnd(), GCLP_HCURSOR, reinterpret_cast<LONG_PTR>(IDC_HAND));
	}

	void mouse_released_event()
	{
		// 设置鼠标样式 有代入感
		SetClassLongPtr(GetHWnd(), GCLP_HCURSOR, reinterpret_cast<LONG_PTR>(IDC_ARROW));
	}
};

既然一切都已经到位了,不妨来写一段测试代码来测试我们的 pushbutton

#include <conio.h>

#include "button.h"

void mouse_on_clicked()
{
	MessageBox(GetHWnd(), L"Hello World", L"Hello World", MB_OK);
}

int main()
{
	initgraph(640, 480);

	pushbutton button;

	button.mask = get_fillrectangle_mask(40, 40);

	button.x = 80;
	button.y = 80;
	button.width = 40;
	button.height = 40;

	button.mouse_on_clicked = mouse_on_clicked;

	ExMessage message;

	while (true)
	{
		getmessage(&message);

		button.draw();
		button.event_process(message);
	}

	_getch();

	return 0;
}

可以看到,程序正常运行且结果正常

效果图

至此本章完结,其实读者到此如果已经全部明白,已经可以开始自己尝试实现一个功能简陋的 UI 了~ 是不是跃跃欲试了呢?这边给大家留个作业:写出一个能够现实文字,自定义背景样式的按钮。如果你对本章内容了解透彻,这绝对将不会花费你超过 20 分钟的时间。

接下来我们将会一步一步地去解读我写的 UI 库,刨析其的实现, 现在的读者们已经算是初步入门了 UI,但本系列的教程才只是个开端。

添加评论