Powrót do strony głównej

Niestandardowe kontrolki WPF: style i attached properties

Artykuł opisuje metody tworzenia niestandardowych kontrolek WPF bez dziedziczenia i UserControl. Omówiono attached properties do parametryzacji stylów, implementację animowanych przełączników za pomocą VisualStateManager oraz architekturę systemu motywów.

Stylizacja kontrolek WPF: przyciski i przełączniki bez codebehind
Advertisement 728x90

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:

Google AdInline article slot
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:

Google AdInline article slot
<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:

Google AdInline article slot
<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

Advertisement 728x90

Czytaj dalej