Sdílet prostřednictvím


Vytvoření ovládacího prvku s přizpůsobitelným vzhledem

Windows Presentation Foundation (WPF) umožňuje vytvořit ovládací prvek, jehož vzhled lze přizpůsobit. Vzhled vlastnosti nastavení můžete například změnit CheckBox tak, že vytvoříte novou ControlTemplate. Následující obrázek ukazuje CheckBox , že používá výchozí ControlTemplate hodnotu a CheckBox která používá vlastní ControlTemplate.

Zaškrtávací políčko s výchozí šablonou ovládacího prvku. Zaškrtávací políčko, které používá výchozí šablonu ovládacího prvku

Zaškrtávací políčko se šablonou vlastního ovládacího prvku Zaškrtávací políčko, které používá vlastní šablonu ovládacího prvku

Pokud při vytváření ovládacího prvku sledujete model částí a stavů, vzhled ovládacího prvku bude přizpůsobitelný. Nástroje návrháře, jako je Blend pro Visual Studio, podporují části a stavový model, takže když budete postupovat podle tohoto modelu, bude ovládací prvek přizpůsobitelný v těchto typech aplikací. Toto téma popisuje části a stavy modelu a postup jeho sledování při vytváření vlastního ovládacího prvku. Toto téma používá příklad vlastního ovládacího prvku , NumericUpDownk ilustraci filozofie tohoto modelu. Ovládací NumericUpDown prvek zobrazí číselnou hodnotu, kterou uživatel může zvýšit nebo snížit kliknutím na tlačítka ovládacího prvku. Následující obrázek znázorňuje NumericUpDown ovládací prvek, který je popsán v tomto tématu.

Vlastní ovládací prvek NumericUpDown Vlastní ovládací prvek NumericUpDown

Toto téma obsahuje následující části:

Požadavky

V tomto tématu se předpokládá, že víte, jak vytvořit nový ControlTemplate pro existující ovládací prvek, znáte prvky kontraktu ovládacího prvku a rozumíte konceptům probíraným v části Vytvoření šablony pro ovládací prvek.

Poznámka:

Chcete-li vytvořit ovládací prvek, který může mít vlastní vzhled, je nutné vytvořit ovládací prvek, který dědí z Control třídy nebo jedné z jejích podtříd, než UserControl. Ovládací prvek, který dědí z UserControl něj, je ovládací prvek, který lze rychle vytvořit, ale nepoužívá ControlTemplate a nemůžete přizpůsobit jeho vzhled.

Model částí a stavů

Části a stavy modelu určují, jak definovat vizuální strukturu a vizuální chování ovládacího prvku. Pokud chcete postupovat podle modelu částí a stavů, měli byste udělat toto:

  • Definujte vizuální strukturu a vizuální chování v ControlTemplate ovládacím prvku.

  • Při interakci logiky ovládacího prvku s částmi šablony ovládacího prvku postupujte podle určitých osvědčených postupů.

  • Zadejte řídicí kontrakt, který určuje, co má být zahrnuto v sadě ControlTemplate.

Když definujete vizuální strukturu a vizuální chování v rámci ControlTemplate ovládacího prvku, autoři aplikací mohou změnit vizuální strukturu a vizuální chování vašeho ovládacího prvku vytvořením nového ControlTemplate místo psaní kódu. Musíte zadat řídicí kontrakt, který říká autorům aplikace, které FrameworkElement objekty a stavy mají být definovány v objektu ControlTemplate. Při interakci s částmi v ovládacím ControlTemplate prvku byste měli postupovat podle některých osvědčených postupů, aby váš ovládací prvek správně zvládl neúplný ControlTemplate. Pokud budete postupovat podle těchto tří principů, autoři aplikací budou moct vytvořit ControlTemplate pro váš ovládací prvek stejně snadno jako pro ovládací prvky, které se dodávají s WPF. Následující část podrobně vysvětluje každé z těchto doporučení.

Definování vizuální struktury a vizuálního chování ovládacího prvku v ControlTemplate

Při vytváření vlastního ovládacího prvku pomocí modelu částí a stavů definujete vizuální strukturu a chování vizuálu ovládacího prvku v jeho ControlTemplate namísto logiky. Vizuální struktura ovládacího prvku je složená z FrameworkElement objektů, které tvoří ovládací prvek. Vizuální chování je způsob, jakým se ovládací prvek zobrazuje, když je v určitém stavu. Další informace o vytvoření ControlTemplate , který určuje vizuální strukturu a vizuální chování ovládacího prvku, naleznete v tématu Vytvoření šablony pro ovládací prvek.

V příkladu ovládacího prvku NumericUpDown vizuální struktura zahrnuje dva ovládací prvky RepeatButton a jeden TextBlock. Jestliže tyto ovládací prvky přidáte do kódu NumericUpDown ovládacího prvku, například v jeho konstruktoru, pozice těchto ovládacích prvků by byly neměnné. Místo definování vizuální struktury ovládacího prvku a chování vizuálu v jeho kódu byste ho měli definovat v objektu ControlTemplate. Vývojář aplikace pak přizpůsobí pozici tlačítek a TextBlock určí, jaké chování nastane, když Value je záporné, protože ControlTemplate může být nahrazen.

Následující příklad ukazuje vizuální strukturu NumericUpDown ovládacího prvku, který zahrnuje RepeatButton zvýšení Value, RepeatButton snížení Valuea TextBlock zobrazení 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>

Vizuální chování NumericUpDown ovládacího prvku je, že hodnota je v červeném písmu, pokud je záporná. Pokud změníte Foreground hodnotu TextBlock v kódu, když Value je záporná, NumericUpDown zobrazí se vždy červená záporná hodnota. Zadáte vizuální chování ovládacího prvku v ControlTemplate přidáním objektů VisualState do ControlTemplate. Následující příklad ukazuje VisualState objekty pro Positive a Negative stavy. Positive a Negative jsou vzájemně se vylučující (ovládací prvek je vždy přesně v jednom z obou), takže příklad umisťuje VisualState objekty do jediného VisualStateGroup. Když ovládací prvek přejde do stavu Negative, ForegroundTextBlock se změní na červenou. Pokud je ovládací prvek ve Positive stavu, Foreground vrátí se k původní hodnotě. Definování VisualState objektů v objektu ControlTemplate je podrobněji popsáno v tématu Vytvoření šablony pro ovládací prvek.

Poznámka:

Nezapomeňte nastavit připojenou VisualStateManager.VisualStateGroups vlastnost v kořenovém adresáři FrameworkElement objektu 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>

Použití částí controlTemplate v kódu

Autor ControlTemplate může vynechat FrameworkElement nebo VisualState objekty buď záměrně, nebo omylem, ale logika vašeho ovládacího prvku může tyto části potřebovat k tomu, aby správně fungovaly. Části a stavy modelu specifikují, že váš ovládací prvek by měl být odolný vůči chybějícímu objektu ControlTemplate nebo FrameworkElement. Ovládací prvek by neměl vyvolat výjimku nebo nahlásit chybu, pokud chybí FrameworkElement, VisualState nebo VisualStateGroup v ControlTemplate. Tato část popisuje doporučené postupy pro interakci s FrameworkElement objekty a správou stavů.

Předvídání chybějících objektů FrameworkElement

Když definujete FrameworkElement objekty v ovládacím prvku ControlTemplate, logika může potřebovat navázat interakci s některými z nich. Například ovládací prvek NumericUpDown se přihlásí k odběru události tlačítek Click a ke zvýšení nebo zmenšení Value a nastaví vlastnost Text prvku TextBlock na hodnotu Value. Pokud vlastní ControlTemplate vynechává TextBlock nebo tlačítka, je v pořádku, že ovládací prvek ztratí některé své funkce, ale měli byste se ujistit, že ovládací prvek nezpůsobí chybu. Pokud například ControlTemplate neobsahuje tlačítka, která se mají změnit Value, NumericUpDown ztratí tato funkce, ale aplikace, která používá ControlTemplate , bude nadále spuštěna.

Následující postupy zajistí, aby váš ovládací prvek správně reagoval na chybějící FrameworkElement objekty:

  1. x:Name Nastavte atribut pro každýFrameworkElement, na který potřebujete odkazovat v kódu.

  2. Definujte soukromé vlastnosti pro každou FrameworkElement , s nimiž potřebujete komunikovat.

  3. Přihlaste se k odběru a odhlaste se od všech událostí, které váš ovládací prvek zpracovává v přístupovém objektu FrameworkElement sady vlastnosti.

  4. FrameworkElement Nastavte vlastnosti, které jste definovali v kroku 2 v OnApplyTemplate metodě. Toto je nejdříve, kdy je FrameworkElement v ControlTemplate k dispozici ovládacímu prvku. x:Name Použijte k FrameworkElement získání z objektu ControlTemplate.

  5. Zkontrolujte, jestli FrameworkElement není null před přístupem ke svým členům. Pokud ano null, neoznamujte chybu.

Následující příklady ukazují, jak NumericUpDown ovládací prvek komunikuje s objekty v souladu s FrameworkElement doporučeními v předchozím seznamu.

V příkladu, který definuje vizuální strukturu NumericUpDown ovládacího prvku v ControlTemplate, RepeatButton který zvyšuje Value má jeho x:Name atribut nastaven na UpButton. Následující příklad deklaruje vlastnost, UpButtonElement která představuje RepeatButton , který je deklarován v ControlTemplate. Přístup set nejprve odhlásí událost tlačítka Click , pokud UpDownElement není null, pak nastaví vlastnost a pak se přihlásí k odběru Click události. Je také definována vlastnost pro druhý RepeatButton, nazvána DownButtonElement, ale zde není zobrazena.

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

Následující příklad ukazuje OnApplyTemplate pro ovládací prvek NumericUpDown. Příklad používá metodu GetTemplateChild k získání FrameworkElement objektů z objektu ControlTemplate. Všimněte si, že příklad chrání proti případům, kdy GetTemplateChild najde zadaný FrameworkElement název, který není očekávaného typu. Osvědčeným postupem je také ignorovat prvky, které mají zadaný x:Name typ, ale jsou nesprávného typu.

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

Podle postupů, které jsou uvedeny v předchozích příkladech, zajistíte, že se ovládací prvek bude dál spouštět, když ControlTemplate chybí FrameworkElement.

Použití VisualStateManageru ke správě stavů

Udržuje VisualStateManager přehled o stavech ovládacího prvku a provádí logiku potřebnou k přechodu mezi stavy. Když přidáte VisualState objekty do objektu ControlTemplate, přidáte je do VisualStateGroup a přidáte VisualStateGroup do VisualStateManager.VisualStateGroups připojené vlastnosti, aby VisualStateManager k nim byl přístup.

Následující příklad opakuje předchozí příklad, který ukazuje VisualState objekty, které odpovídají Positive a Negative stavům ovládacího prvku. Storyboard v NegativeVisualState zbarvuje ForegroundTextBlock červeně. NumericUpDown Když je ovládací prvek ve Negative stavu, začne scénář ve Negative stavu. Pak se Storyboard ve stavu Negative zastaví, když se řízení vrátí do Positive stavu. Nemusí PositiveVisualState obsahovat Storyboard , protože když Storyboard se Negative zarážky zastaví, Foreground vrátí se k původní barvě.

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

Všimněte si, že TextBlock je uveden název, ale TextBlock není ve smlouvě řízení pro NumericUpDown, protože logika ovládacího prvku nikdy neodkazuje na TextBlock. Prvky, na které se odkazuje v ControlTemplate, mají názvy, ale nemusí být součástí kontraktu ovládacího prvku, protože nový ControlTemplate pro ovládací prvek možná nebude potřebovat na tento prvek odkazovat. Například někdo, kdo vytvoří novou ControlTemplate pro, se může rozhodnout, že NumericUpDown není záporná změnou ValueForeground . V takovém případě neodkazuje ani kód, ani ControlTemplate na TextBlock jménem.

Logika ovládacího prvku zodpovídá za změnu stavu ovládacího prvku. Následující příklad ukazuje, že ovládací prvek NumericUpDown volá metodu GoToState, aby se Positive přepnul do stavu, když je Value 0 nebo větší, a do stavu Negative, když je Value menší než 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 provede logiku potřebnou ke spuštění a zastavení scénářů odpovídajícím způsobem. Když ovládací prvek zavolá GoToState ke změně svého stavu, VisualStateManager provede následující:

  • VisualState Pokud ovládací prvek bude mít nějaký Storyboard, začne scénář. Pak, pokud VisualState, ze kterého pochází ovládací prvek, má Storyboard, storyboard skončí.

  • Pokud je ovládací prvek již ve stavu, který je zadán, GoToState neprovádí žádnou akci a vrátí .true

  • Pokud zadaný stav v seznamu ControlTemplatecontrolneexistuje , GoToState neprovádí žádnou akci a vrátí hodnotu false.

Osvědčené postupy pro práci s nástrojem VisualStateManager

Pokud chcete zachovat stavy kontroly, doporučujeme provést následující kroky:

  • Ke sledování stavu použijte vlastnosti.

  • Vytvořte pomocnou metodu pro přechod mezi stavy.

Ovládací NumericUpDown prvek používá svou Value vlastnost ke sledování, zda je ve stavu Positive nebo ve stavu Negative. Ovládací NumericUpDown prvek také definuje Focused a UnFocused stavy, které sledují IsFocused vlastnost. Pokud používáte stavy, které přirozeně neodpovídají vlastnosti ovládacího prvku, můžete definovat soukromou vlastnost pro sledování stavu.

Jedna metoda, která aktualizuje všechny stavy, centralizuje volání na kód VisualStateManager a udržuje váš kód spravovatelný. Následující příklad ukazuje pomocnou metodu ovládacího prvku NumericUpDown, UpdateStates. Pokud Value je větší nebo rovna 0, Control je ve Positive stavu. Je-li Value menší než 0, ovládací prvek je ve Negative stavu. Pokud IsFocused je true, ovládací prvek je ve Focused stavu; jinak je ve Unfocused stavu. Ovládací prvek může volat UpdateStates vždy, když potřebuje změnit svůj stav bez ohledu na to, jaký stav se změní.

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

Pokud předáte název stavu, když GoToState je ovládací prvek již v daném stavu, GoToState nic nedělá, takže nemusíte kontrolovat aktuální stav ovládacího prvku. Pokud Value se například změní z jednoho záporného čísla na jiné záporné číslo, scénář stavu Negative se nepřeruší a uživatel v ovládacím prvku neuvidí změnu.

VisualStateManager používá VisualStateGroup objekty k určení, který stav má být ukončen při volání GoToState. Ovládací prvek je vždy v jednom stavu pro každý VisualStateGroup, který je definován v jeho ControlTemplate, a vyjde ze stavu pouze tehdy, když přejde do jiného stavu ze téhož VisualStateGroup. Například ControlTemplateNumericUpDown ovládací prvek definuje Positive a NegativeVisualState objekty v jednom VisualStateGroup a Focused a UnfocusedVisualState objekty v jiném. (Jak můžete vidět v části Focused tohoto tématu, když ovládací prvek přejde ze Unfocused stavu do VisualState stavu, nebo naopak, ovládací prvek zůstane buď ve stavu , nebo Positive.)

Existují tři typická místa, kde se může změnit stav ovládacího prvku:

Následující příklady ukazují aktualizaci stavu NumericUpDown ovládacího prvku v těchto případech.

Měli byste aktualizovat stav ovládacího prvku v OnApplyTemplate metodě tak, aby se ovládací prvek zobrazil ve správném stavu při ControlTemplate použití. Následující příklad volá UpdateStatesOnApplyTemplate , aby se zajistilo, že ovládací prvek je v příslušných stavech. Předpokládejme například, že vytvoříte ovládací prvek a nastavíte ho NumericUpDown na zelenou Foreground a Value na hodnotu -5. Pokud se při použití UpdateStates ovládacího prvku nevolá ControlTemplateNumericUpDown, ovládací prvek není ve Negative stavu a hodnota je zelená místo červené. Musíte zavolat UpdateStates, aby se ovládací prvek dostal do Negative stavu.

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

Při změně vlastnosti často potřebujete aktualizovat stavy ovládacího prvku. Následující příklad ukazuje celou ValueChangedCallback metodu. Vzhledem k tomu ValueChangedCallback, že je volána při změnách Value, metoda volá UpdateStates v případě Value že se změnilo z pozitivního na negativní nebo naopak. Je přijatelné volat UpdateStates při Value změnách, ale zůstává pozitivní nebo negativní, protože v takovém případě ovládací prvek nezmění stavy.

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

Může být také potřeba aktualizovat stavy, když dojde k události. Následující příklad ukazuje, že NumericUpDown volání UpdateStates na Control zpracování GotFocus události.

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

Pomáhá VisualStateManager spravovat stavy vašeho ovládacího prvku. Použitím VisualStateManager zajistíte, že váš ovládací prvek správně přechází mezi stavy. Pokud budete postupovat podle doporučení popsaných v této části pro práci s kódem VisualStateManagerovládacího prvku, zůstane čitelný a udržovatelný.

Poskytnutí řídicího kontraktu

Zadáte řídicí kontrakt, aby autoři věděli, ControlTemplate co mají do šablony vložit. Kontrakt ovládacího prvku má tři prvky:

  • Vizuální prvky, které používá logika ovládacího prvku.

  • Stavy ovládacího prvku a skupina, do které každý stav patří.

  • Veřejné vlastnosti, které ovládací prvek vizuálně ovlivňují.

Někdo, kdo vytvoří nový ControlTemplate , potřebuje vědět, jaké FrameworkElement objekty logika ovládacího prvku používá, jaký typ je každý objekt a jaký je jeho název. Autor ControlTemplate musí také znát název každého možného stavu, ve kterém může být ovládací prvek, a ve kterém je tento stav VisualStateGroup.

Vracíme se k příkladu NumericUpDown, ovládací prvek očekává ControlTemplate následující FrameworkElement objekty:

Ovládací prvek může být v následujících stavech:

Chcete-li určit, jaké FrameworkElement objekty ovládací prvek očekává, použijte , TemplatePartAttributekterý určuje název a typ očekávaných prvků. Chcete-li určit možné stavy ovládacího prvku, použijte TemplateVisualStateAttribute, který určuje název státu a které VisualStateGroup patří. Umístěte TemplatePartAttribute a TemplateVisualStateAttribute na definici třídy ovládacího prvku.

Všechna veřejná vlastnost, která ovlivňuje vzhled ovládacího prvku, je také součástí smlouvy o ovládacím prvku.

Následující příklad určuje FrameworkElement objekt a stavy pro NumericUpDown ovládací prvek.

[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

Kompletní příklad

Následující příklad představuje ovládací prvek ControlTemplate v jeho celém rozsahu 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>

Následující příklad ukazuje logiku NumericUpDownpro .

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

Viz také