3.3 数据绑定进阶
在掌握了数据绑定的基础之后,本节将深入探讨更高级的绑定技巧,包括自定义转换器、数组操作、嵌套数据模型和性能优化。
一、数据转换器(Data Formatter)
1.1 为什么需要转换器
有时候数据的原始格式不适合直接显示,需要进行格式化:
- 数值 → 百分比、金币格式
- 时间戳 → 可读时间
- 布尔值 → 文本描述
1.2 注册自定义转换器
cpp
#include <RmlUi/Core.h>
// 金币格式化:1000 → "1,000 G"
class GoldFormatter : public Rml::DataFormatter
{
public:
GoldFormatter() : Rml::DataFormatter("gold") {}
bool Format(Rml::String& formatted_string,
const Rml::DataValueList& data_values) override
{
if (data_values.empty())
return false;
int gold = data_values[0].Get<int>();
// 添加千分位分隔符
std::string gold_str = std::to_string(gold);
std::string result;
int count = 0;
for (int i = gold_str.length() - 1; i >= 0; --i)
{
if (count > 0 && count % 3 == 0)
result = "," + result;
result = gold_str[i] + result;
count++;
}
formatted_string = result + " G";
return true;
}
};
// 百分比格式化:0.75 → "75%"
class PercentFormatter : public Rml::DataFormatter
{
public:
PercentFormatter() : Rml::DataFormatter("percent") {}
bool Format(Rml::String& formatted_string,
const Rml::DataValueList& data_values) override
{
if (data_values.empty())
return false;
float value = data_values[0].Get<float>();
formatted_string = Rml::StringFromReal(value * 100, 1) + "%";
return true;
}
};
// 时间格式化:秒数 → "00:00:00"
class TimeFormatter : public Rml::DataFormatter
{
public:
TimeFormatter() : Rml::DataFormatter("time") {}
bool Format(Rml::String& formatted_string,
const Rml::DataValueList& data_values) override
{
if (data_values.empty())
return false;
int total_seconds = data_values[0].Get<int>();
int hours = total_seconds / 3600;
int minutes = (total_seconds % 3600) / 60;
int seconds = total_seconds % 60;
char buffer[32];
sprintf(buffer, "%02d:%02d:%02d", hours, minutes, seconds);
formatted_string = buffer;
return true;
}
};
// 初始化时注册
void RegisterFormatters()
{
Rml::DataFormatter::RegisterDataFormatter("gold", std::make_unique<GoldFormatter>());
Rml::DataFormatter::RegisterDataFormatter("percent", std::make_unique<PercentFormatter>());
Rml::DataFormatter::RegisterDataFormatter("time", std::make_unique<TimeFormatter>());
}1.3 在 RML 中使用转换器
xml
<rml>
<head>
<link type="text/rcss" href="style.rcss"/>
</head>
<body data-model="player">
<div class="stats-panel">
<!-- 金币显示 -->
<span class="gold">{v-pre{ gold | gold }v-pre}</span>
<!-- 经验百分比 -->
<div class="exp-bar">
<div class="exp-fill" style="width: {v-pre{ exp_percent | percent }v-pre}"></div>
</div>
<!-- 游戏时间 -->
<span class="play-time">游戏时间:{v-pre{ play_time | time }v-pre}</span>
<!-- 多个值格式化 -->
<span>{v-pre{ value, multiplier | custom_formatter }v-pre}</span>
</div>
</body>
</rml>二、数组操作和更新
2.1 DataList 的完整操作
cpp
class QuestList : public Rml::DataModel
{
public:
RMLUI_DATA_BINDINGS
{
RMLUI_DATA_BINDING(quests, &quests_)
RMLUI_DATA_BINDING(quest_count, &quest_count_)
}
// 添加任务
void AddQuest(const Rml::String& id,
const Rml::String& title,
const Rml::String& description,
int target_count,
Rml::DataDictionary rewards)
{
Rml::DataDictionary quest;
quest["id"] = id;
quest["title"] = title;
quest["description"] = description;
quest["target"] = target_count;
quest["progress"] = 0;
quest["completed"] = false;
quest["rewards"] = rewards;
quests_.push_back(Rml::DataValue(quest));
quest_count_ = static_cast<int>(quests_.size());
NotifyChanged("quests");
NotifyChanged("quest_count");
}
// 更新任务进度
void UpdateQuestProgress(const Rml::String& quest_id, int progress)
{
for (size_t i = 0; i < quests_.size(); ++i)
{
Rml::DataDictionary& quest = quests_[i].Get<Rml::DataDictionary>();
if (quest.Get<Rml::String>("id") == quest_id)
{
int target = quest.Get<int>("target");
quest["progress"] = progress;
quest["completed"] = (progress >= target);
// 使用索引通知,只更新这一项
NotifyChanged("quests[" + Rml::StringFromReal(i) + "]");
return;
}
}
}
// 完成任务
void CompleteQuest(const Rml::String& quest_id)
{
for (size_t i = 0; i < quests_.size(); ++i)
{
Rml::DataDictionary& quest = quests_[i].Get<Rml::DataDictionary>();
if (quest.Get<Rml::String>("id") == quest_id)
{
quest["completed"] = true;
// 触发完成回调
OnQuestCompleted(quest);
NotifyChanged("quests[" + Rml::StringFromReal(i) + "]");
return;
}
}
}
// 移除任务
void RemoveQuest(const Rml::String& quest_id)
{
for (auto it = quests_.begin(); it != quests_.end(); ++it)
{
Rml::DataDictionary& quest = it->Get<Rml::DataDictionary>();
if (quest.Get<Rml::String>("id") == quest_id)
{
quests_.erase(it);
quest_count_ = static_cast<int>(quests_.size());
NotifyChanged("quests");
NotifyChanged("quest_count");
return;
}
}
}
// 清空所有已完成的任务
void ClearCompletedQuests()
{
quests_.erase(
std::remove_if(quests_.begin(), quests_.end(),
[](const Rml::DataValue& value) {
const Rml::DataDictionary& quest = value.Get<Rml::DataDictionary>();
return quest.Get<bool>("completed");
}),
quests_.end()
);
quest_count_ = static_cast<int>(quests_.size());
NotifyChanged("quests");
NotifyChanged("quest_count");
}
// 获取任务数量
int GetQuestCount() const { return quest_count_; }
private:
Rml::DataList quests_;
int quest_count_ = 0;
void OnQuestCompleted(const Rml::DataDictionary& quest)
{
// 发放奖励等逻辑
Rml::DataDictionary rewards = quest.Get<Rml::DataDictionary>("rewards");
// ...
}
};2.2 在 RML 中处理数组
xml
<rml>
<head>
<link type="text/rcss" href="quest.rcss"/>
</head>
<body data-model="quests">
<div class="quest-panel">
<h3>当前任务 ({v-pre{ quest_count }v-pre})</h3>
<!-- 空列表提示 -->
<div class="empty-tip" if="quest_count == 0">
目前没有进行中的任务
</div>
<!-- 任务列表 -->
<div class="quest-list" for="quest in quests">
<div class="quest-item" class.completed="quest.completed">
<div class="quest-header">
<span class="quest-title">{v-pre{ quest.title }v-pre}</span>
<span class="quest-status" if="quest.completed">✓ 已完成</span>
</div>
<p class="quest-description">{v-pre{ quest.description }v-pre}</p>
<div class="quest-progress">
<div class="progress-bar">
<div class="progress-fill"
style="width: {v-pre{ quest.progress }v-pre} / {v-pre{ quest.target }v-pre} * 100%"></div>
</div>
<span class="progress-text">{v-pre{ quest.progress }v-pre}/{v-pre{ quest.target }v-pre}</span>
</div>
<button class="btn-claim"
if="quest.completed"
onclick="ClaimReward(quest.id)">
领取奖励
</button>
</div>
</div>
</div>
</body>
</rml>三、嵌套数据模型
3.1 多层数据结构
cpp
// 公会数据模型
class GuildData : public Rml::DataModel
{
public:
RMLUI_DATA_BINDINGS
{
RMLUI_DATA_BINDING(guild_name, &guild_name_)
RMLUI_DATA_BINDING(guild_level, &guild_level_)
RMLUI_DATA_BINDING(member_count, &member_count_)
RMLUI_DATA_BINDING(members, &members_)
RMLUI_DATA_BINDING(announcements, &announcements_)
}
// 成员数据结构
struct Member
{
Rml::String id;
Rml::String name;
Rml::String title; // 职位
int level;
int contribution;
bool is_online;
Rml::String last_login;
};
void AddMember(const Member& member)
{
Rml::DataDictionary data;
data["id"] = member.id;
data["name"] = member.name;
data["title"] = member.title;
data["level"] = member.level;
data["contribution"] = member.contribution;
data["is_online"] = member.is_online;
data["last_login"] = member.last_login;
members_.push_back(Rml::DataValue(data));
member_count_ = static_cast<int>(members_.size());
NotifyChanged("members");
NotifyChanged("member_count");
}
// 更新成员在线状态
void UpdateMemberStatus(const Rml::String& member_id, bool is_online)
{
for (size_t i = 0; i < members_.size(); ++i)
{
Rml::DataDictionary& member = members_[i].Get<Rml::DataDictionary>();
if (member.Get<Rml::String>("id") == member_id)
{
member["is_online"] = is_online;
NotifyChanged("members[" + Rml::StringFromReal(i) + "].is_online");
return;
}
}
}
private:
Rml::String guild_name_ = "龙之荣耀";
int guild_level_ = 1;
int member_count_ = 0;
Rml::DataList members_;
Rml::DataList announcements_;
};3.2 访问嵌套数据
xml
<rml>
<head>
<link type="text/rcss" href="guild.rcss"/>
</head>
<body data-model="guild">
<div class="guild-panel">
<div class="guild-header">
<h2>{v-pre{ guild_name }v-pre}</h2>
<span class="level">Lv.{v-pre{ guild_level }v-pre}</span>
<span class="members">成员:{v-pre{ member_count }v-pre}/50</span>
</div>
<!-- 在线成员列表 -->
<div class="member-list">
<div class="member-item"
for="member in members"
class.online="member.is_online"
class.offline="!member.is_online">
<span class="member-name">{v-pre{ member.name }v-pre}</span>
<span class="member-title">{v-pre{ member.title }v-pre}</span>
<span class="member-level">Lv.{v-pre{ member.level }v-pre}</span>
<span class="status-indicator"></span>
</div>
</div>
<!-- 按条件筛选显示 -->
<div class="online-members">
<h4>在线成员</h4>
<div class="member-item"
for="member in members"
if="member.is_online">
<span>{v-pre{ member.name }v-pre} - {v-pre{ member.title }v-pre}</span>
</div>
</div>
</div>
</body>
</rml>四、计算属性
4.1 只读计算属性
cpp
class BattleStats : public Rml::DataModel
{
public:
RMLUI_DATA_BINDINGS
{
// 基础属性
RMLUI_DATA_BINDING(base_attack, &base_attack_)
RMLUI_DATA_BINDING(base_defense, &base_defense_)
// 装备加成
RMLUI_DATA_BINDING(weapon_bonus, &weapon_bonus_)
RMLUI_DATA_BINDING(armor_bonus, &armor_bonus_)
// Buff 加成
RMLUI_DATA_BINDING(buff_attack_percent, &buff_attack_percent_)
RMLUI_DATA_BINDING(buff_defense_percent, &buff_defense_percent_)
// 计算属性(只读)
RMLUI_DATA_BINDING(final_attack, nullptr) // 使用 getter
RMLUI_DATA_BINDING(final_defense, nullptr)
RMLUI_DATA_BINDING(damage_multiplier, nullptr)
}
// Getter 用于计算属性
int GetFinalAttack() const
{
int total_bonus = weapon_bonus_;
return static_cast<int>((base_attack_ + total_bonus) * (1 + buff_attack_percent_));
}
int GetFinalDefense() const
{
int total_bonus = armor_bonus_;
return static_cast<int>((base_defense_ + total_bonus) * (1 + buff_defense_percent_));
}
float GetDamageMultiplier() const
{
return 1.0f + buff_attack_percent_;
}
// 当依赖项变化时,手动通知计算属性
void SetWeaponBonus(int bonus)
{
weapon_bonus_ = bonus;
NotifyChanged("final_attack");
NotifyChanged("damage_multiplier");
}
void SetBuffAttackPercent(float percent)
{
buff_attack_percent_ = percent;
NotifyChanged("final_attack");
NotifyChanged("damage_multiplier");
}
private:
int base_attack_ = 100;
int base_defense_ = 50;
int weapon_bonus_ = 20;
int armor_bonus_ = 10;
float buff_attack_percent_ = 0.0f;
float buff_defense_percent_ = 0.0f;
};4.2 使用计算属性
xml
<rml>
<head>
<link type="text/rcss" href="battle.rcss"/>
</head>
<body data-model="battle">
<div class="stats-panel">
<div class="stat-row">
<span>基础攻击力</span>
<span>{v-pre{ base_attack }v-pre}</span>
</div>
<div class="stat-row">
<span>武器加成</span>
<span class="bonus">+{v-pre{ weapon_bonus }v-pre}</span>
</div>
<div class="stat-row">
<span>Buff 加成</span>
<span class="bonus">+{v-pre{ buff_attack_percent * 100 }v-pre}%</span>
</div>
<div class="stat-row total">
<span>最终攻击力</span>
<span class="highlight">{v-pre{ final_attack }v-pre}</span>
</div>
<div class="divider"></div>
<div class="stat-row">
<span>伤害倍率</span>
<span class="multiplier">x{v-pre{ damage_multiplier }v-pre}</span>
</div>
</div>
</body>
</rml>五、数据绑定性能优化
5.1 批量更新
cpp
class OptimizedDataModel : public Rml::DataModel
{
public:
// 开始批量更新
void BeginUpdate()
{
is_updating_ = true;
changed_fields_.clear();
}
// 结束批量更新并通知
void EndUpdate()
{
is_updating_ = false;
if (!changed_fields_.empty())
{
// 一次性通知所有变化的字段
for (const auto& field : changed_fields_)
{
NotifyChanged(field);
}
changed_fields_.clear();
}
}
// 重写 NotifyChanged 来支持批量
void NotifyChanged(const Rml::String& name) override
{
if (is_updating_)
{
changed_fields_.insert(name);
}
else
{
Rml::DataModel::NotifyChanged(name);
}
}
private:
bool is_updating_ = false;
std::set<Rml::String> changed_fields_;
};
// 使用示例
void UpdatePlayerStats()
{
player_data->BeginUpdate();
player_data->SetHp(new_hp);
player_data->SetMana(new_mana);
player_data->SetAttack(new_attack);
player_data->SetDefense(new_defense);
player_data->EndUpdate(); // 只触发一次 UI 更新
}5.2 延迟更新
cpp
class DebouncedDataModel : public Rml::DataModel
{
public:
void RequestUpdate(const Rml::String& field, int value)
{
pending_updates_[field] = value;
if (!timer_active_)
{
timer_active_ = true;
// 100ms 后应用更新
ScheduleUpdate(100);
}
}
private:
std::unordered_map<Rml::String, int> pending_updates_;
bool timer_active_ = false;
void ScheduleUpdate(int delay_ms)
{
// 使用系统接口的定时器
// 或者在游戏主循环中处理
}
void ApplyPendingUpdates()
{
timer_active_ = false;
for (const auto& [field, value] : pending_updates_)
{
// 应用更新并通知
SetFieldValue(field, value);
NotifyChanged(field);
}
pending_updates_.clear();
}
};5.3 虚拟化列表
对于大型列表,只渲染可见项:
cpp
class VirtualListModel : public Rml::DataModel
{
public:
RMLUI_DATA_BINDINGS
{
RMLUI_DATA_BINDING(visible_items, &visible_items_)
RMLUI_DATA_BINDING(total_count, &total_count_)
}
void SetScrollPosition(int scroll_y)
{
scroll_y_ = scroll_y;
UpdateVisibleItems();
}
void SetItemHeight(int height)
{
item_height_ = height;
UpdateVisibleItems();
}
void SetViewportHeight(int height)
{
viewport_height_ = height;
UpdateVisibleItems();
}
private:
int scroll_y_ = 0;
int item_height_ = 40;
int viewport_height_ = 600;
Rml::DataList all_items_;
Rml::DataList visible_items_;
int total_count_ = 0;
void UpdateVisibleItems()
{
// 计算可见范围
int start_index = scroll_y_ / item_height_;
int visible_count = viewport_height_ / item_height_ + 2; // 多渲染一些作为缓冲
start_index = std::max(0, start_index);
int end_index = std::min(static_cast<int>(all_items_.size()),
start_index + visible_count);
// 更新可见项
visible_items_.clear();
for (int i = start_index; i < end_index; ++i)
{
Rml::DataDictionary item = all_items_[i].Get<Rml::DataDictionary>();
item["virtual_index"] = i; // 保存原始索引
visible_items_.push_back(Rml::DataValue(item));
}
total_count_ = static_cast<int>(all_items_.size());
NotifyChanged("visible_items");
NotifyChanged("total_count");
}
};xml
<!-- 虚拟化列表 UI -->
<div class="virtual-list" style="height: {v-pre{ total_count * item_height }v-pre}px">
<div class="virtual-list-content" style="transform: translateY({v-pre{ scroll_y }v-pre}px)">
<div class="list-item"
for="item in visible_items"
style="height: {v-pre{ item_height }v-pre}px">
<span>{v-pre{ item.name }v-pre}</span>
</div>
</div>
</div>六、实践练习
练习 1:创建排行榜系统
实现一个玩家排行榜:
- 使用自定义格式化显示分数(如 1,000,000)
- 支持按不同条件排序
- 只显示前 100 名,支持滚动加载
练习 2:实现邮件系统
创建游戏邮件界面:
- 邮件列表和邮件详情
- 支持标记已读/未读
- 附件领取功能
- 过期邮件自动清理
练习 3:制作技能树
设计一个技能树界面:
- 显示技能节点和连接
- 技能点分配和重置
- 前置技能检查
- 技能效果预览
📝 检查清单
下一节:自定义数据视图 - 学习如何创建完全自定义的数据视图组件。