Свои элементы управления
В этом руководстве строится 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.
Дальше
- Свои темы: перекрасьте встроенные элементы и добавьте собственные шаблоны.
- Шаблоны и визуальные состояния: идеи, на которых держатся шаги 1 и 3.
- Справочник по разметке: пространства имён и короткие имена.