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 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, 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.
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 Value
oraz 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:
Ustaw atrybut
x:Name
dla każdego FrameworkElement, do którego należy odwołać się w kodzie.Zdefiniuj właściwości prywatne dla każdej FrameworkElement, z którymi musisz się komunikować.
Zapisz się na i wypisz się ze wszystkich zdarzeń obsługiwanych przez kontrolkę w akcesorze ustawiającym właściwości FrameworkElement.
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.Sprawdź, czy FrameworkElement nie jest
null
, zanim uzyskasz dostęp do jego członków. Jeśli jest tonull
, 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 Negative
VisualState 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 ControlTemplate
control
, GoToState nie podejmuje żadnych działań i zwracafalse
.
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 Negative
VisualState w jednym VisualStateGroup oraz obiekty Focused
i Unfocused
VisualState w innym. (W sekcji Complete Example w tym temacie można zobaczyć Focused
i Unfocused
VisualState 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:
Po zastosowaniu ControlTemplate 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
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:
RepeatButton o nazwie
UpButton
.RepeatButton o nazwie
DownButton.
Kontrolka może być w następujących stanach:
W
ValueStates
VisualStateGroupPositive
Negative
W
FocusStates
VisualStateGroupFocused
Unfocused
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ż
- Tworzenie szablonu dla kontrolki
- Kustomizacja kontrolek
.NET Desktop feedback