3.4 自定义数据视图
RmlUi 的 DataViews 允许你创建完全自定义的数据绑定视图,超越基础的文本插值和属性绑定。通过实现 DataView 接口,你可以控制数据如何渲染为 UI 元素。
一、DataView 基础
1.1 什么是 DataView
DataView 是一个接口,允许你:
- 自定义数据到 UI 的转换逻辑
- 创建可复用的数据驱动组件
- 实现复杂的 UI 更新行为
1.2 实现简单的 DataView
cpp
#include <RmlUi/Core.h>
// 创建一个血条视图
class HealthBarView : public Rml::DataView
{
public:
HealthBarView() : Rml::DataView("healthbar") {}
// 初始化时被调用
bool Initialise(Rml::Element* element,
const Rml::DataViewConstructParams& params) override
{
element_ = element;
params_ = params;
return true;
}
// 数据变化时被调用
void OnValueChange(const Rml::DataValue& value) override
{
if (!element_)
return;
// 获取数据值(期望是一个字典)
if (value.GetType() != Rml::DataValueType::Dictionary)
return;
const Rml::DataDictionary& data = value.Get<Rml::DataDictionary>();
int current_hp = data.Get<int>("current", 100);
int max_hp = data.Get<int>("max", 100);
// 计算百分比
float percent = static_cast<float>(current_hp) / max_hp;
// 更新血条填充元素
Rml::Element* fill_element = element_->GetElementById("fill");
if (fill_element)
{
float width = percent * 100.0f;
fill_element->SetProperty(Rml::PropertyId::Width,
Rml::Property(Rml::Unit::PERCENT, width));
}
// 更新文本
Rml::Element* text_element = element_->GetElementById("text");
if (text_element)
{
text_element->SetInnerRML(Rml::StringFromReal(current_hp) + "/" +
Rml::StringFromReal(max_hp));
}
// 根据血量改变颜色
UpdateColor(percent);
}
// 获取默认数据值
Rml::DataValue GetDefaultValue() const override
{
Rml::DataDictionary default_data;
default_data["current"] = 100;
default_data["max"] = 100;
return Rml::DataValue(default_data);
}
private:
Rml::Element* element_ = nullptr;
Rml::DataViewConstructParams params_;
void UpdateColor(float percent)
{
Rml::Element* fill_element = element_->GetElementById("fill");
if (!fill_element)
return;
Rml::Colourb color;
if (percent > 0.5f)
{
color = Rml::Colourb(46, 204, 113); // 绿色
}
else if (percent > 0.25f)
{
color = Rml::Colourb(241, 196, 15); // 黄色
}
else
{
color = Rml::Colourb(231, 76, 60); // 红色
}
fill_element->SetProperty(Rml::PropertyId::BackgroundColor,
Rml::Property(color));
}
};
// 注册视图
void RegisterDataViews()
{
Rml::DataView::RegisterDataView("healthbar", std::make_unique<HealthBarView>());
}1.3 在 RML 中使用自定义视图
xml
<rml>
<head>
<link type="text/rcss" href="style.rcss"/>
</head>
<body data-model="player">
<div class="unit-frame">
<!-- 使用自定义血条视图 -->
<div id="healthbar" data-view="healthbar" data-bind="hp_data">
<div id="fill" class="health-fill"></div>
<span id="text" class="health-text"></span>
</div>
</div>
</body>
</rml>二、高级 DataView 实现
2.1 网格布局视图
cpp
// 物品栏网格视图
class InventoryGridView : public Rml::DataView
{
public:
InventoryGridView() : Rml::DataView("inventory-grid") {}
bool Initialise(Rml::Element* element,
const Rml::DataViewConstructParams& params) override
{
element_ = element;
params_ = params;
// 从属性获取配置
columns_ = params_.element->GetAttribute<int>("columns", 8);
rows_ = params_.element->GetAttribute<int>("rows", 4);
slot_size_ = params_.element->GetAttribute<int>("slot-size", 48);
// 创建网格
CreateGrid();
return true;
}
void OnValueChange(const Rml::DataValue& value) override
{
if (value.GetType() != Rml::DataValueType::List)
return;
const Rml::DataList& items = value.Get<Rml::DataList>();
// 更新每个槽位
for (size_t i = 0; i < slots_.size(); ++i)
{
Rml::Element* slot = slots_[i];
if (i < items.size())
{
UpdateSlot(slot, items[i].Get<Rml::DataDictionary>());
}
else
{
ClearSlot(slot);
}
}
}
Rml::DataValue GetDefaultValue() const override
{
return Rml::DataValue(Rml::DataList());
}
private:
Rml::Element* element_ = nullptr;
Rml::DataViewConstructParams params_;
int columns_ = 8;
int rows_ = 4;
int slot_size_ = 48;
std::vector<Rml::Element*> slots_;
void CreateGrid()
{
element_->SetProperty(Rml::PropertyId::Display,
Rml::Property(Rml::Style::Display::Grid));
element_->SetProperty(Rml::PropertyId::GridTemplateColumns,
Rml::Property(Rml::String(columns_, "fr")));
element_->SetProperty(Rml::PropertyId::GridTemplateRows,
Rml::Property(Rml::String(rows_, "fr")));
element_->SetProperty(Rml::PropertyId::Gap,
Rml::Property(Rml::Unit::PX, 4));
// 创建槽位
int total_slots = columns_ * rows_;
for (int i = 0; i < total_slots; ++i)
{
Rml::Element* slot = element_->GetOwnerDocument()->CreateElement("div");
slot->SetClassNames("item-slot");
slot->SetProperty(Rml::PropertyId::Width,
Rml::Property(Rml::Unit::PX, slot_size_));
slot->SetProperty(Rml::PropertyId::Height,
Rml::Property(Rml::Unit::PX, slot_size_));
// 添加拖拽支持
slot->AddEventListener(Rml::EventId::Click, this);
element_->AppendChild(slot);
slots_.push_back(slot);
}
}
void UpdateSlot(Rml::Element* slot, const Rml::DataDictionary& item)
{
slot->SetClassNames("item-slot has-item");
// 清除子元素
slot->SetInnerRML("");
// 创建图标
Rml::Element* img = slot->GetOwnerDocument()->CreateElement("img");
img->SetAttribute("src", item.Get<Rml::String>("icon", ""));
slot->AppendChild(img);
// 创建数量标签
int count = item.Get<int>("count", 1);
if (count > 1)
{
Rml::Element* count_label = slot->GetOwnerDocument()->CreateElement("span");
count_label->SetClassNames("item-count");
count_label->SetInnerRML(Rml::StringFromReal(count));
slot->AppendChild(count_label);
}
// 存储物品数据
slot->SetAttribute("item-id", item.Get<Rml::String>("id", ""));
}
void ClearSlot(Rml::Element* slot)
{
slot->SetClassNames("item-slot");
slot->SetInnerRML("");
slot->RemoveAttribute("item-id");
}
void ProcessEvent(Rml::Event* event) override
{
if (event->GetId() == Rml::EventId::Click)
{
Rml::Element* slot = event->GetCurrentElement();
Rml::String item_id = slot->GetAttribute<Rml::String>("item-id", "");
if (!item_id.empty())
{
// 触发物品点击事件
Rml::ParameterMap params;
params["item_id"] = item_id;
params["slot_index"] = GetSlotIndex(slot);
element_->GetOwnerDocument()->DispatchEvent(
Rml::StringId("item_clicked"), params);
}
}
}
int GetSlotIndex(Rml::Element* slot)
{
for (size_t i = 0; i < slots_.size(); ++i)
{
if (slots_[i] == slot)
return static_cast<int>(i);
}
return -1;
}
};2.2 使用网格视图
xml
<rml>
<head>
<link type="text/rcss" href="inventory.rcss"/>
</head>
<body data-model="inventory">
<!-- 物品栏 -->
<div class="inventory-panel">
<h3>背包</h3>
<!-- 自定义网格视图 -->
<div id="inventory-grid"
data-view="inventory-grid"
data-bind="items"
columns="8"
rows="5"
slot-size="50">
</div>
<div class="inventory-info">
<span>物品:{v-pre{ item_count }v-pre}/{v-pre{ max_slots }v-pre}</span>
</div>
</div>
</body>
</rml>css
.inventory-panel {
background: #2c3e50;
padding: 20px;
border-radius: 8px;
}
.item-slot {
background: #34495e;
border: 2px solid #4a6278;
border-radius: 4px;
position: relative;
cursor: pointer;
}
.item-slot:hover {
border-color: #3498db;
}
.item-slot.has-item {
border-color: #27ae60;
}
.item-slot img {
width: 100%;
height: 100%;
padding: 4px;
}
.item-count {
position: absolute;
bottom: 2px;
right: 4px;
color: white;
font-size: 12px;
font-weight: bold;
text-shadow: 1px 1px 2px black;
}三、数据驱动的下拉列表
3.1 DropdownView 实现
cpp
class DropdownView : public Rml::DataView
{
public:
DropdownView() : Rml::DataView("dropdown") {}
bool Initialise(Rml::Element* element,
const Rml::DataViewConstructParams& params) override
{
element_ = element;
params_ = params;
selected_index_ = -1;
// 创建下拉按钮
CreateDropdownButton();
// 创建选项面板
CreateOptionsPanel();
// 监听点击事件
dropdown_button_->AddEventListener(Rml::EventId::Click, this);
return true;
}
void OnValueChange(const Rml::DataValue& value) override
{
if (value.GetType() != Rml::DataValueType::List)
return;
const Rml::DataList& options = value.Get<Rml::DataList>();
options_ = options;
// 重建选项
RebuildOptions();
}
Rml::DataValue GetDefaultValue() const override
{
return Rml::DataValue(Rml::DataList());
}
void ProcessEvent(Rml::Event* event) override
{
if (event->GetId() == Rml::EventId::Click)
{
if (event->GetCurrentElement() == dropdown_button_)
{
ToggleOptions();
}
else if (event->GetCurrentElement()->GetClassList().Contains("dropdown-option"))
{
SelectOption(event->GetCurrentElement());
}
else if (!options_panel_->IsPointWithinElement(event->GetParameter<Rml::Vector2f>("mouse_pos", Rml::Vector2f(0, 0))))
{
HideOptions();
}
}
}
private:
Rml::Element* element_ = nullptr;
Rml::DataViewConstructParams params_;
Rml::Element* dropdown_button_ = nullptr;
Rml::Element* selected_label_ = nullptr;
Rml::Element* options_panel_ = nullptr;
Rml::DataList options_;
int selected_index_;
bool is_open_ = false;
void CreateDropdownButton()
{
dropdown_button_ = element_->GetOwnerDocument()->CreateElement("div");
dropdown_button_->SetClassNames("dropdown-button");
selected_label_ = element_->GetOwnerDocument()->CreateElement("span");
selected_label_->SetClassNames("dropdown-selected");
selected_label_->SetInnerRML("请选择");
Rml::Element* arrow = element_->GetOwnerDocument()->CreateElement("span");
arrow->SetClassNames("dropdown-arrow");
arrow->SetInnerRML("▼");
dropdown_button_->AppendChild(selected_label_);
dropdown_button_->AppendChild(arrow);
element_->AppendChild(dropdown_button_);
}
void CreateOptionsPanel()
{
options_panel_ = element_->GetOwnerDocument()->CreateElement("div");
options_panel_->SetClassNames("dropdown-options");
options_panel_->SetProperty(Rml::PropertyId::Visibility,
Rml::Property(Rml::Style::Visibility::Hidden));
element_->AppendChild(options_panel_);
}
void RebuildOptions()
{
options_panel_->SetInnerRML("");
option_elements_.clear();
for (size_t i = 0; i < options_.size(); ++i)
{
const Rml::DataDictionary& option = options_[i].Get<Rml::DataDictionary>();
Rml::Element* option_el = element_->GetOwnerDocument()->CreateElement("div");
option_el->SetClassNames("dropdown-option");
option_el->SetInnerRML(option.Get<Rml::String>("label", ""));
option_el->SetAttribute("data-index", static_cast<int>(i));
options_panel_->AppendChild(option_el);
option_elements_.push_back(option_el);
}
}
void ToggleOptions()
{
if (is_open_)
HideOptions();
else
ShowOptions();
}
void ShowOptions()
{
options_panel_->SetProperty(Rml::PropertyId::Visibility,
Rml::Property(Rml::Style::Visibility::Visible));
is_open_ = true;
}
void HideOptions()
{
options_panel_->SetProperty(Rml::PropertyId::Visibility,
Rml::Property(Rml::Style::Visibility::Hidden));
is_open_ = false;
}
void SelectOption(Rml::Element* option_el)
{
int index = option_el->GetAttribute<int>("data-index", -1);
if (index < 0 || index >= static_cast<int>(options_.size()))
return;
selected_index_ = index;
// 更新选中显示
const Rml::DataDictionary& option = options_[index].Get<Rml::DataDictionary>();
selected_label_->SetInnerRML(option.Get<Rml::String>("label", ""));
// 清除其他选项的选中状态
for (auto* el : option_elements_)
{
el->RemoveClass("selected");
}
option_el->AddClass("selected");
// 触发选择事件
Rml::ParameterMap params;
params["index"] = index;
params["value"] = option.Get<Rml::String>("value", "");
element_->GetOwnerDocument()->DispatchEvent(
Rml::StringId("dropdown_changed"), params);
HideOptions();
}
std::vector<Rml::Element*> option_elements_;
};3.2 使用下拉列表
xml
<rml>
<head>
<link type="text/rcss" href="dropdown.rcss"/>
</head>
<body data-model="settings">
<div class="settings-form">
<!-- 画质设置 -->
<div class="form-row">
<label>画质设置</label>
<div data-view="dropdown" data-bind="graphics_options">
</div>
</div>
<!-- 分辨率设置 -->
<div class="form-row">
<label>分辨率</label>
<div data-view="dropdown" data-bind="resolution_options">
</div>
</div>
</div>
</body>
</rml>cpp
// C++ 中设置数据
class SettingsData : public Rml::DataModel
{
public:
RMLUI_DATA_BINDINGS
{
RMLUI_DATA_BINDING(graphics_options, &graphics_options_)
RMLUI_DATA_BINDING(resolution_options, &resolution_options_)
}
SettingsData()
{
// 画质选项
AddOption(graphics_options_, "低", "low");
AddOption(graphics_options_, "中", "medium");
AddOption(graphics_options_, "高", "high");
AddOption(graphics_options_, "超高", "ultra");
// 分辨率选项
AddOption(resolution_options_, "1280x720", "1280x720");
AddOption(resolution_options_, "1920x1080", "1920x1080");
AddOption(resolution_options_, "2560x1440", "2560x1440");
AddOption(resolution_options_, "3840x2160", "3840x2160");
}
private:
Rml::DataList graphics_options_;
Rml::DataList resolution_options_;
void AddOption(Rml::DataList& list,
const Rml::String& label,
const Rml::String& value)
{
Rml::DataDictionary option;
option["label"] = label;
option["value"] = value;
list.push_back(Rml::DataValue(option));
}
};四、实战:完整的技能树视图
4.1 技能树数据结构
cpp
struct SkillNode
{
Rml::String id;
Rml::String name;
Rml::String description;
Rml::String icon;
int max_level;
int current_level;
std::vector<Rml::String> prerequisites; // 前置技能
std::vector<Rml::String> unlocks; // 解锁的技能
Rml::Dictionary effects; // 技能效果
};
class SkillTreeModel : public Rml::DataModel
{
public:
RMLUI_DATA_BINDINGS
{
RMLUI_DATA_BINDING(skill_points, &skill_points_)
RMLUI_DATA_BINDING(skills, &skills_)
}
bool CanUpgradeSkill(const Rml::String& skill_id) const
{
auto it = skills_.find(skill_id);
if (it == skills_.end())
return false;
const SkillNode& skill = it->second;
// 检查是否已达最大等级
if (skill.current_level >= skill.max_level)
return false;
// 检查技能点
if (skill_points_ <= 0)
return false;
// 检查前置技能
for (const auto& prereq : skill.prerequisites)
{
auto prereq_it = skills_.find(prereq);
if (prereq_it == skills_.end() ||
prereq_it->second.current_level == 0)
{
return false;
}
}
return true;
}
void UpgradeSkill(const Rml::String& skill_id)
{
if (!CanUpgradeSkill(skill_id))
return;
SkillNode& skill = skills_[skill_id];
skill.current_level++;
skill_points_--;
NotifyChanged("skill_points");
NotifyChanged("skills"); // 触发整个技能树更新
}
private:
int skill_points_ = 5;
std::unordered_map<Rml::String, SkillNode> skills_;
};4.2 技能树视图实现
cpp
class SkillTreeView : public Rml::DataView
{
public:
SkillTreeView() : Rml::DataView("skilltree") {}
bool Initialise(Rml::Element* element,
const Rml::DataViewConstructParams& params) override
{
element_ = element;
params_ = params;
// 创建技能节点容器
skills_container_ = element_->GetOwnerDocument()->CreateElement("div");
skills_container_->SetClassNames("skill-tree-container");
element_->AppendChild(skills_container_);
return true;
}
void OnValueChange(const Rml::DataValue& value) override
{
if (value.GetType() != Rml::DataValueType::Dictionary)
return;
const Rml::DataDictionary& skills_dict = value.Get<Rml::DataDictionary>();
// 重建技能树
RebuildSkillTree(skills_dict);
}
Rml::DataValue GetDefaultValue() const override
{
return Rml::DataValue(Rml::DataDictionary());
}
private:
Rml::Element* element_ = nullptr;
Rml::Element* skills_container_ = nullptr;
std::unordered_map<Rml::String, Rml::Element*> skill_elements_;
void RebuildSkillTree(const Rml::DataDictionary& skills)
{
skills_container_->SetInnerRML("");
skill_elements_.clear();
// 遍历所有技能并创建节点
for (const auto& [id, value] : skills)
{
const Rml::DataDictionary& skill_data = value.Get<Rml::DataDictionary>();
CreateSkillNode(id, skill_data);
}
// 创建连接线
CreateConnections(skills);
}
void CreateSkillNode(const Rml::String& id,
const Rml::DataDictionary& skill_data)
{
Rml::Element* node = element_->GetOwnerDocument()->CreateElement("div");
node->SetClassNames("skill-node");
node->SetId("skill-" + id);
node->SetAttribute("data-skill-id", id);
// 图标
Rml::Element* icon = element_->GetOwnerDocument()->CreateElement("img");
icon->SetAttribute("src", skill_data.Get<Rml::String>("icon", ""));
node->AppendChild(icon);
// 名称
Rml::Element* name = element_->GetOwnerDocument()->CreateElement("span");
name->SetClassNames("skill-name");
name->SetInnerRML(skill_data.Get<Rml::String>("name", ""));
node->AppendChild(name);
// 等级
int current = skill_data.Get<int>("current_level", 0);
int max = skill_data.Get<int>("max_level", 5);
Rml::Element* level = element_->GetOwnerDocument()->CreateElement("span");
level->SetClassNames("skill-level");
level->SetInnerRML(Rml::StringFromReal(current) + "/" +
Rml::StringFromReal(max));
node->AppendChild(level);
// 添加点击事件
node->AddEventListener(Rml::EventId::Click, this);
skills_container_->AppendChild(node);
skill_elements_[id] = node;
}
void CreateConnections(const Rml::DataDictionary& skills)
{
// 这里可以使用 SVG 或 Canvas 绘制连接线
// 简化版本:添加 CSS 类来显示连接关系
}
void ProcessEvent(Rml::Event* event) override
{
if (event->GetId() == Rml::EventId::Click)
{
Rml::Element* node = event->GetCurrentElement();
if (node->GetClassList().Contains("skill-node"))
{
Rml::String skill_id = node->GetAttribute<Rml::String>("data-skill-id", "");
if (!skill_id.empty())
{
// 触发升级事件
Rml::ParameterMap params;
params["skill_id"] = skill_id;
element_->GetOwnerDocument()->DispatchEvent(
Rml::StringId("skill_upgrade"), params);
}
}
}
}
};五、实践练习
练习 1:创建天赋树视图
实现一个圆形的天赋树:
- 中心向外辐射的布局
- 点击天赋节点查看效果
- 支持重置天赋点
练习 2:制作装备对比视图
创建装备对比组件:
- 左右显示当前装备和备选装备
- 自动计算并显示属性变化
- 绿色表示提升,红色表示下降
练习 3:实现聊天视图
创建一个数据驱动的聊天框:
- 支持不同类型的消息(系统、玩家、队伍)
- 消息滚动和自动清理
- @提及高亮显示
📝 检查清单
下一节:双向绑定 - 学习如何实现表单数据的双向同步。