图形支持

  YSLib 现阶段不强调图形学功能。作为 GUI 的基础, YSLib 主要在命名空间 Drawing 内提供以下两类图形接口:

  • 在 YSLib::Core::YGDIBase 描述图形的位置、大小等几何属性的对象 PointSizeRect
  • 在 YSLib::Service 中提供简单图形和字形的光栅化、像素操作和块传输等绘制功能。

  在了解 YSLib 开发时会少量涉及上述的第一类接口,需要了解其意义和简单的用法。进一步描述详见接口文档。

标量类型

  YSLib 默认使用整数坐标。表示屏幕坐标位置的有符号数类型 SPos 和大小的无符号数类型 SDst 的范围和平台相关,由 YCLib 提供,一般保证至少 16 位,但应避免依赖其具体范围。

类模板和类类型

  类模板 GBinaryGroup 表示两个标量的有序对,被用于表示屏幕坐标。以 SPos 作为模板参数的实例 PointVec 表示点的位置和二维向量,当前是一致的。

  类 Size 包括两个 SDst 分量,用于表示大小。

  类 Rect 约定了一个边和屏幕坐标系总是共线的矩形:保存一个左上角位置的 Point 和表示宽度和高度的 Size 对象。

  一个 Rect 对象可以直接通过位置和大小构造:

Rect r1(Point(10, 20), Size(40, 50));
Rect r2({10, 20}, {40, 50}); // 同上。
Rect r3(10, 20, 40, 50); // 同上。
Rect r4{10, 20, 40, 50}; // 同上。

  类模板 GGraphics 表示二维图形接口上下文 ,是一个表示缓冲区的指针(不保证具有所有权)和大小组成的数据结构。一般使用的是其实例 GraphicsConstGraphics

  类 PaintContext 包含了 GUI 绘制中的一些必要信息,其中由 Graphics 对象表示目标, Point 对象表示参考位置, Rect 对象表示需要保证绘制的边界范围。

GUI 应用接口概述

Shell 和 GUIState

  Helper::GUIShell 模块提供的类 Shells::GUIShell 是专用于不同平台 GUI 程序处理的 shell ,它隐藏了控制和响应 GUI 需要处理的具体消息,使用户输入被分发到更高级的 UI::GUIState 类的对象中。

  GUIState 是模块 YSLib::UI::YGUI 提供的平台中立的 GUI 公共逻辑处理的实现。默认 GUIState 对象预先构造的单态(monostate) 对象,即被全局共享,可以储存和 GUI 相关的公共状态。

  通过 YSLib::UI::YGUI 提供的函数 UI::FetchGUIState 取得取默认图形用户界面公共状态:

using namespace UI;
auto& state(FetchGUIState());

事件(event)

  UI::GUIState 的成员函数被 Shells::GUIShell 直接或间接调用以按需构造不同的事件

  一般意义的事件本质上是可以容纳回调(callback) 的对象,是发布-订阅模式的实现。用户通过事件提供的接口注册回调,在事件被触发调用时订阅者可以调用这些被预先发布的回调。

  YSLib 事件由模块 YSLib::Core::YEvent 提供的 GEvent 类模板提供,包含多播支持,即 GEvent 中可以有多个回调函数,在触发事件时依优先级和插入顺序调用这些回调函数。 GEvent 支持的回调通过事件处理器(event handler) GHEvent 类模板提供,除限定返回类型 void 外,使用方式基本兼容于 std::function (提供 ISO C++ 定义的可调用对象(callable object) 作为回调),此外提供两个方面的增强:

  • 可比较相等。这意味着 GEvent 不需要保存特定的引用即可支持查询或移除特定回调(若可调用对象自身支持 == 则通过 == 操作定义结果,否则总是认为同类型可调用对象都相等)。
  • 通过 YBase 库模块 YStandardEx::Functional 的 ystdex::make_expanded 模板提供允许比事件处理器提供模板更少参数的可调用对象的支持,允许省略在右边的若干参数。缺乏此支持时,一个可调用对象的函数形参即使未被使用,仍然需要出现在声明的参数列表中,带来一些不便。

  另外, GEvent 的 operator() 会忽略回调抛出的 std::bad_function_call 异常。

  和消息不同, YSLib 的事件默认总是被同步处理的。不同的事件使用不同的枚举标识,携带特定类型的事件参数(event argument) 对象。

  用户通过给事件提供不同的可调用对象作为回调,在其中可以实现应用程序特定的逻辑。

部件(widget)

  部件是 GUI 的可见元素抽象。体现 GUI 逻辑的事件最终被分发到具体的部件上,在部件持有的事件上进行响应。

  部件具有一系列基本的可视化属性,例如位置和大小。其它一些状态决定它如何被绘制以及和其它部件的关系或者具有用户程序关心的数据。 YSLib 中的部件实现模块 YSLib::UI::YWidget 提供的 UI::IWidget 接口以及作为基类的 UI::Widget ,通过处理 UI::Paint 事件使其被绘制。

  基本的部件只能处理 UI::Paint 标识的绘制事件,可以处理其它事件(例如直接响应表示用户输入的事件)的部件称为控件(control) 。模块 YSLib::UI::YControl 提供了控件的基类 UI::Control 。其它大部分部件派生于这个类以提供不同的功能。

  调整部件的特定属性即可以基本完成一个 GUI 应用。

  模块 YSLib::UI::YWidgetEvent 提供了 UI::VisualEvent 枚举标识默认支持的 GUI 事件,其中不同的枚举项表示不同的 GUI 事件,可能对应不同的事件参数类型。默认支持的事件参数类型都是类类型的右值引用类型,但和标准库的右值引用参数类型不同,约定被转移后状态可预测。

  从一个控件取得一个指定事件标识的的事件左值,可以使用 YSLib::UI::YControl 提供的函数模板 UI::FetchEvent 。可以直接在取得的事件上进行操作,所以不必要直接声明一个变量。

using namespace Drawing; // 使用 Drawing::Size 等。模块 YSLib::UI::YControl 已在命名空间 YSLib 中有此声明,若包含了对应的头文件,可以省略。
using namespace UI;
Control ctl(Size(80, 20)); // 创建一个 Control 类型的控件 ctl ,初始大小为 (80, 20) 。
auto& paint_event(FetchEvent<Paint>(ctl)); // 取 ctl 的 UI::Paint 事件,通常是不必要的。

// 使用 lambda 表达式添加回调是简便的做法:
FetchEvent<Click>(ctl) += [](CursorEventArgs&&){
	std::cout << "Control clicked!" << std::endl; 
};
FetchEvent<Click>(ctl) += []{ // 得力于 GHEvent 允许省略参数的特性,这里可以省略没有用到的形式参数。
	std::cout << "Control clicked again!" << std::endl; 
};

  当 ctl 被点击时,上面 UI::Click 事件上添加的两个回调会被依次执行,在标准输出上输出两行字符串。

指定部件的视图属性

  部件的视图属性包括位置大小等。

  上例中通过指定 Size 对象表示部件初始化时的大小。这个类型和下面涉及的位置以及边界类型都由模块 YSLib::Core::YGDIBase 提供,头文件默认已经被包含在 GUI 中所以不需要显式包含。

  位置由二维的点 Point 类型表示。此处并没有明确位置是相对于哪个坐标系的。创建部件以后,并没有直接指定部件应在哪被显示,所以这里的位置本身只具有相对意义。

  也可以通过直接指定 Rect 类型的矩形的边界

Control ctl2(Rect(10, 20, 40, 20)); // 位置为 (10, 20) ,大小为 (40, 20) 。
Control ctl3({10, 20, 40, 20}); // 同上。

  出于动态加载部件的需要, YSLib 提供的部件总是可以通过一个 Rect 值构造以及默认构造(相当于 Rect 为空),因此不限于 UI::Control 使用。

  在创建部件之后,使用函数 GetLocationOfGetSizeOfGetBoundsOf 等查询这些属性;相对地,使用函数 SetLocationOfSetSizeOfSetBoundsOf 等设置这些属性。通过这些函数设置位置和大小会分别触发 MoveResize 事件。

视图树和容器

  YSLib 提供了单一的视图树结构作为显示视图的抽象。通过指定部件所在的容器限定显示的范围。容器自身是一个部件,可以嵌套在另外的部件中。容器中的部件(子部件)的位置使用容器部件的边界的左上角作为原点,即位置表示子部件的边界的左上角相对于容器的边界的左上角。这样的结构典型地构成一颗树,只有作为树根的顶层部件没有容器。

  容器通过 UI::IWdiget 提供一般的迭代遍历支持。不是所有容器都支持动态添加子部件。模块 YSLib::UI::YPanel 提供一般的面板容器 UI::Panel 支持这个特性:

using namespace Drawing;
using namespace UI;
Panel pnl(Size(200, 100));
Control ctl({10, 20, 100, 40});

pnl += ctl; // ctl 作为 pnl 的子部件,在 pnl 左上角的位置 (10, 20) 显示。
yassume(&pnl == FetchContainerPtrOf(ctl)); // 宏 yassume 由 YBase 提供,默认和标准库的宏 assert 行为一致,表示断言; 函数 FetchContainerPtr 取得指向容器的指针。

顶层窗口(top level window)

  部件最终需要通过顶层部件的 UI::Paint 事件被显示。在独立实现中,顶层部件一般是模块 YSLib::UI::YDesktop 提供的 UI::Desktop 。在宿主实现中,由于需要和宿主环境的 GUI 集成,使用不同的接口。在一个有桌面环境的宿主平台中,这样的顶层部件一般称为顶层窗口

  Helper 模块对顶层窗口提供了和平台相关的实现。通过模块 Helper::HostedUI 提供的函数 Host::ShowTopLevel 使一个部件成为一个顶层窗口。

using namespace UI;
Control ctl(Size(200, 100));

Host::ShowTopLevel(ctl);

  对 Windows , Host::ShowTopLevel 支持更多的样式和扩展样式参数调整宿主窗口。具体使用详见 MSDN 。当调用 Host::ShowTopLevel 时,会自动创建具有独立线程和( Win32 意义上的)消息循环 Windows 窗口,这个过程可能发生阻塞。

常用部件

  UI 命名空间下除了 WidgetControlPanelYDesktop 几类直接作为框架必要类型的部件外,还有更多提供不同实用功能的部件。

  注意,非控件的部件不能取 UI::Paint 外的事件,否则会抛出异常。

UI::Label

  这是显示文本标签的部件。

  简单的用法是创建部件后,修改 Text 属性。它接受的是模块 YSLib::Core::YString 提供的 String 类型的 UCS-2 字符串,可以直接通过 std::u16stringconst char16_t* 等构造。因此内建的 u 前缀的字面量可以直接用于赋值 Text 。 YSLib 中 GUI 的其它部分也使用这样的字符串。

UI::Button

  按钮控件。

  类似 UI::Label 可以显示文本,但它是一个控件,可以点击并添加 UI::Click 事件回调。

动态加载

  模块 YSLib::UI::Loader 提供了在运行时读取 NPLA1 配置加载部件视图树的 API 。

  这些 API 当前相对不稳定,可能较容易改变,因此不作详细介绍。具体用法可参照示例程序 YReader 的源代码。

小结

  通过以上讨论,读者可以回顾入门中的示例,分析其中的各个部分的具体意义。

  在这个基础上,参阅 Doxygen 文档查找 UI 命名空间下的其它 API 以开发自己的 GUI 应用。