☰
在 SerenityOS 中使用 REGISTER_WIDGET 注册库级与应用级自定义 Widget:GML 自定义组件完全指南
2026/10/17 14:26:50 网站建设 项目流程

在 SerenityOS 中使用 REGISTER_WIDGET 注册库级与应用级自定义 Widget:GML 自定义组件完全指南

【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity

SerenityOS 的图形界面基于GML(GUI Markup Language)描述,而 GML 之所以能实例化任意控件,核心机制就是REGISTER_WIDGET()宏的组件注册体系。本文以 Base/usr/share/man/man5/GML/Define-widget.md 手册页为主线,结合 LibGUI 的 Widget.h 宏实现 与 LibWebView 的真实使用案例,完整讲解如何让库或应用程序自定义的 Widget 获得 GML 实例化能力,读完即可在自己的应用或库中落地自定义控件。

什么是"库或应用程序自定义 Widget"

SerenityOS 的 GML 文件通过@命名空间::类名 { ... }的形式声明控件树(参见 GML 语法手册),例如:

@GUI::Button { text: "Hello" }

其中GUI::Button是LibGUI 库内置控件。但一些应用和库会发现,把自己定义的控件写进 GML 往往非常有用——例如浏览器应用需要<WebView>这样的自定义视图,系统监控器需要GraphWidget、MemoryStatsWidget这类专用组件。这正是REGISTER_WIDGET()宏存在的意义:它把自定义 C++ 控件类"登记"进 GML 的类注册表,使 GML 解析器能够在运行时按名字构造出对应的控件实例。

在 GML 语法手册 中可以看到这种能力的直接体现——注册过的应用专属控件可以直接写进 GML:

// 注册后,即可在 GML 中使用应用自己的控件 @SystemMonitor::GraphWidget { stack_values: true name: "memory_graph" } @SystemMonitor::MemoryStatsWidget { name: "memory_stats" memory_graph: "memory_graph" }

REGISTER_WIDGET 宏的语法与要求

宏签名

REGISTER_WIDGET()的调用方式与LibGUI内部注册内置控件的方式完全一致:

REGISTER_WIDGET(namespace, class_name)

该宏在 Userland/Libraries/LibGUI/Widget.h 中定义,其完整展开逻辑为:

#define REGISTER_WIDGET(namespace_, class_name) \ namespace GUI::Registration { \ GUI::ObjectClassRegistration registration_##class_name( \ #namespace_ "::" #class_name##sv, []() -> ErrorOr<NonnullRefPtr<GUI::Object>> { return static_ptr_cast<GUI::Object>(TRY(namespace_::class_name::try_create())); }, &registration_Widget); \ }

从展开代码可以看出三件事:

  1. 宏生成一个全局对象GUI::Registration::registration_<class_name>,类型为GUI::ObjectClassRegistration;
  2. 该对象以字符串"命名空间::类名"(如"Web::OutOfProcessWebView")作为注册键,并关联一个工厂 lambda;
  3. 工厂 lambda 调用namespace_::class_name::try_create()来构造实例,父类注册项指向registration_Widget(即GUI::Widget),从而继承 Widget 的全部属性。

命名空间是硬性要求

宏展开后类名以"namespace_::class_name"的完整限定形式作为注册键,因此每个注册的 Widget 都必须放在某个命名空间中。对于通常没有专属命名空间的应用,社区通用的做法是直接使用应用名作为命名空间,例如浏览器应用使用Web(当前仓库中对应代码为 LibWebView/OutOfProcessWebView.cpp 中的REGISTER_WIDGET(WebView, OutOfProcessWebView),即以库名WebView作为命名空间),系统监控器使用SystemMonitor。这样既能避免命名冲突,也能让 GML 中的@命名空间::类名与 C++ 类型一一对应。

必须提供无参构造函数

注册的 Widget必须能够在没有任何参数的情况下被构造(no-argument constructor)。原因从宏展开中即可看出:GML 解析器只知道"类名 + 属性键值对",它无法猜测构造参数,只能依赖无参构造 + 随后设置属性的流程来还原控件。这条约束同时适用于:

  • 类自身的构造函数(如OutOfProcessWebView::OutOfProcessWebView());
  • 注册工厂所调用的try_create()(可返回ErrorOr,便于在构造阶段报错)。

注意:手册页示例写作REGISTER_WIDGET(Web, OutOfProcessWebView),而当前仓库 LibWebView/OutOfProcessWebView.cpp 的实际代码为REGISTER_WIDGET(WebView, OutOfProcessWebView)——请以仓库源码为准,命名空间取WebView。

一个完整的实战示例:OutOfProcessWebView

GML 侧:声明自定义控件

注册完成后,GML 文件就能像使用内置控件一样使用自定义控件:

@Web::OutOfProcessWebView { name: "web_view" min_width: 340 min_height: 160 visible: false }

属性含义与内置控件完全一致:

  • name:控件标识,供 C++ 侧通过find_descendant_of_type_named查找(详见 GML 使用手册);
  • min_width/min_height:最小尺寸约束(支持ui_dimension类型,见 UI Dimensions 手册);
  • visible:初始可见性。

C++ 侧:注册与构造

对应实现位于 Userland/Libraries/LibWebView/OutOfProcessWebView.cpp:

// OutOfProcessWebView.cpp REGISTER_WIDGET(WebView, OutOfProcessWebView) ... OutOfProcessWebView::OutOfProcessWebView() { set_focus_policy(GUI::FocusPolicy::StrongFocus); initialize_client(CreateNewClient::Yes); on_ready_to_paint = [this]() { update(); }; // ... 其余回调初始化 }

值得注意的实现细节:

  • 注册语句放在命名空间外、源文件顶部:REGISTER_WIDGET展开后自建GUI::Registration命名空间,因此在.cpp文件顶部(namespace WebView {之前)直接书写即可,无需包裹在任何命名空间内;
  • 构造函数中完成默认初始化:由于 GML 实例化走无参构造,任何"必须存在的默认行为"(焦点策略、客户端创建、回调绑定)都要在构造函数里完成,而不是依赖外部传入参数;
  • 手册页原示例中的set_should_hide_unnecessary_scrollbars(true)在当前仓库版本已由滚动条机制统一管理,但"构造函数内做默认初始化"的原则不变。

注册机制与 GML 解析器的联动原理

从注册表到实例化

REGISTER_WIDGET生成的GUI::ObjectClassRegistration对象在程序启动时完成静态初始化,把"命名空间::类名"注册进全局类注册表。GML 解析器(位于 LibGUI 的GML子目录)遇到@Foo::Bar {}时,会按字符串在注册表中查找对应的注册项,并调用其工厂 lambda 完成try_create()构造。这在 Userland/Libraries/LibGUI/GML/AutocompleteProvider.cpp 中有直接佐证——GML 的自动补全与校验正是通过GUI::ObjectClassRegistration::find()与for_each()遍历所有已注册控件类来工作的。

控件树加载方式

新式加载采用编译期生成 C++ 代码的方式:在应用的 CMakeLists 中加入compile_gml(),生成实现try_create()的源文件(详见 GML 使用手册)。加载后即可用find_descendant_of_type_named按name属性取回控件:

@MyApp::Widget { @GUI::Button { name: "mem_add_button" text: "M+" } }
m_mem_add_button = *find_descendant_of_type_named<GUI::Button>("mem_add_button");

在运行时还会自动调用类的initialize()成员(若存在),用于绑定回调、挂接数据模型等初始化逻辑。

配套机制:自定义属性(REGISTER_PROPERTY 系列宏)

自定义控件只有控件结构往往还不够,通常还需要暴露控件专属属性,例如SystemMonitor::MemoryStatsWidget的memory_graph属性。这由REGISTER_*_PROPERTY系列宏完成,其通用语法为(详见 GML 属性定义手册):

REGISTER_TYPENAME_PROPERTY(property_name, getter, setter [, additional parameters...]);

示例:

REGISTER_STRING_PROPERTY("alt_text", alt_text, set_alt_text); // enum 属性必须列出所有枚举值与其 GML 字符串表示 REGISTER_ENUM_PROPERTY( "button_style", button_style, set_button_style, Gfx::ButtonStyle, { Gfx::ButtonStyle::Normal, "Normal" }, { Gfx::ButtonStyle::Coolbar, "Coolbar" });

关键约束:

  • 宏在构造函数中调用;property_name是 GML 可用的字符串键;
  • getter不接收参数、返回属性类型值;setter接收属性类型值(int/string/readonly_string/enum/bool为 C++ 值,其余类型为 JSON 值),两者都不可省略但可做成 no-op;
  • 只读字符串用REGISTER_READONLY_STRING_PROPERTY;
  • 大多数宏定义在Core::Object头文件中,布局相关属性位于GUI::Layout头文件中;
  • 自定义控件自动继承父类(GUI::Widget)的所有属性,如x、y、name、visible、min_width等。

实践要点速查

要点说明
宏位置在.cpp文件顶部、命名空间外调用REGISTER_WIDGET(ns, Class)
命名空间必须存在;应用无专属命名空间时,惯例用应用名作命名空间
构造函数必须无参可构造;默认行为初始化一律放入构造函数
注册键"命名空间::类名",GML 中必须用@命名空间::类名完全一致地引用
属性继承自定义控件自动获得GUI::Widget的全部通用属性
自定义属性用REGISTER_*_PROPERTY系列宏在构造函数中注册专属属性
运行时查找通过find_descendant_of_type_named<T>("name")获取 GML 中的实例

参见

  • GML 属性定义手册(Define-property):REGISTER_*_PROPERTY宏详解
  • GML 语法手册(Syntax):GML 文件结构、属性类型与布局语法
  • GML 使用手册(Usage):compile_gml()与try_create()的加载流程
  • GML 内置控件参考(Widget):LibGUI 各内置控件的可用属性
  • UI Dimensions 手册:min_width等尺寸属性的取值规则
  • 源码:宏定义见 Userland/Libraries/LibGUI/Widget.h,实际使用案例见 Userland/Libraries/LibWebView/OutOfProcessWebView.cpp

【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询