WPF France

  • Augmenter la taille
  • Taille par défaut
  • Diminuer la taille

Custom Control WPF (Suite)

Envoyer Imprimer PDF
Note des utilisateurs: / 1
MauvaisTrès bien 

(Article original sur le blog de Sébastien)

Comme je vous l’avait dit dans mon dernier article, j’ai voulu aller encore un peu plus loin dans la création de « Custom Control » en utilisant la suite d’outil Microsoft Expression et plus précisément Expression Design 3 et Expression Blend 3 afin de m’aider dans la conception de contrôle personnalisé.

Pour rester dans la continuité du précédent article, je vais reprendre l’exemple sur la réalisation d’un composant représentant un récipient dans lequel il est possible de faire varier son contenu, mais ce coup-ci je vais m’aider des outils Expression Design 3 et Expression Blend 3.

Pour dessiner mon récipient, je ne vais donc plus le faire à la main mais le dessiner avec Expression Design 3, qui permet de concevoir des graphiques vectoriel assez simplement. Voici donc à quoi va ressembler mon récipient en le dessinant avec cet outil, on remarquera au passage une ellipse rouge qui représente en faite une vanne permettant d’interdire ou pas le remplissage du récipient (Vous excuserez mes pauvres compétences en design :( ) :


Design




Maintenant que j’ai dessiné mon composant je vais l’exporter dans un fichier XAML afin de pouvoir l’exploiter sous l’outil Expression Blend, on notera ici que j’exporte le contenu de mon contrôle dans un Canvas WPF (Format : XAML WPF Canvas):



DesignExport



La première étape est donc terminé, je vais passer maintenant à l’importation dans mon fichier XAML dans l’outil Expression Blend, afin d’apporter des compléments avec notamment la création de deux animations permettant d’appliquer une rotation sur l’ellipse rouge, et ainsi simuler l’ouverture et la fermeture de ma vanne .
Je vais tout d’abord créer un nouveau projet dans Expression Blend de type WPF Control Library :



NewBlend



Ensuite je vais lui ajouter un nouvel élément et sélectionner le fichier XAML que j’ai exporté :

NewItemBlend



Je peut donc maintenant travailler avec mon composant dans Expression Blend et la première chose que je vais faire c’est d’encapsuler mon « Canvas » dans un « ViewBox ». Le « ViewBox » est un élément très puissant de WPF qui permet d’étirer et d’ajuster un enfant unique pour remplir l’espace disponible. Il ne redimensionne pas son contenu, mais il le transforme. Cela signifie également que toutes les tailles de texte et des largeurs de lignes sont transformés, voici un extrait :

<Viewbox Stretch="Uniform">

<Canvas x:Name="CalqueMain" Width="280" Height="160" Canvas.Left="0" Canvas.Top="0" Grid.Column="0" Grid.Row="0">

(...)

<Canvas/>

<Viewbox/>

Passons maintenant à la création de mes deux animations permettant l’ouverture et la fermeture de ma vanne, et pour cela je vais utiliser les outils d’animation intégrés à Expression Blend (A noter que je passe en mode animation dans Windows->WorkSpaces->Animation afin de profiter d’une disposition de fenêtrage optimisé à l’utilisation des outils d’animation ):



AnimBlend



J’applique un mouvement d’une rotation d’un angle de 90°, qui constitue mon animation et qui se compose de deux clés que j’ai nommé « AnimOn » pour l’ouverture. Je crée le mouvement inverse pour la fermeture que je nomme « AnimOff ».



AnimBlend2



Petite astuce au passage, pour effectuer la rotation j’ai du modifier le point centrale de mon ellipse :



AnimBlend3



Voici donc la deuxième étape de mon contrôle terminé, je vais maintenant passer à l’importation de mon contrôle dans Visual Studio afin de finaliser la conception de mon contrôle. Pour cela je vais créer une nouvelle solution avec deux projets, un contenant les sources de mon contrôle et un autre exploitant mon contrôle.
Maintenant mes projets crées, je vais pouvoir modifier mon fichier « Generic.xaml » en copiant dans la balise « ControlTemplate » ce qui à été généré dans Expression Blend dont voici un extrait (On remarquera dans les ressources de ma « ViewBox », la présence des deux animations que j’ai crée au préalable) :

<Viewbox Stretch="Uniform" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="ViewBox">
<Viewbox.Resources>
<Storyboard x:Key="AnimOn">
<PointAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="Ellipse" Storyboard.TargetProperty="(UIElement.RenderTransformOrigin)">
<SplinePointKeyFrame KeyTime="00:00:00" Value="0.038,0.499"/>
</PointAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Ellipse" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.7000000" Value="-89.752"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="AnimOff">
<PointAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="Ellipse" Storyboard.TargetProperty="(UIElement.RenderTransformOrigin)">
<SplinePointKeyFrame KeyTime="00:00:00" Value="0.038,0.499"/>
</PointAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Ellipse" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="-89.752"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.7000000" Value="0.351"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Viewbox.Resources>
<Canvas x:Name="CalqueMain" Width="280" Height="160" Canvas.Left="0" Canvas.Top="0" Grid.Column="0" Grid.Row="0">

<Rectangle x:Name="Rectangle" Width="188.917" Height="151" Canvas.Left="4.75004" Canvas.Top="4.74995" Stretch="Fill" StrokeLineJoin="Round" Stroke="#FF000000"/>
<Path x:Name="Path" Width="192.667" Height="4.56297" Canvas.Left="3.08337" Canvas.Top="2.57957" Stretch="Fill" StrokeLineJoin="Round" Stroke="#FF000000" Data="F1 M 99.4167,3.07957C 152.342,3.07957 195.25,3.87721 195.25,4.86105C 195.25,5.84496 152.344,6.64255 99.4167,6.64255C 46.4894,6.64255 3.58337,5.84496 3.58337,4.86105C 3.58337,3.87721 46.4914,3.07957 99.4167,3.07957 Z ">
<Path.Fill>
<RadialGradientBrush RadiusX="0.503261" RadiusY="27.0724" Center="0.5,0.499998" GradientOrigin="0.5,0.499998">
<RadialGradientBrush.GradientStops>
<GradientStop Color="#FF000000" Offset="0"/>
<GradientStop Color="#00000000" Offset="0.990952"/>
</RadialGradientBrush.GradientStops>
<RadialGradientBrush.RelativeTransform>
<TransformGroup/>
</RadialGradientBrush.RelativeTransform>
</RadialGradientBrush>
</Path.Fill>
</Path>
<Path x:Name="Path_0" Width="192.667" Height="4.97226" Canvas.Left="2.66671" Canvas.Top="153.181" Stretch="Fill" StrokeLineJoin="Round" Stroke="#FF000000" Data="F1 M 99,153.681C 151.925,153.681 194.833,154.57 194.833,155.667C 194.833,156.764 151.927,157.653 99,157.653C 46.0727,157.653 3.16671,156.764 3.16671,155.667C 3.16671,154.57 46.0747,153.681 99,153.681 Z ">
<Path.Fill>
<RadialGradientBrush RadiusX="0.503261" RadiusY="24.283" Center="0.5,0.500008" GradientOrigin="0.5,0.500008">
<RadialGradientBrush.GradientStops>
<GradientStop Color="#FF000000" Offset="0"/>
<GradientStop Color="#00000000" Offset="0.990952"/>
</RadialGradientBrush.GradientStops>
<RadialGradientBrush.RelativeTransform>
<TransformGroup/>
</RadialGradientBrush.RelativeTransform>
</RadialGradientBrush>
</Path.Fill>
</Path>
<Rectangle x:Name="RectangleContent" Width="187" Height="1" Canvas.Left="5.66652" Canvas.Top="154" Stretch="Fill">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="-1.37786e-007,0.500001" EndPoint="1,0.500001">
<LinearGradientBrush.GradientStops>
<GradientStop Color="#FF230FD2" Offset="0"/>
<GradientStop Color="#FF0096FF" Offset="0.977376"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Path x:Name="Path_2" Width="54.7033" Height="15.5424" Canvas.Left="222.848" Canvas.Top="70.0725" Stretch="Fill" Data="F1 M 277.533,70.0725L 277.551,85.5509L 222.866,85.6149L 222.848,70.1365L 277.533,70.0725 Z ">
<Path.Fill>
<LinearGradientBrush StartPoint="0.499834,0.00205923" EndPoint="1.49572,0.00205923">
<LinearGradientBrush.RelativeTransform>
<TransformGroup>
<SkewTransform CenterX="0.499834" CenterY="0.00205923" AngleX="0.217013" AngleY="0"/>
<RotateTransform CenterX="0.499834" CenterY="0.00205923" Angle="89.9802"/>
</TransformGroup>
</LinearGradientBrush.RelativeTransform>
<LinearGradientBrush.GradientStops>
<GradientStop Color="#FF2C2C2C" Offset="0"/>
<GradientStop Color="#FF424242" Offset="0.317797"/>
<GradientStop Color="#FF595959" Offset="0.516949"/>
<GradientStop Color="#FF424242" Offset="0.731138"/>
<GradientStop Color="#FF2C2C2C" Offset="0.991525"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Path.Fill>
</Path>

(...)

</Canvas>
</Viewbox>


J’ai pris soin de renommer certain éléments graphique de mon composant afin de pouvoir les utiliser dans la classe où sera codé la logique de mon composant ( « MyCustomControl.cs » ). Je vais donc pouvoir passer à la partie code qui va entre autre gérer la possibilité de faire varier le contenu de mon récipient ou encore activer/désactiver ma vanne. Pour cela je vais commencer par récupérer les élements graphique que j’ai pris soin de renommer au préalable, ainsi que mes deux animations que je vais invoquer lors du clique sur ma vanne :

public override void OnApplyTemplate()
 {
 base.OnApplyTemplate();

 rectangleContent = this.Template.FindName("RectangleContent", this) as Rectangle;
 heightRectangleContent = rectangleContent.Height;
 canvasTopRectangleContent = Canvas.GetTop(rectangleContent);

 ellipse = this.Template.FindName("Ellipse", this) as Ellipse;
 ellipse.MouseDown += new MouseButtonEventHandler(ellipse_MouseDown);

 Viewbox viewBox = this.Template.FindName("ViewBox", this) as Viewbox;
 stAnimOn = viewBox.Resources["AnimOn"] as Storyboard;
 stAnimOff = viewBox.Resources["AnimOff"] as Storyboard;
 }

 

Je vais créer une propriété « IsOpen » pour connaitre le statut de ma vanne et ainsi faire hériter ma classe « MyCustomControl » de « INotifyPropertyChanged » pour prévenir d’un changement de statut de ma vanne :

public class MyCustomControl : Control, INotifyPropertyChanged
 {
 (...)

#region Events

 public event PropertyChangedEventHandler PropertyChanged;

 #endregion

 #region Event Handlers

 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
 protected void OnPropertyChanged<T>(Expression<Func<T>> propAccess)
 {
 if (PropertyChanged != null)
 {
 var asMember = propAccess.Body as MemberExpression;
 if (asMember == null)
 return;

 PropertyChanged(this, new PropertyChangedEventArgs(asMember.Member.Name));
 }
 }

 #endregion

 }

 

public Boolean isOpen;
 public Boolean IsOpen
 {
 get
 {
 return isOpen;
 }
 set
 {
 isOpen = value;
 OnPropertyChanged(() => IsOpen);
 }
 }

 protected override void OnInitialized(EventArgs e)
 {
 base.OnInitialized(e);
 IsOpen = true;
 }

Je n’oublie pas de créer une « DependencyProperty » pour gérer un pourcentage correspondant à la quantité de remplissage du contenu :

public static readonly DependencyProperty PercentContentProperty =
 DependencyProperty.Register("PercentContent",
 typeof(double),
 typeof(MyCustomControl),
 new UIPropertyMetadata(0.0, new PropertyChangedCallback(ValueContentChanged)));
 public double PercentContent
 {
 get { return (double)GetValue(PercentContentProperty); }
 set { SetValue(PercentContentProperty, value); }
 }

Et enfin je termine par coder quelques petits calculs qui, à chaque fois que la propriété pourcentage est modifié, vont permettre de modifier la taille et la position de mon élément graphique que j’ai nommé « RectangleContent », et ainsi simuler la variation du contenu de mon récipient (On remarquera que le contenu peut varier que si la propriété « IsOpen » est à « true », autrement dit que la vanne est ouverte):

private static void ValueContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
 {
 MyCustomControl c = d as MyCustomControl;
 if (c.IsOpen)
 {
 if ((c.PercentContent >= 0) && (c.PercentContent = 5.5)
 {
 Canvas.SetTop(c.rectangleContent, resCanvasTop);
 c.rectangleContent.Height = resHeight;
 }
 }
 }

 }

Comme je l’ai précisé plus haut, mon contrôle doit permettre à l’aide d’une vanne d’interdire ou pas le remplissage du récipient, et pour cela je vais récupérer mes deux animations et les exécuter au clique de la souris afin de simuler l’ouverture et la fermeture de la vanne :

void ellipse_MouseDown(object sender, MouseButtonEventArgs e)
 {
 if (IsOpen)
 {
 stAnimOn.Begin();
 }
 else
 {
 stAnimOff.Begin();
 }
 IsOpen = !IsOpen;
 }

Maintenant il ne me reste plus qu’à utiliser mon contrôle dans mon projet WPF principale qui est composé de mon contrôle, d’un « Slider » permettant de faire varier le contenu, et de deux boutons permettant de remplir et de vider le contenu :



<Window x:Class="WpfCustomControl2.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:WpfCustomControlLibrary2;assembly=WpfCustomControlLibrary2"
Title="Un Custom Control Simple" Height="518" Width="642">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<custom:MyCustomControl Name="myCustomControl" PercentContent="{Binding ElementName=slider, Path=Value, Mode=TwoWay}" Grid.Row="0">
</custom:MyCustomControl>
<Slider Name="slider" Value="50" Maximum="100" Minimum="0"  Grid.Row="1" IsEnabled="{Binding ElementName=myCustomControl, Path=IsOpen}"></Slider>
<TextBox Text="{Binding ElementName=slider, Path=Value, Mode=TwoWay}" Grid.Row="2" IsEnabled="{Binding ElementName=myCustomControl, Path=IsOpen}"></TextBox>
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>

<Button Name="BtRemplir" Grid.Column="0" Margin="5,5,5,5" Click="BtRemplir_Click" IsEnabled="{Binding ElementName=myCustomControl, Path=IsOpen}">Remplir</Button>
<Button Name="BtVider" Grid.Column="1" Margin="5,5,5,5" Click="BtVider_Click" IsEnabled="{Binding ElementName=myCustomControl, Path=IsOpen}">Vider</Button>
</Grid>

<StatusBar VerticalAlignment="Bottom" Grid.Row="4">
<StatusBarItem>
<StackPanel Orientation="Horizontal">
<Label>Statut Récipient :</Label>
<Label Foreground="Red" Content="{Binding ElementName=myCustomControl, Path=IsOpen}"/>
</StackPanel>
</StatusBarItem>
</StatusBar>
</Grid>
</Window>



Le résultat :

Resultat



Voilà si vous voulez jeter un œil sur la totalité du code voici les sources :


1255272560_zip

(Article original sur le blog de Sébastien)

Sébastien.



Commentaires (0)
Mise à jour le Mardi, 06 Avril 2010 12:18  

Partagez


Article au hasard


  • Tous est dans le titre et c’est une question que l’on m’a souvent posée, à savoir comment personnaliser les fenêtres d’une application WPF. Je vais donc tenter d’y répondre partiellement ici, en vous présentant ce que j’ai fait dans mon petit projet « Media Browser » :



    Lire la suite...