Créer un système de gestion de fenêtres avec silverlight 2

Florian Casabianca

Nous allons voir au travers de cet article comment développer un système de gestion de fenêtres en Silverlight 2 (un peu à la manière du bureau sous Windows). Nous aborderons notamment la création d'un Custom Control et son utilisation.

Commentez cet article : Commentez ♪

- Titre : Créer un système de gestion de fenêtres avec silverlight 2

- Auteur : Florian Casabianca

- Parution : 17 décembre 2008

- Mise à jour : 17 décembre 2008

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2008 Florian Casabianca. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- Introduction

- I. Création du Custom Control

- I-A. Le projet

- I-B. Le style par défaut

- I-C. Le code

- II. Le Window Manager

- III. Client de test

- III-A. Simple fenêtre

- III-B. Fenêtre stylée

- III-C. Fenêtre MDI

- Conclusion

- Sources

- Liens

- Remerciements

- Contact

Introduction

La solution Visual Studio sera composée de trois projets : un projet de type Silverlight Class Library qui contiendra le contrôle Window que nous allons développer, un projet de type Silverlight Application qui contiendra l'application cliente utilisant le contrôle et un projet Web qui hébergera l'application.

L'application de démonstration est disponible en ligne ici.

I - Création du Custom Control

I-A - Le projet

Nous créons donc tout d'abord un projet de type Silverlight Class Library que nous appelons SilverlightWindowsControls. Nous y ajoutons une nouvelle classe appelée Window. Pour que notre fenêtre soit un contrôle, elle doit dériver au minimum de System.Windows.Controls.Control. Dans notre cas, nous la ferons dériver de System.Windows.Controls.ContentControl qui est une classe un peu plus spécialisée ajoutant la propriété Content à notre contrôle.

public class Window : ContentControl
{}

Elle indique par ailleurs que le contrôle ne pourra contenir (via la propriété Content) qu'un seul élément. Il s'agit du même comportement que le contrôle Button par exemple. Ainsi, pour mettre plusieurs éléments dans la fenêtre nous pourrons par exemple utiliser une Grid en tant qu'élément unique et positionner les différents éléments à insérer dans cette Grid.

Nous devons ensuite définir un Template par défaut à notre fenêtre. Pour cela il faut créer un répertoire themes contenant un fichier generic.xaml. Vous devez obligatoirement respecter cette hiérarchie et ces règles de nommage.

Vérifier ensuite que dans les propriétés du fichier generic.xaml la propriété Build Action est positionnée à Resource et que la propriété Custom Tool est vide :

Le projet devrait maintenant ressembler à ceci :

I-B - Le style par défaut

Le fichier generic.xaml contient une balise ResourceDictionnary dans laquelle nous allons déclarer les différents styles et templates utilisés par défaut par notre contrôle.

Nous définissons un template pour le bouton fermer et un style pour la fenêtre. Sur ce dernier nous devons définir sa propriété TargetType. Dans notre cas il s'agit du contrôle Window. Cette valeur est aussi reportée sur le ControlTemplate en dessous.

Fichier generic.xaml

<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:SilverlightWindowsControls"
xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">

<ControlTemplate x:Key="btnCloseTemplate" TargetType="Button">
<!--Template du bouton Close-->
</ControlTemplate>
<Style TargetType="custom:Window">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="custom:Window">
<!--Template de la fenêtre-->
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

</ResourceDictionary>

Notez la déclaration du préfix custom définissant le namespace de notre assembly.

Le code ci-dessus n'est qu'un extrait du fichier generic.xaml utilisé dans le projet. Vous pouvez visualiser l'ensemble du code dans les sources fournies avec l'article. Il est cependant nécessaire de connaître l'organisation du template de la fenêtre afin de comprendre le reste de l'article.

Voici la hiérarchie des différents éléments du template de la fenêtre vue depuis Expression Blend :

Comme vous le voyez, il n'y a pas énormément d'éléments et ceux-ci sont très simples (rectangle, grill, textblock). Certains éléments du template ont un nom afin de pouvoir les manipuler dans le code du contrôle. Par convention, ces noms commencent par le texte « PART_ ». Ils permettent d'indiquer à la personne qui créera un nouveau template pour la fenêtre que ces éléments sont importants et que leur modification entrainera une modification du comportement du contrôle.

Par exemple, en haut de la fenêtre se trouve un élément nommé PART_TranslateZone. Il définit la zone permettant de déplacer la fenêtre. Si l'on attribue un nouveau template à la fenêtre et que celui-ci ne contient pas d'élément nommé PART_TranslateZone, alors la fenêtre ne sera plus déplaçable. Par contre il est possible dans ce nouveau template de modifier l'emplacement de cet élément. On pourrait par exemple le placer en bas de la fenêtre. Ainsi il faudrait cliquer sur le bas de la fenêtre pour la déplacer, mais la fonctionnalité serait toujours présente.

PART_Icone est un contrôle Image permettant d'afficher l'icône de la fenêtre.

PART_Caption est un contrôle TextBlock qui affiche le titre de la fenêtre.

PART_CloseButton est un contrôle Button qui permet de fermer la fenêtre.

PART_RightSide, PART_LeftSide, PART_BottomSide et PART_TopSide sont des éléments placés respectivement sur le côté droit, gauche, bas et haut de la fenêtre. Ils définissent les zones de redimensionnement de la fenêtre.

I-C - Le code

Revenons à la classe Window et voyons ce qu'il faut y mettre.

Nous déclarons un événement WindowClosed qui sera déclenché lorsque l'on cliquera sur le bouton de fermeture ainsi qu'une référence sur les différents éléments nommés du template (voir ci-dessus).

Vous remarquerez que nous utilisons le type FrameworkElement afin de référencer la zone de translation et celles de redimensionnement alors qu'elles sont définies comme étant des Rectangle (qui dérive de FrameworkElement) dans le template. Cela nous évite d'être trop fortement liés au type d'élément défini dans le template. Ainsi nous pourrons appliquer un nouveau template à la fenêtre où la zone de translation serait une Ellipse ou un Path par exemple, car ces éléments dérivent eux aussi de FrameworkElement.

public event EventHandler<EventArgs> WindowClosed;

private FrameworkElement _translateZone;
private TextBlock _captionText;
private Image _icon;
private Button _closeButton;
//redimensionnement
FrameworkElement _rightSide;
FrameworkElement _leftSide;
FrameworkElement _bottomSide;
FrameworkElement _topSide;

public Window()
: base()
{
this.DefaultStyleKey = typeof(Window);
}

La propriété DefaultStyleKey indique la clef à utiliser pour le style du contrôle. Nous l'assignons avec le type du contrôle.

Nous ajoutons ensuite à la fenêtre quatre dependency properties.

ResizeEnabledProperty est un booléen indiquant si la fenêtre peut être redimensionnée ou non :

ResizeEnabledProperty

/// <summary>
/// Indique si la fenêtre peut être redimensionnée
/// </summary>
public bool ResizeEnabled
{
get { return (bool)GetValue(ResizeEnabledProperty); }
set { SetValue(ResizeEnabledProperty, value); }
}

public static readonly DependencyProperty ResizeEnabledProperty =
DependencyProperty.Register("ResizeEnabled ", typeof(bool), typeof(Window), new PropertyMetadata(null));

WindowStartupLocationProperty de type WindowStartupLocation indique la position de la fenêtre lorsqu'elle est ouverte pour la première fois.

WindowStartupLocationProperty

/// <summary>
/// Indique la position de la fenêtre lorsqu'elle est ouverte pour la première fois.
/// </summary>
public WindowStartupLocation WindowStartupLocation
{
get { return (WindowStartupLocation)GetValue(WindowStartupLocationProperty); }
set { SetValue(WindowStartupLocationProperty, value); }
}

public static readonly DependencyProperty WindowStartupLocationProperty =
DependencyProperty.Register("WindowStartupLocation", typeof(WindowStartupLocation), typeof(Window), new PropertyMetadata(null));

Le type WindowStartupLocation est une énumération :

WindowStartupLocation

public enum WindowStartupLocation
{
/// <summary>
/// L'emplacement de démarrage d'un objet Window est défini à partir du code.
/// </summary>
Manual,
/// <summary>
/// L'emplacement de démarrage d'un objet Window correspond au centre de l'objet parent.
/// </summary>
CenterParent
};

TitleProperty permet de spécifier le titre de la fenêtre.

TitleProperty

/// <summary>
/// Titre de la fenêtre
/// </summary>
public String Title
{
get { return (String)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}

public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(String), typeof(Window), new PropertyMetadata(new PropertyChangedCallback(OnTitleChanged)));

private static void OnTitleChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o == null)
throw new ArgumentNullException("o");
Window win = o as Window;
if (win == null)
throw new InvalidCastException(string.Format("Le type '{0}' ne derive pas du type 'Window'.", o.GetType().FullName));

win.ProcessText();
}

Lorsque sa valeur change, nous appelons la méthode ProcessText que nous verrons plus loin.

Enfin, la propriété IconProperty permet de spécifier l'icône de la fenêtre.

IconProperty

/// <summary>
/// Icone de la fenêtre
/// </summary>
public ImageSource Icon
{
get { return (ImageSource)GetValue(IconProperty); }
set { SetValue(IconProperty, value); }
}

public static readonly DependencyProperty IconProperty =
DependencyProperty.Register("Icon", typeof(ImageSource), typeof(Window), new PropertyMetadata(new PropertyChangedCallback(OnIconChanged)));

private static void OnIconChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o == null)
throw new ArgumentNullException("o");
Window win = o as Window;
if (win == null)
throw new InvalidCastException(string.Format("Le type '{0}' ne derive pas du type 'Window'.", o.GetType().FullName));

win.ProcessImage();
}

Lorsque sa valeur change, nous appelons la méthode ProcessImage que nous verrons plus loin.

Nous définissons aussi deux propriétés Top et Left permettant d'accéder plus rapidement aux Attached Properties Canvas.TopProperty et Canvas.LeftProperty :

public double Top
{
get { return (double)GetValue(Canvas.TopProperty); }
set { SetValue(Canvas.TopProperty, value); }
}

public double Left
{
get { return (double)GetValue(Canvas.LeftProperty); }
set { SetValue(Canvas.LeftProperty, value); }
}

Nous allons maintenant redéfinir la méthode OnApplyTemplate qui est l'endroit où nous allons pouvoir accéder aux différents éléments du template afin de s'abonner à certains événements ou définir des DataBinding.

Voici la méthode OnApplyTemplate du contrôle Window :

/// <summary>
/// Appelée lorsque le template est appliqué
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();

_translateZone = GetTemplateChild("PART_TranslateZone") as FrameworkElement;

_captionText = GetTemplateChild("PART_CaptionText") as TextBlock;

_icon = GetTemplateChild("PART_Icone") as Image;

_closeButton = GetTemplateChild("PART_CloseButton") as Button;

_rightSide = GetTemplateChild("PART_RightSide") as FrameworkElement;

_leftSide = GetTemplateChild("PART_LeftSide") as FrameworkElement;

_bottomSide = GetTemplateChild("PART_BottomSide") as FrameworkElement;

_topSide = GetTemplateChild("PART_TopSide") as FrameworkElement;

DefineButtonsEvents();    DefineDragEvents();
DefineResizeEvents();

ProcessText();
ProcessImage();
}

Nous utilisons la méthode GetTemplateChild (que l'on hérite de la classe FrameworkElement) afin de rechercher les éléments du template par leur nom.

Les méthodes ProcessText et ProcessImage sont très simples :

private void ProcessText()
{
if (_captionText != null && Title != null)
{
_captionText.Text = Title;
}
}

private void ProcessImage()
{
if (_icon != null && Icon != null)
{
_icon.Source = Icon;
}
}

La méthode DefineButtonsEvents permet de s'abonner à l'événement Click sur le bouton de fermeture de la fenêtre afin de déclencher l'événement WindowClosed que nous avons défini précédemment.

private void DefineButtonsEvents()
{
if (_closeButton != null)
_closeButton.Click += new RoutedEventHandler(closeButton_Click);
}

private void closeButton_Click(object sender, RoutedEventArgs e)
{
OnWindowClosed();
}

La méthode DefineDragEvents permet de s'abonner aux événements de la souris sur la zone de translation.

private void DefineDragEvents()
{
if (_translateZone != null)
{
_translateZone.MouseLeftButtonDown += new MouseButtonEventHandler(translateZone_MouseLeftButtonDown);
_translateZone.MouseLeftButtonUp += new MouseButtonEventHandler(translateZone_MouseLeftButtonUp);
_translateZone.MouseMove += new MouseEventHandler(translateZone_MouseMove);
}
}

L'algorithme pour la translation de la fenêtre est défini ainsi :

bool _isDrag;
Point StartingDragPoint;

private void translateZone_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_isDrag = true;

//on commence le Drag
FrameworkElement DragBar = (FrameworkElement)sender;
DragBar.CaptureMouse();

// Point de départ du drag
StartingDragPoint = e.GetPosition(this);
}

private void translateZone_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
//on arrête le Drag
FrameworkElement translateZone = (FrameworkElement)sender;
translateZone.ReleaseMouseCapture();

_isDrag = false;
}

private void translateZone_MouseMove(object sender, MouseEventArgs e)
{
if (_isDrag)
{
UIElement ui = (UIElement)this.Parent;
Point Point = e.GetPosition(ui);

Move(Point.X - StartingDragPoint.X, Point.Y - StartingDragPoint.Y);
}
}
// Déplace la fenêtre à l'emplacement spécifié
public void Move(double left, double top)
{
Left = left;
Top = top;
}

La méthode DefineResizeEvents va permettre de s'abonner aux événements liés à la souris sur les différentes zones de redimensionnement. Bien sûr, il faut vérifier que la fonctionnalité est activée (propriété ResizeEnabled) et que les différentes zones existent dans le template.

private void DefineResizeEvents()
{
//si le redimensionnement n'est pas autorisé
if(!ResizeEnabled)
return;

if (_rightSide != null)
{
_rightSide.MouseLeftButtonDown += OnSideMouseLeftButtonDown;
_rightSide.MouseLeftButtonUp += OnSideMouseLeftButtonUp;
_rightSide.MouseMove += OnRightSideMouseMove;
}
if (_leftSide != null)
{
_leftSide.MouseLeftButtonDown += OnSideMouseLeftButtonDown;
_leftSide.MouseLeftButtonUp += OnSideMouseLeftButtonUp;
_leftSide.MouseMove += OnLeftSideMouseMove;
}
if (_bottomSide != null)
{
_bottomSide.MouseLeftButtonDown += OnSideMouseLeftButtonDown;
_bottomSide.MouseLeftButtonUp += OnSideMouseLeftButtonUp;
_bottomSide.MouseMove += OnBottomSideMouseMove;
}
if (_topSide != null)
{
_topSide.MouseLeftButtonDown += OnSideMouseLeftButtonDown;
_topSide.MouseLeftButtonUp += OnSideMouseLeftButtonUp;
_topSide.MouseMove += OnTopSideMouseMove;
}
}

Nous n'expliquerons pas ici le code des différentes méthodes. Vous pouvez le consulter via les sources de l'application donnée en exemple.

II - Le Window Manager

Le contrôle Window étant terminé nous allons nous pencher sur la classe WindowManager dont le rôle sera de gérer un ensemble de fenêtres au sein d'un contrôle Canvas. Cette classe se trouvera dans le projet de l'application Silverlight.

Nous y définissons deux événements qui permettront aux abonnés d'être notifiés de l'ajout ou de la suppression d'une fenêtre sur le Canvas.

/// <summary>
/// Événement lancé lorsqu'une fenêtre est ajoutée au manager
/// </summary>
public event EventHandler<WindowEventArgs> WindowAdded;

/// <summary>
/// Événement lancé lorsqu'une fenêtre est supprimée au manager
/// </summary>
public event EventHandler<WindowEventArgs> WindowRemoved;

public class WindowEventArgs : EventArgs
{
public Window Window { get; private set; }

public WindowEventArgs(Window win)
{
Window = win;
}
}

Il nous faut bien sûr une référence sur le Canvas associé et une sur la liste des fenêtres qui s'y trouvent :

/// <summary>
/// Le canvas où sont affichées les fenêtres
/// </summary>
public Canvas Surface { get; private set; }
/// <summary>
/// Liste des fenêtres gérées par le manager.
/// </summary>
public IList<Window> Windows
{
get { return _listWindows.AsReadOnly(); }
}

private readonly List<Window> _listWindows;

Enfin, une propriété ActiveWindow permettra de renvoyer ou de définir la fenêtre active du Canvas (celle se trouvant au premier plan).

private Window _activeWindow;

/// <summary>
/// Renvoie ou définit la fenêtre active
/// </summary>
public Window ActiveWindow
{
get { return _activeWindow; }
set
{
if (value == null)
return;
if (!_listWindows.Contains(value))
{
throw new InvalidOperationException("L'objet Window ne fait pas partie de la liste des fenêtres gérées par le manager.");
}
_activeWindow = value;

//on met la fenêtre au premier plan
PutWindowOnTop(_activeWindow);

_activeWindow.Focus();
}
}

La méthode PutWindowOnTop agit sur la valeur du ZIndex de la fenêtre. La variable _currentZIndex contient la valeur de la propriété ZIndex de la fenêtre active.

private int _currentZIndex = 1;

//Amène la fenêtre au premier plan
protected void PutWindowOnTop(Window activeWindow)
{
Canvas.SetZIndex(activeWindow, _currentZIndex++);
}

Le constructeur de la classe WindowManager prend en paramètre le Canvas associé et instancie la liste de fenêtres :

public WindowsManager(Canvas surface)
{
if (surface == null)
throw new ArgumentNullException("surface");

Surface = surface;
_listWindows = new List<Window>();
}

Le manager possède deux méthodes public, AddWindow et RemoveWindow qui permettent d'ajouter ou de retirer une fenêtre du Canvas.

Voici le code de la méthode AddWindow :

/// <summary>
/// Ajoute une fenêtre au manager
/// </summary>
/// <param name="window">La fenêtre à ajouter</param>
public void AddWindow(Window window)
{
//si la fenêtre n'est pas déjà dans la liste
if (!_listWindows.Contains(window))
{
_listWindows.Add(window);

//on s'abonne aux différents événements
window.WindowClosed += OnWindowClosed;
window.MouseLeftButtonDown += OnWindowMouseLeftButtonDown;

if (window.WindowStartupLocation == WindowStartupLocation.CenterParent)
{
window.Move(Surface.ActualWidth / 2 - window.Width / 2, Surface.ActualHeight / 2 - window.Height / 2);
}

//on ajoute la fenêtre au canvas
Surface.Children.Add(window);
//on met la fenêtre au premier plan
PutWindowOnTop(window);
//on lance l'événement WindowAdded
OnWindowAdded(new WindowEventArgs(window));
}
}

Nous vérifions tout d'abord que la fenêtre à ajouter n'est pas déjà présente dans la liste des fenêtres gérées par le manager. Si elle ne l'est pas alors nous l'ajoutons à la liste et nous nous abonnons aux événements WindowClosed (déclenché quand on clique sur le bouton de fermeture de la fenêtre) et MouseLeftButtonDown (quand l'utilisateur clique sur la fenêtre). Si la position d'affichage de la fenêtre est définie sur CenterParent alors nous déplaçons la fenêtre au centre du Canvas. Enfin nous ajoutons la fenêtre aux éléments enfants du Canvas, la positionnons au premier plan et déclenchons l'événement WindowAdded.

La méthode RemoveWindow est tout aussi simple et fait l'inverse de la méthode AddWindow :

/// <summary>
/// Supprime la fenêtre du manager
/// </summary>
/// <param name="window">La fenêtre à supprimer</param>
public void RemoveWindow(Window window)
{
//si la fenêtre n'est dans la liste
if (!_listWindows.Contains(window))
{
throw new InvalidOperationException("L'objet Window ne fait pas partie de la liste des fenêtres gérées par le manager.");
}

_listWindows.Remove(window);

//on se désabonne des différents événements
window.WindowClosed -= OnWindowClosed;
window.MouseLeftButtonDown -= OnWindowMouseLeftButtonDown;

//on supprime la fenêtre du canvas
Surface.Children.Remove(window);

//on lance l'événement WindowRemoved
OnWindowRemoved(new WindowEventArgs(window));
}

III - Client de test

Le contrôle Window et le manager étant terminés nous allons pouvoir les utiliser.

III-A - Simple fenêtre

Dans le projet SilverlightWindowsClient nous allons définir le code XAML de la classe Page ainsi :

<UserControl x:Class="SilverlightWindowsClient.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:SilverlightWindowsControls="clr-namespace:SilverlightWindowsControls;assembly=SilverlightWindowsControls"
Width="Auto" Height="Auto">
<Canvas x:Name="LayoutRoot" Background="LemonChiffon">

<Button Content="Créer une fenêtre" Click="CreateWindowButton_Click" Canvas.Left="20" Canvas.Top="20"/>
<Button Content="Créer une fenêtre avec un nouveau style et placement aléatoire"
Click="CreateStyledWindowButton_Click" Canvas.Top="50" Canvas.Left="20"/>
<Button Content="Créer une fenêtre MDI" Click="CreateMdiWindowButton_Click" Canvas.Top="80" Canvas.Left="20"/>

</Canvas>
</UserControl>

L'élément conteneur principal sera un Canvas dans lequel nous ajoutons trois boutons. À l'événement Click de chaque bouton est associée une méthode dont nous découvrirons le code au fur et à mesure de l'article.

Le contrôle Window que nous venons créer ne fait pas partie des contrôles de base de WP et le parser XAML ne va donc pas savoir le traiter. Pour résoudre ce problème, nous devons mapper le namespace .NET du contrôle vers un namespace XML. La syntaxe XAML à utiliser est la suivante :

xmlns:prefix="clr-namespace:Namespace;assembly=AssemblyName"

Prefix est le préfixe XML que nous voulons utiliser dans le code XAML pour représenter le namespace associé.

Namespace est le nom complet du namespace .NET du contrôle.

AssemblyName est le nom de l'assembly (sans .dll) où le type .NET est déclaré.

Dans notre cas nous utiliserons la déclaration suivante :

xmlns:SilverlightWindowsControls="clr-namespace:SilverlightWindowsControls;assembly=SilverlightWindowsControls"

Nous pouvons dans un premier temps créer une fenêtre entièrement en XAML. Pour cela, il suffit de saisir par exemple le code ci-dessous dans la balise Canvas du code précédent :

<SilverlightWindowsControls:Window Canvas.Top="200" Canvas.Left="200"
Width="364" Height="255" Icon="Resources/Generic_Application.png">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock TextWrapping="Wrap" Margin="0,0,0,10">
<Run Text="Ceci est une fenêtre créé depuis le code XAML. "/>
<LineBreak/>
<Run Text="Aucun windowManager ne lui est associé."/>
<LineBreak/>
<Run Text="Vous pouvez y mettre ce que vous voulez"/>
</TextBlock>
<Image Width="Auto" Grid.Row="1" Height="Auto" Source="Resources/microsoft_silverlight.jpg" HorizontalAlignment="Left"/>
<Button HorizontalAlignment="Left" VerticalAlignment="Top" Content="Button" Grid.Row="2"/>
<ComboBox HorizontalAlignment="Left" VerticalAlignment="Top" Grid.Row="3" Width="100">
<ComboBoxItem Content="Choix 1"/>
<ComboBoxItem Content="Choix 2"/>
</ComboBox>
</Grid>
</SilverlightWindowsControls:Window>

Le contrôle Window dérivant de ContentControl, celui-ci ne peut contenir qu'un seul élément enfant. Nous y plaçons donc un contrôle Grid (qui lui pourra accueillir plusieurs autres éléments). À l'intérieur nous y mettons un peu de texte, une image, un bouton et un combobox. Ce n'est qu'un simple exemple, mais vous pourriez, pourquoi pas, y placer une vidéo.

À l'exécution nous obtenons le résultat suivant :

La fenêtre peut être déplacée, mais pas fermée. En effet, aucun WindowManager ne la prend actuellement en charge.

Laissons de côté cet exemple et revenons au code C# de la page. Dans le constructeur nous instancions un WindowManager (en lui passant en paramètres le Canvas déclaré dans le code XAML) ainsi qu'un objet BitmapImage qui nous servira d'icône pour les différentes fenêtres que nous allons créer depuis le code C#.

private WindowsManager _windowsManager;
private BitmapImage _genericImage;

public Page()
{
InitializeComponent();
_windowsManager = new WindowsManager(LayoutRoot);

StreamResourceInfo sr = Application.GetResourceStream(
new Uri("SilverlightWindowsClient;component/Resources/Generic_Application.png", UriKind.Relative));
_genericImage = new BitmapImage();
_genericImage.SetSource(sr.Stream);
}

Un clic sur le premier bouton (« Créer une fenêtre ») exécutera la méthode suivante :

private void CreateWindowButton_Click(object sender, RoutedEventArgs e)
{
Window f = new Window();
f.Title = "Nouvelle fenêtre";
f.Content = "Ceci est une fenêtre avec du texte à l'intérieur.";
f.Icon = _genericImage;
f.Height = 200;
f.Width = 350;
f.WindowStartupLocation = WindowStartupLocation.CenterParent;

//on ajoute la fenêtre au manager
_windowsManager.AddWindow(f);
}

Ici nous créons une simple fenêtre avec uniquement du texte comme contenu et l'affichons à l'écran. Plutôt simple, non ? On dirait presque le code de création d'une fenêtre WPF.

Le résultat à l'exécution :

Les fenêtres étant gérées par un WindowManager le bouton de fermeture est maintenant fonctionnel. De plus, chaque clic sur une fenêtre la fera passer au premier plan (par-dessus les autres fenêtres).

III-B - Fenêtre stylée

L'apparence actuelle de la fenêtre (fond gris, bouton de fermeture carré, texte noir) est définie dans le style par défaut du contrôle (fichier generic.xaml). Mais vous savez sans doute qu'il est facile de modifier le style ou le template d'un contrôle en Silverlight.

Dans les ressources de l'application (fichier App.xaml) se trouve un nouveau style pour le contrôle Window et se nommant NewWindowStyle. Nous ne commenterons pas le code XAML du style, mais allons seulement l'utiliser. Ce style a été créé via Expression Blend.

Un clic sur le deuxième bouton de l'interface déclenche la méthode suivante :

private void CreateStyledWindowButton_Click(object sender, RoutedEventArgs e)
{
int height = 300;
int width = 450;

Window f = new Window();
f.Title = "Fenêtre avec un nouveau style";
f.Content = "Ceci est une fenêtre utilisant un style/template en remplacement de celui par défaut."
+ Environment.NewLine
+ "Les fonctionnalités restent bien sûr les mêmes !";

f.Height = height;
f.Width = width;
//on génère une position aléatoire
Random generator = new Random();
int randomTop = generator.Next(0, (int)LayoutRoot.ActualHeight - height);
int randomLeft = generator.Next(0, (int)LayoutRoot.ActualWidth - width);

//ici on spécifie la position d'affichage depuis le code
f.Left = randomLeft;
f.Top = randomTop;
f.WindowStartupLocation = WindowStartupLocation.Manual;

//on lui applique un style défini dans les ressources de l'application (fichier App.xam)
Style style = Application.Current.Resources["NewWindowStyle"] as Style;
f.Style = style;

//on lui ajoute une icône
f.Icon = _genericImage;

//on ajoute la fenêtre au manager
_windowsManager.AddWindow(f);
}

Ce code est pratiquement similaire au précédent. Les deux seules différences étant une génération aléatoire des coordonnées de la fenêtre (associées à la valeur WindowStartupLocation.Manual de la propriété WindowStartupLocation) et l'attribution d'un nouveau style pour remplacer celui par défaut.

À l'exécution nous obtenons le résultat suivant :

Le comportement des fenêtres n'a pas du tout changé. Le déplacement, la fermeture, la mise au premier plan fonctionnent de la même façon, seule l'apparence visuelle est différente.

III-C - Fenêtre MDI

Nous avons vu que nous pouvions mettre n'importe quoi comme contenu d'une fenêtre. Alors pourquoi ne pas y mettre d'autres fenêtres et ainsi implémenter un système de fenêtres MDI ?

Pour cela nous allons créer un UserControl appelé MdiContent qui sera le contenu de la fenêtre. Le code XAML de ce contrôle est très simple, car il ne fait que déclarer un Canvas :

<UserControl x:Class="SilverlightWindowsClient.MdiContent"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="Auto" Height="Auto">
<Canvas x:Name="LayoutRoot" Background="White">

</Canvas>
</UserControl>

Dans le code C# du UserControl nous allons déclarer un WindowManager et créer cinq fenêtres lorsque le contrôle sera chargé :

public partial class MdiContent : UserControl
{
WindowsManager _windowsManager;

public MdiContent()
{
InitializeComponent();
_windowsManager = new WindowsManager(LayoutRoot);

Loaded += MdiContent_Loaded;
}

void MdiContent_Loaded(object sender, RoutedEventArgs e)
{
for (int i = 0; i < 5; i++)
{
Window f = new Window();
f.Title = "Fenêtre fille " + i;
f.Content = "Ceci est une fenêtre fille.";
f.Height = 200;
f.Width = 200;
f.Left = 50 * i;
f.Top = 50 * i;
f.WindowStartupLocation = WindowStartupLocation.Manual;

_windowsManager.AddWindow(f);
}
}
}

Comme vous le voyez, il n'y a vraiment rien de nouveau au niveau du code.

Revenons au code C# de la page et saisissons le code associé au clic sur le troisième bouton :

private void CreateMdiWindowButton_Click(object sender, RoutedEventArgs e)
{
//le contenu de la fenêtre
MdiContent content = new MdiContent();

Window f = new Window();
f.Title = "Fenêtre MDI";
f.Content = content;
f.Height = 500;
f.Width = 600;
f.WindowStartupLocation = WindowStartupLocation.CenterParent;

f.Icon = _genericImage;

_windowsManager.AddWindow(f);
}

Nous créons simplement une fenêtre comme nous en avons maintenant l'habitude, mais lui mettons comme contenu le UserControl créé précédemment.

À l'exécution nous obtenons le résultat suivant :

Les fenêtres filles étant associées à un WindowManager, les fonctionnalités de fermeture, déplacement et mise au premier plan restent bien sûr fonctionnelles.

Conclusion

Nous avons pu voir qu'il était relativement simple de mettre en place un système de gestion de fenêtres en Silverlight 2. La création d'un Custom Control, bien qu'étant un peu technique, offre une grande souplesse d'utilisation notamment grâce à la possibilité de modifier complètement le style/template associé.

Le système de gestion de fenêtres que nous venons de créer reste très basique, mais il n'attend que vous pour être amélioré. Vous pouvez par exemple ajouter les boutons de réduction et d'agrandissement de la fenêtre et modifier le manager pour qu'il prenne en charge ces nouvelles fonctionnalités. Bref, à vous de jouer !

Sources

Téléchargez les sources de la solution donnée en exemple.

Liens

Un autre article sur la simulation de fenêtres

Création d'un custom control MSDN Magazine

Remerciements

J'adresse ici tous mes remerciements à l'équipe de rédaction de « developpez.com » pour le temps qu'ils ont bien voulu passer à la correction et à l'amélioration de cet article.

Contact

Si vous constatez une erreur dans le tutoriel, dans les sources, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par le forum.