I. SPÉCIFICATIONS▲
L'application se présente de la façon suivante : Un lecteur MP3 rudimentaire reposant sur la brique WPF pour l'aspect visuel et DirectX.AudioVideoPlayBack pour les fonctionnalités audios, plus quelques fonctions d'édition de tags MP3.
Fonctionnalités requises
- Lecture d?un morceau MP3
- Pause
- Arrêt
- Parcourir de fichier sur le PC pour sélectionner le fichier à lire
- Un éditeur de tags pour modifier les tags associés au morceau MP3 en cours de lecture
- Une image d'album associée au morceau MP3, qui peut être redimensionnée à la volée via un slider
Liste des composants dans la partie Lecteur audio
|
Nom Composant |
Type |
Titre |
|
Lecteur |
DirectX.AudioVideoPlayBack.Audio |
|
|
btnParcourir |
Button |
… |
|
lTitre |
Label |
Titre de la chanson |
|
btnPause |
Button |
Pause |
|
btnArret |
Button |
Arrêt |
|
btnLecture |
Button |
Lecture |
|
imgAlbum |
Image |
|
|
sliderImg |
Slider |
|
|
cbxStyle |
comboBox |
Style Garçon / Style Fille |
Liste des composants dans la partie Editeur de tags MP3
|
Nom Composant |
Type |
Titre |
|
txtbTitre |
TextBox |
|
|
txtbAuteur |
TextBox |
|
|
btnOk |
Button |
Enregistrer |
II. PRE-REQUIS▲
Pour exécuter l'application
- .Net Framework 3.0
- Direct X 9.0 (DirectX.AudioVideoPlayBack.dll, mais aussi DirectX.dll) SDK DirectX 9
Pour réaliser l'application
- Visual C# Express 2008 ici
III. ARCHITECTURE▲
Pour construire une application modulaire et évolutive, l'approche MDA conseille de distinguer trois couches au sein de l'application, qui interagissent entre elles :
- La couche graphique (classe Dialogue) ;
- La couche métier (classes Contrôle) ;
- La couche données (classe Entité).
Ça tombe bien, car WPF en décrivant les interfaces grâce à une syntaxe déclarative (le Xaml), isole la couche graphique du reste de l'application. En ce qui concerne la couche métier, on recense deux classes, une concernant les fonctions de lecture et une concernant les fonctions d'édition de tags.
La réalisation de l'application suivra le sens suivant : Classe d'entité -> Classes de contrôle -> Classe de dialogue.
Car la classe de dialogue encapsule (et donc dépend de) les classes de contrôle qui elles-mêmes encapsulent la classe d'entité. En effet, la classe DialogueIHMLecteur appellera les méthodes des classes ControleLecture et ControleEditeur, suivant les actions de l'utilisateur.
IV. RÉALISATION▲
Ouvrir Visual C# Express 2008. Choisir dans le Menu File -> New Project -> WPF Application
IV-A. Conception de la couche Entité (EntiteMorceau.cs)▲
Cette couche ne contient que la définition de la structure strTagsMP3 qui est une représentation d'un fichier MP3 avec ses tags titre, artiste, et genre
public struct strTagsMP3
{
public string tagOK; // 03 car
public String titre; // 30 car
public String artiste; // 30 car
public short genre; // 01 car
// total 128 car
}La classe EntiteMorceau implémente cette structure et contient simplement les accesseurs pour accéder en lecture / modification à chacun des tags.
public class EntiteMorceau
{
private strTagsMP3 TagsMP3;
public strTagsMP3 accTagsMP3
{
get
{
return TagsMP3;
}
set
{
TagsMP3.tagOK = value.tagOK;
TagsMP3.titre = value.titre;
TagsMP3.artiste = value.artiste;
TagsMP3.genre = value.genre;
}
}
public string accTagOk
{
get
{
return TagsMP3.tagOK;
}
set
{
TagsMP3.tagOK = value;
}
}
public String accTitre
{
get
{
return TagsMP3.titre;
}
set
{
TagsMP3.titre = value;
}
}
public String accArtiste
{
...
}
public short accGenre
{
...
}
}
}IV-B. Conception de la couche Contrôle (ControleEditeurTags.cs et ControleLecture.cs))▲
La classe ControleEditeur contient en particulier les trois méthodes OuvrirFichier, PossedeTag et EcrireTag qui sont des adaptations du code source du hors-série code(r) #4. La méthode PossedeTag ouvre un filestream sur le fichier passé en paramètre, se positionne 128 bits avant la fin du fichier, lit les trois premiers octets du filestream et détecte si le tableau de bits ainsi stocké contient la chaîne « TAG ».
Si la chaîne « TAG » est stockée, c'est que ce fichier MP3 contient effectivement des tags.
private bool PossedeTag(string fichier)
{
// ouvrir le fichier
FileStream f = new FileStream(fichier, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// se positionner sur le tag
f.Seek(-128, SeekOrigin.End);
//ouverture d'un binaryreader sur le fichier
BinaryReader br = new BinaryReader(f);
byte[] tagArray = br.ReadBytes(3);
// lire les 3 premiers octets
string tag = System.Text.Encoding.Default.GetString(tagArray);
br.Close();
// si ces octets contiennent "TAG", c'est qu'il y a un tag
if (tag.CompareTo("TAG")==0)
return true;
else
return false;
}La méthode OuvrirFichier prend en paramètre le chemin du fichier à ouvrir (type String), lit les tags du fichier en se positionnant 128 bits avant la fin du fichier puisque c'est à cet endroit d'un fichier MP3 que les tags sont stockés.
Elle vide la structure de l'objet de type EntiteMorceau de ses éventuelles valeurs en mémoire. Elle appelle la méthode PossedeTag. Si le fichier ne possède pas de tag MP3, elle rend l'objet Morceau tel quel, c'est-à-dire vide.
Sinon, la méthode se positionne 128 bits avant la fin du fichier pour lire les tags (f.seek(-128, SeekOrigin.End)). On lit alors le contenu des tags, en éliminant le caractère « / » qui fonctionne comme un espace vide.
Pour le dernier caractère, on utilisera la méthode PeekChar, pour le lire, sans avancer dans le fichier, sinon une exception est levée.
Puis on attribue grâce aux accesseurs les tags adéquats titre, artiste, genre. Concernant les genres musicaux, on a simplifié la liste en ne gardant que six genres principaux :
- genres[8] => « Jazz » ;
- genres[9] => « Metal » ;
- genres[13] => « Pop » ;
- genres[15] => « Rap »;
- genres[18] => « Techno » ;
- genres[32] => « Classical ».
Si le fichier contient un genre différent, on lui attribue le genre[0] c'est-à-dire « divers »
public class ControleEditeurTags
{
private EntiteMorceau Morceau = null;
public EntiteMorceau OuvrirFichier(String fichier)
{
if (fichier != null)
{
Morceau = new EntiteMorceau();
// on "vide" le tag
Morceau.accTagOk = "TAG";
Morceau.accTitre = "";
Morceau.accArtiste = "";
Morceau.accGenre = -1;
// si le mp3 ne possède pas de tag à lire, on retourne le tag vide
if (!PossedeTag(fichier))
{
return Morceau;
}
// ouvrir le fichier
FileStream f = new FileStream(fichier, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// se positionner sur le tag
f.Seek(-128, SeekOrigin.End);
//ouverture d'un binaryreader sur le fichier
BinaryReader br = new BinaryReader(f);
char charsToTrim = '/';
// lire chaque élément
Morceau.accTagOk = System.Text.Encoding.Default.GetString(br.ReadBytes(3));
Morceau.accTitre = System.Text.Encoding.Default.GetString(br.ReadBytes(30));
Morceau.accTitre = Morceau.accTitre.TrimEnd(charsToTrim);
Morceau.accArtiste = System.Text.Encoding.Default.GetString(br.ReadBytes(30));
Morceau.accArtiste = Morceau.accArtiste.TrimEnd(charsToTrim);
Morceau.accGenre = (short)br.PeekChar();
//Attribution d'un genre par défaut "Divers", si le fichier n'appartient à aucune des grandes familles de genres musicaux
if (Morceau.accGenre != 8 && Morceau.accGenre != 9 && Morceau.accGenre != 13 && Morceau.accGenre != 15 && Morceau.accGenre != 18 && Morceau.accGenre != 32)
{
Morceau.accGenre = 0;
}
// fermeture du fichier (la fermeture du BinaryReader ferme le FileStream)
br.Close();
// retourner le tag lu
return Morceau;
}
else
{
return null;
}
}
}Le contenu de notre objet Morceau, généré à partir des boites de dialogue de l'IHM est écrit dans les différents tags. Mais vu que chaque champ de tag a une taille fixe dans le fichier MP3, on complète les espaces vides par le caractère « / », qui sera ignoré à la lecture.
- Titre : 30 car
- Artiste : 30 car
- Genre : 1 car
public void EcrireTag( string fichier)
{
// ouvrir le fichier
FileStream f = new FileStream(fichier, FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
// se positionner suivant qu'il y a un dj tag ou pas
if( PossedeTag(fichier) )
f.Seek(-128, SeekOrigin.End);
else
f.Seek(0, SeekOrigin.End);
// écrire les infos avec un BinaryWriter
BinaryWriter bw = new BinaryWriter(f);
bw.Write(System.Text.Encoding.Default.GetBytes(Morceau.accTagOk));
// pour chaque élément, s'il ne fait pas la longueur voulue, on rajoute le car "/"
bw.Write(System.Text.Encoding.Default.GetBytes(Morceau.accTitre));
for(int i = 0; i < 30-Morceau.accTitre.Length; i ++) bw.Write((char)'/');
bw.Write(System.Text.Encoding.Default.GetBytes(Morceau.accArtiste));
for (int i = 0; i < 30 - Morceau.accArtiste.Length; i++) bw.Write((char)'/');
bw.Write((short) Morceau.accGenre);
bw.Close();
}On génère l'objet Morceau à partir du stackpanel de l'IHM contenant les contrôles et on appelle la méthode EcrireTag
public void EditerTag(StackPanel objStckPan, string fichierATagger)
{
if (Morceau != null)
{
Morceau.accTitre = ((TextBox)objStckPan.FindName("txtbTitre")).Text;
Morceau.accArtiste = ((TextBox)objStckPan.FindName("txtbAuteur")).Text;
Morceau.accGenre = short.Parse(((ComboBox)objStckPan.FindName("cbxType")).SelectedValue.ToString());
}
EcrireTag(fichierATagger);
}La classe ControleLecture encapsule un objet DirectX AudioVideoPlayBack.Audio. Les méthodes _lire(), _arreter(), _interrompre(), ne font qu'appeler les méthodes correspondantes de l'objet Audio : play(), stop(), pause().
L'espace de nom à ajouter est bien Microsoft.DirectX.AudioVideoPlayBack. Pour ouvrir le fichier MP3 en lecture, on utilisera la classe OpenFileDialog, sans oublier d'ajouter l'espace de nom System.IO dans les directives Using du fichier. Une énumération typeFichier permet de factoriser la méthode _ouvrir en l'utilisant aussi bien pour l'ouverture de fichiers musicaux que de fichiers d'images.
public class ControleLecture
{
public Audio Lecteur;
public void _lire()
{
if (Lecteur != null)
{
Lecteur.Play();
}
}
public string _ouvrir(typeFichier tf)
{
OpenFileDialog ofd = new OpenFileDialog();
if (tf == typeFichier.musique)
{
ofd.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyMusic);
ofd.Filter = "mp3 files (*.mp3)|*.mp3";
ofd.FilterIndex = 2;
ofd.RestoreDirectory = true;
if (ofd.ShowDialog() == DialogResult.OK)
{
try
{
if (ofd.OpenFile() != null)// On attribue le chemin du fichier à lire au
{
Lecteur = new Audio(ofd.FileName, false);
return ofd.FileName;
}
}
catch (Exception ex)
{
MessageBox.Show("Error: Could not read file from disk. Original error: " + ex.Message);
return null;
}
}
return null;
}
else if (tf == typeFichier.image)
{
ofd.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
ofd.Filter = "JPG (*.jpg)|*.jpg";
ofd.FilterIndex = 2;
ofd.RestoreDirectory = true;
if (ofd.ShowDialog() == DialogResult.OK)
{
try
{
if (ofd.OpenFile() != null)// On attribue le chemin du fichier à lire au
{
return ofd.FileName;
}
}
catch (Exception ex)
{
MessageBox.Show("Error: Could not read file from disk. Original error: " + ex.Message);
return null;
}
}
return null;
}
return null;
}
public void _arreter()
{
if (Lecteur != null)
{
Lecteur.Stop();
}
}
public void _interrompre()
{
if (Lecteur != null)
{
Lecteur.Pause();
}
}
}IV-C. Conception de la couche Dialogue (DialogueIHMLecteur.xaml)▲
L'IDE Xaml de Visual Studio est scindé en deux horizontalement : une interface RAD où on dépose les contrôles, et une page affichant le code Xaml correspondant.
Dans le fichier DialogueIHMLecteur.xaml, tapez le texte suivant :
<Page x:Class="LecteurAudioXaml.Dialogues.DialogueIHMLecteur"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Page1" Height="505" Width="600">
<Grid>
<TabControl Margin="10,35,36,39" Name="tabControl1">
<TabItem Name="tbiLecteur">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="12*" />
<ColumnDefinition Width="532*" />
</Grid.ColumnDefinitions>
<Button Click="btnLecture_Click" Grid.Column="1" Height="70" HorizontalAlignment="Left"
Margin="140,106.5,0,0" Name="btnLecture" VerticalAlignment="Top" Width="70">Lecture</Button>
<Button Click="btnPause_Click" Grid.Column="1" Height="70" Margin="253,106.5,209,0" Name="btnPause"
VerticalAlignment="Top">Pause</Button>
<Button Click="btnArret_Click" Grid.Column="1" Height="70" HorizontalAlignment="Right" Margin="0,106.5,97,0"
Name="btnArret" VerticalAlignment="Top" Width="70">Arrêt</Button>
<Button Click="btnParcourir_Click" Grid.Column="1" Height="70" HorizontalAlignment="Right" Margin="0,25,97,0"
Name="btnParcourir" VerticalAlignment="Top" Width="70">...</Button>
<Label Grid.Column="1" Height="32" HorizontalAlignment="Left" Margin="12,25,0,0" Name="lTitre"
VerticalAlignment="Top" Width="219">Titre de la chanson</Label>
<Button Click="btnImg_Click" Grid.Column="1" HorizontalAlignment="Right" Margin="0,196,80,145" Name="btnImg" Width="70">
...</Button>
<Image Grid.Column="1" Margin="140,182,192,29" Name="imgAlbum" Stretch="Fill" />
<Slider Grid.Column="1" Height="21" HorizontalAlignment="Right" Margin="0,0,8,97"
Maximum="200" Name="sliderImageAlbum" StyleTickFrequency="1" Value="200"
VerticalAlignment="Bottom" Width="175" />
</Grid>
</TabItem>
<TabItem Name="tbiEditeur">
<StackPanel Name="stckPEditeur">
<Label>Titre</Label>
<TextBox Name="txtbTitre" />
<Label>Auteur</Label>
<TextBox Name="txtbAuteur" />
<Label>Type</Label>
<ComboBox Height="23" Name="cbxType" Width="120" />
<Button Click="btnOk_Click" Name="btnOk" VerticalAlignment="Top" Width="75">Enregistrer</Button>
</StackPanel>
</TabItem>
</TabControl>
<ComboBox Height="23" HorizontalAlignment="Left" Margin="15,10,0,0" Name="cbxStyle" VerticalAlignment="Top" Width="120">
<ComboBoxItem IsSelected="True">Style Garçon</ComboBoxItem>
<ComboBoxItem>Style Fille</ComboBoxItem>
</ComboBox>
</Grid>
</Page>ce qui devrait donner un résultat proche de ceci pour l'onglet Lecteur :
et ceci pour l'onglet éditeur :
IV-C-1. Liaison de données (DataBinding) avec une source Xml, en WPF▲
Pour peupler notre liste déroulante dans l'onglet éditeur, qui contient les différents styles musicaux, nous allons utiliser un fichier Xml comme source. Voici le fichier type_morceau.xml :
<?xml version="1.0"?>
<types>
<Item>
<titre>divers</titre>
<valeur>0</valeur>
</Item>
<Item>
<titre>jazz</titre>
<valeur>8</valeur>
</Item>
<Item>
<titre>métal</titre>
<valeur>9</valeur>
</Item>
<Item>
<titre>pop-rock</titre>
<valeur>13</valeur>
</Item>
<Item>
<titre>rap</titre>
<valeur>15</valeur>
</Item>
<Item>
<titre>techno</titre>
<valeur>18</valeur>
</Item>
<Item>
<titre>classique</titre>
<valeur>32</valeur>
</Item>
</types>Il contient un nombre restreint de styles musicaux, mais bien suffisants pour notre démonstration. Nous rajouterons donc comme ressource à notre fichier DialogueIHMLecteur.xaml, le fournisseur Xml (XmlDataProvider) type_morceau.xml dont
l'identifiant désigné par la balise x :key sera dataProvider.
<Window.Resources>
<XmlDataProvider x:Key="dataProvider" XPath="types" Source="../type_morceau.xml"></XmlDataProvider>
</Window.Resources>Nous n'avons donc plus qu'à assurer le mappage entre la liste déroulante (Text et Value) et les deux champs (titre et valeur) de la source Xml, grâce aux instructions DisplayMemberPath et SelectedValuePath, sans oublier de préciser grâce à la requête XPath que le champ du fichier source concerné est Item.
<ComboBox Height="23" Width="120" Name="cbxType" ItemsSource="{Binding Source={StaticResource dataProvider},XPath=Item}"
DisplayMemberPath="titre" SelectedValuePath="valeur">
</ComboBox>Voici donc sans une seule ligne de code (mais uniquement avec des balises Xaml), ce que vous devriez obtenir :

IV-C-2. Lier une propriété à la valeur d'un contrôle▲
Un des gros avantages de WPF, c'est que le rendu graphique est entièrement vectoriel. Nous allons illustrer cette idée, par la mise au point d'une image qui diminue à la volée suivant la valeur d'un slider.
Pour faire cela, une simple instruction de binding suffit dans la balise du contrôle Image :
<Image Margin="140,182,192,29" Name="imgAlbum" Stretch="Fill" Width="{Binding ElementName=sliderImageAlbum, Path=Value}"
Height="{Binding ElementName=sliderImageAlbum, Path=Value}"
Grid.Column="1" />
<Slider Height="21" Margin="0,0,8,97" Name="sliderImageAlbum" Style="{DynamicResource TplSlider}"
Value="200" Maximum="200" TickFrequency="1" HorizontalAlignment="Right"
VerticalAlignment="Bottom" Width="175" Grid.Column="1" />IV-C-3. Mettre un peu de style dans notre application (DialogueIHMLecteur.xaml)▲
Que diriez-vous de personnaliser un peu le style de l'application, comme on peut le voir sur certains sites web où l'utilisateur choisit la feuille de style qui veut appliquer sur la page. Il faut alors définir les propriétés de chaque contrôle dans une page Xaml que l'on désigne par le terme de Theme. Notre fichier s'appelle ThemeGarcon.xaml et voici son contenu :
ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="TplArrierePlan">
<Setter Property="Control.Background" Value="CornflowerBlue"/>
</Style>
<Style BasedOn="{StaticResource TplArrierePlan}"
TargetType="Button"
x:Key="TplBouton">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontFamily" Value="Arial"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property=" HorizontalAlignment" Value="Left"/>
<Setter Property="Width" Value="70"/>
<Setter Property="Height" Value="70"/>
</Style>
<Style BasedOn="{StaticResource TplArrierePlan}"
TargetType="Label"
x:Key="TplLabel">
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontFamily" Value="Arial"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property=" HorizontalAlignment" Value="Left"/>
<Setter Property="Width" Value="300"/>
<Setter Property="Height" Value="32"/>
</Style>
<Style BasedOn="{StaticResource TplArrierePlan}"
TargetType="Slider"
x:Key="TplSlider">
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="32"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style BasedOn="{StaticResource TplArrierePlan}"
TargetType="ComboBox"
x:Key="TplCbx">
</Style>
</ResourceDictionary>Un style de base dont l'identifiant est TplArrierPlan, qui applique une couleur d'arrière-plan bleue aux contrôles :
<Style x:Key="TplArrierePlan">
<Setter Property="Control.Background" Value="CornflowerBlue"/>
</Style>Tous les autres styles se réfèrent à ce style de base, pour définir d'autres propriétés plus spécifiques. Ici le style TplBouton dont le contrôle cible est de type Button, définit des propriétés de police, alignement, de longueur, de hauteur.
<Style BasedOn="{StaticResource TplArrierePlan}"
TargetType="Button"
x:Key="TplBouton">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontFamily" Value="Arial"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property=" HorizontalAlignment" Value="Left"/>
<Setter Property="Width" Value="70"/>
<Setter Property="Height" Value="70"/>
</Style>On définit avec des valeurs différentes le thème Fille dans le fichier ThemeFille.xaml. Ces deux fichiers sont placés dans le répertoire Themes de l'application Par défaut, on attribue le thème
Garçon à l'application, en appliquant cette directive dans le fichier App.xaml
<ResourceDictionary x:Name="Style">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Themes\StyleGarcon.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>Maintenant, dans le fichier DialogueIHMLecteur.xaml nous devons lier chaque contrôle avec son style défini dans le thème, grâce à la directive Style, en précisant bien que c'est une ressource dynamique, puisque le style des contrôles est susceptible d'évoluer suivant les actions de l'utilisateur.
<Button Name="btnLecture" Click="btnLecture_Click" Margin="140,106.5,0,0" Style="{DynamicResource TplBouton}"
Grid.Column="1" Height="70" VerticalAlignment="Top" HorizontalAlignment="Left" Width="70">Lecture</Button>
<Label Name="lTitre" Margin="12,25,0,0" Style="{DynamicResource TplLabel}" Grid.Column="1"
Height="32" VerticalAlignment="Top" HorizontalAlignment="Left" Width="219">Titre de la chanson</Label>Puis grâce à une liste déroulante, on donne le choix à l'utilisateur de sélectionner son style préféré.
private void cbxStyle_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ResourceDictionary rd = new ResourceDictionary();
ResourceDictionary newDictionary = new ResourceDictionary();
switch (cbxStyle.SelectedIndex)
{
case 0:
newDictionary.Source = new Uri("Themes/StyleGarcon.xaml",UriKind.Relative);
break;
case 1:
newDictionary.Source = new Uri("Themes/StyleFille.xaml",UriKind.Relative);
break;
}
System.Windows.Application.Current.Resources.MergedDictionaries[0] = newDictionary;
}Voici, le style appliqué lorsque l'utilisateur choisit dans la liste déroulante « Style Fille » :
V. CONCLUSION▲
Par ce bref tutoriel, vous avez appris à réaliser un mini lecteur MP3 en utilisant les technologies WPF et XAML. Ces technologies semblent prometteuses, puisqu'elles ont l'énorme avantage de dissocier totalement la couche Présentation de la couche Métier, idée relativement évidente dans une architecture web, mais plutôt étrangère aux architectures Windows classiques. XAML se propose donc de réunir le meilleur du monde Windows et du monde Web.











