Desktop UI

Меню, Toolbar, ContextMenu, StatusBar

Побудова повної системи меню та панелей інструментів для професійних desktop-застосунків

Меню, Toolbar, ContextMenu, StatusBar

Професійні desktop-застосунки мають стандартизовану структуру інтерфейсу: меню зверху, панель інструментів під меню, контекстне меню на правий клік та статусний рядок знизу. Ця структура знайома користувачам з десятиліть роботи з програмами на кшталт Microsoft Word, Visual Studio, Adobe Photoshop.

У цій статті ми навчимося створювати всі ці елементи інтерфейсу, інтегрувати їх з MVVM через Commands та побудуємо повноцінну систему навігації для складного застосунку.

Для кого ця стаття?Ця стаття призначена для студентів, які вже знайомі з базовими контролами WPF, розуміють MVVM pattern та Commands. Якщо ви вже створювали застосунки з кнопками та командами, ви готові до побудови професійного інтерфейсу.

Menu — це горизонтальна панель меню, що зазвичай розташовується у верхній частині вікна. Вона містить MenuItem елементи, які можуть мати вкладені підменю.

Базова структура Menu

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Додавання іконок робить меню більш зрозумілим та привабливим:

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Keyboard Shortcuts (InputGestureText)

Професійні застосунки завжди мають клавіатурні скорочення для швидкого доступу:

<MenuItem Header="Файл">
    <MenuItem Header="Створити" 
              InputGesture="Ctrl+N">
        <MenuItem.Icon>
            <TextBlock Text="📄" FontSize="16" />
        </MenuItem.Icon>
    </MenuItem>
    
    <MenuItem Header="Відкрити..." 
              InputGesture="Ctrl+O">
        <MenuItem.Icon>
            <TextBlock Text="📂" FontSize="16" />
        </MenuItem.Icon>
    </MenuItem>
    
    <MenuItem Header="Зберегти" 
              InputGesture="Ctrl+S">
        <MenuItem.Icon>
            <TextBlock Text="💾" FontSize="16" />
        </MenuItem.Icon>
    </MenuItem>
    
    <Separator />
    
    <MenuItem Header="Друк..." 
              InputGesture="Ctrl+P">
        <MenuItem.Icon>
            <TextBlock Text="🖨️" FontSize="16" />
        </MenuItem.Icon>
    </MenuItem>
</MenuItem>

<MenuItem Header="Редагувати">
    <MenuItem Header="Скасувати" 
              InputGesture="Ctrl+Z" />
    <MenuItem Header="Повторити" 
              InputGesture="Ctrl+Y" />
    <Separator />
    <MenuItem Header="Вирізати" 
              InputGesture="Ctrl+X" />
    <MenuItem Header="Копіювати" 
              InputGesture="Ctrl+C" />
    <MenuItem Header="Вставити" 
              InputGesture="Ctrl+V" />
</MenuItem>
InputGesture vs KeyBindingInputGesture — це лише текстова підказка для користувача. Вона не активує команду автоматично. Для реальної роботи клавіатурних скорочень потрібно додати KeyBinding у Window.InputBindings:
<Window.InputBindings>
    <KeyBinding Key="N" Modifiers="Ctrl" Command="{Binding NewCommand}" />
    <KeyBinding Key="O" Modifiers="Ctrl" Command="{Binding OpenCommand}" />
    <KeyBinding Key="S" Modifiers="Ctrl" Command="{Binding SaveCommand}" />
</Window.InputBindings>

Command Binding у Menu

Інтеграція з MVVM через Commands:

public class MainViewModel : ObservableObject
{
    public ICommand NewCommand { get; }
    public ICommand OpenCommand { get; }
    public ICommand SaveCommand { get; }
    public ICommand ExitCommand { get; }
    
    public ICommand UndoCommand { get; }
    public ICommand RedoCommand { get; }
    public ICommand CutCommand { get; }
    public ICommand CopyCommand { get; }
    public ICommand PasteCommand { get; }
    
    private bool _hasUnsavedChanges;
    
    public MainViewModel()
    {
        NewCommand = new RelayCommand(New);
        OpenCommand = new AsyncRelayCommand(OpenAsync);
        SaveCommand = new AsyncRelayCommand(SaveAsync, CanSave);
        ExitCommand = new RelayCommand(Exit);
        
        UndoCommand = new RelayCommand(Undo, CanUndo);
        RedoCommand = new RelayCommand(Redo, CanRedo);
        CutCommand = new RelayCommand(Cut, CanCut);
        CopyCommand = new RelayCommand(Copy, CanCopy);
        PasteCommand = new RelayCommand(Paste, CanPaste);
    }
    
    private void New()
    {
        if (_hasUnsavedChanges)
        {
            // Показати діалог "Зберегти зміни?"
        }
        
        // Створити новий документ
    }
    
    private async Task OpenAsync()
    {
        // Відкрити діалог вибору файлу
        var file = await _dialogService.OpenFileAsync();
        if (file != null)
        {
            // Завантажити файл
        }
    }
    
    private async Task SaveAsync()
    {
        // Зберегти документ
        await _documentService.SaveAsync();
        _hasUnsavedChanges = false;
    }
    
    private bool CanSave() => _hasUnsavedChanges;
    
    private void Exit()
    {
        if (_hasUnsavedChanges)
        {
            // Показати діалог підтвердження
        }
        
        Application.Current.Shutdown();
    }
    
    private void Undo() { /* Скасувати останню дію */ }
    private bool CanUndo() => _undoStack.Count > 0;
    
    private void Redo() { /* Повторити скасовану дію */ }
    private bool CanRedo() => _redoStack.Count > 0;
    
    private void Cut() { /* Вирізати */ }
    private bool CanCut() => _selectedText != null;
    
    private void Copy() { /* Копіювати */ }
    private bool CanCopy() => _selectedText != null;
    
    private void Paste() { /* Вставити */ }
    private bool CanPaste() => Clipboard.ContainsText();
}

XAML прив'язка:

<Menu DockPanel.Dock="Top">
    <MenuItem Header="Файл">
        <MenuItem Header="Створити" 
                  InputGesture="Ctrl+N"
                  Command="{Binding NewCommand}">
            <MenuItem.Icon>
                <TextBlock Text="📄" FontSize="16" />
            </MenuItem.Icon>
        </MenuItem>
        
        <MenuItem Header="Відкрити..." 
                  InputGesture="Ctrl+O"
                  Command="{Binding OpenCommand}">
            <MenuItem.Icon>
                <TextBlock Text="📂" FontSize="16" />
            </MenuItem.Icon>
        </MenuItem>
        
        <MenuItem Header="Зберегти" 
                  InputGesture="Ctrl+S"
                  Command="{Binding SaveCommand}">
            <MenuItem.Icon>
                <TextBlock Text="💾" FontSize="16" />
            </MenuItem.Icon>
        </MenuItem>
        
        <Separator />
        
        <MenuItem Header="Вихід" 
                  Command="{Binding ExitCommand}">
            <MenuItem.Icon>
                <TextBlock Text="🚪" FontSize="16" />
            </MenuItem.Icon>
        </MenuItem>
    </MenuItem>
    
    <MenuItem Header="Редагувати">
        <MenuItem Header="Скасувати" 
                  InputGesture="Ctrl+Z"
                  Command="{Binding UndoCommand}" />
        <MenuItem Header="Повторити" 
                  InputGesture="Ctrl+Y"
                  Command="{Binding RedoCommand}" />
        <Separator />
        <MenuItem Header="Вирізати" 
                  InputGesture="Ctrl+X"
                  Command="{Binding CutCommand}" />
        <MenuItem Header="Копіювати" 
                  InputGesture="Ctrl+C"
                  Command="{Binding CopyCommand}" />
        <MenuItem Header="Вставити" 
                  InputGesture="Ctrl+V"
                  Command="{Binding PasteCommand}" />
    </MenuItem>
</Menu>

<Window.InputBindings>
    <KeyBinding Key="N" Modifiers="Ctrl" Command="{Binding NewCommand}" />
    <KeyBinding Key="O" Modifiers="Ctrl" Command="{Binding OpenCommand}" />
    <KeyBinding Key="S" Modifiers="Ctrl" Command="{Binding SaveCommand}" />
    <KeyBinding Key="Z" Modifiers="Ctrl" Command="{Binding UndoCommand}" />
    <KeyBinding Key="Y" Modifiers="Ctrl" Command="{Binding RedoCommand}" />
    <KeyBinding Key="X" Modifiers="Ctrl" Command="{Binding CutCommand}" />
    <KeyBinding Key="C" Modifiers="Ctrl" Command="{Binding CopyCommand}" />
    <KeyBinding Key="V" Modifiers="Ctrl" Command="{Binding PasteCommand}" />
</Window.InputBindings>
CanExecute та IsEnabledКоли CanExecute команди повертає false, MenuItem автоматично стає disabled (сірий, неклікабельний). Це чудовий спосіб показати користувачу, які дії доступні в поточному стані застосунку.

Checkable MenuItems

Деякі пункти меню можуть бути "перемикачами":

<MenuItem Header="Вигляд">
    <MenuItem Header="Панель інструментів" 
              IsCheckable="True"
              IsChecked="{Binding IsToolbarVisible}" />
    <MenuItem Header="Статусний рядок" 
              IsCheckable="True"
              IsChecked="{Binding IsStatusBarVisible}" />
    <MenuItem Header="Бічна панель" 
              IsCheckable="True"
              IsChecked="{Binding IsSidebarVisible}" />
    <Separator />
    <MenuItem Header="Повноекранний режим" 
              IsCheckable="True"
              IsChecked="{Binding IsFullScreen}"
              InputGesture="F11" />
</MenuItem>

ContextMenu: контекстне меню

ContextMenu — це меню, що з'являється при правому кліку миші. Воно надає швидкий доступ до найбільш релевантних дій для конкретного елемента.

Базове ContextMenu

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

ContextMenu для ListBox

Контекстне меню особливо корисне для списків:

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

DataContext Issues у ContextMenu

Одна з найпоширеніших проблем з ContextMenu — він не успадковує DataContext від батьківського елемента, оскільки не є частиною візуального дерева.

Проблема:

<!-- ❌ Не працює! ContextMenu не бачить DataContext -->
<ListBox ItemsSource="{Binding Items}">
    <ListBox.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Видалити" 
                      Command="{Binding DeleteCommand}" />
        </ContextMenu>
    </ListBox.ContextMenu>
</ListBox>

Рішення 1: PlacementTarget

<!-- ✅ Працює через PlacementTarget -->
<ListBox ItemsSource="{Binding Items}">
    <ListBox.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Видалити" 
                      Command="{Binding PlacementTarget.DataContext.DeleteCommand, 
                                RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
        </ContextMenu>
    </ListBox.ContextMenu>
</ListBox>

Рішення 2: BindingProxy (Freezable Trick)

public class BindingProxy : Freezable
{
    protected override Freezable CreateInstanceCore()
    {
        return new BindingProxy();
    }
    
    public object Data
    {
        get => GetValue(DataProperty);
        set => SetValue(DataProperty, value);
    }
    
    public static readonly DependencyProperty DataProperty =
        DependencyProperty.Register(nameof(Data), typeof(object), 
            typeof(BindingProxy), new PropertyMetadata(null));
}

Використання:

<Window.Resources>
    <local:BindingProxy x:Key="Proxy" Data="{Binding}" />
</Window.Resources>

<ListBox ItemsSource="{Binding Items}">
    <ListBox.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Видалити" 
                      Command="{Binding Data.DeleteCommand, Source={StaticResource Proxy}}"
                      CommandParameter="{Binding PlacementTarget.SelectedItem, 
                                         RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
        </ContextMenu>
    </ListBox.ContextMenu>
</ListBox>

Динамічне ContextMenu

Різні елементи можуть мати різні контекстні меню:

<ListBox ItemsSource="{Binding Items}">
    <ListBox.ItemContainerStyle>
        <Style Selector="ListBoxItem">
            <Setter Property="ContextMenu">
                <Setter.Value>
                    <ContextMenu>
                        <MenuItem Header="{Binding Name}" IsEnabled="False" FontWeight="Bold" />
                        <Separator />
                        <MenuItem Header="Відкрити" Command="{Binding OpenCommand}" />
                        <MenuItem Header="Редагувати" Command="{Binding EditCommand}" />
                        <Separator />
                        <MenuItem Header="Видалити" Command="{Binding DeleteCommand}" />
                    </ContextMenu>
                </Setter.Value>
            </Setter>
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>

ToolBar: панель інструментів

ToolBar — це горизонтальна панель з кнопками швидкого доступу до найбільш використовуваних команд.

Базовий ToolBar

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

ToolBar з Command Binding

<Border DockPanel.Dock="Top" 
        Background="#f3f4f6" 
        BorderBrush="#d1d5db" 
        BorderThickness="0,0,0,1"
        Padding="5">
    <StackPanel Orientation="Horizontal" Spacing="5">
        <Button Content="📄" 
                ToolTip.Tip="Створити (Ctrl+N)"
                Command="{Binding NewCommand}"
                Padding="8"
                Width="36" Height="36" />
        <Button Content="📂" 
                ToolTip.Tip="Відкрити (Ctrl+O)"
                Command="{Binding OpenCommand}"
                Padding="8"
                Width="36" Height="36" />
        <Button Content="💾" 
                ToolTip.Tip="Зберегти (Ctrl+S)"
                Command="{Binding SaveCommand}"
                Padding="8"
                Width="36" Height="36" />
        
        <Separator Width="1" Height="30" Margin="5,0" />
        
        <Button Content="✂️" 
                ToolTip.Tip="Вирізати (Ctrl+X)"
                Command="{Binding CutCommand}"
                Padding="8"
                Width="36" Height="36" />
        <Button Content="📋" 
                ToolTip.Tip="Копіювати (Ctrl+C)"
                Command="{Binding CopyCommand}"
                Padding="8"
                Width="36" Height="36" />
        <Button Content="📌" 
                ToolTip.Tip="Вставити (Ctrl+V)"
                Command="{Binding PasteCommand}"
                Padding="8"
                Width="36" Height="36" />
    </StackPanel>
</Border>
Синхронізація Menu та ToolBarВикористовуйте ті самі Commands для Menu та ToolBar. Коли CanExecute повертає false, і пункт меню, і кнопка на панелі інструментів автоматично стануть disabled. Це забезпечує консистентність інтерфейсу.

ToggleButton у ToolBar

Для перемикачів (Bold, Italic, Underline) використовуйте ToggleButton:

<StackPanel Orientation="Horizontal" Spacing="5">
    <ToggleButton Content="B" 
                  ToolTip.Tip="Жирний (Ctrl+B)"
                  IsChecked="{Binding IsBold}"
                  FontWeight="Bold"
                  Width="36" Height="36" />
    <ToggleButton Content="I" 
                  ToolTip.Tip="Курсив (Ctrl+I)"
                  IsChecked="{Binding IsItalic}"
                  FontStyle="Italic"
                  Width="36" Height="36" />
    <ToggleButton Content="U" 
                  ToolTip.Tip="Підкреслений (Ctrl+U)"
                  IsChecked="{Binding IsUnderline}"
                  TextDecorations="Underline"
                  Width="36" Height="36" />
</StackPanel>

ComboBox у ToolBar

Для вибору зі списку (шрифт, розмір):

<StackPanel Orientation="Horizontal" Spacing="10">
    <ComboBox Width="150" 
              SelectedItem="{Binding SelectedFont}"
              ToolTip.Tip="Шрифт">
        <ComboBoxItem Content="Arial" />
        <ComboBoxItem Content="Times New Roman" />
        <ComboBoxItem Content="Courier New" />
        <ComboBoxItem Content="Verdana" />
    </ComboBox>
    
    <ComboBox Width="80" 
              SelectedItem="{Binding FontSize}"
              ToolTip.Tip="Розмір шрифту">
        <ComboBoxItem Content="8" />
        <ComboBoxItem Content="10" />
        <ComboBoxItem Content="12" IsSelected="True" />
        <ComboBoxItem Content="14" />
        <ComboBoxItem Content="16" />
        <ComboBoxItem Content="18" />
        <ComboBoxItem Content="24" />
    </ComboBox>
</StackPanel>

StatusBar: статусний рядок

StatusBar — це панель внизу вікна, що показує інформацію про поточний стан застосунку.

Базовий StatusBar

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

StatusBar з прогресом

<Border DockPanel.Dock="Bottom" 
        Background="#f3f4f6" 
        BorderBrush="#d1d5db" 
        BorderThickness="0,1,0,0"
        Padding="10,5">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="200" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        
        <TextBlock Grid.Column="0" 
                   Text="{Binding StatusMessage}" 
                   VerticalAlignment="Center"
                   Foreground="#6b7280" />
        
        <ProgressBar Grid.Column="1" 
                     Value="{Binding Progress}" 
                     Maximum="100"
                     Height="16"
                     Margin="10,0"
                     IsVisible="{Binding IsProcessing}" />
        
        <TextBlock Grid.Column="2" 
                   Text="{Binding Progress, StringFormat='{}{0}%'}" 
                   VerticalAlignment="Center"
                   Foreground="#6b7280"
                   IsVisible="{Binding IsProcessing}" />
    </Grid>
</Border>

ViewModel:

public class MainViewModel : ObservableObject
{
    private string _statusMessage = "Готово";
    private double _progress;
    private bool _isProcessing;
    
    public string StatusMessage
    {
        get => _statusMessage;
        set => SetProperty(ref _statusMessage, value);
    }
    
    public double Progress
    {
        get => _progress;
        set => SetProperty(ref _progress, value);
    }
    
    public bool IsProcessing
    {
        get => _isProcessing;
        set => SetProperty(ref _isProcessing, value);
    }
    
    public async Task ProcessAsync()
    {
        IsProcessing = true;
        StatusMessage = "Обробка...";
        
        for (int i = 0; i <= 100; i++)
        {
            Progress = i;
            await Task.Delay(50);
        }
        
        IsProcessing = false;
        StatusMessage = "Готово";
    }
}

Ribbon: Office-подібний інтерфейс

Ribbon — це сучасна альтернатива традиційному меню та панелі інструментів, популяризована Microsoft Office.

Fluent.Ribbon бібліотекаWPF не має вбудованого Ribbon контролу. Найпопулярніша бібліотека — Fluent.Ribbon:
dotnet add package Fluent.Ribbon
Офіційна документація: https://github.com/fluentribbon/Fluent.Ribbon

Базовий Ribbon (концепція)

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:fluent="clr-namespace:Fluent;assembly=Fluent">
    <fluent:RibbonWindow>
        <fluent:Ribbon>
            <!-- Вкладка "Головна" -->
            <fluent:RibbonTabItem Header="Головна">
                <fluent:RibbonGroupBox Header="Буфер обміну">
                    <fluent:Button Header="Вставити" 
                                   Icon="📌" 
                                   LargeIcon="📌"
                                   Command="{Binding PasteCommand}" />
                    <fluent:Button Header="Вирізати" 
                                   Icon="✂️"
                                   Command="{Binding CutCommand}" />
                    <fluent:Button Header="Копіювати" 
                                   Icon="📋"
                                   Command="{Binding CopyCommand}" />
                </fluent:RibbonGroupBox>
                
                <fluent:RibbonGroupBox Header="Шрифт">
                    <fluent:ComboBox Header="Шрифт" Width="150" />
                    <fluent:ComboBox Header="Розмір" Width="80" />
                    <fluent:ToggleButton Header="B" FontWeight="Bold" />
                    <fluent:ToggleButton Header="I" FontStyle="Italic" />
                    <fluent:ToggleButton Header="U" TextDecorations="Underline" />
                </fluent:RibbonGroupBox>
            </fluent:RibbonTabItem>
            
            <!-- Вкладка "Вставка" -->
            <fluent:RibbonTabItem Header="Вставка">
                <fluent:RibbonGroupBox Header="Таблиці">
                    <fluent:Button Header="Таблиця" Icon="📊" />
                </fluent:RibbonGroupBox>
                
                <fluent:RibbonGroupBox Header="Ілюстрації">
                    <fluent:Button Header="Зображення" Icon="🖼️" />
                    <fluent:Button Header="Фігури" Icon="⬜" />
                </fluent:RibbonGroupBox>
            </fluent:RibbonTabItem>
        </fluent:Ribbon>
        
        <Grid>
            <!-- Вміст застосунку -->
        </Grid>
    </fluent:RibbonWindow>
</Window>
Коли використовувати RibbonRibbon підходить для застосунків з великою кількістю команд, організованих за категоріями (як Office). Для простіших застосунків традиційне меню + панель інструментів більш доречні.

Повний приклад: текстовий редактор

Об'єднаємо всі компоненти в один застосунок:

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Практичні завдання

Рівень 1: Menu з File, Edit, View та keyboard shortcuts

Мета: Навчитися створювати базове меню з клавіатурними скороченнями.

Завдання: Створіть застосунок з повноцінним меню та підтримкою клавіатурних скорочень.

Вимоги:

  • Створіть меню з трьома розділами: Файл, Редагувати, Вигляд
  • Файл: Створити (Ctrl+N), Відкрити (Ctrl+O), Зберегти (Ctrl+S), Вихід
  • Редагувати: Скасувати (Ctrl+Z), Повторити (Ctrl+Y), Вирізати (Ctrl+X), Копіювати (Ctrl+C), Вставити (Ctrl+V)
  • Вигляд: Панель інструментів (checkable), Статусний рядок (checkable)
  • Додайте іконки до всіх пунктів меню
  • Реалізуйте KeyBinding для всіх скорочень
  • Створіть ViewModel з Commands
  • Додайте CanExecute логіку (наприклад, "Зберегти" доступне тільки якщо є зміни)

Підказка:

public class MainViewModel : ObservableObject
{
    private bool _hasUnsavedChanges;
    
    public ICommand SaveCommand { get; }
    
    public MainViewModel()
    {
        SaveCommand = new RelayCommand(Save, CanSave);
    }
    
    private void Save()
    {
        // Зберегти файл
        _hasUnsavedChanges = false;
        SaveCommand.NotifyCanExecuteChanged();
    }
    
    private bool CanSave() => _hasUnsavedChanges;
}

Рівень 2: ContextMenu для ListBox з Cut/Copy/Paste

Мета: Опанувати створення контекстних меню з правильною прив'язкою до DataContext.

Завдання: Створіть список завдань з контекстним меню для кожного елемента.

Вимоги:

  • Створіть клас TaskItem з властивостями: Title, Description, IsCompleted
  • Використайте ObservableCollection<TaskItem> у ViewModel
  • Додайте ListBox з DataTemplate для відображення завдань
  • Створіть ContextMenu з пунктами:
    • Редагувати
    • Позначити як виконане/невиконане
    • Дублювати
    • Видалити
  • Вирішіть проблему DataContext через PlacementTarget або BindingProxy
  • Реалізуйте всі команди у ViewModel
  • Додайте підтвердження перед видаленням

Додаткові виклики:

  • Різні контекстні меню для виконаних та невиконаних завдань
  • Drag and Drop для зміни порядку завдань
  • Збереження списку при закритті застосунку

Рівень 3: Повне меню + ToolBar + StatusBar для текстового редактора

Мета: Створити повноцінний інтерфейс застосунку з усіма компонентами.

Завдання: Розробіть текстовий редактор з професійним інтерфейсом.

Вимоги:

  • Створіть повне меню:
    • Файл: Створити, Відкрити, Зберегти, Зберегти як, Друк, Останні файли (підменю), Вихід
    • Редагувати: Скасувати, Повторити, Вирізати, Копіювати, Вставити, Знайти, Замінити, Виділити все
    • Формат: Шрифт, Розмір, Жирний, Курсив, Підкреслений, Колір тексту
    • Вигляд: Панель інструментів, Статусний рядок, Повноекранний режим (F11)
    • Довідка: Документація, Про програму
  • Створіть панель інструментів з:
    • Кнопками файлових операцій
    • Кнопками буфера обміну
    • ToggleButton для форматування (B, I, U)
    • ComboBox для вибору шрифту
    • ComboBox для вибору розміру шрифту
  • Реалізуйте StatusBar з:
    • Повідомленням про статус (ліворуч)
    • Позицією курсору (рядок, стовпець)
    • Кількістю символів
    • Кодуванням файлу
    • ProgressBar для довгих операцій
  • Додайте ContextMenu для TextBox
  • Реалізуйте всю функціональність через MVVM
  • Додайте підтримку відкриття/збереження файлів
  • Реалізуйте Undo/Redo стек
  • Додайте діалог "Зберегти зміни?" при закритті

Структура ViewModel:

public class TextEditorViewModel : ObservableObject
{
    private string _content;
    private string _currentFilePath;
    private bool _hasUnsavedChanges;
    private int _cursorLine;
    private int _cursorColumn;
    private string _selectedFont;
    private int _fontSize;
    private bool _isBold;
    private bool _isItalic;
    private bool _isUnderline;
    
    private Stack<string> _undoStack;
    private Stack<string> _redoStack;
    
    public ICommand NewCommand { get; }
    public ICommand OpenCommand { get; }
    public ICommand SaveCommand { get; }
    public ICommand SaveAsCommand { get; }
    public ICommand ExitCommand { get; }
    
    public ICommand UndoCommand { get; }
    public ICommand RedoCommand { get; }
    public ICommand CutCommand { get; }
    public ICommand CopyCommand { get; }
    public ICommand PasteCommand { get; }
    
    // Реалізація...
}

Додаткові виклики:

  • Підсвічування синтаксису для коду
  • Автозбереження кожні 5 хвилин
  • Список останніх відкритих файлів
  • Пошук та заміна з підтримкою regex
  • Статистика документа (слова, речення, абзаци)
  • Експорт у PDF

Резюме

У цій статті ми детально розібрали систему меню та панелей інструментів:

Menu:

  • MenuItem з Header, Icon, InputGesture
  • Вкладені підменю через вкладеність MenuItem
  • Separator для розділення груп команд
  • Command binding для інтеграції з MVVM
  • IsCheckable для перемикачів
  • KeyBinding для реальної роботи клавіатурних скорочень

ContextMenu:

  • Контекстне меню на правий клік
  • Проблема DataContext та рішення через PlacementTarget
  • BindingProxy (Freezable Trick) для складних сценаріїв
  • Динамічні контекстні меню для різних елементів

ToolBar:

  • Панель швидкого доступу до команд
  • Синхронізація з Menu через спільні Commands
  • ToggleButton для перемикачів
  • ComboBox для вибору зі списку
  • Tooltip для підказок

StatusBar:

  • Інформація про стан застосунку
  • Розташування: статус (ліворуч), деталі (праворуч)
  • ProgressBar для довгих операцій
  • Прив'язка до ViewModel для динамічного оновлення

Ribbon:

  • Сучасна альтернатива традиційному меню
  • Бібліотека Fluent.Ribbon
  • Організація команд за вкладками та групами
  • Підходить для застосунків з великою кількістю команд

Наступні кроки: У наступному блоці ми розглянемо навігацію між вікнами, діалогові вікна та створення власних UserControl і Custom Controls.

Глосарій

Основні терміни:
  • Menu — горизонтальна панель меню у верхній частині вікна
  • MenuItem — елемент меню, може містити вкладені підменю
  • InputGesture — текстова підказка клавіатурного скорочення
  • KeyBinding — реальна прив'язка клавіатурного скорочення до команди
  • ContextMenu — контекстне меню, що з'являється на правий клік
  • PlacementTarget — елемент, на якому відкрито ContextMenu
  • ToolBar — панель інструментів з кнопками швидкого доступу
  • StatusBar — статусний рядок внизу вікна
  • Ribbon — Office-подібний інтерфейс з вкладками та групами команд
  • Separator — роздільник між групами елементів меню
  • IsCheckable — властивість для створення перемикачів у меню

Додаткові ресурси

📚 Microsoft Docs: Menu

Офіційна документація по Menu з прикладами

🎨 WPF Tutorial: ContextMenu

Детальний туторіал по ContextMenu та вирішенню проблем DataContext

⚡ Fluent.Ribbon

Офіційний репозиторій бібліотеки Fluent.Ribbon

🔧 GitHub: Menu Examples

Офіційні приклади застосунків з повноцінними меню