使用 EasyX 实现 UI 原理教程(章三 基础 UI 程序的结构与基础按钮)
一篇适合普通大一大二学生(指需要使用 EasyX 构建普通小界面来完成小作业)阅读的文章,在阅读完本文以后,你们已经具备了构造一个适合自己作业中的 UI 库的能力,而往后的所有章节,难度将会提高,所学习的知识对于你用 EasyX 来实现小作业(甚至是部分大作业)没有任何帮助,只会徒增学习成本,特此做出提醒。
正文
在开始第三章的学习之前,我们先来理解几个概念:
- 父与子
图上所示,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 的父窗口。
- 事件(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 写出来,并且将它封装于 object.h 中。
注意:base_object 应继承于 geometry_trigger,所以图中的 x, y,height,width 不用在代码中重复定义。
// object.h
#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 ,接下来进入下一小节的内容
★一些细节上的调整
如果我们直接开始写按钮的话,现在我们的代码还是无法正常运行,我们需要做如下几个调整:
- 在 <cell.h> 中将 image_cell 改为继承自 base_object
#include "object.h"class image_cell :public base_object
{
// ...
}
为什么?因为到时候我们写按钮控件的时候将需要继承自 image_cell 并重写 draw 函数。
- 在 <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));}
};
可以看到,程序正常运行且结果正常
到此如果已经全部明白,可以开始自己尝试实现一个功能简陋的 UI 了~ 是不是跃跃欲试了呢?给大家留个例题:写出一个能够现实文字,自定义背景样式的按钮。如果你对文章内容了解透彻,这绝对将不会花费你超过 20 分钟的时间。
关注B站号:小鱼快来啊,+q粉丝群:725022484 免费领取300G精品编程资料
免费领取300g精品编程资料https://jq.qq.com/?_wv=1027&k=HTkOJ4WE