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.
<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 :
///
<
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.
///
<
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 :
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.
///
<
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.
///
<
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▲
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.