Tworzenie niestandardowych kontrolek WPF: stylizacja przycisków i przełączników bez dziedziczenia
Przy rozwijaniu aplikacji WPF często pojawia się potrzeba dostosowania standardowych kontrolek do firmowego designu bez kupowania komercyjnych bibliotek. Zamiast tworzenia własnych UserControl lub dziedziczenia od klas bazowych, efektywniej jest używać stylów i attached properties — to zachowuje kompatybilność z frameworkiem i upraszcza utrzymanie.
Architektura systemu motywów w WPF
Do zarządzania wyglądem aplikacji zaleca się centralizować wszystkie zasoby w folderze Style. Każdy motyw (np. jasny i ciemny) jest realizowany jako oddzielny katalog zawierający plik palety kolorów i główny słownik zasobów. Pozwala to na łatwą zmianę motywów w trakcie działania, poprzez zastąpienie pojedynczego ResourceDictionary w Application.Resources.
Realizacja przełączania motywów wygląda następująco:
public void ChangeTheme(Theme theme)
{
if (CurrentTheme == theme) return;
CurrentTheme = theme;
Uri themeSource = theme switch
{
Theme.Dark => new Uri("pack://application:,,,/CustomWpfControls;component/Style/DarkTheme/DarkTheme.xaml", UriKind.Absolute),
Theme.Light => new Uri("pack://application:,,,/CustomWpfControls;component/Style/LightTheme/LightTheme.xaml", UriKind.Absolute),
_ => throw new ArgumentOutOfRangeException(nameof(theme))
};
var dictionary = Resources.MergedDictionaries[0];
dictionary.Clear();
dictionary.Source = themeSource;
}
Taki podejście gwarantuje, że wszystkie elementy interfejsu natychmiast zaktualizują swój wygląd bez przeładowania okna.
Stylizacja przycisków za pomocą attached properties
Zamiast tworzenia niestandardowej klasy RoundedButton, lepiej zdefiniować attached property CornerRadius w statycznej klasie. Pozwala to na ustawianie promienia zaokrąglenia bezpośrednio w XAML bez naruszania zasady kompozycji:
public static class RoundedButton
{
public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.RegisterAttached(
"CornerRadius",
typeof(CornerRadius),
typeof(RoundedButton),
new FrameworkPropertyMetadata(new CornerRadius(0d),
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsRender));
public static void SetCornerRadius(UIElement element, CornerRadius value) =>
element.SetValue(CornerRadiusProperty, value);
public static CornerRadius GetCornerRadius(UIElement element) =>
(CornerRadius)element.GetValue(CornerRadiusProperty);
}
W szablonie stylu przycisku ta właściwość jest używana poprzez powiązanie z TemplatedParent:
<Border CornerRadius="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(customWpfControls:RoundedButton.CornerRadius)}" ... />
Zalety takiego rozwiązania:
- Brak potrzeby pisania dodatkowego kodu behind
- Zachowana pełna kompatybilność z Button
- Możliwość nadpisywania promienia zarówno w stylu, jak i na poziomie instancji
- Obsługa animacji i wyzwalaczy stanów
Realizacja ToggleButton z animowanym przełącznikiem
Niestandardowy przełącznik imitujący styl iOS można w pełni zaimplementować za pomocą ControlTemplate bez jednej linii C#. Główne komponenty szablonu:
- Canvas do precyzyjnego pozycjonowania elementów
- DrawingImage dla ikon (ptaszek i kółko)
- Border z RenderTransform do animowanego przemieszczania
- VisualStateManager do reakcji na Checked/Unchecked
Animacja przesunięcia jest realizowana za pomocą DoubleAnimation wewnątrz Storyboard:
<DoubleAnimation Duration="0:0:0.1"
To="20"
AccelerationRatio="0.2"
DecelerationRatio="0.7"
Storyboard.TargetName="Switcher"
Storyboard.TargetProperty="(RenderTransform).(TranslateTransform.X)"/>
Ważne jest, aby nie podawać wartości początkowej (From), dzięki czemu animacja zawsze zaczyna się od aktualnej pozycji — zapobiega to skokom przy częstych przełączaniach.
Zalecenia dotyczące organizacji stylów
Dla skalowalności projektu należy przestrzegać następujących zasad:
- Styl bazowy (bez x:Key) powinien zawierać tylko Template i ogólne Settery
- Konkretne warianty (PrimaryButton, IconButton itp.) dziedziczą poprzez BasedOn
- Wszystkie kolory są wyodrębniane do oddzielnego pliku (np. DarkColors.xaml)
- Attached properties są grupowane według funkcjonalności w statycznych klasach
Ułatwia to refaktoring i pozwala szybko tworzyć nowe typy kontrolek na bazie istniejących szablonów.
Co ważne
- Unikaj dziedziczenia od Button/ToggleButton, jeśli logika zachowania się nie zmienia
- Attached properties to preferowany sposób rozszerzania funkcjonalności stylów
- VisualStateManager zapewnia deklaratywne zarządzanie stanami bez kodu
- Zcentralizowany system motywów upraszcza obsługę wielu schematów kolorystycznych
- Animacje powinny używać wartości względnych (bez From), aby uniknąć artefaktów wizualnych
— Editorial Team
Brak komentarzy.