/
docNemo
/
clothes-graph
Обзор
Документация
Войти
/
docNemo
/
clothes-graph
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/ClothesGraph.App/MainWindow.axaml.cs
278 строк
10 KB
docNemo
Вкладка деталей выкройки, создание документа и пересчёт производных вкладок
09 авг 2026, 14:03
09 авг 2026, 14:03
dd71293
Код
Авторство
О чём код?
using System; using System.IO; using System.Threading.Tasks; using System.Windows.Input; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media.Imaging; using Avalonia.Rendering.Composition; using ClothesGraph.App.ViewModels; using ClothesGraph.Model; using CommunityToolkit.Mvvm.Input; namespace ClothesGraph.App; public partial class MainWindow : Window { private const string ApplicationName = "Clothes Graph"; /// <summary>Название документа, ещё не связанного с файлом.</summary> private const string NewDocumentCaption = "Новая выкройка"; /// <summary> /// Название заготовки, открываемой при запуске без файла. /// </summary> /// <remarks> /// Помечена примером прямо в названии: без этого при первом запуске /// открывается чертёж, который выглядит как чужой проект неизвестного /// происхождения. /// </remarks> private const string StarterCaption = "Пример: основа плечевого изделия"; private readonly FileCommands _files; private PatternDocument _document = null!; private string _caption = NewDocumentCaption; private bool _closeConfirmed; public MainWindow() { InitializeComponent(); _files = new FileCommands(this); BindFileCommands(); var (document, caption) = LoadStartupDocument(); Adopt(document, caption); Closing += OnClosing; Tabs.SelectionChanged += (_, _) => RefreshShownTab(); if (Program.ScreenshotPath is not null) { // Снимок вкладки, заданной ключом запуска: иначе проверить внешний // вид можно было бы только у той, что открывается первой. if (Program.ScreenshotTab is { } tab && tab < Tabs.Items.Count) Tabs.SelectedIndex = tab; Opened += (_, _) => _ = CaptureAndCloseAsync(Program.ScreenshotPath); } } /// <summary>Документ, открытый при запуске, и его название для заголовка.</summary> private (PatternDocument Document, string Caption) LoadStartupDocument() { if (Program.DocumentPath is { } documentPath) { try { var opened = Model.Storage.PatternFile.Load(documentPath); // Документ сразу связывается с файлом: иначе первое сохранение // спрашивало бы путь к файлу, только что открытому с диска. _files.Attach(documentPath); return (opened, Path.GetFileName(documentPath)); } catch (Exception error) { Console.WriteLine($"Не удалось открыть {documentPath}: {error.Message}"); } } var document = StarterDocument.Create(); if (Program.UnderlayPath is { } path && UnderlayLoader.Add(document, path) is { } image) { // Прикидка на всю ширину сетки основы: точный масштаб пользователь // задаёт калибровкой. UnderlayLoader.FitToWidth(image, Geometry.Point2.FromMillimetres(0, 0), Geometry.Length.FromMillimetres(520)); } return (document, StarterCaption); } /// <summary> /// Пересчитывает открываемую вкладку. /// </summary> /// <remarks> /// Детали, швы и раскладка листов производны от чертежа и мерок, а правят /// их на других вкладках. Пересчёт привязан к показу, а не к каждой правке /// чертежа: тот пересчитывается и на наведение курсора, а строить по нему /// припуски и раскладку на каждое движение мыши незачем — в этот момент их /// всё равно не видно. /// </remarks> private void RefreshShownTab() { if (DataContext is not MainWindowViewModel model) return; switch (Tabs.SelectedContent) { case Views.PiecesView: model.Pieces.Refresh(); break; case Views.SeamsView: model.Seams.RefreshCommand.Execute(null); break; case Views.PrintView: model.Print.Refresh(); break; } } private void BindFileCommands() { var create = new AsyncRelayCommand(CreateAsync); var open = new AsyncRelayCommand(OpenAsync); var save = new AsyncRelayCommand(SaveAsync); var saveAs = new AsyncRelayCommand(SaveAsAsync); NewItem.Command = create; OpenItem.Command = open; SaveItem.Command = save; SaveAsItem.Command = saveAs; // Сочетания клавиш объявляются отдельно: InputGesture в меню только // подписывает пункт и сам ничего не нажимает. Bind(Key.N, KeyModifiers.Control, create); Bind(Key.O, KeyModifiers.Control, open); Bind(Key.S, KeyModifiers.Control, save); Bind(Key.S, KeyModifiers.Control | KeyModifiers.Shift, saveAs); } private void Bind(Key key, KeyModifiers modifiers, ICommand command) => KeyBindings.Add(new KeyBinding { Gesture = new KeyGesture(key, modifiers), Command = command }); /// <summary>Создаёт пустой документ вместо открытого.</summary> private async Task CreateAsync() { if (!await _files.ConfirmDiscardAsync(_document)) return; _files.Forget(); Adopt(new PatternDocument(), NewDocumentCaption); } private async Task OpenAsync() { if (!await _files.ConfirmDiscardAsync(_document)) return; if (await _files.OpenAsync() is { } opened && _files.CurrentPath is { } path) Adopt(opened, Path.GetFileName(path)); } private async Task SaveAsync() { if (await _files.SaveAsync(_document)) RenameAfterSave(); } private async Task SaveAsAsync() { if (await _files.SaveAsAsync(_document)) RenameAfterSave(); } /// <summary>Подхватывает имя файла после записи: «Сохранить как» его меняет.</summary> private void RenameAfterSave() { if (_files.CurrentPath is { } path) _caption = Path.GetFileName(path); UpdateTitle(); } /// <summary> /// Делает документ открытым в окне. /// </summary> /// <remarks> /// Модели представления пересоздаются целиком: они держат производное /// состояние — вычисленную геометрию, раскладку листов, — и обновлять его /// по частям надёжнее не выйдет. /// </remarks> private void Adopt(PatternDocument document, string caption) { if (_document is not null) _document.ModifiedChanged -= OnModifiedChanged; _document = document; _document.ModifiedChanged += OnModifiedChanged; _caption = caption; DataContext = new MainWindowViewModel(document, () => this); UpdateTitle(); } private void OnModifiedChanged(object? sender, EventArgs e) => UpdateTitle(); /// <summary> /// Заголовок окна: название документа, звёздочка при несохранённых правках, /// затем имя программы. /// </summary> /// <remarks> /// Название идёт первым потому, что в панели задач и при переключении окон /// виден только его начальный кусок, а имя программы там ничего не решает. /// </remarks> private void UpdateTitle() => Title = (_document.IsModified ? "* " : string.Empty) + _caption + " — " + ApplicationName; /// <summary> /// Не даёт закрыть окно, потеряв несохранённую работу. /// </summary> /// <remarks> /// Ответ приходит из диалога, а показать его синхронно нельзя, поэтому /// закрытие отменяется и повторяется уже после ответа. /// </remarks> private async void OnClosing(object? sender, WindowClosingEventArgs e) { if (_closeConfirmed || !_document.IsModified) return; e.Cancel = true; if (!await _files.ConfirmDiscardAsync(_document)) return; _closeConfirmed = true; Close(); } private async Task CaptureAndCloseAsync(string path) { // Кадру нужно дать пройти разметку, вывод чертежа и передачу текстуры // композитору, иначе в снимок попадёт незаполненное окно. await Task.Delay(TimeSpan.FromSeconds(2)); try { var visual = ElementComposition.GetElementVisual(this); if (visual is not null) { var bitmap = await visual.Compositor.CreateCompositionVisualSnapshot(visual, 1.0); bitmap.Save(path, new PngBitmapEncoderOptions()); Console.WriteLine($"Снимок сохранён: {path}"); } } catch (Exception error) { Console.WriteLine($"Снимок не удался: {error.Message}"); } Close(); } }