Первая форма 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 — в архитектуре десктопа.
Что получится
Окно "Заметки" с:
- Полем заголовка и выпадающим списком категорий.
- Кнопкой "Добавить", оформленной через ControlTemplate.
- Списком заметок, где каждая строка — DataTemplate (карточка с заголовком и меткой категории).
- Общими Style для
TextBoxи цветами вApp.xaml.
Логика — в ViewModel с привязками (Binding); разметка остаётся в XAML, без обработчиков Click в code-behind.
┌─────────────────────────────────────┐
│ Заметки │
├─────────────────────────────────────┤
│ Заголовок: [________________] │
│ Категория: [ Работа ▼ ] │
│ [ Добавить ] │
├─────────────────────────────────────┤
│ ┌─────────────────────────────┐ │
│ │ Купить молоко [Личное] │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ Созвон с командой [Работа] │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
Словарь терминов
| Термин | Простыми словами |
|---|---|
| XAML | XML-разметка, описывающая дерево 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 или отдельном файле. |
| MVVM | Model — данные, 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Загрузка примера кода…
| Элемент | Зачем |
|---|---|
INotifyPropertyChanged | UI обновляется при изменении NewTitle и других свойств. |
ObservableCollection | ListBox узнаёт о добавлении и удалении элементов без ручного Refresh. |
RelayCommand | Минимальная реализация ICommand для кнопки "Добавить" без сторонних пакетов. |
CanExecute | Кнопка неактивна, пока заголовок пустой. |
Все изменения коллекции и свойств здесь идут из 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.DataContext | ViewModel создаётся в XAML; окно — View в MVVM. |
Grid + RowDefinitions | Три зоны — форма, подпись, список на оставшуюся высоту (*). |
UpdateSourceTrigger=PropertyChanged | NewTitle обновляется при каждом символе — CanExecute кнопки реагирует сразу. |
Command="{Binding AddNoteCommand}" | Клик без 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="{StaticResource AccentButtonStyle}". |
| ControlTemplate | Заменяет визуальное дерево кнопки: Border + ContentPresenter вместо стандартной серой кнопки Windows. |
TemplateBinding Padding | Берёт Padding из свойств кнопки в Style. |
ContentPresenter | Место, куда подставляется Content="Добавить". |
DataTemplate описывает данные (карточку заметки). ControlTemplate описывает сам контрол (форму кнопки). Оба — виды шаблонов в WPF.
Шаг 6 — запуск и проверка
dotnet run
Проверьте:
- Кнопка "Добавить" серая и неактивна, пока поле заголовка пустое.
- После ввода текста кнопка активна; по нажатию строка появляется в списке, поле очищается.
- Бейдж категории меняет цвет при смене пункта в
ComboBox. - При наведении кнопка темнеет (
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 не в DataContext | ItemsSource="{Binding Notes}", блок Window.DataContext. |
| Поле не очищается | OneWay-привязка без INotifyPropertyChanged | SetProperty в ViewModel, UpdateSourceTrigger=PropertyChanged. |
| Кнопка всегда активна | Нет CanExecute или не вызывается RaiseCanExecuteChanged | Обработчик в SetProperty для NewTitle. |
StaticResource not found | Стиль объявлен ниже использования или в другом словаре без merge | Порядок в App.xaml, MergedDictionaries. |
Белый TextBox без скругления | Конфликт локального Style | Уберите дублирующий Style на конкретном TextBox. |
| Ошибка XAML при сборке | Опечатка в xmlns:vm | Namespace совпадает с namespace NoteKeeper.ViewModels. |
Что попробовать дальше
- Удаление заметки — кнопка в
DataTemplate, командаRemoveNoteCommandс параметром{Binding}. - Сохранение в JSON — при закрытии окна сериализуйте
Notesв файл; при старте загружайте (System.Text.Json). - CommunityToolkit.Mvvm — атрибуты
[ObservableProperty]и[RelayCommand]вместо ручного boilerplate. - Отдельные UserControl — форму ввода вынесите в
NoteFormView.xamlдля переиспользования. - Упаковка —
dotnet publish -c Release -r win-x64 --self-containedи MSIX по Microsoft Store и публикация Windows-приложений.
Связанные материалы
- C# WinForms и WPF — простые окна (Lab) — галерея с построчным разбором для лабораторной и первого окна
- Практикум WPF и клиент-сервер — TaskDesk — MVVM, ASP.NET Core Web API, Prism, тесты
- Справочник по WPF — элементы UI — рецепты Label, Button, DataGrid и др.
- WinForms — элементы UI — альтернатива с конструктором форм
- Архитектура десктопных приложений
- XAML — справочник
- Разработка приложений для Windows (Microsoft Learn)
- Особенности разработки десктопных приложений — MVVM, потоки UI
- Windows Forms (WinForms) — альтернатива с конструктором форм
- Первая программа Electron с React — тот же формат практикума на JS-стеке
- Платформа .NET
Как превратить учебный пример в рабочий модуль
Следующий шаг по архитектуре
- Вынесите
MainViewModelв отдельную сборкуNoteKeeper.Application. - Добавьте сервис хранения заметок (
INoteStorage) и внедрение зависимости. - Подключите журналирование операций добавления, удаления и сохранения.
- Реализуйте валидацию через
INotifyDataErrorInfoдля полей формы.
Что это дает в реальном проекте
- UI остаётся простым и предсказуемым.
- Логику можно покрыть unit-тестами без запуска окна.
- Дальше проще добавить экспорт, синхронизацию и автосохранение.
Практика командной работы с XAML
Разделение ролей в проекте
- Разработчик UI ведет
ResourceDictionary, шаблоны и стили. - Разработчик доменной логики развивает ViewModel и сервисы.
- Общий контракт между ними проходит через свойства и команды.
Технические правила, которые снижают регрессии
- Не храните бизнес-логику в code-behind окна.
- Для каждого важного сценария держите unit-тест на ViewModel.
- Новые стили добавляйте через словари ресурсов и переиспользование.
- Проверяйте визуальные изменения на разных DPI и темах Windows.