使用 EasyX 实现 UI 原理教程(章三 基础 UI 程序的结构与基础按钮)
[返回:本文目录](/margoo/easyx-control-menu)
## 写在前面 本篇文章是最后一篇适合普通大一大二学生(指需要使用 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
// <创 建 时 间> : 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 ,接下来进入下一小节的内容
★一些细节上的调整
如果我们直接开始写按钮的话,现在我们的代码还是无法正常运行,我们需要做如下几个调整:
- 在 <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));
}
};
既然一切都已经到位了,不妨来写一段测试代码来测试我们的 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,但本系列的教程才只是个开端。
添加评论
取消回复