Перейти к основному содержимому

Первая форма WPF — XAML, стили и шаблоны

Разработчику Инженеру

Как проще пройти материал

Держите разделение "данные отдельно, UI отдельно" с самого первого шага:

  • сначала модель и ViewModel;
  • потом XAML;
  • стили. Так код практикума сразу складывается в структуру, которую легко переносить в рабочий проект.

Практикум. Вы знаете C# и хотите закрепить XAML — декларативную разметку интерфейса. Здесь собираем полное WPF-приложение с нуля — форма ввода, список карточек, общие стили и шаблоны элементов. Если нужны короткие примеры "скопировать и запустить" (окно, кнопка, MessageBox, to-do) — сначала C# WinForms и WPF — простые окна. На Java те же задачи — Lab — Swing. Обзор платформы — в Разработка приложений для Windows (Microsoft Learn); синтаксис XAML — в справочнике XAML. Следующий уровеньсквозной практикум WPF + REST + Prism (клиент-сервер TaskDesk).

WPF (Windows Presentation Foundation) — презентационная подсистема .NET: UI описывают в XAML, отрисовку выполняет DirectX (в отличие от WinForms на GDI+). WPF использует векторную графику с аппаратно-независимым пикселем (1/96 дюйма), поэтому интерфейс масштабируется на мониторах с разным DPI без "лесенки" на иконках и шрифтах. Подробнее о стеке — в Разработка приложений для Windows (Microsoft Learn); теория GUI — в архитектуре десктопа.


Что получится

Окно "Заметки" с:

  1. Полем заголовка и выпадающим списком категорий.
  2. Кнопкой "Добавить", оформленной через ControlTemplate.
  3. Списком заметок, где каждая строка — DataTemplate (карточка с заголовком и меткой категории).
  4. Общими Style для TextBox и цветами в App.xaml.

Логика — в ViewModel с привязками (Binding); разметка остаётся в XAML, без обработчиков Click в code-behind.

┌─────────────────────────────────────┐
│ Заметки │
├─────────────────────────────────────┤
│ Заголовок: [________________] │
│ Категория: [ Работа ▼ ] │
│ [ Добавить ] │
├─────────────────────────────────────┤
│ ┌─────────────────────────────┐ │
│ │ Купить молоко [Личное] │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ Созвон с командой [Работа] │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘

Словарь терминов

ТерминПростыми словами
XAMLXML-разметка, описывающая дерево UI-элементов. Компилируется вместе с проектом.
Code-behindФайл *.xaml.cs с C# для окна; в MVVM здесь почти нет логики — только InitializeComponent().
BindingСвязь свойства контрола с полем ViewModel: {Binding NewTitle}.
StyleНабор Setter и Trigger, переиспользуемый для одного типа контрола (TextBox, Button).
DataTemplateШаблон отображения одного элемента данных в ListBox, ItemsControl и т.п.
ControlTemplateШаблон внешнего вида самого контрола — как рисуется кнопка "изнутри".
ResourceDictionaryСловарь ресурсов (Style, кисти, шаблоны), обычно в App.xaml или отдельном файле.
MVVMModel — данные, View — XAML, ViewModel — свойства и команды для привязок.
DirectXГрафический API Windows; WPF рисует через Direct3D, WinForms — через GDI+.
Binding (расширения)MultiBinding, TemplateBinding, PriorityBinding — варианты привязки в WPF.

Поиграйте с Grid и StackPanel в симуляторе ниже — те же панели используем в форме.

Play ITЗагрузка интерактивного демо…


Шаг 0 — среда и создание проекта

Нужно: .NET SDK 8+, Windows, Visual Studio 2022 или VS Code с C# Dev Kit.

dotnet new wpf -n NoteKeeper -o NoteKeeper
cd NoteKeeper
dotnet run

Появится пустое окно — шаблон wpf создал App.xaml, MainWindow.xaml и точку входа.

Структура после шага 0:

NoteKeeper/
├── NoteKeeper.csproj
├── App.xaml
├── App.xaml.cs
├── MainWindow.xaml
└── MainWindow.xaml.cs

В NoteKeeper.csproj уже есть <UseWPF>true</UseWPF> — без этой строки XAML не соберётся.


Шаг 1 — модель данных

Создайте папку Models и файл Models/NoteItem.cs:

namespace NoteKeeper.Models;

public sealed class NoteItem
{
public required string Title { get; init; }
public required string Category { get; init; }
}

NoteItem — простой объект без логики. Список таких объектов покажем в ListBox; для каждого элемента WPF применит DataTemplate.


Шаг 2 — ViewModel и команда

Папка ViewModels, файл ViewModels/MainViewModel.cs:

Код ITЗагрузка примера кода…

ЭлементЗачем
INotifyPropertyChangedUI обновляется при изменении NewTitle и других свойств.
ObservableCollectionListBox узнаёт о добавлении и удалении элементов без ручного Refresh.
RelayCommandМинимальная реализация ICommand для кнопки "Добавить" без сторонних пакетов.
CanExecuteКнопка неактивна, пока заголовок пустой.
Поток UI

Все изменения коллекции и свойств здесь идут из UI-потока. Для сетевых запросов используйте async/await — см. пример в 112.md.


Шаг 3 — ресурсы и стили в App.xaml

Глобальные кисти и стиль TextBox удобно держать в корне приложения. Замените содержимое App.xaml:

Код ITЗагрузка примера кода…

Style с TargetType="TextBox" применится ко всем TextBox в приложении. StaticResource BrushBorder ссылается на кисть из того же словаря. Trigger меняет рамку при фокусе.

ControlTemplate для TextBox — скруглённая рамка вокруг внутреннего PART_ContentHost (обязательная часть шаблона поля ввода).

Стиль AccentButtonStyle с ControlTemplate для кнопки мы разберём подробнее на шаге 5.


Шаг 4 — разметка формы MainWindow.xaml

Полностью замените MainWindow.xaml:

Код ITЗагрузка примера кода…


Разбор разметки

ФрагментСмысл
Window.DataContextViewModel создаётся в XAML; окно — View в MVVM.
Grid + RowDefinitionsТри зоны — форма, подпись, список на оставшуюся высоту (*).
UpdateSourceTrigger=PropertyChangedNewTitle обновляется при каждом символе — CanExecute кнопки реагирует сразу.
Command="&#123;Binding AddNoteCommand&#125;"Клик без Click= в code-behind.
ListBox.ItemTemplate / DataTemplateДля каждого NoteItem рисуется карточка; Binding Title внутри шаблона — свойство элемента списка.
DataTriggerЦвет бейджа категории зависит от строки Category.

MainWindow.xaml.cs оставьте минимальным:

namespace NoteKeeper;

public partial class MainWindow
{
public MainWindow() => InitializeComponent();
}

Шаг 5 — ControlTemplate для кнопки "Добавить"

На шаге 3 мы уже добавили AccentButtonStyle в App.xaml. Разберём, как устроен этот фрагмент:

Код ITЗагрузка примера кода…

ПонятиеОтличие
Style (AccentButtonStyle)Именованный набор свойств; подключается через Style="&#123;StaticResource AccentButtonStyle&#125;".
ControlTemplateЗаменяет визуальное дерево кнопки: Border + ContentPresenter вместо стандартной серой кнопки Windows.
TemplateBinding PaddingБерёт Padding из свойств кнопки в Style.
ContentPresenterМесто, куда подставляется Content="Добавить".

DataTemplate описывает данные (карточку заметки). ControlTemplate описывает сам контрол (форму кнопки). Оба — виды шаблонов в WPF.


Шаг 6 — запуск и проверка

dotnet run

Проверьте:

  1. Кнопка "Добавить" серая и неактивна, пока поле заголовка пустое.
  2. После ввода текста кнопка активна; по нажатию строка появляется в списке, поле очищается.
  3. Бейдж категории меняет цвет при смене пункта в ComboBox.
  4. При наведении кнопка темнеет (IsMouseOver).

В Visual Studio откройте Live Visual Tree (Отладка → Окна → Live Visual Tree) — увидите дерево XAML в рантайме.


Шаг 7 — вынести стили в отдельный файл

В больших проектах ресурсы дробят на файлы. Создайте Styles/Buttons.xaml:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Сюда перенесите AccentButtonStyle целиком -->
</ResourceDictionary>

Подключите в App.xaml:

<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Styles/Buttons.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- кисти и Style для TextBox -->
</ResourceDictionary>
</Application.Resources>

MergedDictionaries объединяет несколько словарей — стили из Buttons.xaml видны во всём приложении.


Типичные сбои

СимптомВероятная причинаЧто проверить
Пустой список после кликаItemsSource не привязан или ViewModel не в DataContextItemsSource="&#123;Binding Notes&#125;", блок Window.DataContext.
Поле не очищаетсяOneWay-привязка без INotifyPropertyChangedSetProperty в ViewModel, UpdateSourceTrigger=PropertyChanged.
Кнопка всегда активнаНет CanExecute или не вызывается RaiseCanExecuteChangedОбработчик в SetProperty для NewTitle.
StaticResource not foundСтиль объявлен ниже использования или в другом словаре без mergeПорядок в App.xaml, MergedDictionaries.
Белый TextBox без скругленияКонфликт локального StyleУберите дублирующий Style на конкретном TextBox.
Ошибка XAML при сборкеОпечатка в xmlns:vmNamespace совпадает с namespace NoteKeeper.ViewModels.

Что попробовать дальше

  1. Удаление заметки — кнопка в DataTemplate, команда RemoveNoteCommand с параметром &#123;Binding&#125;.
  2. Сохранение в JSON — при закрытии окна сериализуйте Notes в файл; при старте загружайте (System.Text.Json).
  3. CommunityToolkit.Mvvm — атрибуты [ObservableProperty] и [RelayCommand] вместо ручного boilerplate.
  4. Отдельные UserControl — форму ввода вынесите в NoteFormView.xaml для переиспользования.
  5. Упаковкаdotnet publish -c Release -r win-x64 --self-contained и MSIX по Microsoft Store и публикация Windows-приложений.

Связанные материалы


Как превратить учебный пример в рабочий модуль

Следующий шаг по архитектуре

  1. Вынесите MainViewModel в отдельную сборку NoteKeeper.Application.
  2. Добавьте сервис хранения заметок (INoteStorage) и внедрение зависимости.
  3. Подключите журналирование операций добавления, удаления и сохранения.
  4. Реализуйте валидацию через INotifyDataErrorInfo для полей формы.

Что это дает в реальном проекте

  • UI остаётся простым и предсказуемым.
  • Логику можно покрыть unit-тестами без запуска окна.
  • Дальше проще добавить экспорт, синхронизацию и автосохранение.

Практика командной работы с XAML

Разделение ролей в проекте

  • Разработчик UI ведет ResourceDictionary, шаблоны и стили.
  • Разработчик доменной логики развивает ViewModel и сервисы.
  • Общий контракт между ними проходит через свойства и команды.

Технические правила, которые снижают регрессии

  1. Не храните бизнес-логику в code-behind окна.
  2. Для каждого важного сценария держите unit-тест на ViewModel.
  3. Новые стили добавляйте через словари ресурсов и переиспользование.
  4. Проверяйте визуальные изменения на разных DPI и темах Windows.