Tworzenie formantu, którego wygląd można dostosować
Windows Presentation Foundation (WPF) umożliwia utworzenie kontrolki, której wygląd można dostosować. Można na przykład zmienić wygląd CheckBox właściwości poza właściwościami ustawienia, tworząc nowy ControlTemplateelement . Na poniższej ilustracji przedstawiono element CheckBox , który używa wartości domyślnej ControlTemplate i CheckBox , która używa niestandardowego ControlTemplateelementu .
Pole wyboru używające domyślnego szablonu 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, więc po obserwowaniu tego modelu kontrolka będzie dostosowywalna w tych typach aplikacji. W tym temacie omówiono model części i stanów oraz sposób ich śledzenia podczas tworzenia własnej kontrolki. W tym temacie użyto przykładu niestandardowej kontrolki , 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. Na poniższej ilustracji przedstawiono kontrolkę NumericUpDown , która została omówiona w tym temacie.
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, zapoznasz się z elementami kontraktu sterowania i rozumiesz 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 jest kontrolką, którą można szybko utworzyć, ale nie używa elementu 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, wykonaj 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 elemecie 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 najlepszymi rozwiązaniami podczas interakcji z częściami w ControlTemplate taki sposób, aby kontrolka prawidłowo obsłużył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 są dostarczane z WPF. W poniższej sekcji szczegółowo opisano każde z tych zaleceń.
Definiowanie struktury wizualnej i wizualnego zachowania kontrolki w elemecie ControlTemplate
Podczas tworzenia kontrolki niestandardowej przy użyciu modelu części i stanów należy zdefiniować strukturę wizualizacji i zachowanie ControlTemplate wizualizacji kontrolki zamiast jej logiki. Struktura wizualna kontrolki to złożone FrameworkElement obiekty tworzące kontrolkę. Zachowanie wizualizacji jest sposobem wyświetlania kontrolki w określonym stanie. Aby uzyskać więcej informacji na temat tworzenia ControlTemplate kontrolki, która określa 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 jej 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 elemecie ControlTemplate. Następnie deweloper aplikacji dostosuje położenie przycisków i TextBlock określ, jakie zachowanie ma miejsce, gdy Value jest ujemne, ponieważ ControlTemplate można je zamienić.
W poniższym przykładzie przedstawiono strukturę wizualizacji kontrolki NumericUpDown , która zawiera RepeatButton element , który ma zwiększyć Valuewartość , RepeatButton a do zmniejszenia Valuewartości , i parametr do TextBlock wyświetlenia 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 czcionki, jeśli jest ujemna. Jeśli zmienisz ForegroundTextBlock wartość w kodzie, gdy Value jest ujemna, NumericUpDown zawsze będzie wyświetlana czerwona wartość ujemna. Zachowanie wizualne kontrolki w elemecie ControlTemplate 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 wykluczają się (kontrolka jest zawsze w dokładnie jednej z dwóch), więc przykład umieszcza VisualState obiekty w jednym VisualStateGroupobiekcie . Gdy kontrolka przechodzi do Negative stanu, Foreground kontrolka TextBlock zmieni się na czerwono. Gdy kontrolka jest w Positive stanie, funkcja Foreground zwraca wartość oryginalną. Bardziej szczegółowo omówiono definiowanie VisualState obiektów w elemecie ControlTemplateCreate a template for a control (Tworzenie szablonu dla kontrolki).
Uwaga
Pamiętaj, aby ustawić dołączoną VisualStateManager.VisualStateGroups właściwość w katalogu głównym ControlTemplateFrameworkElement elementu .
<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 ControlTemplate na brakujące FrameworkElement obiekty lub VisualState . Kontrolka nie powinna zgłaszać wyjątku ani zgłaszać błędu, jeśli FrameworkElementw obiekcie ControlTemplatebrakuje wartości , 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 FrameworkElement obiektów w ControlTemplatelogice kontrolki może być konieczne interakcja z niektórymi z nich. Na przykład kontrolka NumericUpDown subskrybuje zdarzenie przycisków Click , aby zwiększyć lub zmniejszyć Value i ustawi Text właściwość na TextBlockValuewartość . Jeśli niestandardowe ControlTemplateTextBlock pomija przyciski lub, dopuszczalne jest, że kontrolka traci niektóre z jego funkcji, ale należy upewnić się, że kontrolka nie powoduje błędu. Jeśli na przykład element ControlTemplate nie zawiera przycisków do zmiany Value, NumericUpDown utraci te funkcje, ale aplikacja korzystająca z tej ControlTemplate funkcji będzie nadal działać.
Następujące rozwiązania zapewnią prawidłowe reagowanie kontrolki na brakujące FrameworkElement obiekty:
x:NameUstaw atrybut dla każdego, do którego FrameworkElement należy odwołać się w kodzie.Zdefiniuj właściwości prywatne dla każdego, z którym FrameworkElement musisz korzystać.
Subskrybuj i anuluj subskrypcję wszystkich zdarzeń obsługiwanych przez kontrolkę w FrameworkElement zestawie dostępu właściwości.
FrameworkElement Ustaw właściwości zdefiniowane w kroku 2 w metodzie OnApplyTemplate . Jest to najwcześniejsze, że element FrameworkElement w elemecie ControlTemplate jest dostępny dla kontrolki.
x:NameUżyj elementu , FrameworkElement aby pobrać go z obiektu ControlTemplate.Sprawdź, czy element FrameworkElement nie
nullznajduje się przed uzyskaniem dostępu do jego członków. Jeśli jestnullto , nie zgłaszaj błędu.
W poniższych przykładach pokazano, jak kontrolka NumericUpDown wchodzi w interakcje z obiektami FrameworkElement zgodnie z zaleceniami na powyższej liście.
W przykładzie definiującym strukturę wizualizacji kontrolki NumericUpDown w elemecie ControlTemplate, RepeatButton który zwiększa Value , ma jego x:Name atrybut ustawiony na UpButton. Poniższy przykład deklaruje właściwość o nazwie UpButtonElement , która reprezentuje RepeatButton wartość zadeklarowaną w elemecie 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 tutaj wyświetlana 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
Poniższy przykład przedstawia kontrolkę OnApplyTemplateNumericUpDown . W przykładzie użyto GetTemplateChild metody , aby pobrać FrameworkElement obiekty 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 mieć do nich dostęp.
Poniższy przykład powtarza poprzedni przykład pokazujący VisualState obiekty odpowiadające Positive stanom i Negative kontrolki. NegativeVisualState Z Storyboard kolei ForegroundTextBlock kolor czerwony. Gdy kontrolka NumericUpDown jest w Negative stanie, rozpoczyna się scenorys w Negative stanie. Storyboard Następnie wartość w Negative stanie zostanie zatrzymana, gdy kontrolka Positive powróci do stanu. Element PositiveVisualState nie musi zawierać elementu , Storyboard ponieważ po Storyboard zatrzymaniu element Foreground powraca do oryginalnego Negative 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 NumericUpDown kontrolnym, ponieważ logika kontrolki nigdy nie odwołuje się do TextBlockelementu . Elementy, do których odwołują ControlTemplate się nazwy, ale nie muszą być częścią kontraktu sterującego, ponieważ nowe ControlTemplate dla kontrolki mogą nie wymagać odwołania się do tego elementu. Na przykład ktoś, kto tworzy nowy ControlTemplate element, NumericUpDown może zdecydować się nie wskazywać, że Value jest to negatywne, zmieniając wartość Foreground. W takim przypadku ani kod, ani ControlTemplate odwołanie do nazwy TextBlock .
Logika kontrolki jest odpowiedzialna za zmianę stanu kontrolki. W poniższym przykładzie pokazano, że kontrolka NumericUpDown wywołuje metodę GoToState , aby przejść do Positive stanu, gdy Value jest 0 lub większa, a Negative stan, 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 metodę , aby zmienić jej 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 żadnych działań i zwraca wartość
true.Jeśli określony stan nie istnieje w obiekcie ControlTemplate
control, GoToState nie podejmuje żadnych działań 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 w celu przejścia 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 parametr ma truewartość , kontrolka Focused jest w stanie ; w przeciwnym razie jest w Unfocused stanie . Kontrolka może być wywoływana UpdateStates za każdym razem, 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 przekazujesz 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 i 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 ControlTemplateNumericUpDown definiuje Positive obiekty iVisualStateNegativew jednym VisualStateGroup obiekcie i Focused i UnfocusedVisualState w innym. (Można zobaczyć Focused iVisualStateUnfocusedzdefiniowane 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 UpdateStates metodę , OnApplyTemplate aby upewnić się, że kontrolka znajduje się w odpowiednich stanach. Załóżmy na przykład, że tworzysz kontrolkę NumericUpDown , a następnie ustawiasz jej Foreground kolor na zielony i Value na -5. Jeśli kontrolka nie zostanie wywołana UpdateStatesNumericUpDown, ControlTemplate kontrolka nie jest w Negative stanie , a wartość jest zielona zamiast czerwonej. Należy wywołać metodę UpdateStates , 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 metodę w przypadku Value zmiany z dodatniej na ujemną lub odwrotnie. Dopuszczalne jest wywołanie UpdateStates w przypadku Value zmian, ale pozostaje 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 w przypadku wystąpienia 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 postępujesz zgodnie z zaleceniami opisanymi w tej sekcji na potrzeby pracy z VisualStateManagerkodem kontrolki, pozostanie czytelny i możliwy do utrzymania.
Dostarczanie kontraktu kontrolnego
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:
O RepeatButton nazwie
UpButton.Wywołana RepeatButton
DownButton.
Kontrolka może być w następujących stanach:
W
ValueStatesVisualStateGroupPositiveNegative
W
FocusStatesVisualStateGroupFocusedUnfocused
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ść elementy 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