Créer un controle qui se retourne !

Vous trouverez dans ce billet les clés pour créer votre propre ContentControl, disposant d’une animation et de deux Contents.

Dans l’exemple ci-dessous, cliquer sur le bouton central pour retourner le contenu :

Commençons par créer notre projet Silverlight puis dans ce projet Silverlight à lui ajouter une nouvelle item.
Cette item sera un « Silverlight Templated Control », et non un « Silverlight User Control », afin d’utiliser toute la puissance des ContentControls, les UserControls n’étant pas très à l’aise avec les contenus.

Nous appellerons ce control Card :

Le controle est constitué de Card.cs et du fichier Generic.xaml dans le dossier Theme

Dans le fichier Card.cs, on changera l’héritage par défaut de Card pour le passer de Control à ContentControl.

On crée ensuite rapidement sur Expression Blend (parce que c’est plus facile), le design de notre contrôle :

		<Border Width="200" Height="125" x:Name="BorderCard" BorderThickness="1" BorderBrush="#FF65A4FF" CornerRadius="10">

			<Border.Projection>
				<PlaneProjection/>
			</Border.Projection>

			<Border.Background>
				<LinearGradientBrush EndPoint="1,0" StartPoint="0,0">
					<GradientStop Color="#FF8CBBFF" Offset="0"/>
					<GradientStop Color="White" Offset="1"/>
				</LinearGradientBrush>
			</Border.Background>
			
			<Grid>
				<ContentPresenter Margin="10" x:Name="ContentFront" Visibility="Visible">
					<Button Content="Pile" Click="ButtonPile_Click"/>
				</ContentPresenter>
				<ContentPresenter Margin="10" x:Name="ContentBack" Visibility="Collapsed">
					<Button Content="Face" Click="ButtonFace_Click" />
				</ContentPresenter>
			</Grid>	
		</Border>

Remarquez les deux ContentPresenters « ContentFront » et « ContentBack ». L’un est visible et l’autre pas. Ils vont nous permettre de placer des contrôles sur le devant de la carte et sur son arrière.

Toujours avec Blend, on va créer 2 Storyboards permettant de faire tourner la carte dans un sens puis dans l’autre en jouant sur le PlaneProjection du Border et la Visibilité des ContentPresenters que l’on vient de designer :

		<Storyboard x:Name="StoryboardFlipPile">
			<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderCard" Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationX)">
				<EasingDoubleKeyFrame KeyTime="00:00:01" Value="90"/>
				<EasingDoubleKeyFrame KeyTime="00:00:02" Value="0"/>
			</DoubleAnimationUsingKeyFrames>
			<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="ContentFront" Storyboard.TargetProperty="(UIElement.Visibility)">
				<DiscreteObjectKeyFrame KeyTime="00:00:01">
					<DiscreteObjectKeyFrame.Value>
						<Visibility>Collapsed</Visibility>
					</DiscreteObjectKeyFrame.Value>
				</DiscreteObjectKeyFrame>
			</ObjectAnimationUsingKeyFrames>
			<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="ContentBack" Storyboard.TargetProperty="(UIElement.Visibility)">
				<DiscreteObjectKeyFrame KeyTime="00:00:01">
					<DiscreteObjectKeyFrame.Value>
						<Visibility>Visible</Visibility>
					</DiscreteObjectKeyFrame.Value>
				</DiscreteObjectKeyFrame>
			</ObjectAnimationUsingKeyFrames>
		</Storyboard>

		<Storyboard x:Name="StoryboardFlipFace">
			<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderCard" Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationX)">
				<EasingDoubleKeyFrame KeyTime="00:00:01" Value="90"/>
				<EasingDoubleKeyFrame KeyTime="00:00:02" Value="0"/>
			</DoubleAnimationUsingKeyFrames>
			<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="ContentFront" Storyboard.TargetProperty="(UIElement.Visibility)">
				<DiscreteObjectKeyFrame KeyTime="00:00:01">
					<DiscreteObjectKeyFrame.Value>
						<Visibility>Visible</Visibility>
					</DiscreteObjectKeyFrame.Value>
				</DiscreteObjectKeyFrame>
			</ObjectAnimationUsingKeyFrames>
			<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="ContentBack" Storyboard.TargetProperty="(UIElement.Visibility)">
				<DiscreteObjectKeyFrame KeyTime="00:00:01">
					<DiscreteObjectKeyFrame.Value>
						<Visibility>Collapsed</Visibility>
					</DiscreteObjectKeyFrame.Value>
				</DiscreteObjectKeyFrame>
			</ObjectAnimationUsingKeyFrames>
		</Storyboard>


Le Storyboard effectue une Rotation en X du Plan. Quand l’angle de rotation est de 90°, le contrôle devient fin comme du papier. On échange alors la propriété Visibility des ContentPresenter. Ainsi une face devient visible et l’autre pas. on reprend ensuite la rotation mais (petite astuce) en partant de -90° pour arriver à 0°. Ainsi la face arrière sera visible mais pas inversée.

Une fois Border et Storyboard crées, il ne nous reste plus qu’a copier-coller l’ensemble sous Visual Studio dans le fichier Generic.xaml du style de notre contrôle :

    <Style TargetType="local:Card">

        <Setter Property="Width" Value="200"/>
        <Setter Property="Height" Value="125"/>
        
        <Setter Property="BorderBrush" Value="#FF65A4FF"/>
        <Setter Property="BorderThickness" Value="1"/>
        
        <Setter Property="CornerRadius" Value="10"/>
        <Setter Property="Padding" Value="10"/>
        <Setter Property="BackGround">
            <Setter.Value>
                <LinearGradientBrush EndPoint="1,0" StartPoint="0,0">
                    <GradientStop Color="#FF8CBBFF" Offset="0"/>
                    <GradientStop Color="White" Offset="1"/>
                </LinearGradientBrush>
            </Setter.Value>
        </Setter>
        
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:Card">

                    <Border 
                            Width="{TemplateBinding Width}" 
                            Height="{TemplateBinding Height}" 
                            Background="{TemplateBinding Background}" 
                            x:Name="BorderCard" 
                            CornerRadius="{TemplateBinding CornerRadius}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            >

                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="ActionStates">
                                <VisualState x:Name="TurnBackState">
                                    <Storyboard SpeedRatio="3">
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderCard" Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationX)">
                                            <EasingDoubleKeyFrame KeyTime="00:00:01" Value="90"/>
                                            <EasingDoubleKeyFrame KeyTime="00:00:01" Value="-90"/>
                                            <EasingDoubleKeyFrame KeyTime="00:00:02" Value="0"/>
                                        </DoubleAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="ContentFront" Storyboard.TargetProperty="(UIElement.Visibility)">
                                            <DiscreteObjectKeyFrame KeyTime="00:00:01">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Collapsed</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="ContentBack" Storyboard.TargetProperty="(UIElement.Visibility)">
                                            <DiscreteObjectKeyFrame KeyTime="00:00:01">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="TurnFrontState">
                                    <Storyboard SpeedRatio="3">
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderCard" Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationX)">
                                            <EasingDoubleKeyFrame KeyTime="00:00:01" Value="90"/>
                                            <EasingDoubleKeyFrame KeyTime="00:00:01" Value="-90"/>
                                            <EasingDoubleKeyFrame KeyTime="00:00:02" Value="0"/>
                                        </DoubleAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="ContentFront" Storyboard.TargetProperty="(UIElement.Visibility)">
                                            <DiscreteObjectKeyFrame KeyTime="00:00:01">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="ContentBack" Storyboard.TargetProperty="(UIElement.Visibility)">
                                            <DiscreteObjectKeyFrame KeyTime="00:00:01">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Collapsed</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>

                        <Border.Projection>
                            <PlaneProjection/>
                        </Border.Projection>

                        <Grid>
                            <ContentPresenter Margin="{TemplateBinding Padding}" Content="{TemplateBinding Content}" x:Name="ContentFront" Visibility="Visible">
                            </ContentPresenter>
                            <ContentPresenter Margin="{TemplateBinding Padding}" Content="{TemplateBinding ContentBack}" x:Name="ContentBack" Visibility="Collapsed">
                            </ContentPresenter>
                        </Grid>

                    </Border>

                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Afin d’intégrer convenablement notre Border dans le contrôle on remplacera certaine valeur de propriété par des TemplateBinding. Si celle-ci existe déjà (comme Width ou Content) dans le ContentControl pas de problème. Si celle-ci n’existe pas (comme CornerRadius ou ContentBack), on les rajoutera en tant que Dependency Property via le snippet propdp dans le code de Card.cs

Notons aussi, que les Storyboards ont été intégrés dans des VisualStates afin d’en faciliter l’utilisation dans le Controle. Deux états sont disponibles « TurnBackState » & « TurnFrontState ».

On rajoute une propriété (encore une propdp) IsTurnOver qui lancera l’un ou l’autre des états selon la valeur passée.
Pour finir, une méthode TurnOver sera chargée d’affecter automatiquement à « IsTurnOver », l’inverse de sa valeur.

        /// <summary>
        /// Doit-on tourner ?
        /// </summary>

        public bool IsTurnOver
        {
            get { return (bool)GetValue(IsTurnOverProperty); }
            set { SetValue(IsTurnOverProperty, value); }
        }

        // Using a DependencyProperty as the backing store for IsTurnOver.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsTurnOverProperty =
            DependencyProperty.Register("IsTurnOver", typeof(bool), typeof(Card), new PropertyMetadata(false, OnIsTurnOverChanged ));

        /// <summary>
        /// On se retourne
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>

        private static void OnIsTurnOverChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            Card card = sender as Card;

            bool result = (bool)e.NewValue;

            if (result == true)
            {
                VisualStateManager.GoToState(card, TurnBackState, true);
            }
            else
            {
                VisualStateManager.GoToState(card, TurnFrontState, true);
            }
        }

        /// <summary>
        /// Retourne la carte
        /// </summary>

        public void TurnOver()
        {
            this.IsTurnOver = !this.IsTurnOver;
        }

Et voila ! C’est fini !

à l’utilisation c’est simple :


        <my:Card x:Name="Card">

            <!-- *** Contenu du devant de la Carte *** -->
            <!-- Button_Click lance la methode TurnOver() -->
            <Button Content="Front" Click="Button_Click"></Button>

            <!-- *** Contenu de l'arrière de la carte *** -->
            <my:Card.ContentBack>
                <StackPanel Orientation="Vertical">
                <!-- Button_Click lance la methode TurnOver() -->
                <Button Height="22" Content="Back" Click="Button_Click"></Button>
                <controls:DatePicker></controls:DatePicker>
                </StackPanel>
            </my:Card.ContentBack>

            <my:Card.Effect>
                <DropShadowEffect Opacity="0.5"></DropShadowEffect>
            </my:Card.Effect>

        </my:Card>        

Vous trouverez la source complète du contrôle ici.

PS : Je n’ai pas ajouté les attributs spécifiques à Blend, libre à vous de le faire.

2 Responses to Créer un controle qui se retourne !

  1.  

    Bonjour,
    N’est-ce possible de le faire avec WPF?

    Au fait, je veux créer une application qui va avoir des fenêtre 3D, Ex. Comme tu l’as fait avec le bouton au dessus pour Silverlight, je voudrais que le bouton soit une fenêtre.

    Merci pour la réponse…

  2. Hello, Le problème c’est que WPF ne comporte pas nativement de balise PlaneProjection qui te permet de faire l’effet simili 3D de retournement. En revanche tu dois pouvoir l’émuler assez simplement je pense.
    Jettes un coup d’oeil à ça par exemple : http://blog.endquote.com/post/710116433/planeprojection-in-wpf

     

leave your comment