🧩 WPF: Создание пользовательских элементов управления
Как сделать свой собственный компонент, который можно переиспользовать в любом проекте
🎯 Зачем это нужно?
• В WPF есть много стандартных элементов: Button, TextBox, Slider…
• Но иногда нужен свой, особенный элемент (например, счётчик, звёздный рейтинг, часы).
• Создаём один раз — используем везде! Это экономит часы работы.
📌 Для кого лекция: для тех, кто уже немного знает WPF (создал хотя бы одно окно с кнопкой). Всё объясняется на пальцах.
💡 После лекции вы сможете создать свой элемент управления и встроить его в любое приложение.
📦 Что такое «пользовательский элемент управления»?
Это как конструктор Lego — вы собираете несколько стандартных деталей в один блок
Стандартный элемент
Button, TextBox, Slider — уже есть в WPF.
Пользовательский элемент
Создаёте сами, например, «Счётчик» (кнопки + текстовое поле + логика внутри).
<!-- Вот так выглядит использование вашего элемента на форме: -->
<local:CounterControl Minimum="0" Maximum="10" Value="5" />
✨ Магия: после создания ваш элемент можно перетаскивать из панели инструментов как обычную кнопку!
⚡ Два способа: UserControl (проще) и CustomControl (мощнее)
Для новичков начинаем с UserControl — его проще понять и сделать
✅ UserControl
• Собирается из готовых элементов.
• Быстро, наглядно.
• Подходит для 99% задач новичка.
🔧 CustomControl
• Полная кастомизация внешнего вида.
• Сложнее, требует шаблонов (ControlTemplate).
• Используется в библиотеках (например, кнопка с особым дизайном).
🎓 На этом уроке делаем UserControl — самый понятный для старта.
🚀 Шаг 1. Создаём проект и добавляем UserControl
Открываем Visual Studio и делаем первые шаги
1️⃣ Создайте новый проект WPF (назовите, например, «MyUserControlDemo»).
2️⃣ В обозревателе решений (Solution Explorer) нажмите правой кнопкой на проект → Add → User Control (WPF).
3️⃣ Назовите его, например, CounterControl.xaml.
✅ Visual Studio создаст два файла:
- CounterControl.xaml (дизайн вашего элемента)
- CounterControl.xaml.cs (логика вашего элемента)
📌 Поздравляю! Вы только что создали заготовку своего первого элемента управления.
🎨 Шаг 2. Рисуем внутренность элемента (CounterControl.xaml)
Внутри UserControl может быть Grid, StackPanel, любые элементы
<UserControl x:Class="MyUserControlDemo.CounterControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Border BorderBrush="#ccc" BorderThickness="1" CornerRadius="8" Padding="10">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button x:Name="MinusButton" Content="-" Width="40" Height="40" FontSize="20"/>
<TextBlock x:Name="ValueText" Text="0" Width="60" TextAlignment="Center"
FontSize="24" FontWeight="Bold" VerticalAlignment="Center"/>
<Button x:Name="PlusButton" Content="+" Width="40" Height="40" FontSize="20"/>
</StackPanel>
</Border>
</UserControl>
🧪 Что здесь сделано: Border (рамка), внутри — кнопки и текстовый блок. Пока ничего не работает, просто картинка.
⚙️ Шаг 3. Оживляем элемент (CounterControl.xaml.cs)
Добавляем логику: при нажатии на кнопки значение меняется
using System.Windows;
using System.Windows.Controls;
namespace MyUserControlDemo
{
public partial class CounterControl : UserControl
{
private int _currentValue = 0;
public CounterControl()
{
InitializeComponent();
MinusButton.Click += (s, e) => ChangeValue(-1);
PlusButton.Click += (s, e) => ChangeValue(1);
UpdateDisplay();
}
private void ChangeValue(int delta)
{
_currentValue += delta;
UpdateDisplay();
}
private void UpdateDisplay() => ValueText.Text = _currentValue.ToString();
}
}
🔧 Проверка: нажмите F5, вы увидите окно… но элемента ещё нет! Его надо добавить в главное окно.
🏠 Шаг 4. Используем наш элемент в MainWindow.xaml
Добавляем пространство имён и просто пишем тег
<!-- В самом верху MainWindow.xaml добавляем строку: -->
xmlns:local="clr-namespace:MyUserControlDemo"
<!-- А внутрь Grid или StackPanel вставляем: -->
<StackPanel Margin="20">
<TextBlock Text="Мой первый пользовательский элемент:" FontSize="16" Margin="0,0,0,10"/>
<local:CounterControl />
</StackPanel>
✨ Результат: запускаем — видим готовый счётчик! Кнопки работают, значение меняется.
📌 Теперь этот элемент можно копировать в любое место в любом окне.
⚠️ Но есть проблема: элемент негибкий
Сейчас начальное значение всегда 0, нельзя задать минимальное/максимальное число извне
Хотим, чтобы можно было писать:
<local:CounterControl Minimum="0" Maximum="10" Value="5" />
Сейчас так не работает, потому что у элемента нет свойств Minimum, Maximum, Value.
🔥 Нужны настоящие Dependency Properties — особые свойства, которые поддерживают привязку и наследование.
🧩 DependencyProperty — особое свойство для элементов управления
Оно позволяет: привязку {Binding}, стили, анимацию, наследование значения
// Обычное свойство C# (НЕ ПОДХОДИТ для элементов управления)
public int Value { get; set; }
// DependencyProperty (ПРАВИЛЬНО)
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof(int), typeof(CounterControl),
new FrameworkPropertyMetadata(0, OnValueChanged));
public int Value
{
get => (int)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (CounterControl)d;
control.UpdateDisplay();
}
📖 Как запомнить: DependencyProperty — это «умное» свойство, которое умеет общаться с WPF.
🔧 Шаг 5. Добавляем DependencyProperty для Minimum, Maximum, Value
Вставляем этот код в CounterControl.xaml.cs (внутрь класса)
public static readonly DependencyProperty MinimumProperty =
DependencyProperty.Register(nameof(Minimum), typeof(int), typeof(CounterControl),
new FrameworkPropertyMetadata(0, OnLimitChanged));
public static readonly DependencyProperty MaximumProperty =
DependencyProperty.Register(nameof(Maximum), typeof(int), typeof(CounterControl),
new FrameworkPropertyMetadata(10, OnLimitChanged));
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof(int), typeof(CounterControl),
new FrameworkPropertyMetadata(0, OnValueChanged));
public int Minimum
{
get => (int)GetValue(MinimumProperty);
set => SetValue(MinimumProperty, value);
}
public int Maximum
{
get => (int)GetValue(MaximumProperty);
set => SetValue(MaximumProperty, value);
}
public int Value
{
get => (int)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
private static void OnLimitChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (CounterControl)d;
control.ClampValue();
control.UpdateDisplay();
}
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (CounterControl)d;
control.ClampValue();
control.UpdateDisplay();
}
private void ClampValue()
{
if (Value < Minimum) Value = Minimum;
if (Value > Maximum) Value = Maximum;
}
🎯 Теперь элемент понимает границы и автоматически подрезает значение.
🔁 Шаг 6. Меняем логику кнопок — работаем через свойство Value
private void MinusButton_Click(object sender, RoutedEventArgs e) => Value--;
private void PlusButton_Click(object sender, RoutedEventArgs e) => Value++;
// При изменении Value автоматически срабатывает OnValueChanged,
// который вызывает ClampValue() и UpdateDisplay().
✨ Итог: теперь можно писать Value="{Binding SomeProperty}" — элемент поддерживает привязку!
🖼️ Пример: используем элемент с привязкой к ViewModel
<!-- MainWindow.xaml -->
<StackPanel>
<local:CounterControl Minimum="0" Maximum="10" Value="{Binding Age}" />
<TextBlock Text="{Binding Age}" />
</StackPanel>
// ViewModel
public class MainViewModel : INotifyPropertyChanged
{
private int _age = 5;
public int Age
{
get => _age;
set { _age = value; OnPropertyChanged(); }
}
}
🔥 Значение элемента и TextBlock синхронизируются автоматически!
📢 Шаг 7. Добавляем событие, чтобы уведомлять внешний код
Пусть при изменении Value можно подписаться на событие
// Объявляем событие в классе CounterControl
public event RoutedPropertyChangedEventHandler<int> ValueChanged;
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (CounterControl)d;
control.ClampValue();
control.UpdateDisplay();
// Вызываем событие
control.ValueChanged?.Invoke(control,
new RoutedPropertyChangedEventArgs<int>((int)e.OldValue, (int)e.NewValue));
}
📢 Теперь в коде окна можно написать: counter.ValueChanged += Counter_ValueChanged;
📄 Полный листинг CounterControl.xaml.cs (финал)
using System.Windows;
using System.Windows.Controls;
namespace MyUserControlDemo
{
public partial class CounterControl : UserControl
{
public static readonly DependencyProperty MinimumProperty =
DependencyProperty.Register(nameof(Minimum), typeof(int), typeof(CounterControl),
new FrameworkPropertyMetadata(0, OnLimitChanged));
public static readonly DependencyProperty MaximumProperty =
DependencyProperty.Register(nameof(Maximum), typeof(int), typeof(CounterControl),
new FrameworkPropertyMetadata(10, OnLimitChanged));
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof(int), typeof(CounterControl),
new FrameworkPropertyMetadata(0, OnValueChanged));
public int Minimum { get => (int)GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); }
public int Maximum { get => (int)GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); }
public int Value { get => (int)GetValue(ValueProperty); set => SetValue(ValueProperty, value); }
public event RoutedPropertyChangedEventHandler<int> ValueChanged;
public CounterControl()
{
InitializeComponent();
MinusButton.Click += (s, e) => Value--;
PlusButton.Click += (s, e) => Value++;
UpdateDisplay();
}
private static void OnLimitChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var c = (CounterControl)d;
c.ClampValue();
c.UpdateDisplay();
}
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var c = (CounterControl)d;
c.ClampValue();
c.UpdateDisplay();
c.ValueChanged?.Invoke(c, new RoutedPropertyChangedEventArgs<int>((int)e.OldValue, (int)e.NewValue));
}
private void ClampValue()
{
if (Value < Minimum) Value = Minimum;
if (Value > Maximum) Value = Maximum;
}
private void UpdateDisplay() => ValueText.Text = Value.ToString();
}
}
🧪 САМОСТОЯТЕЛЬНАЯ РАБОТА (обязательная часть)
Создать пользовательский элемент управления «Звёздный рейтинг» (RatingControl)
📌 Тема: Рейтинг звёздами (как в онлайн‑магазинах).
🎯 Цель: закрепить навыки создания UserControl, DependencyProperty и событий.
✅ Что должно быть реализовано (базовый уровень):
• Элемент управления RatingControl (UserControl).
• Отображает 5 звёзд (⭐) — можно использовать обычные кнопки с текстом ★ и ☆.
• При клике на звезду выставляется рейтинг (1–5) и подсвечиваются звёзды (все до текущей включительно).
• Свойство Value (int, от 1 до 5) с использованием DependencyProperty.
• Событие RatingChanged (стандартное RoutedEvent или просто .NET событие).
• Минимальное и максимальное значение: 1 и 5 (защита от выхода за границы).
• Элемент должен корректно работать с привязкой данных (Binding) — проверьте на простой ViewModel.
⭐ Подсказка для отображения звёзд: можно использовать WrapPanel с пятью кнопками. В кнопках писать символы ★ (закрашенная) и ☆ (пустая).
🧪 Самостоятельная работа — продвинутый уровень (+2 балла)
Дополнительные требования для тех, кто хочет больше
🚀 Дополнительные задачи (по желанию, но приветствуются):
• Добавить свойство ReadOnly (bool), которое блокирует изменение рейтинга (звёзды не кликаются).
• Добавить свойство StarSize (double) — размер звёзд (можно привязать к FontSize).
• Добавить подсказку ToolTip для каждой звезды («Ужасно», «Нормально», «Отлично» и т.д.).
• Реализовать возможность выбора половинчатых оценок (0.5 шаг) — повышенная сложность.
<!-- Пример использования готового элемента: -->
<local:RatingControl Value="{Binding ProductRating}" ReadOnly="False" StarSize="24"/>
📌 Важно: код должен быть написан самостоятельно. При проверке будут заданы вопросы по коду (защита от простого копирования).
📋 Как будет оцениваться самостоятельная работа
Баллы и требования к сдаче
📤 Формат сдачи:
• Архив .zip с проектом (папка bin/obj не обязательна).
• Файл Readme.txt с краткими пояснениями и скриншотом работающего элемента.
• Краткое описание, как работают DependencyProperty и событие в вашем элементе.
🎯 Критерии оценки (максимум 10 баллов):
• ✅ Создан UserControl RatingControl — 2 балла
• ✅ Реализовано свойство Value (DependencyProperty) — 2 балла
• ✅ Работает отображение и выбор звёзд — 2 балла
• ✅ Элемент работает с привязкой (проверить на ViewModel) — 2 балла
• ✅ Код аккуратный, есть комментарии — 1 балл
• ✅ Добавлены продвинутые фишки (ReadOnly, ToolTip и т.п.) — до +1 балла
• ✅ Элемент оформлен красиво (цвета, отступы) — 1 балл
🗣️ Защита работы: будьте готовы объяснить, что делает каждая строчка в коде элемента. Преподаватель может попросить изменить размер звёзд или заблокировать рейтинг — код должен быть гибким.
🏁 Итоги лекции и самостоятельной работы
Что мы узнали, и что вы теперь умеете
✅ UserControl — компоновка готовых элементов
✅ DependencyProperty — умные свойства с привязкой
✅ События в пользовательских элементах
✅ Создание элемента «Звёздный рейтинг»
🎉 Теперь вы можете создавать свои собственные переиспользуемые компоненты для WPF. Это сильно ускорит разработку и сделает код чище.
📚 Что дальше? Изучайте CustomControl, а также пробуйте создавать более сложные элементы (часы, календарь, график). Удачи в практике!