3.6 DOM 操作
虽然数据绑定可以处理大部分 UI 更新,但有时你需要直接操作 DOM 元素来创建动态内容、实现复杂交互或优化性能。本节将详细介绍 RmlUi 的 DOM 操作 API。
一、元素查找和访问
1.1 获取元素的方式
cpp
#include <RmlUi/Core.h>
// 假设我们已经有一个文档
Rml::ElementDocument* document = context->LoadDocument("ui.rml");
// 方式 1:通过 ID 获取单个元素
Rml::Element* button = document->GetElementById("submit-btn");
// 方式 2:通过选择器获取单个元素
Rml::Element* firstItem = document->QuerySelector(".item");
Rml::Element* activeItem = document->QuerySelector(".item.active");
// 方式 3:通过选择器获取多个元素
Rml::ElementList items;
document->QuerySelectorAll(items, ".item");
// 方式 4:遍历子元素
Rml::Element* container = document->GetElementById("container");
for (int i = 0; i < container->GetNumChildren(); ++i)
{
Rml::Element* child = container->GetChild(i);
// 处理子元素
}
// 方式 5:获取特定类型的子元素
for (auto* child : container->GetChildren())
{
if (child->GetTagName() == "button")
{
// 处理按钮元素
}
}1.2 元素导航
cpp
// 获取父元素
Rml::Element* parent = element->GetParentNode();
// 获取文档
Rml::ElementDocument* doc = element->GetOwnerDocument();
// 获取上下文
Rml::Context* context = doc->GetContext();
// 兄弟元素
Rml::Element* nextSibling = element->GetNextSibling();
Rml::Element* prevSibling = element->GetPreviousSibling();
// 第一个和最后一个子元素
Rml::Element* firstChild = element->GetFirstChild();
Rml::Element* lastChild = element->GetLastChild();1.3 元素信息
cpp
// 标签名
Rml::String tagName = element->GetTagName(); // "div", "button", etc.
// ID
Rml::String id = element->GetId();
// 类名
Rml::String className = element->GetClassNames();
// 检查是否包含某个类
bool hasClass = element->GetClassList().Contains("active");
// 属性
Rml::String value = element->GetAttribute<Rml::String>("data-value", "default");
bool hasAttribute = element->HasAttribute("data-value");
// 获取所有属性
const Rml::AttributeList& attributes = element->GetAttributes();
for (const auto& attr : attributes)
{
printf("Attribute: %s = %s\n",
attr.first.c_str(),
attr.second.Get<Rml::String>().c_str());
}
// 尺寸和位置
Rml::Vector2f size = element->GetClientSize();
Rml::Vector2f pos = element->GetAbsoluteLeftTop();
Rml::Vector2f offset = element->GetRelativeOffset();二、元素创建和删除
2.1 创建元素
cpp
// 方式 1:使用文档创建元素
Rml::ElementDocument* doc = context->LoadDocument("template.rml");
Rml::Element* newElement = doc->CreateElement("div");
// 方式 2:使用上下文创建元素
Rml::Element* button = context->CreateElement("button");
// 设置属性
newElement->SetId("my-element");
newElement->SetClassNames("my-class another-class");
newElement->SetAttribute("data-id", "123");
newElement->SetAttribute("data-value", 42);
// 设置内容
newElement->SetInnerRML("Hello, World!");
// 设置样式
newElement->SetProperty(Rml::PropertyId::Color,
Rml::Property(Rml::Colourb(255, 0, 0)));
newElement->SetProperty(Rml::PropertyId::FontSize,
Rml::Property(Rml::Unit::PX, 16));
newElement->SetProperty(Rml::PropertyId::Padding,
Rml::Property(Rml::Unit::PX, 10));2.2 添加和删除子元素
cpp
Rml::Element* parent = document->GetElementById("container");
// 添加子元素
parent->AppendChild(newElement);
// 插入到指定位置
parent->InsertBefore(newElement, referenceElement);
// 替换子元素
parent->ReplaceChild(newElement, oldElement);
// 删除子元素
parent->RemoveChild(oldElement);
// 从父元素移除自己
element->GetParentNode()->RemoveChild(element);
// 删除所有子元素
parent->SetInnerRML("");2.3 动态创建列表
cpp
class DynamicList
{
public:
DynamicList(Rml::Element* container) : container_(container) {}
void AddItem(const Rml::String& text, const Rml::String& icon = "")
{
// 创建列表项
Rml::Element* item = container_->GetOwnerDocument()->CreateElement("div");
item->SetClassNames("list-item");
// 添加图标
if (!icon.empty())
{
Rml::Element* img = container_->GetOwnerDocument()->CreateElement("img");
img->SetAttribute("src", icon);
item->AppendChild(img);
}
// 添加文本
Rml::Element* label = container_->GetOwnerDocument()->CreateElement("span");
label->SetInnerRML(text);
item->AppendChild(label);
// 添加删除按钮
Rml::Element* deleteBtn = container_->GetOwnerDocument()->CreateElement("button");
deleteBtn->SetClassNames("delete-btn");
deleteBtn->SetInnerRML("×");
deleteBtn->AddEventListener(Rml::EventId::Click,
[this, item](Rml::Event*) {
container_->RemoveChild(item);
UpdateIndices();
});
item->AppendChild(deleteBtn);
// 存储索引
item->SetAttribute("data-index", container_->GetNumChildren());
// 添加到容器
container_->AppendChild(item);
// 更新所有索引
UpdateIndices();
}
void Clear()
{
container_->SetInnerRML("");
}
void RemoveItem(int index)
{
if (index >= 0 && index < container_->GetNumChildren())
{
container_->RemoveChild(container_->GetChild(index));
UpdateIndices();
}
}
private:
Rml::Element* container_;
void UpdateIndices()
{
for (int i = 0; i < container_->GetNumChildren(); ++i)
{
container_->GetChild(i)->SetAttribute("data-index", i);
}
}
};
// 使用示例
Rml::Element* listContainer = document->GetElementById("list-container");
DynamicList list(listContainer);
list.AddItem("项目 1", "icons/item1.png");
list.AddItem("项目 2", "icons/item2.png");
list.AddItem("项目 3", "icons/item3.png");三、属性和样式操作
3.1 类名操作
cpp
// 添加类
element->AddClass("active");
// 移除类
element->RemoveClass("active");
// 切换类
element->ToggleClass("active");
// 检查是否包含类
bool isActive = element->GetClassList().Contains("active");
// 设置所有类
element->SetClassNames("class1 class2 class3");
// 获取所有类
Rml::String classNames = element->GetClassNames();
// 使用 ClassList 进行更复杂的操作
Rml::ClassList& classList = element->GetClassList();
classList.AddClass("highlighted");
classList.RemoveClass("dimmed");3.2 样式操作
cpp
// 设置单个属性
element->SetProperty(Rml::PropertyId::Width,
Rml::Property(Rml::Unit::PX, 200));
element->SetProperty(Rml::PropertyId::Height,
Rml::Property(Rml::Unit::PX, 100));
// 设置颜色
element->SetProperty(Rml::PropertyId::Color,
Rml::Property(Rml::Colourb(255, 255, 255)));
element->SetProperty(Rml::PropertyId::BackgroundColor,
Rml::Property(Rml::Colourb(0, 0, 0, 128)));
// 设置边距
element->SetProperty(Rml::PropertyId::MarginLeft,
Rml::Property(Rml::Unit::PX, 10));
element->SetProperty(Rml::PropertyId::MarginRight,
Rml::Property(Rml::Unit::PX, 10));
// 设置变换
Rml::Transform transform;
transform.TranslateBy(Rml::Vector2f(100, 50));
transform.Rotate(45.0f); // 旋转 45 度
element->SetProperty(Rml::PropertyId::Transform,
Rml::Property(transform));
// 移除属性
element->RemoveProperty(Rml::PropertyId::BackgroundColor);
// 获取计算后的属性
Rml::Property widthProp = element->GetProperty(Rml::PropertyId::Width);
float width = widthProp.Get<Rml::Unit::PX>();
// 获取计算值(解析后的值)
Rml::Property computedProp = element->GetComputedValues()[Rml::PropertyId::Width];3.3 数据属性操作
cpp
// 设置数据属性
element->SetAttribute("data-id", "12345");
element->SetAttribute("data-index", 42);
element->SetAttribute("data-enabled", true);
// 获取数据属性
Rml::String id = element->GetAttribute<Rml::String>("data-id", "");
int index = element->GetAttribute<int>("data-index", 0);
bool enabled = element->GetAttribute<bool>("data-enabled", false);
// 检查属性是否存在
bool hasDataId = element->HasAttribute("data-id");
// 删除属性
element->RemoveAttribute("data-id");四、事件处理
4.1 添加事件监听器
cpp
// 使用 lambda
button->AddEventListener(Rml::EventId::Click,
[](Rml::Event* event) {
printf("Button clicked!\n");
Rml::Element* target = event->GetCurrentElement();
target->AddClass("clicked");
});
// 使用 EventListener 类
class MyListener : public Rml::EventListener
{
public:
void ProcessEvent(Rml::Event* event) override
{
printf("Event received: %d\n", (int)event->GetId());
}
};
button->AddEventListener(Rml::EventId::Mouseover, new MyListener());
// 带参数的监听
element->AddEventListener(Rml::EventId::Click,
[](Rml::Event* event) {
Rml::Vector2f mousePos = event->GetParameter<Rml::Vector2f>("mouse_pos", Rml::Vector2f(0, 0));
printf("Clicked at: %.1f, %.1f\n", mousePos.x, mousePos.y);
});4.2 移除事件监听器
cpp
// 移除特定事件的监听器
element->RemoveEventListener(Rml::EventId::Click, listener);
// 移除所有监听器(元素销毁时自动进行)4.3 触发自定义事件
cpp
// 在元素上触发自定义事件
Rml::StringId customEventId = Rml::StringId("my_custom_event");
Rml::ParameterMap params;
params["message"] = Rml::String("Hello!");
params["value"] = 42;
element->DispatchEvent(customEventId, params);
// 在文档上触发
Rml::ElementDocument* doc = element->GetOwnerDocument();
doc->DispatchEvent(customEventId, params);五、实战:动态通知系统
5.1 通知管理器
cpp
// NotificationManager.h
#pragma once
#include <RmlUi/Core.h>
#include <queue>
#include <memory>
enum class NotificationType
{
Info,
Success,
Warning,
Error
};
struct Notification
{
Rml::String title;
Rml::String message;
NotificationType type;
float duration; // 秒,0 表示不自动消失
};
class NotificationManager
{
public:
static NotificationManager& Instance()
{
static NotificationManager instance;
return instance;
}
void Initialize(Rml::Context* context)
{
context_ = context;
CreateContainer();
}
void Show(const Rml::String& title,
const Rml::String& message,
NotificationType type = NotificationType::Info,
float duration = 3.0f)
{
Notification notif{title, message, type, duration};
CreateNotificationElement(notif);
}
void ShowInfo(const Rml::String& title, const Rml::String& message)
{
Show(title, message, NotificationType::Info, 3.0f);
}
void ShowSuccess(const Rml::String& title, const Rml::String& message)
{
Show(title, message, NotificationType::Success, 3.0f);
}
void ShowWarning(const Rml::String& title, const Rml::String& message)
{
Show(title, message, NotificationType::Warning, 5.0f);
}
void ShowError(const Rml::String& title, const Rml::String& message)
{
Show(title, message, NotificationType::Error, 0.0f); // 不自动消失
}
void Update()
{
// 检查并移除过期的通知
auto now = std::chrono::steady_clock::now();
for (auto it = activeNotifications_.begin();
it != activeNotifications_.end();)
{
auto& [element, createTime, duration] = *it;
auto elapsed = std::chrono::duration<float>(now - createTime).count();
if (duration > 0 && elapsed >= duration)
{
Dismiss(element);
it = activeNotifications_.erase(it);
}
else
{
++it;
}
}
}
private:
Rml::Context* context_ = nullptr;
Rml::Element* container_ = nullptr;
struct ActiveNotification
{
Rml::Element* element;
std::chrono::steady_clock::time_point createTime;
float duration;
};
std::vector<ActiveNotification> activeNotifications_;
void CreateContainer()
{
// 创建通知容器
container_ = context_->CreateElement("div");
container_->SetId("notification-container");
container_->SetClassNames("notification-container");
// 添加到文档
Rml::ElementDocument* doc = context_->GetDocument(0);
doc->AppendChild(container_);
}
void CreateNotificationElement(const Notification& notif)
{
Rml::Element* notifEl = context_->CreateElement("div");
notifEl->SetClassNames(GetTypeClassName(notif.type));
// 内容
Rml::Element* contentEl = context_->CreateElement("div");
contentEl->SetClassNames("notification-content");
// 标题
Rml::Element* titleEl = context_->CreateElement("div");
titleEl->SetClassNames("notification-title");
titleEl->SetInnerRML(notif.title);
contentEl->AppendChild(titleEl);
// 消息
Rml::Element* msgEl = context_->CreateElement("div");
msgEl->SetClassNames("notification-message");
msgEl->SetInnerRML(notif.message);
contentEl->AppendChild(msgEl);
// 关闭按钮
Rml::Element* closeBtn = context_->CreateElement("button");
closeBtn->SetClassNames("notification-close");
closeBtn->SetInnerRML("×");
closeBtn->AddEventListener(Rml::EventId::Click,
[this, notifEl](Rml::Event*) {
Dismiss(notifEl);
});
notifEl->AppendChild(contentEl);
notifEl->AppendChild(closeBtn);
// 添加到容器
container_->AppendChild(notifEl);
// 记录活动通知
activeNotifications_.push_back({
notifEl,
std::chrono::steady_clock::now(),
notif.duration
});
// 添加进入动画
notifEl->SetProperty(Rml::PropertyId::Opacity,
Rml::Property(Rml::Unit::NUMBER, 0.0f));
notifEl->SetProperty(Rml::PropertyId::Transform,
Rml::Property("translateX(300px)"));
// 使用定时器触发动画(简单实现)
AnimateIn(notifEl);
}
void AnimateIn(Rml::Element* element)
{
// 简化版本:直接设置最终状态
// 实际项目中可以使用过渡或逐帧动画
element->SetProperty(Rml::PropertyId::Opacity,
Rml::Property(Rml::Unit::NUMBER, 1.0f));
element->SetProperty(Rml::PropertyId::Transform,
Rml::Property("translateX(0)"));
}
void Dismiss(Rml::Element* element)
{
// 添加退出动画
element->SetProperty(Rml::PropertyId::Opacity,
Rml::Property(Rml::Unit::NUMBER, 0.0f));
element->SetProperty(Rml::PropertyId::Transform,
Rml::Property("translateX(300px)"));
// 延迟删除
// 简化版本:直接删除
container_->RemoveChild(element);
}
const char* GetTypeClassName(NotificationType type)
{
switch (type)
{
case NotificationType::Info: return "notification notification-info";
case NotificationType::Success: return "notification notification-success";
case NotificationType::Warning: return "notification notification-warning";
case NotificationType::Error: return "notification notification-error";
}
return "notification";
}
};
// 样式定义
/*
.notification-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 10px;
}
.notification {
min-width: 300px;
max-width: 400px;
padding: 15px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
display: flex;
justify-content: space-between;
align-items: flex-start;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(300px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.notification-content {
flex: 1;
}
.notification-title {
font-weight: bold;
margin-bottom: 5px;
}
.notification-message {
color: #666;
font-size: 14px;
}
.notification-close {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #999;
padding: 0;
margin-left: 10px;
}
.notification-close:hover {
color: #333;
}
.notification-info { border-left: 4px solid #3498db; }
.notification-success { border-left: 4px solid #27ae60; }
.notification-warning { border-left: 4px solid #f39c12; }
.notification-error { border-left: 4px solid #e74c3c; }
*/5.2 使用通知系统
cpp
// 在游戏中使用
void OnPlayerLevelUp(int newLevel)
{
NotificationManager::Instance().ShowSuccess(
"升级了!",
"恭喜你达到了 " + Rml::StringFromReal(newLevel) + " 级!"
);
}
void OnPlayerDamaged(int damage)
{
NotificationManager::Instance().ShowInfo(
"受到伤害",
"你受到了 " + Rml::StringFromReal(damage) + " 点伤害"
);
}
void OnQuestCompleted(const Rml::String& questName)
{
NotificationManager::Instance().ShowSuccess(
"任务完成",
"你已完成任务:" + questName
);
}
void OnLowHealth()
{
NotificationManager::Instance().ShowWarning(
"警告",
"生命值过低,请尽快治疗!"
);
}
void OnGameOver()
{
NotificationManager::Instance().ShowError(
"游戏结束",
"你已失败,点击重新开始"
);
}
// 在游戏主循环中更新
void GameLoop()
{
while (running)
{
// ...
NotificationManager::Instance().Update();
// ...
}
}六、实战:模态对话框系统
6.1 对话框管理器
cpp
// DialogManager.h
#pragma once
#include <RmlUi/Core.h>
#include <functional>
#include <memory>
class DialogManager
{
public:
static DialogManager& Instance()
{
static DialogManager instance;
return instance;
}
void Initialize(Rml::Context* context)
{
context_ = context;
CreateOverlay();
}
using DialogCallback = std::function<void(int buttonIndex)>;
// 显示简单对话框
void ShowDialog(const Rml::String& title,
const Rml::String& message,
DialogCallback callback = nullptr)
{
ShowDialog(title, message, {"确定"}, callback);
}
// 显示确认对话框
void ShowConfirmDialog(const Rml::String& title,
const Rml::String& message,
DialogCallback callback = nullptr)
{
ShowDialog(title, message, {"确定", "取消"}, callback);
}
// 显示自定义按钮对话框
void ShowDialog(const Rml::String& title,
const Rml::String& message,
const std::vector<Rml::String>& buttons,
DialogCallback callback = nullptr)
{
// 创建对话框元素
Rml::Element* dialog = context_->CreateElement("div");
dialog->SetClassNames("dialog");
// 标题
Rml::Element* titleEl = context_->CreateElement("div");
titleEl->SetClassNames("dialog-title");
titleEl->SetInnerRML(title);
dialog->AppendChild(titleEl);
// 内容
Rml::Element* contentEl = context_->CreateElement("div");
contentEl->SetClassNames("dialog-content");
contentEl->SetInnerRML(message);
dialog->AppendChild(contentEl);
// 按钮
Rml::Element* buttonContainer = context_->CreateElement("div");
buttonContainer->SetClassNames("dialog-buttons");
for (size_t i = 0; i < buttons.size(); ++i)
{
Rml::Element* btn = context_->CreateElement("button");
btn->SetClassNames("dialog-btn");
btn->SetInnerRML(buttons[i]);
btn->AddEventListener(Rml::EventId::Click,
[this, dialog, callback, i](Rml::Event*) {
CloseDialog(dialog);
if (callback) callback(static_cast<int>(i));
});
buttonContainer->AppendChild(btn);
}
dialog->AppendChild(buttonContainer);
// 存储回调
dialogCallbacks_[dialog] = callback;
// 添加到遮罩层
overlay_->AppendChild(dialog);
overlay_->SetProperty(Rml::PropertyId::Visibility,
Rml::Property(Rml::Style::Visibility::Visible));
}
void CloseAllDialogs()
{
overlay_->SetInnerRML("");
overlay_->SetProperty(Rml::PropertyId::Visibility,
Rml::Property(Rml::Style::Visibility::Hidden));
dialogCallbacks_.clear();
}
private:
Rml::Context* context_ = nullptr;
Rml::Element* overlay_ = nullptr;
std::unordered_map<Rml::Element*, DialogCallback> dialogCallbacks_;
void CreateOverlay()
{
overlay_ = context_->CreateElement("div");
overlay_->SetClassNames("dialog-overlay");
overlay_->SetProperty(Rml::PropertyId::Visibility,
Rml::Property(Rml::Style::Visibility::Hidden));
// 点击遮罩关闭对话框
overlay_->AddEventListener(Rml::EventId::Click,
[this](Rml::Event* event) {
if (event->GetCurrentElement() == overlay_)
{
CloseAllDialogs();
}
});
Rml::ElementDocument* doc = context_->GetDocument(0);
doc->AppendChild(overlay_);
}
void CloseDialog(Rml::Element* dialog)
{
overlay_->RemoveChild(dialog);
dialogCallbacks_.erase(dialog);
if (overlay_->GetNumChildren() == 0)
{
overlay_->SetProperty(Rml::PropertyId::Visibility,
Rml::Property(Rml::Style::Visibility::Hidden));
}
}
};
// 样式
/*
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 9998;
display: flex;
justify-content: center;
align-items: center;
}
.dialog {
background: white;
border-radius: 12px;
min-width: 400px;
max-width: 500px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
animation: dialogPop 0.2s ease;
}
@keyframes dialogPop {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.dialog-title {
padding: 20px;
font-size: 18px;
font-weight: bold;
border-bottom: 1px solid #eee;
}
.dialog-content {
padding: 20px;
color: #333;
max-height: 300px;
overflow-y: auto;
}
.dialog-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 15px 20px;
border-top: 1px solid #eee;
}
.dialog-btn {
padding: 8px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.dialog-btn:first-child {
background: #3498db;
color: white;
}
.dialog-btn:not(:first-child) {
background: #e0e0e0;
color: #333;
}
*/6.3 使用对话框
cpp
// 确认退出
void RequestQuit()
{
DialogManager::Instance().ShowConfirmDialog(
"确认退出",
"确定要退出游戏吗?未保存的进度将丢失。",
[](int result) {
if (result == 0) // 点击了"确定"
{
// 退出游戏
}
});
}
// 游戏结束
void ShowGameOver()
{
DialogManager::Instance().ShowDialog(
"游戏结束",
"你的得分:10000\n排名:第 1 名",
{"再来一局", "返回主菜单"},
[](int result) {
if (result == 0)
RestartGame();
else
ShowMainMenu();
});
}
// 简单提示
void ShowSaveComplete()
{
DialogManager::Instance().ShowDialog(
"保存完成",
"游戏已成功保存。"
);
}七、实践练习
练习 1:创建工具提示系统
实现一个动态工具提示:
- 鼠标悬停显示
- 支持富文本内容
- 自动调整位置避免超出屏幕
练习 2:制作可折叠面板
创建手风琴效果的面板:
- 点击标题展开/折叠
- 支持多个面板
- 添加平滑过渡动画
练习 3:实现拖拽排序列表
创建一个可拖拽排序的列表:
- 拖拽项目重新排序
- 显示拖拽预览
- 触发排序事件
📝 检查清单
恭喜完成阶段三!你已经掌握了事件系统和数据绑定的全部知识。
下一阶段:阶段四:动画与视觉效果 - 学习如何创建流畅的动画和视觉特效。