reckfellENGINE

Приложение на движке: документ, окна и закрытие

Полный текстовый редактор C# с сохранением, защитой несохранённых изменений и проверкой интеграции с рабочим столом.

На этой странице

Запустить текстовое приложение

Добавьте UiSampleStyle.cs и NotesAppDemo.cs в C#-проект. Прикрепите NotesAppDemo к корню Control отдельного уровня, соберите и запустите F6.

Введите текст, сохраните его и измените снова. Закрытие главного окна предлагает сохранить, отменить выход или явно отказаться от изменений. Загрузка запрещена, пока есть несохранённый текст. Файл находится в user://ui-notes.txt; путь каталога можно получить ProjectSettings.GlobalizePath. По умолчанию подтверждённый выход завершает процесс. QuitOnApproved = false предназначен для проверки обработчиков без остановки тестового проекта.

NotesAppDemo.cs
C#
using System;
using Reckfell;

public partial class NotesAppDemo : Control
{
    [Export] public string FilePath { get; set; } = "user://ui-notes.txt";
    [Export] public bool QuitOnApproved { get; set; } = true;
    public TextEdit Editor { get; private set; } = null!;
    public ConfirmationDialog ClosePrompt { get; private set; } = null!;
    public AcceptDialog About { get; private set; } = null!;
    public Label Status { get; private set; } = null!;
    public bool Dirty { get; private set; }
    public int ApprovedCount { get; private set; }
    private string _saved = "";
    private Window _window = null!;
    private bool _autoAccept;

    public override void _Ready()
    {
        UiSampleStyle.Prepare(this);
        _window = GetWindow(); _autoAccept = GetTree().AutoAcceptQuit; GetTree().AutoAcceptQuit = false;
        _window.CloseRequested += RequestClose;
        var margin = new MarginContainer(); AddChild(margin); margin.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect, margin: 16);
        var column = new VBoxContainer(); margin.AddChild(column);
        var buttons = new HBoxContainer(); column.AddChild(buttons);
        AddButton(buttons, UiSampleStyle.Text("Сохранить", "Save"), () => Status.Text = SaveDocument().ToString());
        AddButton(buttons, UiSampleStyle.Text("Загрузить", "Load"), () => Status.Text = LoadDocument().ToString());
        AddButton(buttons, UiSampleStyle.Text("О программе", "About"), () => About.PopupCentered(new Vector2I(320, 150)));
        Editor = new TextEdit { SizeFlagsVertical = SizeFlags.ExpandFill }; column.AddChild(Editor);
        Status = new Label { Text = FilePath, AutowrapMode = TextServer.AutowrapMode.WordSmart }; column.AddChild(Status);
        Editor.TextChanged += UpdateDirty;
        ClosePrompt = new ConfirmationDialog
        {
            Title = UiSampleStyle.Text("Несохранённый текст", "Unsaved text"),
            DialogText = UiSampleStyle.Text("Сохранить перед выходом?", "Save before closing?"),
            DialogHideOnOk = false, Theme = Theme
        };
        AddChild(ClosePrompt); ClosePrompt.GetOkButton().Text = UiSampleStyle.Text("Сохранить и выйти", "Save and close");
        ClosePrompt.AddButton(UiSampleStyle.Text("Выйти без сохранения", "Discard and close"), action: "discard");
        ClosePrompt.Confirmed += () =>
        {
            Error result = SaveDocument(); Status.Text = result.ToString();
            if (result == Error.Ok) { ClosePrompt.Hide(); ApproveClose(); }
        };
        ClosePrompt.Canceled += () => ClosePrompt.Hide();
        ClosePrompt.CustomAction += action => { if (action == "discard") { ClosePrompt.Hide(); ApproveClose(); } };
        About = new AcceptDialog { Title = "Reckfell UI", DialogText = UiSampleStyle.Text("Пример текстового приложения", "A text application example"), Theme = Theme };
        AddChild(About);
    }
    private static void AddButton(HBoxContainer row, string text, Action action)
    {
        var button = new Button { Text = text }; row.AddChild(button); button.Pressed += action;
    }
    private void UpdateDirty() => Dirty = Editor.Text != _saved;
    public void ReplaceText(string text) { Editor.Text = text; UpdateDirty(); }
    private bool ValidPath() => FilePath.StartsWith("user://") && !FilePath.Contains("..") && FilePath.EndsWith(".txt");
    public Error SaveDocument()
    {
        if (!ValidPath() || Editor.Text.Length > 1000000) return Error.InvalidParameter;
        string target = ProjectSettings.GlobalizePath(FilePath);
        string temporary = target + ".tmp";
        try
        {
            System.IO.File.WriteAllText(temporary, Editor.Text, new System.Text.UTF8Encoding(false));
            System.IO.File.Move(temporary, target, overwrite: true);
        }
        catch (System.IO.IOException) { return Error.FileCantWrite; }
        catch (UnauthorizedAccessException) { return Error.FileCantWrite; }
        catch (ArgumentException) { return Error.InvalidParameter; }
        _saved = Editor.Text; UpdateDirty(); return Error.Ok;
    }
    public Error LoadDocument()
    {
        if (!ValidPath()) return Error.InvalidParameter;
        if (Dirty) return Error.Busy;
        if (!FileAccess.FileExists(FilePath)) return Error.FileNotFound;
        using var file = FileAccess.Open(FilePath, FileAccess.ModeFlags.Read);
        if (file is null) return FileAccess.GetOpenError();
        if (file.GetLength() > 4000000) return Error.InvalidData;
        string text = file.GetAsText(); Error error = file.GetError();
        if (error != Error.Ok && error != Error.FileEof) return error;
        _saved = text; Editor.Text = text; UpdateDirty(); return Error.Ok;
    }
    public void RequestClose() { if (Dirty) ClosePrompt.PopupCentered(new Vector2I(420, 180)); else ApproveClose(); }
    private void ApproveClose() { ApprovedCount++; if (QuitOnApproved) GetTree().Quit(); }
    public override void _ExitTree()
    {
        if (ReckfellObject.IsInstanceValid(_window)) _window.CloseRequested -= RequestClose;
        GetTree().AutoAcceptQuit = _autoAccept;
    }
}

Что обеспечивает сохранение

TextEdit.Text при программном присваивании не является пользовательским TextChanged. Поэтому ReplaceText и загрузка явно согласуют Dirty с сохранённым снимком. При ошибке сохранения Dirty не сбрасывается и диалог остаётся открытым. Ввод из UI также приходит через TextChanged; подписи кнопок не заменяют проверку состояния.

Сначала пишется временный файл в том же каталоге, затем File.Move с overwrite заменяет назначение. Исключения превращаются в Error. Это не гарантирует сохранность при отключении питания и не решает одновременную запись несколькими процессами. Сбой записи/замены не должен считаться успешным сохранением. Подробности .NET: File.Move.

Пример ограничивает текст миллионом UTF-16 единиц и загрузку четырьмя миллионами байт, хранит один документ и не следит за внешними правками файла. Для полноценного редактора нужны выбор файла, конфликты внешних изменений, история/автосохранение и политика восстановления; сохраняйте эти решения отдельно от состояния окна.

Окна и интеграция рабочего стола

Window — viewport с собственной областью UI. AcceptDialog/ConfirmationDialog наследуют Window. Встроенные подокна и нативные окна — разные режимы; display/window/subwindows/embed_subwindows выбирает встраивание, а возможность отдельных окон проверяется DisplayServer.Feature.Subwindows. Закрытие вторичного окна обрабатывайте его CloseRequested; скрытие и QueueFree имеют разный срок жизни. Главное окно управляет жизнью процесса.

ЗадачаAPI / что проверить
Минимальные/максимальные размерыWindow.MinSize/MaxSize и реальные длинные переводы
Нативный выбор файловFileDialog.UseNativeDialog и Feature.NativeDialogFile; оставьте встроенный вариант для неподдерживаемой среды
Значок в трееFeature.StatusIndicator, CreateStatusIndicator, DeleteStatusIndicator; удаляйте созданный ID при завершении
Глобальное менюNativeMenu и его HasFeature/HasSystemMenu; меню системы не одинаково на платформах
Клиентское оформление окнаПоддержка ExtendToTitle, перетаскивания и системных кнопок выбранного backend
Прогресс задачиWindowSetTaskbarProgressState/Value; задавайте состояние и допустимое значение
УведомленияSendToastNotification; поддержка backend, ограничения ОС и инициированное пользователем действие
Прозрачный overlayРазрешение per-pixel transparency, прозрачный viewport, ввод/фокус; невидимый фон не означает пропуск мыши
Отложенный показОтдельно настройте splash/первую отрисовку и момент Show; не показывайте недостроенное окно
Экранный дикторFeature.AccessibilityScreenReader, доступные роли/имена/фокус; проверяйте с реальным средством доступности

Headless возвращает заглушки для многих DisplayServer-вызовов и не подтверждает работу интеграции ОС. Наличие метода в SDK не равно наличию функции в текущем backend. Встроенный запуск в редакторе тоже не заменяет проверку экспортированного приложения.

Сохранить геометрию окна

WindowStateStore сохраняет только нормальный оконный режим. Перед Restore получите актуальную DisplayServer.ScreenGetUsableRect для нужного экрана. При отключённом мониторе старые координаты могут оказаться вне всех экранов; Fit ограничивает положение и размер доступной областью, включая мониторы с отрицательными координатами. Встроенные окна используют область родительского viewport, поэтому не передавайте им экранную геометрию без согласования координат.

WindowStateStore.cs
C#
using System;
using Reckfell;

public static class WindowStateStore
{
    public static Rect2I Fit(Rect2I requested, Rect2I usable)
    {
        if (usable.Size.X <= 0 || usable.Size.Y <= 0) throw new ArgumentOutOfRangeException(nameof(usable));
        int width = Math.Clamp(requested.Size.X, Math.Min(320, usable.Size.X), usable.Size.X);
        int height = Math.Clamp(requested.Size.Y, Math.Min(240, usable.Size.Y), usable.Size.Y);
        int x = Math.Clamp(requested.Position.X, usable.Position.X, usable.End.X - width);
        int y = Math.Clamp(requested.Position.Y, usable.Position.Y, usable.End.Y - height);
        return new Rect2I(x, y, width, height);
    }
    public static Error Save(Window window, string path = "user://window-state.cfg")
    {
        if (window.Mode != Window.ModeEnum.Windowed) return Error.Busy;
        var cfg = new ConfigFile(); cfg.SetValue("window", "position", window.Position); cfg.SetValue("window", "size", window.Size);
        return cfg.Save(path);
    }
    public static Error Restore(Window window, Rect2I usable, string path = "user://window-state.cfg")
    {
        var cfg = new ConfigFile(); Error error = cfg.Load(path); if (error != Error.Ok) return error;
        if (!cfg.HasSectionKey("window", "position") || !cfg.HasSectionKey("window", "size")) return Error.InvalidData;
        Variant position = cfg.GetValue("window", "position"), size = cfg.GetValue("window", "size");
        if (position.VariantType != Variant.Type.Vector2I || size.VariantType != Variant.Type.Vector2I) return Error.InvalidData;
        if (usable.Size.X <= 0 || usable.Size.Y <= 0) return Error.Unavailable;
        Rect2I fitted = Fit(new Rect2I(position.AsVector2I(), size.AsVector2I()), usable);
        window.Size = fitted.Size; window.Position = fitted.Position; return Error.Ok;
    }
}

Настройки и выпуск

Для простого инструмента задайте основной уровень, заголовок/иконки, режим HiDPI и ограничения окна. В UI-макете различайте размер контента, масштаб рендера и DPI. application/run/low_processor_mode может уменьшить работу в простое; непрерывная анимация, _Process и постоянные redraw влияют на результат. Не отключайте подсистемы сборки до проверки используемых функций. Для мобильного приложения дополнительно проверьте безопасные области, экранную клавиатуру, жизненный цикл и системную кнопку возврата.

Экспортируйте с подходящими шаблонами и .NET-зависимостями. Один exe, portable-каталог, установщик и подпись — разные задачи выпуска; не обещайте самодостаточный exe без проверки артефакта. Пользовательские данные обычно остаются в user://; переносимый режим должен иметь явный путь и права записи. При обновлении установщика не удаляйте данные пользователя вместе с файлами программы. Начните с экспорта проекта.

Автотесты разделяйте на логику документов, файловые ошибки, UI/фокус и реальные устройства/экспорт. Проверка этого примера использует отдельный каталог данных, встроенные окна, сигналы закрытия и отдельный процесс, который действительно завершается. Она не сертифицирует нативный titlebar, уведомления, трей или установщик.

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.