Table of Contents

Свои элементы управления

В этом руководстве строится HealthBar — элемент для игрового HUD: он показывает, сколько осталось здоровья, имеет свои типизированные свойства, используется в разметке как любой встроенный элемент и краснеет, когда здоровья меньше четверти. По пути вы увидите, что нужно элементу, чтобы вести себя как встроенные: свойства, доступные разметке и привязкам, компоновка, заменяемый шаблон и состояния, которые может стилизовать тема.

Вам понадобится игра с подключённым IcyUI (Начало работы) и представление о шаблонах и визуальных состояниях.

Совет

Многим «своим элементам» код не нужен. ProgressBar со стилем и шаблоном уже даёт хорошую полосу здоровья. Пишите элемент, когда у него есть собственное поведение или свойства, с которыми работает код игры.

1. Наследуйтесь от Control

Наследуйтесь от Control, если нужен элемент с фоном, рамкой и шаблоном, или от UIElement, если вы рисуете и компонуете его сами. HealthBar — это Control, который держит заливку внутри рамки:

/// <summary>
/// A bar that shows how much health is left. Below a quarter it switches to a "critical" look, which the theme styles.
/// </summary>
public class HealthBar : Control
{
    private const float CriticalRatio = 0.25f;

    private readonly Border defaultFill;
    private Border fill;
    private float maximum = 100;
    private float health = 100;

    public HealthBar()
    {
        defaultFill = fill = new Border
        {
            HorizontalAlignment = HorizontalAlignment.Left,
            VerticalAlignment = VerticalAlignment.Stretch,
            Background = new SolidColorBrush(Color.MediumSeaGreen),
        };
        ((Border)Chrome).Child = fill;
    }

    /// <summary>The brush of the filled part. A template binds its fill to this property.</summary>
    [Category("Appearance")]
    [RegisterReference]
    public IBrush FillBrush
    {
        get => fill.Background;
        set => fill.Background = value;
    }

    /// <summary>The health that fills the bar.</summary>
    [Category("Behavior")]
    [DefaultValue(100f)]
    [RegisterReference]
    [AffectsArrange]
    public float Maximum
    {
        get => maximum;
        set
        {
            if (SetProperty(ref maximum, value))
            {
                Value = health;
                UpdateCriticalState();
                InvalidateArrange();
            }
        }
    }

    /// <summary>The current health, kept between 0 and <see cref="Maximum"/>.</summary>
    [Category("Behavior")]
    [DefaultValue(100f)]
    [RegisterReference]
    [AffectsArrange]
    public float Value
    {
        get => health;
        set
        {
            if (SetProperty(ref health, float.Clamp(value, 0, Maximum)))
            {
                UpdateCriticalState();
                InvalidateArrange();
            }
        }
    }

    protected override Size MeasureContent() => new(160, 18);

    protected override void ArrangeContent()
    {
        float ratio = Maximum > 0 ? Value / Maximum : 0;
        fill.Width = ContentBounds.Width * ratio;
        base.ArrangeContent();
    }

    // A template provides the fill as a part named PART_Fill; without one, the built-in fill stays.
    protected override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        if (Template != null && GetTemplateChild<Border>("PART_Fill") is { } part)
        {
            fill = part;
            return;
        }

        fill = defaultFill;
        if (Template == null)
            ((Border)Chrome).Child = fill;
    }

    // Highlighted is the state a theme styles: a custom control can only use the states IcyUI defines.
    private void UpdateCriticalState()
    {
        bool critical = Maximum > 0 && Value / Maximum < CriticalRatio;
        ControlState = critical ? ControlState | ControlState.Highlighted : ControlState & ~ControlState.Highlighted;
    }
}

Четыре вещи делают его «хорошим гражданином»:

  • Свойства. [RegisterReference] делает свойство доступным атрибутам разметки, стилям, привязкам и инструментам дизайна; вызывать регистрацию не нужно. SetProperty вызывает PropertyChanged, и именно так привязки и стили замечают изменение. [Category] и [DefaultValue] описывают свойство для редактора.
  • Компоновка. MeasureContent сообщает естественный размер (HealthBar — 160 × 18, пока страница не задаст размер), а ArrangeContent раскладывает части, когда окончательный размер известен. [AffectsArrange] (или [AffectsMeasure]) сообщает системе свойств, что изменение требует нового прохода компоновки; из кода это делает InvalidateArrange().
  • Заменяемый шаблон. Тема или страница может задать элементу Template (см. шаг 3). Элемент хранит ссылку на часть, которую двигает, — заливку, — поэтому переопределяет OnApplyTemplate(), чтобы найти PART_Fill шаблона через GetTemplateChild, либо вернуться к собственной заливке, когда шаблона нет.
  • Состояния. Тема стилизует элемент через его ControlState. Набор флагов фиксирован (ControlState): свой элемент выбирает тот, что подходит по смыслу. HealthBar использует Highlighted для «критического» уровня и сохраняет флаги, которые ставит система ввода, например Hovered.

2. Используйте его в разметке

Разметка находит тип через префикс clr-namespace с именем сборки вашей игры:

<!-- A HUD with the custom HealthBar. The data context has a Health number. -->
<StackPanel xmlns:game="clr-namespace:MyGame.Controls;assembly=MyGame"
            HorizontalAlignment="Left" VerticalAlignment="Top" Margin="16" Width="200">
  <TextBlock Margin="0,0,0,4">Health</TextBlock>
  <game:HealthBar x:Name="PlayerHealth" Maximum="100" Value="{Binding Health}" />
</StackPanel>

Value="{Binding Health}" привязывает полосу к модели представления HUD, как у любого другого элемента. Чтобы обойтись без префикса, один раз при запуске зарегистрируйте короткое имя через RegisterShortName и пишите <HealthBar/>. Форма clr-namespace работает всегда; короткие имена лишь убирают префикс.

3. Задайте внешний вид по умолчанию в своей теме

У элемента нет вида во встроенной теме, только простая заливка из конструктора. Шаблон и состояния ему даёт тема вашей игры. Тема — это файл, который накладывается на встроенную через ExtendDefaultTheme; см. Свои темы:

<!-- Themes/MyGame.xml: the default look of HealthBar, with a red fill below a quarter (the Highlighted state). -->
<ResourceDictionary xmlns:game="clr-namespace:MyGame.Controls;assembly=MyGame">
  <ControlTemplate x:Key="HealthBarTemplate" TargetType="game:HealthBar">
    <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
      <Border x:Name="PART_Fill" HorizontalAlignment="Left" VerticalAlignment="Stretch" Background="{TemplateBinding FillBrush}"/>
    </Border>
  </ControlTemplate>

  <Style TargetType="game:HealthBar" Template="{StaticResource HealthBarTemplate}"
         Background="#FF2A2A32" BorderBrush="#FF56566A" BorderThickness="1" FillBrush="#FF3CB371">
    <Style.StateGroups>
      <VisualStateGroup Name="HealthStates">
        <VisualState Name="Critical" State="Highlighted" FillBrush="#FFD83C3C"/>
      </VisualStateGroup>
    </Style.StateGroups>
  </Style>
</ResourceDictionary>
  • PART_Fill шаблона — это часть, которую ищет элемент. Шаблон без неё всё равно применяется, но полоса не показывает заливку.
  • State="Highlighted" — состояние, которое HealthBar ставит при уровне ниже четверти; стиль меняет только FillBrush.
  • Файл называет тип через префикс clr-namespace, а не через короткое имя: файл темы загружается при построении конфигурации, до того как ваш код запуска успеет зарегистрировать короткое имя.

Загрузите тему и страницу при запуске игры:

using Icy.Configuration;
using Icy.Stride.Configuration;
using Stride.Engine;

/// <summary>Add this script to an entity in your scene: it sets up IcyUI with the game's theme and the HealthBar.</summary>
public class HudStartup : StartupScript
{
    public override void Start()
    {
        // The theme file names HealthBar with a clr-namespace prefix, so it loads while the configuration is built.
        StrideIcyHost ui = Game.UseIcyUI(builder => builder.ConfigureTheme().ExtendDefaultTheme("Themes/MyGame.xml"));

        // Optional: lets pages write <HealthBar/> without an xmlns prefix.
        ui.Configuration.Types.Markup.RegisterShortName<HealthBar>();

        ui.MainCanvas.LoadMarkup("Hud.xml", new HudViewModel());
    }
}

Меняйте Health в модели представления из кода игры — полоса следует за ним; ниже 25 заливка становится красной.

Что вы построили

Элемент с типизированными свойствами, которые могут использовать разметка, стили и привязки, сам себя компонует, работает с шаблоном и без него и открывает состояние, которое стилизует тема, — без единой строки кода рисования.

Примечание

Свой элемент пока не может добавлять новые флаги визуальных состояний: берите ближайший существующий и описывайте в теме, что он значит, как это делает HealthBar с Highlighted.

Дальше