Udostępnij za pośrednictwem


Tworzenie kontrolki, która ma dostosowywalny wygląd

Windows Presentation Foundation (WPF) umożliwia utworzenie kontrolki, której wygląd można dostosować. Na przykład, można zmienić wygląd CheckBox poza tym, co można osiągnąć ustawiając właściwości, poprzez stworzenie nowego ControlTemplate. Na poniższej ilustracji przedstawiono CheckBox używającą domyślnego ControlTemplate i CheckBox używającego niestandardowego ControlTemplate.

pole wyboru Z domyślnym szablonem kontrolki. Pole wyboru używające domyślnego szablonu kontrolki

pole wyboru Z niestandardowym szablonem kontrolki. Pole wyboru używające niestandardowego szablonu kontrolki

W przypadku korzystania z modelu części i stanów podczas tworzenia kontrolki wygląd kontrolki będzie możliwy do dostosowania. Narzędzia projektanta, takie jak Blend for Visual Studio, obsługują model części i stanów, co umożliwia dostosowanie kontrolki w tego typu aplikacjach, gdy stosujesz ten model. W tym temacie omówiono model części i stanów oraz jak go zastosować podczas tworzenia własnej kontrolki. W tym temacie użyto przykładu kontrolki niestandardowej, NumericUpDown, aby zilustrować filozofię tego modelu. Kontrolka NumericUpDown wyświetla wartość liczbową, którą użytkownik może zwiększyć lub zmniejszyć, klikając przyciski kontrolki. Poniższa ilustracja przedstawia kontrolkę NumericUpDown, która została omówiona w tym temacie.

niestandardowej kontrolki NumericUpDown. niestandardowa kontrolka NumericUpDown

Ten temat zawiera następujące sekcje:

Warunki wstępne

W tym temacie założono, że wiesz, jak utworzyć nowy ControlTemplate dla istniejącej kontrolki, zapoznać się z elementami kontraktu sterowania i zrozumieć pojęcia omówione w Tworzenie szablonu dla kontrolki.

Notatka

Aby utworzyć kontrolkę, która może mieć dostosowany wygląd, należy utworzyć kontrolkę dziedziczą z klasy Control lub jednej z jej podklas innych niż UserControl. Kontrolka dziedziczona po UserControl to kontrolka, którą można szybko utworzyć, ale nie używa ControlTemplate i nie można dostosować jej wyglądu.

Części i stany — model

Model części i stanów określa sposób definiowania struktury wizualizacji i zachowania wizualnego kontrolki. Aby postępować zgodnie z modelem części i stanów, należy wykonać następujące czynności:

  • Zdefiniuj strukturę wizualną i zachowanie wizualne w ControlTemplate kontroli.

  • Postępuj zgodnie z pewnymi najlepszymi rozwiązaniami, gdy logika kontrolki wchodzi w interakcje z częściami szablonu kontrolki.

  • Podaj kontrakt kontrolny, aby określić, co należy uwzględnić w ControlTemplate.

Podczas definiowania struktury wizualizacji i zachowania wizualnego w ControlTemplate kontrolki autorzy aplikacji mogą zmienić strukturę wizualizacji i zachowanie wizualne kontrolki, tworząc nowy ControlTemplate zamiast pisać kod. Należy podać kontrakt kontrolny, który informuje autorów aplikacji, które obiekty i stany FrameworkElement powinny być zdefiniowane w ControlTemplate. Podczas interakcji z częściami w ControlTemplate należy kierować się najlepszymi praktykami, aby zarządzanie prawidłowo obsługiwało niekompletne ControlTemplate. Jeśli zastosujesz się do tych trzech zasad, autorzy aplikacji będą mogli utworzyć ControlTemplate dla kontrolki tak samo łatwo, jak w przypadku kontrolek, które są dostarczane z WPF. W poniższej sekcji opisano szczegółowo każdą z tych zaleceń.

Definiowanie struktury wizualnej i zachowania wizualnego kontrolki w ControlTemplate

Podczas tworzenia kontrolki niestandardowej przy użyciu modelu części i stanów, należy zdefiniować jej strukturę wizualną i zachowanie wizualne w jej ControlTemplate, a nie w logice kontrolki. Struktura wizualna kontrolki jest złożona z obiektów FrameworkElement tworzących kontrolkę. Zachowanie wizualne to, jak kontrolka jest wyświetlana, gdy znajduje się w określonym stanie. Aby uzyskać więcej informacji na temat tworzenia ControlTemplate określającej strukturę wizualizacji i zachowanie wizualne kontrolki, zobacz Tworzenie szablonu dla kontrolki.

W przykładzie kontrolki NumericUpDown struktura wizualizacji zawiera dwie kontrolki RepeatButton i TextBlock. Jeśli dodasz te kontrolki do kodu kontrolki NumericUpDown — na przykład w jej konstruktorze — pozycje tych kontrolek będą niezmienne. Zamiast definiować strukturę wizualizacji i zachowanie wizualne kontrolki w kodzie, należy zdefiniować ją w ControlTemplate. Następnie deweloper aplikacji dostosuje położenie przycisków i TextBlock i określ, jakie zachowanie występuje, gdy Value jest ujemna, ponieważ można zamienić ControlTemplate.

W poniższym przykładzie przedstawiono strukturę wizualizacji kontrolki NumericUpDown, która obejmuje RepeatButton w celu zwiększenia Value, RepeatButton w celu zmniejszenia Valueoraz TextBlock wyświetlania Value.

<ControlTemplate TargetType="src:NumericUpDown">
  <Grid  Margin="3" 
         Background="{TemplateBinding Background}">
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
      </Grid.ColumnDefinitions>

      <Border BorderThickness="1" BorderBrush="Gray" 
              Margin="7,2,2,2" Grid.RowSpan="2" 
              Background="#E0FFFFFF"
              VerticalAlignment="Center" 
              HorizontalAlignment="Stretch">

        <!--Bind the TextBlock to the Value property-->
        <TextBlock Name="TextBlock"
                   Width="60" TextAlignment="Right" Padding="5"
                   Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                     AncestorType={x:Type src:NumericUpDown}}, 
                     Path=Value}"/>
      </Border>

      <RepeatButton Content="Up" Margin="2,5,5,0"
        Name="UpButton"
        Grid.Column="1" Grid.Row="0"/>
      <RepeatButton Content="Down" Margin="2,0,5,5"
        Name="DownButton"
        Grid.Column="1" Grid.Row="1"/>

      <Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2" 
        Stroke="Black" StrokeThickness="1"  
        Visibility="Collapsed"/>
    </Grid>

  </Grid>
</ControlTemplate>

Zachowanie wizualne kontrolki NumericUpDown polega na tym, że wartość znajduje się w czerwonej czcionce, jeśli wartość jest ujemna. Jeśli zmienisz ForegroundTextBlock w kodzie, gdy Value będzie ujemna, NumericUpDown zawsze będzie pokazywać czerwoną wartość ujemną. Zachowanie wizualne kontrolki w ControlTemplate można określić przez dodanie obiektów VisualState do ControlTemplate. W poniższym przykładzie przedstawiono obiekty VisualState dla stanów Positive i Negative. Positive i Negative wzajemnie się wykluczają (kontrolka znajduje się zawsze dokładnie w jednej z tych dwóch), więc przykład umieszcza obiekty VisualState w jednym VisualStateGroup. Gdy kontrolka przechodzi w stan Negative, ForegroundTextBlock zmieni kolor na czerwony. Gdy kontrolka znajduje się w stanie Positive, Foreground powróci do oryginalnej wartości. Bardziej szczegółowo omówiono definiowanie obiektów VisualState w ControlTemplate w Tworzenie szablonu dla kontrolki.

Notatka

Pamiętaj, aby ustawić właściwość dołączoną VisualStateManager.VisualStateGroups na FrameworkElement katalogu głównego ControlTemplate.

<ControlTemplate TargetType="local:NumericUpDown">
  <Grid  Margin="3" 
         Background="{TemplateBinding Background}">

    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup Name="ValueStates">

        <!--Make the Value property red when it is negative.-->
        <VisualState Name="Negative">
          <Storyboard>
            <ColorAnimation To="Red"
              Storyboard.TargetName="TextBlock" 
              Storyboard.TargetProperty="(Foreground).(Color)"/>
          </Storyboard>

        </VisualState>

        <!--Return the TextBlock's Foreground to its 
            original color.-->
        <VisualState Name="Positive"/>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
  </Grid>
</ControlTemplate>

Używanie części ControlTemplate w kodzie

Autor ControlTemplate może pominąć FrameworkElement lub VisualState obiekty, celowo lub przez pomyłkę, ale logika kontrolki może wymagać prawidłowego działania tych części. Model części i stanów określa, że kontrola powinna być odporna na brak obiektów FrameworkElement lub VisualState w ControlTemplate. Twoja kontrolka nie powinna wywoływać wyjątku ani zgłaszać błędu, jeśli w ControlTemplatebrakuje FrameworkElement, VisualStatelub VisualStateGroup. W tej sekcji opisano zalecane rozwiązania dotyczące interakcji z obiektami FrameworkElement i zarządzaniem stanami.

Przewidywanie brakujących obiektów FrameworkElement

Podczas definiowania obiektów FrameworkElement w ControlTemplatelogika kontrolki może wymagać interakcji z niektórymi z nich. Na przykład kontrolka NumericUpDown subskrybuje zdarzenie Click przycisków w celu zwiększenia lub zmniejszenia Value i ustawia właściwość TextTextBlock na Value. Jeśli niestandardowy ControlTemplate pomija TextBlock lub przyciski, akceptowalne jest, że kontrolka utraci niektóre funkcje, ale upewnij się, że kontrolka nie powoduje błędu. Jeśli na przykład ControlTemplate nie zawiera przycisków do zmiany Value, NumericUpDown utraci te funkcje, ale aplikacja korzystająca z ControlTemplate będzie nadal działać.

Poniższe rozwiązania zapewnią prawidłowe reagowanie kontrolki na brakujące obiekty FrameworkElement:

  1. Ustaw atrybut x:Name dla każdego FrameworkElement, do którego należy odwołać się w kodzie.

  2. Zdefiniuj właściwości prywatne dla każdej FrameworkElement, z którymi musisz się komunikować.

  3. Zapisz się na i wypisz się ze wszystkich zdarzeń obsługiwanych przez kontrolkę w akcesorze ustawiającym właściwości FrameworkElement.

  4. Ustaw właściwości FrameworkElement zdefiniowane w kroku 2 w metodzie OnApplyTemplate. Jest to najwcześniejsze, że FrameworkElement w ControlTemplate jest dostępny dla kontrolki. Użyj x:Name z FrameworkElement, aby go pobrać z ControlTemplate.

  5. Sprawdź, czy FrameworkElement nie jest null, zanim uzyskasz dostęp do jego członków. Jeśli jest to null, nie zgłaszaj błędu.

W poniższych przykładach pokazano, jak kontrolka NumericUpDown współdziała z obiektami FrameworkElement zgodnie z zaleceniami na powyższej liście.

W przykładzie definiującym strukturę wizualizacji kontrolki NumericUpDown w ControlTemplateRepeatButton, która zwiększa Value ma atrybut x:Name ustawiony na wartość UpButton. Poniższy przykład deklaruje właściwość o nazwie UpButtonElement reprezentującą RepeatButton zadeklarowaną w ControlTemplate. Akcesorium set najpierw anuluje subskrypcję zdarzenia Click przycisku, jeśli UpDownElement nie jest null, następnie ustawia właściwość, a następnie subskrybuje zdarzenie Click. Istnieje również zdefiniowana właściwość, ale nie jest tutaj wyświetlana dla innego RepeatButtono nazwie DownButtonElement.

private RepeatButton upButtonElement;

private RepeatButton UpButtonElement
{
    get
    {
        return upButtonElement;
    }

    set
    {
        if (upButtonElement != null)
        {
            upButtonElement.Click -=
                new RoutedEventHandler(upButtonElement_Click);
        }
        upButtonElement = value;

        if (upButtonElement != null)
        {
            upButtonElement.Click +=
                new RoutedEventHandler(upButtonElement_Click);
        }
    }
}
Private m_upButtonElement As RepeatButton

Private Property UpButtonElement() As RepeatButton
    Get
        Return m_upButtonElement
    End Get

    Set(ByVal value As RepeatButton)
        If m_upButtonElement IsNot Nothing Then
            RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
        End If
        m_upButtonElement = value

        If m_upButtonElement IsNot Nothing Then
            AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
        End If
    End Set
End Property

W poniższym przykładzie przedstawiono OnApplyTemplate dla kontrolki NumericUpDown. W przykładzie użyto metody GetTemplateChild w celu pobrania obiektów FrameworkElement z ControlTemplate. Zwróć uwagę, że przykład chroni przed przypadkami, w których GetTemplateChild znajduje FrameworkElement o określonej nazwie, która nie jest oczekiwanym typem. Najlepszym rozwiązaniem jest również ignorowanie elementów, które mają określone x:Name, ale są niewłaściwym typem.

public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    //TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()

    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

    UpdateStates(False)
End Sub

Postępując zgodnie z praktykami przedstawionymi w poprzednich przykładach, upewnij się, że kontrolka będzie nadal działać, gdy w ControlTemplate brakuje FrameworkElement.

Zarządzanie stanami za pomocą programu VisualStateManager

VisualStateManager śledzi stany kontrolki i wykonuje logikę niezbędną do przejścia między stanami. Po dodaniu obiektów VisualState do ControlTemplatenależy dodać je do VisualStateGroup i dodać VisualStateGroup do właściwości dołączonej VisualStateManager.VisualStateGroups, aby VisualStateManager miał do nich dostęp.

Poniższy przykład powtarza poprzedni przykład pokazujący obiekty VisualState odpowiadające Positive i Negative stanów kontrolki. Storyboard w NegativeVisualState zmienia ForegroundTextBlock na czerwony. Gdy kontrolka NumericUpDown jest w stanie Negative, rozpoczyna się scenorys w stanie Negative. Następnie Storyboard w stanie Negative zatrzymuje się, gdy kontrolka powróci do stanu Positive. Positive VisualState nie musi zawierać Storyboard, ponieważ gdy Storyboard dla Negative zostanie zatrzymana, Foreground powróci do oryginalnego koloru.

<ControlTemplate TargetType="local:NumericUpDown">
  <Grid  Margin="3" 
         Background="{TemplateBinding Background}">

    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup Name="ValueStates">

        <!--Make the Value property red when it is negative.-->
        <VisualState Name="Negative">
          <Storyboard>
            <ColorAnimation To="Red"
              Storyboard.TargetName="TextBlock" 
              Storyboard.TargetProperty="(Foreground).(Color)"/>
          </Storyboard>

        </VisualState>

        <!--Return the TextBlock's Foreground to its 
            original color.-->
        <VisualState Name="Positive"/>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
  </Grid>
</ControlTemplate>

Należy pamiętać, że TextBlock ma nazwę, ale TextBlock nie znajduje się w kontrakcie kontroli dla NumericUpDown, ponieważ logika kontrolki nigdy nie odwołuje się do TextBlock. Elementy, do których odwołuje się ControlTemplate, mają nazwy, ale nie muszą być częścią kontraktu sterowania, ponieważ nowa ControlTemplate kontrolki może nie wymagać odwołania się do tego elementu. Na przykład osoba, która tworzy nowy ControlTemplate dla NumericUpDown, może zdecydować się na to, aby nie wskazywać, że Value jest ujemny, zmieniając Foreground. W takim przypadku ani kod, ani ControlTemplate nie odwołuje się do TextBlock według nazwy.

Logika kontrolki jest odpowiedzialna za zmianę stanu kontrolki. W poniższym przykładzie pokazano, że kontrolka NumericUpDown wywołuje metodę GoToState, aby przejść do stanu Positive, gdy Value jest 0 lub większa, a stan Negative, gdy Value jest mniejszy niż 0.

if (Value >= 0)
{
    VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
    VisualStateManager.GoToState(this, "Negative", useTransitions);
}
If Value >= 0 Then
    VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
    VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If

Metoda GoToState wykonuje logikę niezbędną do odpowiedniego uruchamiania i zatrzymywania scenorysów. Gdy kontrolka wywołuje GoToState, aby zmienić jego stan, VisualStateManager wykonuje następujące czynności:

  • Jeśli VisualState, do którego zmierza kontrolka, ma Storyboard, rozpocznie się scenorys. Następnie, jeśli VisualState, z którego pochodzi kontrolka, ma Storyboard, scenorys kończy się.

  • Jeśli kontrolka znajduje się już w określonym stanie, GoToState nie podejmuje żadnej akcji i zwraca true.

  • Jeśli określony stan nie istnieje w ControlTemplatecontrol, GoToState nie podejmuje żadnych działań i zwraca false.

Najlepsze rozwiązania dotyczące pracy z visualStateManager

Zaleca się wykonanie następujących czynności w celu zachowania stanów kontroli:

  • Użyj właściwości, aby śledzić jego stan.

  • Utwórz metodę pomocnika, aby przejść między stanami.

Kontrolka NumericUpDown używa jej właściwości Value do śledzenia, czy znajduje się w stanie Positive, czy Negative. Kontrolka NumericUpDown definiuje również stany Focused i UnFocused, które śledzą właściwość IsFocused. Jeśli używasz stanów, które nie odpowiadają naturalnie właściwości kontrolki, możesz zdefiniować właściwość prywatną do śledzenia stanu.

Pojedyncza metoda, która aktualizuje wszystkie stany, centralizuje wywołania do VisualStateManager i zapewnia możliwość zarządzania kodem. W poniższym przykładzie przedstawiono metodę pomocniczą kontrolki NumericUpDown, UpdateStates. Jeśli Value jest większa lub równa 0, Control jest w stanie Positive. Gdy Value jest mniejsza niż 0, kontrolka jest w stanie Negative. Gdy IsFocused jest true, kontrolka jest w stanie Focused; w przeciwnym razie jest w stanie Unfocused. Kontrolka może wywoływać UpdateStates zawsze, gdy potrzebuje zmienić swój stan, niezależnie od tego, jaki stan ulega zmianie.

private void UpdateStates(bool useTransitions)
{
    if (Value >= 0)
    {
        VisualStateManager.GoToState(this, "Positive", useTransitions);
    }
    else
    {
        VisualStateManager.GoToState(this, "Negative", useTransitions);
    }

    if (IsFocused)
    {
        VisualStateManager.GoToState(this, "Focused", useTransitions);
    }
    else
    {
        VisualStateManager.GoToState(this, "Unfocused", useTransitions);
    }
}
Private Sub UpdateStates(ByVal useTransitions As Boolean)

    If Value >= 0 Then
        VisualStateManager.GoToState(Me, "Positive", useTransitions)
    Else
        VisualStateManager.GoToState(Me, "Negative", useTransitions)
    End If

    If IsFocused Then
        VisualStateManager.GoToState(Me, "Focused", useTransitions)
    Else
        VisualStateManager.GoToState(Me, "Unfocused", useTransitions)

    End If
End Sub

Jeśli przekażesz nazwę stanu do GoToState, gdy kontrolka jest już w tym stanie, GoToState nic nie robi, więc nie musisz sprawdzać bieżącego stanu kontrolki. Jeśli na przykład Value zmieni się z jednej liczby ujemnej na inną liczbę ujemną, scenorys stanu Negative nie zostanie przerwany, a użytkownik nie zobaczy zmiany w kontrolce.

VisualStateManager używa obiektów VisualStateGroup do określenia, z którego stanu wyjść podczas wywoływania GoToState. Kontrola jest zawsze w jednym stanie dla każdego VisualStateGroup zdefiniowanego w jego ControlTemplate i opuszcza stan tylko wtedy, gdy przechodzi do innego stanu z tego samego VisualStateGroup. Na przykład ControlTemplate kontrolki NumericUpDown definiuje obiekty Positive i NegativeVisualState w jednym VisualStateGroup oraz obiekty Focused i UnfocusedVisualState w innym. (W sekcji Complete Example w tym temacie można zobaczyć Focused i UnfocusedVisualState zdefiniowane, gdy sterowanie przechodzi ze stanu Positive do stanu Negative, lub na odwrót, sterowanie pozostaje w stanie Focused lub Unfocused.

Istnieją trzy typowe miejsca, w których stan kontrolki może ulec zmianie:

W poniższych przykładach pokazano aktualizowanie stanu kontrolki NumericUpDown w tych przypadkach.

Należy zaktualizować stan kontrolki w metodzie OnApplyTemplate, tak aby kontrolka pojawiała się w prawidłowym stanie po zastosowaniu ControlTemplate. Poniższy przykład wywołuje UpdateStates w OnApplyTemplate, aby upewnić się, że element sterujący znajduje się w odpowiednich stanach. Załóżmy na przykład, że tworzysz kontrolkę NumericUpDown, a następnie ustawiasz jej Foreground na zielony i Value na -5. Jeśli nie wywołasz UpdateStates po zastosowaniu ControlTemplate do kontrolki NumericUpDown, kontrolka nie znajduje się w stanie Negative, a wartość jest zielona, a nie czerwona. Należy wywołać UpdateStates, aby umieścić kontrolkę w stanie Negative.

public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    //TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()

    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

    UpdateStates(False)
End Sub

Często trzeba zaktualizować stany kontrolki po zmianie właściwości. W poniższym przykładzie przedstawiono całą metodę ValueChangedCallback. Ponieważ ValueChangedCallback jest wywoływana w przypadku zmiany Value, metoda wywołuje UpdateStates w przypadku, gdy Value zmienił się z dodatniego na ujemny lub odwrotnie. Dopuszczalne jest wywołanie UpdateStates, gdy Value zmienia się, ale pozostaje dodatnie lub ujemne, ponieważ w takim przypadku kontrola nie zmieni stanu.

private static void ValueChangedCallback(DependencyObject obj,
    DependencyPropertyChangedEventArgs args)
{
    NumericUpDown ctl = (NumericUpDown)obj;
    int newValue = (int)args.NewValue;

    // Call UpdateStates because the Value might have caused the
    // control to change ValueStates.
    ctl.UpdateStates(true);

    // Call OnValueChanged to raise the ValueChanged event.
    ctl.OnValueChanged(
        new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
            newValue));
}
Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
                                        ByVal args As DependencyPropertyChangedEventArgs)

    Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
    Dim newValue As Integer = CInt(args.NewValue)

    ' Call UpdateStates because the Value might have caused the
    ' control to change ValueStates.
    ctl.UpdateStates(True)

    ' Call OnValueChanged to raise the ValueChanged event.
    ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
End Sub

Może być również konieczne zaktualizowanie stanów po wystąpieniu zdarzenia. W poniższym przykładzie pokazano, że NumericUpDown wywołuje UpdateStates w Control w celu obsługi zdarzenia GotFocus.

protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);
    UpdateStates(true);
}
Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
    MyBase.OnGotFocus(e)
    UpdateStates(True)
End Sub

VisualStateManager ułatwia zarządzanie stanami elementu sterującego. Korzystając z VisualStateManager, upewnij się, że kontrolka prawidłowo przechodzi między stanami. Jeśli zastosujesz się do zaleceń opisanych w tej sekcji na potrzeby pracy z VisualStateManager, kod kontrolki pozostanie czytelny i konserwowany.

Zapewnianie kontraktu kontroli

Należy dostarczyć dokument kontrolny, aby autorzy ControlTemplate wiedzieli, co umieścić w szablonie. Kontrakt kontrolny ma trzy elementy:

  • Elementy wizualne używane przez logikę kontrolki.

  • Stany kontrolki i grupy, do których należy każdy stan.

  • Właściwości publiczne, które wizualnie wpływają na element sterujący.

Ktoś, kto tworzy nowy ControlTemplate, musi wiedzieć, jakie obiekty są używane przez logikę kontrolki FrameworkElement, jakiego są typu oraz jakie mają nazwy. Autor ControlTemplate musi również znać nazwę każdego możliwego stanu, w jakim może znajdować się kontrolka, oraz w jakim VisualStateGroup stanie się znajduje.

W przykładzie NumericUpDown kontrolka oczekuje, że ControlTemplate powinien mieć następujące obiekty FrameworkElement:

Kontrolka może być w następujących stanach:

Aby określić, jakich obiektów FrameworkElement oczekuje kontrolka, należy użyć TemplatePartAttribute, która określa nazwę i typ oczekiwanych elementów. Aby określić możliwe stany kontrolki, należy użyć TemplateVisualStateAttribute, który określa nazwę stanu i do którego należy VisualStateGroup. Umieść TemplatePartAttribute i TemplateVisualStateAttribute w definicji klasy kontrolki.

Każda właściwość publiczna, która ma wpływ na wygląd kontrolki, jest również częścią kontraktu kontroli.

W poniższym przykładzie określono obiekt i stany FrameworkElement dla kontrolki NumericUpDown.

[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
    public static readonly DependencyProperty BackgroundProperty;
    public static readonly DependencyProperty BorderBrushProperty;
    public static readonly DependencyProperty BorderThicknessProperty;
    public static readonly DependencyProperty FontFamilyProperty;
    public static readonly DependencyProperty FontSizeProperty;
    public static readonly DependencyProperty FontStretchProperty;
    public static readonly DependencyProperty FontStyleProperty;
    public static readonly DependencyProperty FontWeightProperty;
    public static readonly DependencyProperty ForegroundProperty;
    public static readonly DependencyProperty HorizontalContentAlignmentProperty;
    public static readonly DependencyProperty PaddingProperty;
    public static readonly DependencyProperty TextAlignmentProperty;
    public static readonly DependencyProperty TextDecorationsProperty;
    public static readonly DependencyProperty TextWrappingProperty;
    public static readonly DependencyProperty VerticalContentAlignmentProperty;

    public Brush Background { get; set; }
    public Brush BorderBrush { get; set; }
    public Thickness BorderThickness { get; set; }
    public FontFamily FontFamily { get; set; }
    public double FontSize { get; set; }
    public FontStretch FontStretch { get; set; }
    public FontStyle FontStyle { get; set; }
    public FontWeight FontWeight { get; set; }
    public Brush Foreground { get; set; }
    public HorizontalAlignment HorizontalContentAlignment { get; set; }
    public Thickness Padding { get; set; }
    public TextAlignment TextAlignment { get; set; }
    public TextDecorationCollection TextDecorations { get; set; }
    public TextWrapping TextWrapping { get; set; }
    public VerticalAlignment VerticalContentAlignment { get; set; }
}
<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))>
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))>
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")>
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")>
Public Class NumericUpDown
    Inherits Control
    Public Shared ReadOnly TextAlignmentProperty As DependencyProperty
    Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
    Public Shared ReadOnly TextWrappingProperty As DependencyProperty

    Public Property TextAlignment() As TextAlignment

    Public Property TextDecorations() As TextDecorationCollection

    Public Property TextWrapping() As TextWrapping
End Class

Kompletny przykład

Poniższy przykład to cała ControlTemplate kontrolki NumericUpDown.

<!--This is the contents of the themes/generic.xaml file.-->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:VSMCustomControl">


  <Style TargetType="{x:Type local:NumericUpDown}">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="local:NumericUpDown">
          <Grid  Margin="3" 
                Background="{TemplateBinding Background}">


            <VisualStateManager.VisualStateGroups>

              <VisualStateGroup Name="ValueStates">

                <!--Make the Value property red when it is negative.-->
                <VisualState Name="Negative">
                  <Storyboard>
                    <ColorAnimation To="Red"
                      Storyboard.TargetName="TextBlock" 
                      Storyboard.TargetProperty="(Foreground).(Color)"/>
                  </Storyboard>

                </VisualState>

                <!--Return the control to its initial state by
                    return the TextBlock's Foreground to its 
                    original color.-->
                <VisualState Name="Positive"/>
              </VisualStateGroup>

              <VisualStateGroup Name="FocusStates">

                <!--Add a focus rectangle to highlight the entire control
                    when it has focus.-->
                <VisualState Name="Focused">
                  <Storyboard>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisual" 
                                                   Storyboard.TargetProperty="Visibility" Duration="0">
                      <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                          <Visibility>Visible</Visibility>
                        </DiscreteObjectKeyFrame.Value>
                      </DiscreteObjectKeyFrame>
                    </ObjectAnimationUsingKeyFrames>
                  </Storyboard>
                </VisualState>

                <!--Return the control to its initial state by
                    hiding the focus rectangle.-->
                <VisualState Name="Unfocused"/>
              </VisualStateGroup>

            </VisualStateManager.VisualStateGroups>

            <Grid>
              <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
              </Grid.RowDefinitions>
              <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
              </Grid.ColumnDefinitions>

              <Border BorderThickness="1" BorderBrush="Gray" 
                Margin="7,2,2,2" Grid.RowSpan="2" 
                Background="#E0FFFFFF"
                VerticalAlignment="Center" 
                HorizontalAlignment="Stretch">
                <!--Bind the TextBlock to the Value property-->
                <TextBlock Name="TextBlock"
                  Width="60" TextAlignment="Right" Padding="5"
                  Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                                 AncestorType={x:Type local:NumericUpDown}}, 
                                 Path=Value}"/>
              </Border>

              <RepeatButton Content="Up" Margin="2,5,5,0"
                Name="UpButton"
                Grid.Column="1" Grid.Row="0"/>
              <RepeatButton Content="Down" Margin="2,0,5,5"
                Name="DownButton"
                Grid.Column="1" Grid.Row="1"/>

              <Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2" 
                Stroke="Black" StrokeThickness="1"  
                Visibility="Collapsed"/>
            </Grid>

          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

W poniższym przykładzie przedstawiono logikę NumericUpDown.

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;

namespace VSMCustomControl
{
    [TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
    [TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
    [TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
    [TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
    [TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
    [TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
    public class NumericUpDown : Control
    {
        public NumericUpDown()
        {
            DefaultStyleKey = typeof(NumericUpDown);
            this.IsTabStop = true;
        }

        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(
                "Value", typeof(int), typeof(NumericUpDown),
                new PropertyMetadata(
                    new PropertyChangedCallback(ValueChangedCallback)));

        public int Value
        {
            get
            {
                return (int)GetValue(ValueProperty);
            }

            set
            {
                SetValue(ValueProperty, value);
            }
        }

        private static void ValueChangedCallback(DependencyObject obj,
            DependencyPropertyChangedEventArgs args)
        {
            NumericUpDown ctl = (NumericUpDown)obj;
            int newValue = (int)args.NewValue;

            // Call UpdateStates because the Value might have caused the
            // control to change ValueStates.
            ctl.UpdateStates(true);

            // Call OnValueChanged to raise the ValueChanged event.
            ctl.OnValueChanged(
                new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
                    newValue));
        }

        public static readonly RoutedEvent ValueChangedEvent =
            EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
                          typeof(ValueChangedEventHandler), typeof(NumericUpDown));

        public event ValueChangedEventHandler ValueChanged
        {
            add { AddHandler(ValueChangedEvent, value); }
            remove { RemoveHandler(ValueChangedEvent, value); }
        }

        protected virtual void OnValueChanged(ValueChangedEventArgs e)
        {
            // Raise the ValueChanged event so applications can be alerted
            // when Value changes.
            RaiseEvent(e);
        }

        private void UpdateStates(bool useTransitions)
        {
            if (Value >= 0)
            {
                VisualStateManager.GoToState(this, "Positive", useTransitions);
            }
            else
            {
                VisualStateManager.GoToState(this, "Negative", useTransitions);
            }

            if (IsFocused)
            {
                VisualStateManager.GoToState(this, "Focused", useTransitions);
            }
            else
            {
                VisualStateManager.GoToState(this, "Unfocused", useTransitions);
            }
        }

        public override void OnApplyTemplate()
        {
            UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
            DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
            //TextElement = GetTemplateChild("TextBlock") as TextBlock;

            UpdateStates(false);
        }

        private RepeatButton downButtonElement;

        private RepeatButton DownButtonElement
        {
            get
            {
                return downButtonElement;
            }

            set
            {
                if (downButtonElement != null)
                {
                    downButtonElement.Click -=
                        new RoutedEventHandler(downButtonElement_Click);
                }
                downButtonElement = value;

                if (downButtonElement != null)
                {
                    downButtonElement.Click +=
                        new RoutedEventHandler(downButtonElement_Click);
                }
            }
        }

        void downButtonElement_Click(object sender, RoutedEventArgs e)
        {
            Value--;
        }

        private RepeatButton upButtonElement;

        private RepeatButton UpButtonElement
        {
            get
            {
                return upButtonElement;
            }

            set
            {
                if (upButtonElement != null)
                {
                    upButtonElement.Click -=
                        new RoutedEventHandler(upButtonElement_Click);
                }
                upButtonElement = value;

                if (upButtonElement != null)
                {
                    upButtonElement.Click +=
                        new RoutedEventHandler(upButtonElement_Click);
                }
            }
        }

        void upButtonElement_Click(object sender, RoutedEventArgs e)
        {
            Value++;
        }

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);
            Focus();
        }


        protected override void OnGotFocus(RoutedEventArgs e)
        {
            base.OnGotFocus(e);
            UpdateStates(true);
        }

        protected override void OnLostFocus(RoutedEventArgs e)
        {
            base.OnLostFocus(e);
            UpdateStates(true);
        }
    }

    public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs e);

    public class ValueChangedEventArgs : RoutedEventArgs
    {
        private int _value;

        public ValueChangedEventArgs(RoutedEvent id, int num)
        {
            _value = num;
            RoutedEvent = id;
        }

        public int Value
        {
            get { return _value; }
        }
    }
}
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Controls.Primitives
Imports System.Windows.Input
Imports System.Windows.Media

<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))> _
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))> _
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")> _
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")> _
Public Class NumericUpDown
    Inherits Control

    Public Sub New()
        DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
        Me.IsTabStop = True
    End Sub

    Public Shared ReadOnly ValueProperty As DependencyProperty =
        DependencyProperty.Register("Value", GetType(Integer), GetType(NumericUpDown),
                          New PropertyMetadata(New PropertyChangedCallback(AddressOf ValueChangedCallback)))

    Public Property Value() As Integer

        Get
            Return CInt(GetValue(ValueProperty))
        End Get

        Set(ByVal value As Integer)

            SetValue(ValueProperty, value)
        End Set
    End Property

    Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
                                            ByVal args As DependencyPropertyChangedEventArgs)

        Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
        Dim newValue As Integer = CInt(args.NewValue)

        ' Call UpdateStates because the Value might have caused the
        ' control to change ValueStates.
        ctl.UpdateStates(True)

        ' Call OnValueChanged to raise the ValueChanged event.
        ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
    End Sub

    Public Shared ReadOnly ValueChangedEvent As RoutedEvent =
        EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
                                         GetType(ValueChangedEventHandler), GetType(NumericUpDown))

    Public Custom Event ValueChanged As ValueChangedEventHandler

        AddHandler(ByVal value As ValueChangedEventHandler)
            Me.AddHandler(ValueChangedEvent, value)
        End AddHandler

        RemoveHandler(ByVal value As ValueChangedEventHandler)
            Me.RemoveHandler(ValueChangedEvent, value)
        End RemoveHandler

        RaiseEvent(ByVal sender As Object, ByVal e As RoutedEventArgs)
            Me.RaiseEvent(e)
        End RaiseEvent

    End Event


    Protected Overridable Sub OnValueChanged(ByVal e As ValueChangedEventArgs)
        ' Raise the ValueChanged event so applications can be alerted
        ' when Value changes.
        MyBase.RaiseEvent(e)
    End Sub


#Region "NUDCode"
    Private Sub UpdateStates(ByVal useTransitions As Boolean)

        If Value >= 0 Then
            VisualStateManager.GoToState(Me, "Positive", useTransitions)
        Else
            VisualStateManager.GoToState(Me, "Negative", useTransitions)
        End If

        If IsFocused Then
            VisualStateManager.GoToState(Me, "Focused", useTransitions)
        Else
            VisualStateManager.GoToState(Me, "Unfocused", useTransitions)

        End If
    End Sub

    Public Overloads Overrides Sub OnApplyTemplate()

        UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
        DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

        UpdateStates(False)
    End Sub

    Private m_downButtonElement As RepeatButton

    Private Property DownButtonElement() As RepeatButton
        Get
            Return m_downButtonElement
        End Get

        Set(ByVal value As RepeatButton)

            If m_downButtonElement IsNot Nothing Then
                RemoveHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
            End If
            m_downButtonElement = value

            If m_downButtonElement IsNot Nothing Then
                AddHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
            End If
        End Set
    End Property

    Private Sub downButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Value -= 1
    End Sub

    Private m_upButtonElement As RepeatButton

    Private Property UpButtonElement() As RepeatButton
        Get
            Return m_upButtonElement
        End Get

        Set(ByVal value As RepeatButton)
            If m_upButtonElement IsNot Nothing Then
                RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
            End If
            m_upButtonElement = value

            If m_upButtonElement IsNot Nothing Then
                AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
            End If
        End Set
    End Property

    Private Sub upButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Value += 1
    End Sub

    Protected Overloads Overrides Sub OnMouseLeftButtonDown(ByVal e As MouseButtonEventArgs)
        MyBase.OnMouseLeftButtonDown(e)
        Focus()
    End Sub


    Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
        MyBase.OnGotFocus(e)
        UpdateStates(True)
    End Sub

    Protected Overloads Overrides Sub OnLostFocus(ByVal e As RoutedEventArgs)
        MyBase.OnLostFocus(e)
        UpdateStates(True)
    End Sub
#End Region
End Class


Public Delegate Sub ValueChangedEventHandler(ByVal sender As Object,
                                             ByVal e As ValueChangedEventArgs)

Public Class ValueChangedEventArgs
    Inherits RoutedEventArgs

    Public Sub New(ByVal id As RoutedEvent,
                   ByVal num As Integer)

        Value = num
        RoutedEvent = id
    End Sub

    Public ReadOnly Property Value() As Integer
End Class

Zobacz też