ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Serenity GML Calendar 控件完全指南:从 GML 声明到 LibGUI 源码实现

2026/9/11 5:08:45 拓冰建站 浏览量
Serenity GML Calendar 控件完全指南:从 GML 声明到 LibGUI 源码实现 Serenity GML Calendar 控件完全指南从 GML 声明到 LibGUI 源码实现【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity导读GUI::Calendar是 Serenity 操作系统中基于 GMLGUI Markup Language声明式描述的日历控件用于在 GUI 应用中展示月历或年历视图、处理日期选择与翻页交互。本文以系统手册页 Base/usr/share/man/man5/GML/Widget/Calendar.md 为核心骨架结合 LibGUI 的 Calendar.h 与 Calendar.cpp 源码以及 Taskbar、Calendar 应用与 DatePicker 对话框的真实用法讲解如何在 GML 文件中声明日历控件、如何通过 C API 驱动它、有哪些系统配置项可定制显示行为以及如何通过子类化扩展出自定义日历。Calendar 控件在 Serenity 中的位置GUI::Calendar继承自GUI::AbstractScrollableWidget同时实现了Config::Listener是 LibGUI 提供的标准控件之一。它在系统中有多处实际落地场景任务栏时钟弹窗中的日历ClockWidget.cpp日期选择对话框DatePickerDialog.gml日历应用主界面中的事件日历CalendarWidget.gml 中的Calendar::EventCalendar。在 GML 中它通过GUI::Calendar这一注册名被引用这一能力来自源码中的REGISTER_WIDGET(GUI, Calendar)Calendar.cpp宏注册。GML 中的基本声明方式手册页给出的最小用法如下GUI::Calendar { name: calendar }name属性用于在 C 代码中通过find_descendant_of_type_namedGUI::Calendar(calendar)之类的查找方式定位控件实例。一个更完整的真实示例来自日期选择对话框 DatePickerDialog.gml其中日历被放置在一个固定 200×200 的区域中并通过mode属性指定显示模式GUI::Widget { fixed_width: 200 fixed_height: 200 layout: GUI::VerticalBoxLayout {} GUI::Calendar { name: calendar_view mode: Month } }mode 属性GML 中唯一注册的 Calendar 属性从源码看日历控件在构造时通过REGISTER_ENUM_PROPERTY注册了mode属性Calendar.cppREGISTER_ENUM_PROPERTY(mode, this-mode, this-set_mode, Calendar::Mode, { Calendar::Mode::Month, Month }, { Calendar::Mode::Year, Year });因此 GML 中可以合法地为mode赋值为以下两种枚举值之一值对应枚举显示效果MonthCalendar::Mode::Month单月视图显示星期栏与当前月份的日期格子默认YearCalendar::Mode::Year全年视图12 个月并排展示Mode枚举定义于 Calendar.h构造函数的默认参数为Mode::MonthCalendar.h。set_mode内部在模式不一致时会调用toggle_mode()Calendar.cpp而toggle_mode()会同步切换星期栏、年份标题与月份标题的显示状态并重新计算布局Calendar.cpp。需要说明的是从源码结构看mode是 Calendar 控件向 GML 暴露的唯一注册属性其余显示开关网格、年份、星期栏等主要通过 C API 和系统配置控制详见下文。C 编程接口在纯 C 布局不使用 GML的场合可以直接addGUI::Calendar()创建控件例如任务栏时钟弹窗中就是这样实例化的ClockWidget.cpp。日期与视图控制Calendar.h 提供了一组核心方法// 选中日期 void set_selected_date(Core::DateTime date_time); Core::DateTime selected_date() const; // 视图聚焦的年月区别于选中日期 void set_view_date(unsigned year, unsigned month); unsigned view_year() const; unsigned view_month() const; // 标题格式化ShortMonthYear / LongMonthYear / MonthOnly / YearOnly ErrorOrString formatted_date(Format format LongMonthYear); // 模式切换 Mode mode() const; void set_mode(Mode); void toggle_mode(); // 显示开关 void set_grid(bool); // 是否绘制网格线 void set_show_year(bool b); // 年视图下是否显示年份标题 void set_show_month_and_year(bool b); // 月视图下是否显示月份 年份标题 void set_show_days_of_the_week(bool b); // 是否显示星期栏 // 翻页 void show_previous_date(); void show_next_date(); // 瓦片尺寸 void set_unadjusted_tile_size(int width, int height); Gfx::IntSize unadjusted_tile_size() const;formatted_date依据视图年月生成标题文本ShortMonthYear使用short_month_names如 JanLongMonthYear使用long_month_names如 JanuaryMonthOnly只输出月份全名YearOnly只输出年份数字Calendar.cpp。翻页逻辑在月视图与年视图下语义不同月视图下show_previous_date()将月份减一跨年时年份同步回退年视图下则直接切换年份Calendar.cpp。事件回调控件暴露了四个Function回调成员Calendar.hFunctionvoid() on_scroll; // 滚动翻页时 Functionvoid() on_tile_click; // 点击某个日期格子时 Functionvoid() on_tile_doubleclick;// 双击日期格子时 Functionvoid() on_month_click; // 年视图中点击某个月份格子时任务栏时钟弹窗正是利用这些回调实时更新标题按钮上的日期文本ClockWidget.cpp并将其与上一月/下一月按钮、模式切换按钮联动ClockWidget.cpp。系统配置项以 LibConfig 定制显示行为日历控件在构造函数中读取LibConfig的四个配置项域名为Calendar分组为View见 Calendar.cpp这意味着用户可以通过 Serenity 的Config系统或config命令行工具调整日历的星期起始日、周末定义和默认视图配置键默认值作用FirstDayOfWeekSunday每周的起始日取值见DayOfWeek枚举Sunday…SaturdayFirstDayOfWeekendSaturday周末的第一天WeekendLength2周末持续天数DefaultViewMonth默认视图取Month或Year为Year时同时隐藏星期栏、显示年份标题读取逻辑如下Calendar.cppauto first_day_of_week Config::read_string(Calendarsv, Viewsv, FirstDayOfWeeksv, Sundaysv); m_first_day_of_week static_castDayOfWeek(day_of_week_index(first_day_of_week)); auto first_day_of_weekend Config::read_string(Calendarsv, Viewsv, FirstDayOfWeekendsv, Saturdaysv); auto weekend_length Config::read_i32(Calendarsv, Viewsv, WeekendLengthsv, 2); auto default_view Config::read_string(Calendarsv, Viewsv, DefaultViewsv, Monthsv); if (default_view Year) { m_mode Year; m_show_days false; m_show_year true; m_show_month_year true; }DayOfWeek枚举与m_first_day_of_week、m_weekend_length、m_weekend_length等成员定义于 Calendar.h。day_of_week_index用于将配置字符串映射为枚举下标。由于控件实现了Config::Listener当这些配置在运行时发生变更时config_string_did_change/config_i32_did_change会被回调以刷新显示Calendar.h。底层渲染机制瓦片Tile模型日历的每个日期格子在源码中称为Tile其结构体定义于 Calendar.hstruct Tile { unsigned year; unsigned month; unsigned day; Gfx::IntRect rect; int width { 0 }; int height { 0 }; bool is_today { false }; // 是否为今天 bool is_selected { false }; // 是否为选中日期 bool is_hovered { false }; // 是否悬停 bool is_outside_selected_month { false };// 是否属于相邻月份月视图补位格子 };控件为 12 个月各预分配了 42 个Tile6 行 × 7 列Calendar.cpp保证任何月份都能在固定网格内完整铺满。update_tiles日期格子的填充算法update_tiles(year, month)Calendar.cpp负责计算每个格子的真实年月日以当月 1 号算出其在星期序列中的偏移start_of_month从而确定需要从上一月补位多少天42 个格子依次填充前半段补上一月末尾日期中间段为本月日期末尾段补下一月开头日期通过is_outside_selected_month标记补位格子并据此计算is_selected与is_today与Core::DateTime::now()对比。年视图模式下会循环处理 112 月为每个月单独填充一组格子。resize_event自适应的网格布局控件在resize_event中根据可用尺寸动态计算网格Calendar.cpp月视图按 7 列 × 6 行划分区域可用宽高小于 160×130 时隐藏月份 年份标题格子尺寸小于 30px 时自动隐藏网格线星期名称会根据宽度在四档micro / mini / short / long day names之间切换对应宽度阈值 138 / 200 / 480px年视图12 个月以 4 列 × 3 行排布可用宽高小于 140×120 时隐藏年份标题当月格子过小小于 17×13时退化为只显示月份名称的月份瓦片布局月份名称同样根据宽度在short_month_names与long_month_names之间切换。paint_event随后调用虚函数paint_tile逐个绘制瓦片Calendar.h该虚函数为子类扩展留出了挂钩点见下节。实战扩展继承 Calendar 打造事件日历日历应用Userland/Applications/Calendar在标准GUI::Calendar之上派生了EventCalendar在日期格子上叠加绘制事件文本EventCalendar.cppREGISTER_WIDGET(::Calendar, EventCalendar); void EventCalendar::paint_tile(GUI::Painter painter, GUI::Calendar::Tile tile, Gfx::IntRect tile_rect, int x_offset, int y_offset, int day_offset) { Calendar::paint_tile(painter, tile, tile_rect, x_offset, y_offset, day_offset); if (tile.width tile_breakpoint || tile.height tile_breakpoint) // 格子过小则不绘制 return; for (auto const event : m_event_manager-events()) { auto start event.start; if (start.year() tile.year start.month() tile.month start.day() tile.day) { // 在格子内绘制 HH:MM 事件摘要 文本 auto event_text String::formatted({} {}, start.to_byte_string(%H:%Msv), event.summary); painter.draw_text(/* ... */); } } }关键点通过REGISTER_WIDGET(::Calendar, EventCalendar)将子类注册进 GML 的 widget 名称空间使其能够在 GML 中以Calendar::EventCalendar引用重写paint_tile时先调用基类实现完成基础绘制再叠加自定义内容是典型的继承 装饰扩展模式对应的 GML 文件 CalendarWidget.gml 中直接声明了该子类控件Calendar::EventCalendar { name: calendar }小结GUI::Calendar是 Serenity 中一处兼具声明式易用与深度可定制的控件在 GML 层面只需GUI::Calendar { name: ... }即可嵌入月历/年历并通过mode属性切换形态在 C 层面则有完整的日期控制、显示开关、回调事件与可重写的paint_tile渲染钩子再加上LibConfig提供的星期起始日、周末定义与默认视图等运行时配置足以支撑从任务栏时钟弹窗到完整事件日历应用的各种场景。若需深入了解 GML 语法本身可继续阅读系统手册页 GML.md 及其列出的 GML 使用与语法文档。【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考