Тема интерфейса: наследование и локальные изменения
Рабочий пример общей темы, порядка поиска и отдельного StyleBox для одной кнопки.
Где хранить оформление
Theme объединяет цвета, константы, шрифты, размеры шрифтов, текстуры, StyleBox и звуки по имени элемента и типу Control. Например, font_color типа Button и font_color типа Label — разные записи. Имена и регистр должны совпадать с API конкретного элемента. Список доступных имён есть в свойствах темы страницы типа API.
Назначьте общую тему корню UI. Дочерние Control и Window могут использовать её и дополнять своими темами. Для одного исключения удобно локальное Theme Override, для повторяемого варианта кнопки — именованная вариация типа. Не копируйте всю тему ради одного цвета.
Запустить пример
Добавьте UiSampleStyle.cs и ThemeBasicsDemo.cs в C#-проект. Прикрепите ThemeBasicsDemo к корню Control отдельного уровня, соберите и запустите F6.
Кнопка изменения общего фона меняет StyleBox у двух первых кнопок. Отдельный фон первой копирует ресурс и меняет только первую. Удаление override возвращает её к общей теме. Цвет фона состояния normal лучше виден, когда указатель находится вне этой кнопки: hover и pressed имеют отдельные записи.
ThemeBasicsDemo.cs
using Reckfell;
public partial class ThemeBasicsDemo : Control
{
public Button Left { get; private set; } = null!;
public Button Right { get; private set; } = null!;
public StyleBoxFlat SharedNormal { get; private set; } = null!;
public override void _Ready()
{
UiSampleStyle.Prepare(this);
Theme.SetColor("font_color", "Button", Colors.White);
SharedNormal = (StyleBoxFlat)Theme.GetStylebox("normal", "Button");
var margin = new MarginContainer(); AddChild(margin); margin.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect, margin: 24);
var column = new VBoxContainer(); margin.AddChild(column);
column.AddChild(new Label { Text = UiSampleStyle.Text("Общая тема и локальное изменение", "Shared theme and local override") });
Left = new Button { Text = UiSampleStyle.Text("Первая кнопка", "First button") };
Right = new Button { Text = UiSampleStyle.Text("Вторая кнопка", "Second button") };
column.AddChild(Left); column.AddChild(Right);
var shared = new Button { Text = UiSampleStyle.Text("Изменить общий фон", "Change shared background") }; column.AddChild(shared);
shared.Pressed += () => SharedNormal.BgColor = new Color(.08f, .16f, .35f);
var local = new Button { Text = UiSampleStyle.Text("Отдельный фон первой", "Separate first background") }; column.AddChild(local);
local.Pressed += SeparateLeft;
var reset = new Button { Text = UiSampleStyle.Text("Убрать локальное изменение", "Remove local override") }; column.AddChild(reset);
reset.Pressed += () => Left.RemoveThemeStyleboxOverride("normal");
}
public void SeparateLeft()
{
var own = (StyleBoxFlat)Left.GetThemeStylebox("normal").Duplicate();
own.BgColor = new Color(.12f, .28f, .7f);
Left.AddThemeStyleboxOverride("normal", own);
}
}Как выбирается значение
Для обычного запроса Control сначала проверяет своё локальное переопределение. Затем ищет в темах ветви от ближайшего владельца к родителям; внутри каждой темы учитывает вариацию и цепочку типов. После ветви используются проектная/стандартная темы и резервные значения.
Владелец темы здесь — Control или Window с ресурсом Theme в цепочке UI, а не свойство Entity.Owner, используемое при сохранении иерархии. Обычная Entity между элементами разрывает наследование: Control(Theme) → Entity → Button не передаёт тему кнопке. Для UI-группировки используйте Control/Container либо явно назначьте Theme дочернему UI-корню. Изменение Entity.Owner не восстанавливает наследование.
Это порядок «ближайшая тема, затем её типы», а не глобальное правило «вариация всегда сильнее Button везде». Частичная локальная тема может заменить только один параметр, а остальные получить выше. Control.GetThemeColor/GetThemeStylebox возвращают разрешённое для элемента значение. Theme.GetColor/GetStylebox читают сам ресурс и не обходят иерархию Control.
| Изменение | Где действует |
|---|---|
| AddThemeColorOverride у одной кнопки | Только этот Control; RemoveThemeColorOverride отменяет исключение |
| SetColor в общей Theme | На пользователей этой записи, если более близкое значение её не перекрыло |
| Изменение общего StyleBoxFlat | На все элементы, разделяющие ресурс |
| Duplicate StyleBox и локальный override | На отдельную копию |
Duplicate без глубокого копирования достаточно для показанного StyleBoxFlat. Для ресурса с вложенными изменяемыми ресурсами проверьте, какие из них всё ещё разделяются; глубокое копирование имеет собственные правила ресурсов. Получение GetThemeStylebox не создаёт независимую копию.
Для диагностики различайте три вопроса:
| Проверка | Что показывает |
|---|---|
Theme.HasColor("font_color", "Button") | Есть ли такая запись в конкретном ресурсе |
button.HasThemeColorOverride("font_color") | Есть ли локальное исключение именно у кнопки |
button.GetThemeColor("font_color") | Какое значение получено после поиска по темам |
Практический порядок настройки
Сначала задайте типографику, внутренние отступы и обычные состояния. Затем hover, pressed, disabled и заметный focus. Ширина рамки/отступы StyleBox могут менять требуемый размер; проверьте раскладку с длинными переводами. Modulate меняет итоговый рисунок целиком и не заменяет настройку конкретного цвета темы.
Для нескольких локальных изменений можно использовать BeginBulkThemeOverride/EndBulkThemeOverride, чтобы объединить уведомления. Завершайте блок через finally при возможном исключении: незавершённый bulk оставляет уведомления отложенными. Это флаг, а не счётчик вложенности; не вкладывайте такие блоки друг в друга. Удаляйте устаревшие overrides при переходе на общую тему: иначе они продолжат перекрывать новый дизайн. Для проекта целиком есть gui/theme/custom, для шрифта — gui/theme/custom_font; эти настройки требуют соответствующего перезапуска.