Afin d'illustrer une des plus importantes innovations apportées par WPF 3.5, je remets au goût du jour un contrôle que j'avais réalisé avec la CTP de février 2005: le contrôle MatrixExpander. A cette occasion, le contrôle aura subit des améliorations de conception. C'est toujours un dérivé du contrôle Expander intégrant une interface audio et vidéo, mais cette fois les media sont compilés avec le source et le contrôle sera en 3D full interactif.

Introduction
WPF 3.5 améliore la richesse du Viewport3D par l'ajout de la classe Viewport2DVisual3D qui permet de rendre n'importe quel visuel 2D totalement interactif en tant que modèle 3D. Autrefois appliqué comme texture à un modèle 3D, le contrôle reste totalement opérationnel. J'ai alors considéré la conception du contrôle de manière indépendante, tel qu'il sera utilisé dans n'importe quelle application puis je lui rajoute une décoration 3D que je façonne selon le résultat final à obtenir. Le contrôle n'a par conséquent pas besoin d'être conçu dans la perspective d'être appliqué sur un modèle 3D.
Custom Expander
Le contrôle dérive de la classe de base Expander et encapsule le template pour fournir le visuel. Un contrôle ToggleButton templaté est utilisé pour être bindé à la propriété IsExpanded de l'Expander.
<Expander.Template>
<ControlTemplate TargetType="{x:Type lc:RDExpander}">
<Grid x:Name="EnterTheMatrix" Width="168" Height="210">
<Grid>
<Grid x:Name="_expanderContent" Panel.ZIndex="0">
<Image Source="../Images/MatrixFrame.png" Width="160" Height="101" Stretch="Uniform" StretchDirection="Both" />
<ContentPresenter />
<Grid.RenderTransform>
<TranslateTransform X="0" Y="0" />
</Grid.RenderTransform>
</Grid>
<Image x:Name="PART_MatrixBg" Width="168" Height="109" Stretch="Uniform" StretchDirection="Both" Source="../Images/MatrixScreen.png" Panel.ZIndex="10" VerticalAlignment="Top" />
<Border x:Name="PART_Border" Width="168" Height="101" Background="Transparent" Panel.ZIndex="-10" VerticalAlignment="Bottom" />
</Grid>
<ContentPresenter x:Name="_header" ContentSource="Header" />
<ToggleButton x:Name="_matrix" IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}">
<ToggleButton.Template>
<ControlTemplate>
<Grid>
<MediaElement x:Name="MatrixMovie" Width="135" Height="77" Stretch="UniformToFill" Opacity="0">
<MediaElement.Triggers>
<EventTrigger RoutedEvent="MediaElement.MediaOpened">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:0.5" To="1" Storyboard.TargetProperty="Opacity" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</MediaElement.Triggers>
</MediaElement>
</Grid>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<MediaTimeline Source="{local:EmbeddedMedia Source='pack://application:,,,/FX/matrix.avi'}" RepeatBehavior="Forever" Storyboard.TargetName="MatrixMovie" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
</Grid>
La difficulté est de fournir le comportement adéquat selon la direction de l'expander. Dans le cas précis, je décide de ne fournir que les comportements pour les directions Down et Up. Les autres sont assimilées à la direction Down. Il semble que plusieurs méthodes soient possibles pour animer le comportement de l'expander. Pour ma part, j'override la méthode OnExpanded et exécute les Storyboards en conséquence.
protected override void OnExpanded()
{
base.OnExpanded();
Grid __expanderContent = (Grid)base.GetTemplateChild("_expanderContent");
switch (base.ExpandDirection)
{
case ExpandDirection.Up:
__expanderContent.BeginStoryboard((Storyboard)this.Resources["Expanded_Up"]);
break;
case ExpandDirection.Down:
case ExpandDirection.Left:
case ExpandDirection.Right:
default:
__expanderContent.BeginStoryboard((Storyboard)this.Resources["Expanded_Down"]);
break;
}
}
Une autre nouveauté réside dans l'intégration des fichiers audio et vidéo dans l'application. Effectivement, le contrôle MediaElement n'accepte toujours pas des URI pointant vers des ressources embedded. Un MarkupExtension nommé EmbeddedMedia permettra de combler cette lacune. La seule contrainte est la création d'un dossier caché afin de "descendre" les fichiers sur le disque avant de les charger (merci à Avalonboy pour sa contribution).
<MediaTimeline Source="{local:EmbeddedMedia Source='pack://application:,,,/FX/matrix.avi'}" RepeatBehavior="Forever" Storyboard.TargetName="MatrixMovie" />public sealed class EmbeddedMediaExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
StreamResourceInfo __resourceStream = Application.GetResourceStream(this._source);
DirectoryInfo __directoryInfo = new DirectoryInfo(string.Format("{0}\\{1}", Directory.GetCurrentDirectory(), CACHE_FOLDER));
if (!__directoryInfo.Exists)
{
__directoryInfo.Create();
__directoryInfo.Attributes = FileAttributes.Hidden;
}
FileInfo __fileInfo = new FileInfo(string.Format("{0}/{1}", __directoryInfo.FullName, this._source.Segments[this._source.Segments.Length - 1]));
if (!_cache.ContainsKey(this._source))
{
if (!__fileInfo.Exists)
{
int __byte;
FileStream stream = new FileStream(__fileInfo.FullName, FileMode.CreateNew);
while ((__byte = __resourceStream.Stream.ReadByte()) != -1)
{
stream.WriteByte((byte)__byte);
}
stream.Close();
__resourceStream.Stream.Close();
}
_cache.Add(this._source, __fileInfo);
return new Uri(__fileInfo.FullName);
}
return new Uri(_cache[this._source].FullName);
}
}
Décoration 3D
Les contrôles sont prêts à être consommés par l'application tant en visuel 2D qu'en 3D. La classe Viewport2DVisual3D va jouer son rôle de décorateur 3D pour les contrôles Expander. Son utilisation est simple puisqu'elle s'ajoute comme élément enfant du contrôle Viewport3D tel qu'un Model3D classique. Il suffit de définir la Geometry3D à utiliser, dans mon cas une forme de type Plane sera suffisant, de définir le contrôle en tant qu'élément visuel et de spécifier que la texture à utiliser est un élément interactif.
<Viewport3D ClipToBounds="False" Width="168" Height="210">
<Viewport3D.Camera>
<PerspectiveCamera FarPlaneDistance="10" LookDirection="0,0,-1" UpDirection="0,1,0" NearPlaneDistance="1" Position="0,0,1.9" FieldOfView="45" />
</Viewport3D.Camera>
<Viewport2DVisual3D>
<Viewport2DVisual3D.Geometry>
<MeshGeometry3D Positions="-1,1,0 -1,-1,0 1,-1,0 1,1,0" TextureCoordinates="0,0 0,1 1,1 1,0" TriangleIndices="0,1,2 2,3,0" />
</Viewport2DVisual3D.Geometry>
<Viewport2DVisual3D.Material>
<DiffuseMaterial Viewport2DVisual3D.IsVisualHostMaterial="True" />
</Viewport2DVisual3D.Material>
<Viewport2DVisual3D.Transform>
<Transform3DGroup>
<ScaleTransform3D ScaleX="0.8" ScaleY="1" ScaleZ="1" />
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D Angle="18" Axis="1 0 0"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<TranslateTransform3D OffsetX="-0.25" OffsetY="-0.045" OffsetZ="0.24" />
</Transform3DGroup>
</Viewport2DVisual3D.Transform>
<lc:RDExpander Margin="2" ExpandDirection="Up" MouseEnter="_ExpanderEnter">
<lc:RDExpander.Header>
<lc:MatrixTextBlock Text="Enter Thor's matrix" ImmediateWrite="False" Width="128" Height="Auto" TextAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center" TextWrapping="WrapWithOverflow" />
</lc:RDExpander.Header>
<Grid>
<StackPanel Width="130" Height="70">
<TextBlock Margin="2">
<Hyperlink NavigateUri="http://blog.rioterdecker.net/blogs/valhalla">Thor's blog</Hyperlink>
</TextBlock>
<TextBlock Margin="2">
<Hyperlink NavigateUri="http://www.netfxfactory.org">NetFXFactory</Hyperlink>
</TextBlock>
<TextBlock Margin="2">
<Hyperlink NavigateUri="http://www.mexedge.com">MexEdge</Hyperlink>
</TextBlock>
</StackPanel>
</Grid>
</lc:RDExpander>
</Viewport2DVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<AmbientLight Color="#333333" />
<DirectionalLight Color="#FFFFFF" Direction="-0.612372,-0.5,-0.612372" />
<DirectionalLight Color="#FFFFFF" Direction="0.612372,-0.5,-0.612372" />
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
Le projet est attaché ainsi que le binaire.
J'adore WPF !! Une seule limite...celle de votre imagination !!