Свой Control: рисунок, мышь и клавиатура
Полный интерактивный индикатор: минимальный размер, перетаскивание, фокус, ограничение значения и сигнал.
Когда нужен собственный элемент
Для обычного ползунка используйте HSlider: у него уже есть готовое поведение и настройки. Свой Control нужен для другого представления или специфического взаимодействия. Ниже — минимальная рабочая основа: значение 0…1, рисование полосы, мышь, клавиатура и сигнал. Она не обещает все возможности готового Slider или средств доступности.
Добавьте UiSampleStyle.cs, MeterControl.cs и MeterDemo.cs. Прикрепите MeterDemo к корню Control, соберите и запустите F6. Щелчок и перетаскивание задают значение, стрелки меняют его на 0.05. Белая рамка обозначает фокус; подпись показывает процент. После изменения окна полоса использует новую ширину.
MeterControl.cs
using Reckfell;
public partial class MeterControl : Control
{
[Signal] public delegate void ValueChangedEventHandler(float value);
private float _value = .25f;
private bool _dragging;
[Export(PropertyHint.Range, "0,1,0.01")]
public float Value
{
get => _value;
set
{
if (!float.IsFinite(value)) return;
float next = Mathf.Clamp(value, 0, 1);
if (Mathf.IsEqualApprox(next, _value)) return;
_value = next; QueueRedraw(); EmitSignal(SignalName.ValueChanged, next);
}
}
public override Vector2 _GetMinimumSize() => new(160, 40);
public override void _Ready()
{
FocusMode = FocusModeEnum.All; MouseFilter = MouseFilterEnum.Stop;
FocusEntered += QueueRedraw;
FocusExited += () => { _dragging = false; QueueRedraw(); };
Resized += QueueRedraw;
VisibilityChanged += () => { if (!IsVisibleInTree()) _dragging = false; };
}
public override void _Draw()
{
DrawRect(new Rect2(0, 10, Size.X, 20), new Color(.16f, .16f, .18f));
DrawRect(new Rect2(0, 10, Size.X * _value, 20), new Color(.15f, .35f, 1));
if (HasFocus()) DrawRect(new Rect2(Vector2.Zero, Size), Colors.White, false, 2);
}
public override void _GuiInput(InputEvent @event)
{
if (@event is InputEventMouseButton click && click.ButtonIndex == MouseButton.Left)
{
_dragging = click.Pressed;
if (click.Pressed) { GrabFocus(); Value = click.Position.X / Mathf.Max(1, Size.X); }
AcceptEvent();
}
else if (@event is InputEventMouseMotion motion && _dragging)
{
Value = motion.Position.X / Mathf.Max(1, Size.X); AcceptEvent();
}
else if (@event.IsActionPressed("ui_left", true)) { Value -= .05f; AcceptEvent(); }
else if (@event.IsActionPressed("ui_right", true)) { Value += .05f; AcceptEvent(); }
}
}MeterDemo.cs
using Reckfell;
public partial class MeterDemo : Control
{
public MeterControl Meter { get; private set; } = null!;
public Label Status { get; private set; } = null!;
public override void _Ready()
{
UiSampleStyle.Prepare(this);
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("Мышь или стрелки ← →", "Mouse or arrow keys") });
Meter = new MeterControl { Name = "Meter", SizeFlagsHorizontal = SizeFlags.ExpandFill }; column.AddChild(Meter);
Status = new Label(); column.AddChild(Status); UpdateLabel(Meter.Value);
Meter.ValueChanged += UpdateLabel;
Callable.From(() => Meter.GrabFocus()).CallDeferred();
}
private void UpdateLabel(float value) => Status.Text = $"{value:P0}";
}Разделите размер, состояние и рисунок
_GetMinimumSize() сообщает контейнеру требование 160 × 40, но не задаёт положение. _Draw() использует текущую Size. При изменении состояния вызывается QueueRedraw; не нужно пересоздавать сущность или рисовать из _Process каждый кадр. Если меняется само требование к минимуму, вызовите UpdateMinimumSize отдельно.
Команды DrawRect и другие Draw* вызывайте из _Draw или обработчика Draw, а не напрямую из сеттера значения. QueueRedraw откладывает перерисовку и объединяет повторные запросы; это не немедленный вызов _Draw. Изменение минимального размера и запрос рисунка — отдельные операции.
Export Range помогает редактору, но не защищает присваивания из кода. Сеттер отклоняет NaN/Infinity, ограничивает значение и не выдаёт повторный сигнал, если оно не изменилось. ValueChanged сообщает именно принятое состояние, поэтому подпись согласована с полосой.
Нарисованная полоса занимает только часть высоты Control, но стандартная область клика — весь прямоугольник от Vector2.Zero до Size. Рисование за его пределами само не расширяет область клика. Для нестандартной геометрии переопределите _HasPoint(Vector2 point) с локальными координатами; отдельно проверьте минимум, clipping и расположение дочерних элементов.
Локальные координаты и обработка события
В _GuiInput() Position мыши уже локальна относительно этого Control: деление X на Size.X даёт относительную долю. Не вычитайте GlobalPosition повторно. После нажатия viewport сохраняет получателя мыши до отпускания, поэтому пример может ограничить значение и при перетаскивании за край. Отпускание прекращает drag; скрытие и потеря фокуса тоже сбрасывают его состояние.
| MouseFilter | Что происходит |
|---|---|
| Stop | Элемент получает указатель и останавливает обычное распространение вверх |
| Pass | Необработанное событие может идти к родителю; это не щелчок по соседу под ним |
| Ignore | Элемент исключён из поиска получателя; подходит декоративной картинке поверх кнопки |
Для клавиатуры нужны FocusMode.All и реальный GrabFocus. Пример принимает ui_left/right в _GuiInput() и вызывает AcceptEvent, поэтому стрелка меняет значение вместо перехода фокуса или попадания в _UnhandledInput(). AcceptEvent не отменяет более ранний _Input() и не меняет состояние опрашиваемых Input-действий.
Проверьте нажатие, удержание, отпускание за пределами, повтор клавиши, скрытие во время drag и изменение размера. Для touch, wheel, экранного диктора и контроллера нужно реализовать и проверить соответствующее поведение отдельно.