(Article original sur le blog de Sébastien)
Le petit exemple que je vais vous exposer est très simpliste mais permet déjà d’avoir un premier aperçu sur les possibilités offerte par les « Custom Controls » en WPF. A noter qu’il ne faut pas confondre « Custom Control » et « User Control », en effet un « User Control » est un contrôle composé d’autres contrôles un peu comme ce qui existe dans les Winforms, et les « Custom Controls » sont des contrôles entièrement personnalisable c’est à dire que vous allez pouvoir créer entièrement en partant de rien ou d’un contrôle existant.
Pour créer un « Custom Control » rien de plus simple, dans Visual Studio je crée un premier projet WPF qui utilisera mon « Custom Control », puis j’ajoute à ma solution un projet de type Bibliothèque de contrôles personnalisés Windows Presentation Foundation (.NET Framework 3.5) qui contiendra le code de mon « Custom Control » :

Voici à quoi ressemble la structure de ma solution :

- Un projet WpfCustomControl : Projet principale de ma solution qui va utiliser mon « Custom Control ».
- Un projet WpfCustomControlLibrary : Projet contenant le code de mon « Custom Control », on constate que Visual Studio à crée un répertoire « Themes » avec un fichier « Generic.xaml », c’est dans ce fichier que je vais définir l’apparence de mon contrôle, et un fichier contenant la logique de mon contrôle (MyCustomControl.cs).
Si on regarde d’un peu plus prés le fichier « MyCustomControl.cs », on constate la génération d’un constructeur static qui va en faite permettre d’indiquer au moteur WPF qu’il y a un style particulier à appliquer à mon contrôle :
MyCustomControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyCustomControl), new FrameworkPropertyMetadata(typeof(MyCustomControl)));
}
Pour savoir où se situe le style en question il suffit de regarder le fichier « AssemblyInfo.cs », Visual Studio a généré automatiquement cette ligne qui indique où situe le dictionnaire de ressource :
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //où se trouvent les dictionnaires de ressources spécifiques à un thème
//(utilisé si une ressource est introuvable dans la page,
// ou dictionnaires de ressources de l'application)
ResourceDictionaryLocation.SourceAssembly //où se trouve le dictionnaire de ressources générique
//(utilisé si une ressource est introuvable dans la page,
// dans l'application ou dans l'un des dictionnaires de ressources spécifiques à un thème)
)]
Je vais donc pouvoir maintenant passer à la conception de mon « Custom Control », et pour cet exemple je vais créer une représentation très simpliste d’un récipient dans lequel je vais faire varier la quantité du contenu.
Voici le résultat attendu de l’interface :

Pour cela je vais garder l’héritage de la classe « Control ». Je dois donc tout créer de A à Z, à noter que j’aurais peu modifier l’héritage si j’avais voulu personnaliser un contrôle existant mais ce n’est pas le but ici. Passons maintenant à la conception de l’interface en modifiant le fichier « Generic.xaml » :
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfCustomControlLibrary">
<LinearGradientBrush x:Key="MyCustomControlMouseOverFill" EndPoint="0.854,0.854" StartPoint="0.146,0.146">
<GradientStop Color="#FFD7D7D7" Offset="0"/>
<GradientStop Color="#FF262626" Offset="1"/>
</LinearGradientBrush>
<Style TargetType="{x:Type local:MyCustomControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyCustomControl}">
<Border BorderBrush="Black" CornerRadius="10,10,10,10" BorderThickness="5" x:Name="Part_Background">
<Rectangle
Height="{Binding PercentContent, RelativeSource={RelativeSource TemplatedParent}}"
VerticalAlignment="Bottom"
x:Name="Part_content"
RadiusX="3"
RadiusY="3">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="0.854,0.854" StartPoint="0.146,0.146">
<GradientStop Color="#FF262626" Offset="0"/>
<GradientStop Color="#FFD7D7D7" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Fill" TargetName="Part_content" Value="{StaticResource MyCustomControlMouseOverFill}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Le but du jeu ici est de pouvoir faire varier la quantité du contenu, je vais donc modifier un peu ma classe « MyCustomControl » afin de pouvoir gérer une quantité de remplissage ainsi que sa valeur maximum, et pour cela je vais passer par des « DependencyProperty » :
public static readonly DependencyProperty PercentContentProperty =
DependencyProperty.Register("PercentContent",
typeof(int),
typeof(MyCustomControl),
new UIPropertyMetadata(0, new PropertyChangedCallback(ValueContentChanged)));
public int PercentContent
{
get { return (int)GetValue(PercentContentProperty); }
set { SetValue(PercentContentProperty, value); }
}
public static readonly DependencyProperty MaximumContentProperty =
DependencyProperty.Register("MaximumContent",
typeof(int),
typeof(MyCustomControl),
new UIPropertyMetadata(100, new PropertyChangedCallback(ValueContentChanged)));
public int MaximumContent
{
get { return (int)GetValue(MaximumContentProperty); }
set { SetValue(MaximumContentProperty, value); }
}
A chaque modification de la valeur de la quantité, je vais appliquer un calcul très simple sur la hauteur du rectangle représentant le contenu, ce calcule est effectué dans la méthode « ValueContentChanged » que j’ai défini comme « PropertyChangedCallback » de mes « DependencyProperty » :
private static void ValueContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MyCustomControl c = d as MyCustomControl;
if (c.partContent != null)
{
if (c.PercentContent <= 95)
{
c.partContent.Height = (c.PercentContent * c.Height) / c.MaximumContent;
c.partContent.RadiusX = 3;
c.partContent.RadiusY = 3;
}
else
{
c.partContent.RadiusX = 8;
c.partContent.RadiusY = 8;
}
}
}
On remarque dans ce code l’utilisation d’une propriété « partContent », qui correspond à mon rectangle défini dans mon « Generic.xaml », et que je récupère comme ceci :
private Rectangle partContent;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
partContent = this.Template.FindName("Part_content", this) as Rectangle;
}
Il ne me reste plus qu’a l’utiliser dans mon projet WpfCustomControl :
<Window x:Class="WpfCustomControl.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:WpfCustomControlLibrary;assembly=WpfCustomControlLibrary"
Title="Window1" Height="379" Width="392">
<Grid>
<custom:MyCustomControl MaximumContent="{Binding ElementName=slider, Path=Maximum, Mode=OneWay}"
PercentContent="{Binding ElementName=slider, Path=Value, Mode=OneWay}" Height="200" Width="200">
</custom:MyCustomControl>
<Slider Name="slider" Value="50" Maximum="100" Minimum="0" VerticalAlignment="Bottom"></Slider>
</Grid>
</Window>
On voit dans ce code que dans un premier temps je référence mon projet « WpfCustomControlLibrary », puis que je « bind » mes propriétés « MaximumContent » et « PercentContent » avec les valeurs « Maximum » et « Value » de mon « Slider ».
Le résultat :

Comme vous avez pu le voir, les possibilités de création de contrôles personnalisés sont très puissante, bien sûr le contrôle que j’ai réalisé ici est très simpliste mais vous montre les possibilités offerte par WPF. J’essaierais dans un prochain article d’aller un plus loin dans la conception de contrôles personnalisés en utilisant le suite d’outil Microsoft Expression.
Voilà si vous voulez jeter un œil sur la totalité du code voici les sources :

(Article original sur le blog de Sébastien)
Sébastien.