跳转到内容

3.2 数据绑定基础

RmlUi 的数据绑定系统允许你将 UI 元素与数据模型连接起来,实现数据驱动的界面。当数据发生变化时,UI 会自动更新,无需手动操作 DOM。


一、数据绑定概述

1.1 什么是数据绑定

数据绑定是一种将数据源(模型)与 UI 元素(视图)自动同步的技术:

数据变化 → 自动更新 UI
UI 交互 → 自动更新数据

1.2 绑定的优势

  • 减少样板代码:无需手动更新每个 UI 元素
  • 降低耦合:数据和视图分离
  • 提高可维护性:数据逻辑集中管理
  • 自动同步:避免数据不一致

1.3 支持的绑定类型

绑定类型描述示例
文本绑定绑定文本内容{{ player.name }}
属性绑定绑定元素属性value="{{ score }}"
条件绑定根据条件显示/隐藏if="player.is_alive"
循环绑定遍历数组渲染for="item in items"
类名绑定动态类名class.active="{{ is_active }}"

二、数据模型基础

2.1 创建数据模型

RmlUi 使用 DataModel 类来定义数据模型:

cpp
#include <RmlUi/Core.h>
#include <RmlUi/Debugger.h>

class PlayerData : public Rml::DataModel
{
public:
    // 定义数据字段
    RMLUI_DATA_BINDINGS
    {
        RMLUI_DATA_BINDING(name, &name_)
        RMLUI_DATA_BINDING(level, &level_)
        RMLUI_DATA_BINDING(hp, &hp_)
        RMLUI_DATA_BINDING(max_hp, &max_hp_)
        RMLUI_DATA_BINDING(is_alive, &is_alive_)
    }

    // Getter 和 Setter
    const Rml::String& GetName() const { return name_; }
    void SetName(const Rml::String& name)
    {
        name_ = name;
        NotifyChanged("name");  // 通知 UI 更新
    }

    int GetLevel() const { return level_; }
    void SetLevel(int level)
    {
        level_ = level;
        NotifyChanged("level");
    }

    int GetHp() const { return hp_; }
    void SetHp(int hp)
    {
        hp_ = hp;
        NotifyChanged("hp");
        NotifyChanged("is_alive");  // 依赖属性也要通知
    }

    int GetMaxHp() const { return max_hp_; }

    bool GetIsAlive() const { return hp_ > 0; }

private:
    Rml::String name_ = "勇者";
    int level_ = 1;
    int hp_ = 100;
    int max_hp_ = 100;
};

2.2 注册数据模型

cpp
#include <RmlUi/Core.h>

void InitDataBinding()
{
    // 创建数据模型实例
    auto player_data = std::make_shared<PlayerData>();

    // 创建数据模型包
    Rml::DataModelConstructor constructor =
        Rml::DataModelFactory::RegisterDataModel("player", player_data);

    // 或者在创建 Context 时注册
    Rml::Context* context = Rml::CreateContext("game", Rml::Vector2i(1920, 1080));
    context->AddDataModel("player", player_data);
}

2.3 在 RML 中使用数据模型

xml
<!-- player_info.rml -->
<rml>
<head>
    <link type="text/rcss" href="style.rcss"/>
</head>
<body data-model="player">
    <div class="player-info">
        <!-- 文本绑定 -->
        <h2 class="player-name">{v-pre{ name }v-pre}</h2>

        <!-- 显示等级 -->
        <span class="level">Lv.{v-pre{ level }v-pre}</span>

        <!-- 血量条 -->
        <div class="hp-bar">
            <div class="hp-fill" style="width: {v-pre{ hp }v-pre} / {v-pre{ max_hp }v-pre} * 100%"></div>
        </div>
        <span class="hp-text">{v-pre{ hp }v-pre}/{v-pre{ max_hp }v-pre}</span>

        <!-- 状态显示 -->
        <div class="status" if="is_alive">
            状态:存活
        </div>
        <div class="status defeated" if="!is_alive">
            状态:已击败
        </div>
    </div>
</body>
</rml>

三、绑定语法

3.1 文本插值

xml
<!-- 基本语法 -->
<div>{v-pre{ variable_name }v-pre}</div>

<!-- 对象属性 -->
<div>{v-pre{ player.name }v-pre}</div>
<div>{v-pre{ player.stats.hp }v-pre}</div>

<!-- 表达式(有限支持) -->
<div>{v-pre{ hp }v-pre} / {v-pre{ max_hp }v-pre}</div>

3.2 属性绑定

xml
<!-- 绑定到 value 属性 -->
<input type="text" value="{v-pre{ player_name }v-pre}"/>

<!-- 绑定到 src 属性 -->
<img src="{v-pre{ avatar_url }v-pre}"/>

<!-- 绑定到 href 属性 -->
<a href="{v-pre{ link_url }v-pre}">点击</a>

3.3 条件渲染

xml
<!-- 简单条件 -->
<div if="is_visible">
    这个元素会根据 is_visible 显示或隐藏
</div>

<!-- 比较表达式 -->
<div if="level >= 10">
    只有等级 >= 10 时显示
</div>

<!-- 多条件 -->
<div if="is_alive && hp < 30">
    低血量警告
</div>

<!-- 否定条件 -->
<div if="!has_item">
    没有物品时显示
</div>

3.4 类名绑定

xml
<!-- 动态添加类 -->
<div class.active="is_selected">
    选中时添加 active 类
</div>

<!-- 多类绑定 -->
<div class.disabled="!can_interact" class.highlighted="is_highlighted">
    多个动态类
</div>
css
/* RCSS */
.disabled {
    opacity: 0.5;
    pointer-events: none;
}

.highlighted {
    border: 2px solid gold;
}

四、数组和列表绑定

4.1 基本数组绑定

cpp
class InventoryData : public Rml::DataModel
{
public:
    RMLUI_DATA_BINDINGS
    {
        RMLUI_DATA_BINDING(items, &items_)
    }

    // 使用 DataList 存储数组数据
    Rml::DataList items_;

    // 添加物品
    void AddItem(const Rml::String& name, int count)
    {
        Rml::DataDictionary item;
        item["name"] = name;
        item["count"] = count;
        item["icon"] = "icons/" + name + ".png";

        items_.push_back(Rml::DataValue(item));
        NotifyChanged("items");
    }

    // 移除物品
    void RemoveItem(size_t index)
    {
        if (index < items_.size())
        {
            items_.erase(items_.begin() + index);
            NotifyChanged("items");
        }
    }
};
xml
<!-- 遍历数组 -->
<rml>
<head>
    <link type="text/rcss" href="inventory.rcss"/>
</head>
<body data-model="inventory">
    <div class="inventory-grid">
        <!-- for 循环渲染 -->
        <div class="item-slot" for="item in items">
            <img src="{v-pre{ item.icon }v-pre}"/>
            <span class="item-name">{v-pre{ item.name }v-pre}</span>
            <span class="item-count">{v-pre{ item.count }v-pre}</span>
        </div>
    </div>
</body>
</rml>

4.2 带索引的循环

xml
<!-- 使用索引 -->
<div class="item-slot" for="item, index in items">
    <span class="slot-index">{v-pre{ index + 1 }v-pre}</span>
    <img src="{v-pre{ item.icon }v-pre}"/>
    <span>{v-pre{ item.name }v-pre}</span>
</div>

4.3 条件过滤

cpp
// 在 C++ 中过滤数据
class FilteredInventory : public Rml::DataModel
{
public:
    // 只返回武器类物品
    Rml::DataList GetWeapons() const
    {
        Rml::DataList result;
        for (const auto& item : all_items_)
        {
            if (item.GetType() == Rml::DataValueType::Dictionary)
            {
                const auto& dict = item.Get<Rml::DataDictionary>();
                if (dict.Get<Rml::String>("type") == "weapon")
                {
                    result.push_back(item);
                }
            }
        }
        return result;
    }
};
xml
<!-- 使用计算属性 -->
<div class="weapon-slot" for="weapon in weapons">
    <img src="{v-pre{ weapon.icon }v-pre}"/>
    <span>{v-pre{ weapon.name }v-pre}</span>
</div>

五、实战:角色属性面板

5.1 数据模型定义

cpp
// CharacterData.h
#pragma once
#include <RmlUi/Core.h>

class CharacterData : public Rml::DataModel
{
public:
    CharacterData()
    {
        // 初始化属性
        strength_ = 10;
        agility_ = 10;
        intelligence_ = 10;
        available_points_ = 5;
        UpdateDerivedStats();
    }

    RMLUI_DATA_BINDINGS
    {
        // 基础属性
        RMLUI_DATA_BINDING(name, &name_)
        RMLUI_DATA_BINDING(level, &level_)
        RMLUI_DATA_BINDING(exp, &exp_)
        RMLUI_DATA_BINDING(max_exp, &max_exp_)

        // 战斗属性
        RMLUI_DATA_BINDING(strength, &strength_)
        RMLUI_DATA_BINDING(agility, &agility_)
        RMLUI_DATA_BINDING(intelligence, &intelligence_)

        // 派生属性
        RMLUI_DATA_BINDING(hp, &hp_)
        RMLUI_DATA_BINDING(mana, &mana_)
        RMLUI_DATA_BINDING(attack, &attack_)
        RMLUI_DATA_BINDING(defense, &defense_)

        // 状态
        RMLUI_DATA_BINDING(available_points, &available_points_)
        RMLUI_DATA_BINDING(can_upgrade_strength, &can_upgrade_strength_)
        RMLUI_DATA_BINDING(can_upgrade_agility, &can_upgrade_agility_)
        RMLUI_DATA_BINDING(can_upgrade_intelligence, &can_upgrade_intelligence_)
    }

    // 升级属性
    void UpgradeStrength()
    {
        if (CanUpgradeStrength())
        {
            strength_++;
            available_points_--;
            UpdateDerivedStats();
            NotifyAllChanged();
        }
    }

    void UpgradeAgility()
    {
        if (CanUpgradeAgility())
        {
            agility_++;
            available_points_--;
            UpdateDerivedStats();
            NotifyAllChanged();
        }
    }

    void UpgradeIntelligence()
    {
        if (CanUpgradeIntelligence())
        {
            intelligence_++;
            available_points_--;
            UpdateDerivedStats();
            NotifyAllChanged();
        }
    }

    // 获得经验
    void GainExp(int amount)
    {
        exp_ += amount;
        if (exp_ >= max_exp_)
        {
            LevelUp();
        }
        NotifyChanged("exp");
    }

private:
    // 基础信息
    Rml::String name_ = "勇者";
    int level_ = 1;
    int exp_ = 0;
    int max_exp_ = 100;

    // 基础属性
    int strength_;
    int agility_;
    int intelligence_;

    // 派生属性
    int hp_ = 100;
    int mana_ = 50;
    int attack_ = 20;
    int defense_ = 10;

    // 状态
    int available_points_;

    // 计算属性
    bool GetCanUpgradeStrength() const { return available_points_ > 0; }
    bool GetCanUpgradeAgility() const { return available_points_ > 0; }
    bool GetCanUpgradeIntelligence() const { return available_points_ > 0; }

    void UpdateDerivedStats()
    {
        // 根据基础属性计算派生属性
        hp_ = 100 + (strength_ * 10);
        mana_ = 50 + (intelligence_ * 5);
        attack_ = 10 + (strength_ * 2) + (agility_ * 1);
        defense_ = 5 + (strength_ * 1) + (agility_ * 1);
    }

    void LevelUp()
    {
        level_++;
        exp_ -= max_exp_;
        max_exp_ = static_cast<int>(max_exp_ * 1.5);
        available_points_ += 3;

        // 升级回满血
        hp_ = 100 + (strength_ * 10);
    }
};

5.2 UI 定义

xml
<!-- character_panel.rml -->
<rml>
<head>
    <link type="text/rcss" href="character.rcss"/>
</head>
<body data-model="character">
    <div class="character-panel">
        <!-- 基本信息 -->
        <div class="header">
            <h2 class="name">{v-pre{ name }v-pre}</h2>
            <span class="level">Lv.{v-pre{ level }v-pre}</span>
        </div>

        <!-- 经验条 -->
        <div class="exp-bar">
            <div class="exp-fill" style="width: {v-pre{ exp }v-pre} / {v-pre{ max_exp }v-pre} * 100%"></div>
        </div>
        <span class="exp-text">{v-pre{ exp }v-pre}/{v-pre{ max_exp }v-pre} EXP</span>

        <!-- 属性区域 -->
        <div class="attributes">
            <!-- 力量 -->
            <div class="attr-row">
                <span class="attr-name">力量</span>
                <span class="attr-value">{v-pre{ strength }v-pre}</span>
                <button class="btn-upgrade"
                        if="can_upgrade_strength"
                        onclick="UpgradeStrength()">
                    +
                </button>
            </div>

            <!-- 敏捷 -->
            <div class="attr-row">
                <span class="attr-name">敏捷</span>
                <span class="attr-value">{v-pre{ agility }v-pre}</span>
                <button class="btn-upgrade"
                        if="can_upgrade_agility"
                        onclick="UpgradeAgility()">
                    +
                </button>
            </div>

            <!-- 智力 -->
            <div class="attr-row">
                <span class="attr-name">智力</span>
                <span class="attr-value">{v-pre{ intelligence }v-pre}</span>
                <button class="btn-upgrade"
                        if="can_upgrade_intelligence"
                        onclick="UpgradeIntelligence()">
                    +
                </button>
            </div>
        </div>

        <!-- 剩余点数 -->
        <div class="points-remaining">
            剩余点数:{v-pre{ available_points }v-pre}
        </div>

        <!-- 派生属性 -->
        <div class="derived-stats">
            <div class="stat-row">
                <span>生命值</span>
                <span>{v-pre{ hp }v-pre}</span>
            </div>
            <div class="stat-row">
                <span>魔法值</span>
                <span>{v-pre{ mana }v-pre}</span>
            </div>
            <div class="stat-row">
                <span>攻击力</span>
                <span>{v-pre{ attack }v-pre}</span>
            </div>
            <div class="stat-row">
                <span>防御力</span>
                <span>{v-pre{ defense }v-pre}</span>
            </div>
        </div>
    </div>
</body>
</rml>

5.3 样式定义

css
/* character.rcss */

.character-panel {
    background: linear-gradient(135deg, #2c3e50 0%, #1a1a2e 100%);
    border: 2px solid #34495e;
    border-radius: 10px;
    padding: 20px;
    width: 350px;
    color: white;
    font-family: "Noto Sans CJK SC", sans-serif;
}

.header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
}

.name {
    margin: 0;
    font-size: 24px;
    color: #f1c40f;
}

.level {
    background: #3498db;
    padding: 5px 15px;
    border-radius: 20px;
    font-size: 14px;
}

.exp-bar {
    height: 8px;
    background: rgba(0, 0, 0, 0.3);
    border-radius: 4px;
    overflow: hidden;
    margin: 10px 0;
}

.exp-fill {
    height: 100%;
    background: linear-gradient(90deg, #3498db, #2980b9);
    transition: width 0.3s ease;
}

.exp-text {
    font-size: 12px;
    color: rgba(255, 255, 255, 0.7);
}

.attributes {
    margin: 20px 0;
    padding: 15px;
    background: rgba(0, 0, 0, 0.2);
    border-radius: 8px;
}

.attr-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 0;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.attr-row:last-child {
    border-bottom: none;
}

.attr-name {
    font-size: 16px;
}

.attr-value {
    font-size: 20px;
    font-weight: bold;
    color: #3498db;
    min-width: 40px;
    text-align: right;
}

.btn-upgrade {
    width: 30px;
    height: 30px;
    border-radius: 50%;
    border: none;
    background: #27ae60;
    color: white;
    font-size: 18px;
    cursor: pointer;
    margin-left: 10px;
}

.btn-upgrade:hover {
    background: #2ecc71;
    transform: scale(1.1);
}

.points-remaining {
    text-align: center;
    padding: 10px;
    color: #f39c12;
    font-size: 14px;
}

.derived-stats {
    margin-top: 15px;
    padding: 15px;
    background: rgba(0, 0, 0, 0.2);
    border-radius: 8px;
}

.stat-row {
    display: flex;
    justify-content: space-between;
    padding: 5px 0;
    font-size: 14px;
}

.stat-row span:last-child {
    color: #3498db;
}

5.4 C++ 集成

cpp
// main.cpp
#include <RmlUi/Core.h>
#include "CharacterData.h"

int main()
{
    // 初始化 RmlUi
    Rml::SetRenderInterface(&g_RenderInterface);
    Rml::SetSystemInterface(&g_SystemInterface);
    Rml::Initialise();

    // 创建上下文
    Rml::Context* context = Rml::CreateContext("game", Rml::Vector2i(1920, 1080));

    // 创建并注册数据模型
    auto character = std::make_shared<CharacterData>();
    context->AddDataModel("character", character);

    // 加载文档
    Rml::ElementDocument* document = context->LoadDocument("character_panel.rml");
    document->Show();

    // 游戏主循环
    bool running = true;
    while (running)
    {
        context->Update();

        // 模拟获得经验
        static int timer = 0;
        timer++;
        if (timer % 60 == 0)  // 每秒获得一次经验
        {
            character->GainExp(10);
        }

        // 渲染...
        g_RenderInterface.BeginScene();
        context->Render();
        g_RenderInterface.EndScene();

        // 处理退出...
    }

    Rml::Shutdown();
    return 0;
}

六、实践练习

练习 1:创建任务列表

实现一个任务追踪面板:

  • 使用数组绑定显示任务列表
  • 每个任务显示名称、描述、进度
  • 完成任务后从列表中移除

练习 2:实现 Buff 系统

创建一个 Buff 显示区域:

  • 显示当前所有的增益/减益效果
  • 每个 Buff 显示图标和剩余时间
  • Buff 过期自动移除

练习 3:制作装备面板

设计一个装备界面:

  • 显示角色各部位的装备
  • 点击装备槽显示装备详情
  • 支持装备更换

📝 检查清单


下一节:数据绑定进阶 - 深入学习高级绑定技巧、自定义数据视图和性能优化。

基于 MIT 许可发布