Udostępnij za pośrednictwem


Tworzenie formantu, którego wygląd można dostosować

Program Windows Presentation Foundation (WPF) umożliwia utworzenie kontrolki, której wygląd można dostosować. Można na przykład zmienić wygląd elementu CheckBox poza właściwościami ustawienia, tworząc nowy ControlTemplateelement . Poniższa ilustracja przedstawia element CheckBox , który używa wartości domyślnej ControlTemplate i , CheckBox która używa niestandardowego ControlTemplateelementu .

A checkbox with the default control template. Pole wyboru używające domyślnego szablonu kontrolki

A checkbox with a custom control template. 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. Projektant narzędzi, takich jak Blend for Visual Studio, obsługują model części i stanów, więc w przypadku korzystania z tego modelu kontrolka będzie można dostosowywać w tych typach aplikacji. W tym temacie omówiono części i stany modelu oraz sposób ich wykonywania podczas tworzenia własnej kontrolki. W tym temacie użyto przykładu kontrolki niestandardowej , NumericUpDownaby 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.

NumericUpDown custom control. Niestandardowa kontrolka NumericUpDown

Ten temat zawiera następujące sekcje:

Wymagania wstępne

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

Uwaga

Aby utworzyć kontrolkę, która może mieć dostosowany wygląd, należy utworzyć kontrolkę dziedziczą z Control klasy lub jednej z jej podklas innych niż UserControl. Kontrolka dziedziczona po UserControl kontrolce jest kontrolką, którą można szybko utworzyć, ale nie używa ControlTemplate elementu 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ę wizualizacji i zachowanie wizualne w ControlTemplate kontrolce.

  • 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 obiekcie ControlTemplate.

Podczas definiowania struktury wizualizacji i zachowania wizualnego w ControlTemplate kontrolce autorzy aplikacji mogą zmieniać 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 FrameworkElement obiekty i stany powinny być zdefiniowane w obiekcie ControlTemplate. Należy postępować zgodnie z niektórymi najlepszymi rozwiązaniami w przypadku interakcji z częściami w ControlTemplate programie , tak aby kontrolka prawidłowo obsługiwała niekompletny ControlTemplateelement . Jeśli zastosujesz się do tych trzech zasad, autorzy aplikacji będą mogli utworzyć dla ControlTemplate Twojej kontroli tak samo łatwo, jak w przypadku kontrolek, które wysyłają z WPF. W poniższej sekcji opisano szczegółowo każdą z tych zaleceń.

Definiowanie struktury wizualizacji i wizualnego zachowania kontrolki w kontrolce ControlTemplate

Podczas tworzenia kontrolki niestandardowej przy użyciu modelu części i stanów należy zdefiniować strukturę wizualizacji i zachowanie wizualne kontrolki w jej ControlTemplate zamiast w logice. Struktura wizualna kontrolki to złożone FrameworkElement obiekty tworzące kontrolkę. Zachowanie wizualizacji to sposób, w jaki kontrolka jest wyświetlana, gdy znajduje się w określonym stanie. Aby uzyskać więcej informacji na temat tworzenia obiektu ControlTemplate określającego strukturę wizualizacji i zachowanie wizualne kontrolki, zobacz Tworzenie szablonu dla kontrolki.

W przykładzie kontrolki NumericUpDown struktura wizualizacji zawiera dwie RepeatButton kontrolki i .TextBlock W przypadku dodania tych kontrolek w kodzie kontrolki NumericUpDown — na przykład jego konstruktora — pozycja tych kontrolek byłaby nie do pomyślenia. Zamiast definiować strukturę wizualizacji i zachowanie wizualne kontrolki w kodzie, należy zdefiniować ją w pliku ControlTemplate. Następnie deweloper aplikacji dostosuje położenie przycisków i TextBlock określ, jakie zachowanie występuje, gdy Value jest ujemne, ponieważ ControlTemplate można je zamienić.

W poniższym przykładzie pokazano strukturę wizualizacji kontrolki NumericUpDown , która zawiera element RepeatButton , który ma zwiększyć Valuewartość , a RepeatButton do zmniejszenia Valuewartości i , TextBlock aby wyświetlić Valueelement .

<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>

Wizualne zachowanie kontrolki NumericUpDown polega na tym, że wartość znajduje się w czerwonej czcionki, jeśli jest ujemna. Jeśli zmienisz ForegroundTextBlock wartość w kodzie, gdy Value wartość jest ujemna, NumericUpDown zawsze będzie wyświetlana czerwona wartość ujemna. Zachowanie wizualne kontrolki w ControlTemplate obiekcie określa się przez dodanie VisualState obiektów do obiektu ControlTemplate. W poniższym przykładzie przedstawiono VisualState obiekty dla Positive stanów i Negative . Positive i Negative wzajemnie się wykluczają (kontrolka jest zawsze w dokładnie jednej z dwóch), więc przykład umieszcza VisualState obiekty w jednym VisualStateGroupobiekcie . Gdy kontrolka przejdzie w Negative stan, kontrolka ForegroundTextBlock zmieni kolor na czerwony. Gdy kontrolka jest w Positive stanie, Foreground funkcja zwraca wartość oryginalną. Definiowanie VisualState obiektów w obiekcie ControlTemplate zostało dokładniej omówione w temacie Tworzenie szablonu dla kontrolki.

Uwaga

Pamiętaj, aby ustawić dołączoną VisualStateManager.VisualStateGroups właściwość w katalogu głównym FrameworkElement elementu 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 kontrolkiTemplate 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 kontrolka powinna być odporna na ControlTemplate brakujące FrameworkElement obiekty lub VisualState . Kontrolka nie powinna zgłaszać wyjątku ani zgłaszać błędu, jeśli FrameworkElementw obiekcie ControlTemplatebrakuje elementu , VisualStatelub VisualStateGroup . W tej sekcji opisano zalecane rozwiązania dotyczące interakcji z obiektami FrameworkElement i zarządzania stanami.

Przewidywanie brakujących obiektów FrameworkElement

Podczas definiowania FrameworkElement obiektów w ControlTemplateobiekcie logika kontrolki może wymagać interakcji z niektórymi z nich. Na przykład kontrolka NumericUpDown subskrybuje zdarzenie przycisków Click w celu zwiększenia lub zmniejszenia Value i ustawia Text właściwość na TextBlockValuewartość . Jeśli niestandardowy ControlTemplateTextBlock pomija przyciski lub, dopuszczalne jest, aby kontrolka utraciła część jej funkcjonalności, ale upewnij się, że kontrolka nie powoduje błędu. Na przykład jeśli kontrolka ControlTemplate nie zawiera przycisków do zmiany Value, NumericUpDown funkcja utraci te funkcje, ale aplikacja korzystająca z tej ControlTemplate funkcji będzie nadal działać.

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

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

  2. Zdefiniuj właściwości prywatne dla każdego FrameworkElement , z którym chcesz korzystać.

  3. Subskrybuj i anuluj subskrypcję wszystkich zdarzeń obsługiwanych przez kontrolkę w FrameworkElement metodzie dostępu zestawu właściwości.

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

  5. Sprawdź, czy element FrameworkElement nie null znajduje się przed uzyskaniem dostępu do jego członków. Jeśli jest nullto , 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 ControlTemplateobiekcie , RepeatButton który zwiększa Value , ma jego x:Name atrybut ustawiony na UpButtonwartość . Poniższy przykład deklaruje właściwość o nazwie UpButtonElement , która reprezentuje RepeatButton zadeklarowany w obiekcie ControlTemplate. Akcesorium set najpierw anuluje subskrypcję zdarzenia przycisku Click , jeśli UpDownElement nie nulljest , a następnie ustawia właściwość, a następnie subskrybuje Click zdarzenie. Istnieje również zdefiniowana właściwość, ale nie jest wyświetlana w tym miejscu dla innego RepeatButtonelementu o 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 pokazano OnApplyTemplate dla kontrolki NumericUpDown . W przykładzie użyto GetTemplateChild metody w celu pobrania FrameworkElement obiektów z obiektu ControlTemplate. Zwróć uwagę, że przykład chroni przed przypadkami, w których GetTemplateChild znajduje element FrameworkElement o określonej nazwie, który nie jest oczekiwanym typem. Najlepszym rozwiązaniem jest również ignorowanie elementów, które mają określony x:Name typ, ale są nieprawidłowe.

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 ControlTemplate brakuje FrameworkElementelementu .

Zarządzanie stanami za pomocą programu VisualStateManager

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

Poniższy przykład powtarza poprzedni przykład, który pokazuje VisualState obiekty, które odpowiadają Positive stanom i Negative kontrolki. ForegroundNegativeVisualState Na Storyboard kolei czerwonyTextBlock. Gdy kontrolka NumericUpDown jest w Negative stanie, rozpoczyna się scenorys w Negative stanie . Następnie wartość Storyboard w Negative stanie zatrzymuje się, gdy kontrolka powraca do Positive stanu. Element PositiveVisualState nie musi zawierać elementu Storyboard , ponieważ gdy Storyboard dla zatrzymania Negative , Foreground element powraca 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 element ma nazwę, ale TextBlock nie znajduje się w kontrakcie kontrolnym, NumericUpDown ponieważ logika kontrolki nigdy nie odwołuje się do TextBlockelementu . Elementy, do których odwołuje się ControlTemplate nazwa, ale nie muszą być częścią kontraktu sterowania, ponieważ nowy ControlTemplate element kontrolki może nie wymagać odwołowania się do tego elementu. Na przykład osoba tworząca nowy ControlTemplate element NumericUpDown może zdecydować się nie wskazać, że Value jest to negatywne, zmieniając element Foreground. W takim przypadku ani kod, ani ControlTemplate nazwa nie odwołuje się do nazwy TextBlock .

Logika kontrolki jest odpowiedzialna za zmianę stanu kontrolki. W poniższym przykładzie pokazano, że kontrolka NumericUpDown wywołuje metodę , aby przejść do stanu Value 0 lub większegoPositive, a Negative stan, gdy Value jest mniejszy niż GoToState 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 polecenie , aby zmienić jego stan, VisualStateManager wykonuje następujące czynności:

  • Jeśli kontrolka VisualState będzie zawierać Storyboardelement , rozpocznie się scenorys. Następnie, jeśli kontrolka VisualState pochodzi z elementu , Storyboardscenorys kończy się.

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

  • Jeśli określony stan nie istnieje w elemecie ControlTemplatecontrol, GoToState nie podejmuje żadnej akcji i zwraca wartość 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 Value właściwości do śledzenia, czy jest w Positive stanie lub Negative . Kontrolka NumericUpDown definiuje Focused również stany i UnFocused , które śledzą IsFocused właściwość . 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 metody VisualStateManager i zapewnia możliwość zarządzania kodem. W poniższym przykładzie przedstawiono metodę NumericUpDown pomocnika kontrolki . UpdateStates Gdy Value wartość jest większa lub równa 0, Control wartość jest w Positive stanie . Gdy Value wartość jest mniejsza niż 0, kontrolka Negative jest w stanie . Gdy IsFocused jest true, kontrolka Focused jest w stanie ; w przeciwnym razie jest w Unfocused stanie . Kontrolka może wywoływać UpdateStates zawsze, gdy musi zmienić swój stan, niezależnie od tego, jaki stan się zmienia.

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, nic nie robi, GoToState 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.

Obiekt VisualStateManager używa VisualStateGroup obiektów do określenia, który stan ma zakończyć się po wywołaniu metody GoToState. Kontrolka jest zawsze w jednym stanie dla każdego VisualStateGroup zdefiniowanego w nim ControlTemplate i pozostawia stan tylko wtedy, gdy przechodzi do innego stanu z tego samego VisualStateGroup. Na przykład kontrolka definiuje obiekty iVisualStateNegativew jednej VisualStateGroup i UnfocusedVisualStateFocused i w innej.PositiveNumericUpDownControlTemplate (Możesz zobaczyć Focused wartości i UnfocusedVisualState zdefiniowane w sekcji Kompletny przykład w tym temacie Gdy kontrolka przechodzi ze Positive stanu do Negative stanu lub na odwrót, kontrolka pozostaje w Focused stanie lub Unfocused .

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

  • Gdy element ControlTemplate jest stosowany do .Control

  • Gdy właściwość ulegnie zmianie.

  • Gdy wystąpi zdarzenie.

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 metodę UpdateStates , OnApplyTemplate aby upewnić się, że kontrolka znajduje się w odpowiednich stanach. Załóżmy na przykład, że tworzysz kontrolkę, a następnie ustawiasz jej Foreground wartość na zieloną NumericUpDown i Value na -5. Jeśli kontrolka nie UpdateStates jest wywoływana, gdy ControlTemplate kontrolka jest stosowana, NumericUpDown kontrolka nie znajduje się w Negative stanie, a wartość jest zielona, a nie czerwona. Należy wywołać UpdateStates polecenie , aby umieścić kontrolkę Negative w stanie .

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. Poniższy przykład przedstawia całą ValueChangedCallback metodę. Ponieważ ValueChangedCallback jest wywoływana, gdy Value zmiany, metoda wywołuje UpdateStates w przypadku Value zmiany z dodatniego na ujemny lub odwrotnie. Dopuszczalne jest wywołanie UpdateStates , gdy Value zmiany pozostaną pozytywne lub negatywne, ponieważ w takim przypadku kontrolka nie zmieni stanów.

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łania UpdateStates elementu w Control celu obsługi GotFocus zdarzenia.

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

Ułatwia VisualStateManager zarządzanie stanami kontrolki. Korzystając z elementu 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 VisualStateManagerkodem , kod kontrolki pozostanie czytelny i możliwy do utrzymania.

Zapewnianie kontraktu kontroli

Należy podać kontrakt kontrolny, ControlTemplate aby autorzy wiedzieli, co należy 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 kontrolkę.

Ktoś, kto tworzy nowy ControlTemplate , musi wiedzieć, jakich FrameworkElement obiektów używa logika kontrolki, jakiego typu jest każdy obiekt i jaka jest jego nazwa. Autor ControlTemplate musi również znać nazwę każdego możliwego stanu, w którym może znajdować się kontrolka i w którym VisualStateGroup znajduje się stan.

Wracając do przykładu NumericUpDown , kontrolka oczekuje ControlTemplate następujących FrameworkElement obiektów:

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

Aby określić, jakich FrameworkElement obiektów oczekuje kontrolka, należy użyć TemplatePartAttributeelementu , który określa nazwę i typ oczekiwanych elementów. Aby określić możliwe stany kontrolki, należy użyć TemplateVisualStateAttributeelementu , który określa nazwę stanu i do którego VisualStateGroup należy. Umieść kontrolkę 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 FrameworkElement obiekt i stany 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 dotyczy całej 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ę dla elementu 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ż