Lancez du son comme un pro avec UWP !

Vous avez besoin de jouer une mélodie en boucle ou des bruits de lasers pour votre jeu ou votre application orientée son ? Vous ne savez pas comment faire en UWP ? Cet article est fait pour vous !

Pour commencer, il est possible de jouer des sons avec MediaPlayerElement mais ce n’est pas vraiment la solution idéale car il faudra stocker autant d’instance de MediaPlayerElement que de sons. De plus il y a souvent un petit délai lorsque l’on lance le son avec ce contrôle. On préférera donc une solution plus professionnelle.

Au début d’UWP, la manipulation du son, s’effectuait via une librairie en C++ pas toujours très simple d’accès : XAudio2. Heureusement, Microsoft, a penser aux développeurs C# en ajoutant AudioGraph dans sa boite à outils.

Cette classe permet d’ouvrir des fichiers audio au format mp3, wav, wma et m4a.

AudioGraph permet de créer des noeuds d’entrée (en général par le biais de fichier audio) que l’on peut associer à des noeuds de sortie (carte son ou fichier audio de sortie). Il est également possible de mixer différents noeuds d’entrées. Ce n’est pas très compliqué mais c’est assez verbeux en terme d’écriture.

Pour simplifier l’accès à cette classe je viens d’écrire une petite librairie permettant de faire le travail pour vous (hourra !) : AudioPlayer

Les bases

Avant tout de chose on va créer une instance de de la class AudioPlayer. AudioPlayer permet d’associer un fichier audio à une clé pour faciliter son stockage. Comme AudioPlayer est très souple d’utilisation, vous pouvez lui fournir le type de clé que vous souhaitez (entier, string, …) . Par exemple j’aime bien utiliser des enums comme clé représentant mes sons (AudioKeys), AudioPlayer sera donc déclaré ainsi :

// key for the sound dictionnary of the AudioPlayer
enum MyAudioKeys
{
   Loop,
   Connected,
   Connected10Channels
}

AudioPlayer<MyAudioKeys> audioPlayer = new AudioPlayer<MyAudioKeys>();

On pourra ensuite l’initialiser :

await audioPlayer.InitializeAsync();

Et enfin ajouter des sons associé à nos clés :

await audioPlayer.AddSoundFromApplicationAsync(MyAudioKeys.Loop, "ms-appx:///Assets/Sounds/Loop.wav");

await audioPlayer.AddSoundFromApplicationAsync(MyAudioKeys.Connected, "ms-appx:///Assets/Sounds/Connected.wav");

Il est possible d’ajouter des son à partir d’un StorageFile (méthode AddSoundAsync) ou, comme dans l’exemple, d’une uri absolue pointant vers un fichier dont l’action de génération est “Content” (méthode AddSoundFromApplicationAsync).

Un son peut être retiré d’AudioPlayer avec la méthode RemoveSound associé à sa clé. RemoveSound peut être appelé également sans clé si vous désirez retirer l’ensemble des sons de la librairies. Si vous n’avez plus besoin de son dans votre application, c’est une bonne pratique d’appeler RemoveSound qui disposera correctement les noeuds d’entrée utilisées dans AudioPlayer.

Let’s play !

Pour jouer les sons que vous avez préalablement stockés à l’aide de la clé vous avez le choix entre trois méthodes :

  • PlayLoop : vous permet de jouer un son en boucle.
  • PlaySound : Vous permet de jouer un son immédiatement, sans attendre sa fin.
  • PlaySoundAsync : Vous permet également de jouer un son mais en attendant sa fin.
audioPlayer.PlayLoop(MyAudioKeys.Loop);
audioPlayer.PlaySound(MyAudioKeys.Connected);
await audioPlayer.PlaySoundAsync(MyAudioKeys.Connected);

Si vous testez la démo fournit avec la librairie vous constaterez que lorsque vous lancer le son ‘Connected’, par exemple, puis relancer quelques secondes après ce même son, alors qu’il continue toujours de jouer, il sera stoppé pour être joué de nouveau. Malheureusement, ce n’est pas toujours le comportement souhaité. C’est particulièrement le cas dans les jeux de tir ou chaque coup de feu nécessite un nouveau canal sonore. Normalement ce comportement n’est pas supporté par AudioGraph mais AudioPlayer le simule en ajoutant un noeud d’entrée par canal sonore désiré.

await audioPlayer.AddSoundFromApplicationAsync(MyAudioKeys.Connected10Channels, "ms-appx:///Assets/Sounds/Connected.wav", 10); // 10 canaux de rajoutés

audioPlayer.Play(MyAudioKeys.Connected10Channels); // le son peut être simultanément joué 10 fois sans être interrompu.

Pump up the volume !

Il est possible de fixer individuellement le volume du son à jouer en rajoutant un paramètre volume sur les méthodes Play. La valeur du volume est normalisée de 0 à 1 (un volume de 50% sera exprimé par 0.5).

// Play with a 50% volume 
audioPlayer.PlaySound(MyAudioKeys.Connected, 0.5);

Le volume peut être également ajusté via la méthode SetVolume

audioPlayer.SetVolume(MyAudioKeys.Loop, 0.25);

Un volume global peut être également fixé via la propriété Volume de AudioPlayer

audioPlayer.Volume = 0.75;

Il est parfois pratique de pouvoir coupez globalement le son de l’application en conservant le volume d’origine. C’est possible par le biais de la propriété IsMute et de la méthode SwitchMute qui passe d’un état à un autre.

// passe le volume général à 75%
audioPlayer.Volume = 0.75;
// coupe le volume
audioPlayer.IsMute = true;
// remet le volume à 75%
audioPlayer.SwitchMute();

Stop !

pour stopper un son en train d’être joué, il suffit d’appeler la méthode Stop avec la clé du son qui doit être stoppé.

audioPlayer.Stop(MyAudioKets.Loop); // Arrête la mélodie joué en boucle

Il également possible d’arrêter tous les sons en train d’être joués.

audioPlayer.Stop();

Conclusion

Vous avez maintenant toutes les clés en main pour gérer parfaitement l’environnement sonore de vos applications et de vos jeux.

Vous pouvez trouver le code de la librairie AudioPlayer et un exemple de fonctionnement sur GitHub :

https://github.com/samoteph/AudioPlayer

SlateView, le contrôle UWP pour afficher une image avec élégance, très rapidement.

Slateview est un contrôle UWP qui permet d’afficher une image Bitmap et éventuellement un fond qui reprend l’image en la floutant.

SlateView affiche une image et un fond flouté

On l’instancie en XAML de cette manière.

<Page 
    ...
    xmlns:my="using:SamuelBlanchard.Xaml.Controls.BlurPixelView" 
>
    <my:SlateView x:Name="slateView"/>
</Page>

Chargement d’une image

Pour charger une image par exemple en provenance d’une URI ou d’un StorageFile, on peut utiliser les méthodes suivantes :

await slateView.LoadImage(new Uri("ms-appx:///Assets/Images/House.jpg")); // par Uri
await slatView.LoadImage(myStorageFile); // avec un storageFile

Le chargement par Uri ne permet que l’utilisation des schemes ms-appx: et ms-data:

Il est possible également de charger une image par Uri en XAML (avec les mêmes contraintes que plus haut)

<my:SlateView 
  x:Name="slateView" 
  SourceUri="ms-appx:///Assets/Images/House.jpg"
/>

Affectation d’un bloc de pixels

Lorsque SlateView charge une image il écrit dans ces propriétés :

  • Pixels : Les data bruts de l’image au format BGRA8
  • PixelWidth : La longueur en pixel de l’image
  • PixelHeight : La hauteur en pixel de l’image

Ces propriétés seront ensuite utilisées pour afficher l’image à l’aide de Win2D.

Il est possible d’utiliser d’autres méthodes que le chargement d’image pour affecter ces propriétés. Elles permettent de prendre en charge des paramètres comme un tableaux de byte, WriteableBitmap, SofwareBitmap en passant par la VideoFrame. Il est ainsi facile de connecté SlateView à une caméra ou à une vidéo.

// Affectation par tableau de byte BGRA8 et taille
public void SetPixels(byte[] pixels, int pixelWidth, int pixelHeight)
// Affectation par WriteableBitmap
public void SetPixels(WriteableBitmap bitmap)
// Affectation par VideoFrame
public void SetPixels(VideoFrame frame)
// Affectation par SoftwareBitmap
public void SetPixels(SoftwareBitmap bitmap)

La méthode SetPixels permet d’afficher une image à partir d’un tableau de pixels. Elle n’effectue pas de copie interne du tableau mais passe sa référence à un CanvasBitmap. C’est une méthode d’affichage très rapide sur lesquelles se repose toutes les autres méthodes d’affectation.

Créer un bloc de pixels vide

Si vous ne désirez pas charger une image il est possible de créer une image vide de taille quelconque en Code-Behind.

public void CreatePixels(int pixelWidth, int pixelHeight)
public void CreatePixels(int pixelWidth, int pixelHeight, Pixel clearPixel) // avec une couleur

Dessiner dans le tableau de pixels

SlateView est capable de lire ou d’écrire certaines pixels de l’image :

public bool SetPixel(int x, int y, byte r, byte g, byte b, byte a = 0xFF) // Ecriture d'une pixel en position x,y
public bool SetPixel(int x, int y, Pixel pixel) // Ecriture d'une pixel en position x,y
public Pixel GetPixel(int x, int y) // Lit une pixel composée des valeurs r,g,b,a à la position x,y.

Il est également possible de nettoyer l’image :

public void ClearPixels(byte r = 0x00, byte g = 0x00, byte b = 0x00, byte a = 0xFF) // nettoyage de l'image avec la couleur r,g,b,a
public void ClearPixels(Pixel pixel)

Convertir les pixels

SlateView autorise la convertion des Pixels en WriteableBitmap, SoftwareBitmap et SoftwareBitmapSource.

public WriteableBitmap GetWriteableBitmap()
public SoftwareBitmap GetSoftwareBitmap()
public async Task<SoftwareBitmapSource> GetSoftwareBitmapSourceAsync()

Fond et gestion du flou

Lorsque vous chargez une image ou affectez la propriété Pixels, une image de fond est automatiquement affiché sous l’image. L’image de premier plan est affiché en mode Uniform alors que celle du fond l’est en mode UniformToFill. Bien que l’image de fond soit allongé elle n’est pas pour autant affichée complètement. Les performances sont donc conservées.

ImageMargin à 0 pour l’image

Comme on le voit sur l’image ci-dessus, l’image de fond est floutée. Il est possible de faire varier le blur de l’image grâce ces paramètres :

<my:SlateView             
    AllowBlur="True"
    BlurEffectAmount="12"
    BlurEffectBorderMode="Hard"
    BlurEffectOptimization="Speed"
    />

Vous pouvez également faire varier la couleur de fond et l’opacité de l’image de fond pour encore plus de précision dans l’affichage.

<my:SlateView          
    BackgroundImageOpacity="0.5"
    Background="Black"
    />

Interpolation et marge

Il est possible également de gérer une marge pour l’image de premier plan (ImageMargin) et de changer l’interpolation des images (fond et premier plan) grâce à la propriété ImageInterpolation. Si l’image est zoomé, en utilisant la valeur NearestNeighbor pour ImageInterpolation vous obtiendrez un affichage pixelisé. Les autres permettront un affichage plus doux.

<my:SlateView 
    ImageInterpolation="NearestNeighbor"
    ImageMargin="30"
    />
Emulateur GameBoy écrit en C# utilisant SlateView comme écran avec un ImageInterpolation à NearestNeighbor et une marge de l’image à 30

Selection des elements à afficher

Vous pouvez sélectionner facilement les éléments de SlateView que vous désirez afficher grâce à la propriété ElementShown. Les valeurs possibles sont :

  • None : Aucun element n’est affiché (à part la couleur de fond)
  • FrontAndBack : L’image de premier plan et de fond sont affichées (valeur par défaut)
  • FrontOnly : Seule l’image de premier plan est affichée
  • BackOnly : Seule l’image de fond est affichée.

Evenements

SlateView repose sur Win2D et dispose des mêmes événements qu’un CanvasAnimatedControl classique. Une exception en revanche, l’événement Draw n’existe pas mais est remplacé par plusieurs événement permettant de prendre la main entre les phases de dessin des images pour un meilleur contrôle.

  • CreateResources : Même que Win2D. Initialisation des ressources
  • Update : Même que Win2D. On peut calculer les pixels de l’image si besoin. Si vous désirez affecter des pixels c’est l’événement à privilégier.
  • DrawStart : Début du dessin
  • DrawBack : L’image de fond vient d’être dessiné (avec le blur)
  • DrawFront : L’image de premier plan est dessiné
  • DrawStop : Fin de l’evenement Draw

Enjoy !

Grâce à SlateView, vous avez toutes les clés en main pour afficher des pixels rapidement et avec élégance !

SlateView en action !
Link’s awakening sur (ma) XBOX grâce à SlateView !

Vous pouvez retrouver le code source de SlateView sur mon GitHub

https://github.com/samoteph/SlateView

Pico-8 et la stéganographie

En parcourant un article de Scott Hanselman sur la console virtuelle Pico-8 ( https://www.hanselman.com/blog/ThePICO8VirtualFantasyConsoleIsAnIdealizedConstrainedModernDayGameMaker.aspx) , un paragraphe sur la cartouche de jeu à particulièrement attiré mon attention.

One of the many genius parts of the PICO-8 is that the “Cartridges” are actually PNG pictures of cartridges. Drink that in for a second. They save a screenshot of the game while the cart is running, then they hide the actual code in a steganographic process

Pour résumé, le screenshot du jeu est enregistré dans une image en même temps que le code binaire et l’état du jeu. Image et code sont donc mélangés pour ne former qu’une seule image sauvée au format PNG. Mais comment est-ce possible ?

Mixer Image et données

On va simplement utiliser une particularité de l’oeil humain qui a du mal à distinguer les teintes très proches. L’oeil humain est capable de distinguer 200 teintes différentes mais par sur tous les canaux de couleurs (en RGB) soit 8 millions de couleurs maximum pour une personne entraîné. Mais en réalité 300.000 couleurs différentes semble un chiffre plus réaliste (64 teintes par canaux). Pour plus d’informations vous pouvez consultez cet article très intéressant :

https://www.guide-gestion-des-couleurs.com/oeil-perception-couleurs.html

Maintenant que l’on connait les limite de l’oeil humain voyons quelles sont celle de la machine. Une pixel au format ARGB8 est capable d’afficher 16 millions de couleurs soit 256 teintes par canal (1 octet). C’est beaucoup plus que l’oeil humain n’est capable de percevoir.

Si l’on reprend les 64 teintes par canaux perçues par l’oeil humain cela correspond à 6 bits sur un octet. Il reste donc 2 bits sur chaque canal dont on peut se servir pour encoder des données. Bien sur on utilisera les 2 bits les plus bas (bit 1-0) pour ne pas que les données puissent influencer de manière significatives les teintes des pixels.

Les 2 bits les plus bas des canaux sont utilisés pour former une donnée d’un octet

En associant les canaux RGB et Alpha (A) (et oui cela fonctionne également sur l’opacité !) on dispose de 4 canaux dont les 2 bits peuvent former les parties d’un octet de donnée (4 * 2 bits = 8 bits = 1 octet). On peut donc considérer qu’un octet égale à une pixel.

Les images de cartouches de Pico-8 ont une dimension de 160×205 pixels. C’est à dire 32800 pixels soit 32K de données (code ou ressource). Impressionnant !

Les images sont au format PNG car il donne la possibilité de sauver des images de 16 millions de couleurs, compressés sans perte de données. C’est essentiel pour ne pas perdre les données mélangées à l’image. En revanche, le format JPG n’est pas adapté à notre usage car son format de compression, très performant, autorise, en contre partie, une perte de données sur l’image.

La cartouche Pico-8 du jeu “FEZ”
En zoomant la cartouche on peut voir des teintes légèrement différentes dans les couleurs (bleu notamment) signe de données cachés.

Le processus permettant de cacher des données dans un autre flux de données s’appelle la Stéganographie. Nous avons évoqué ici seulement la possibilité de cacher ses données dans une image mais potentiellement il est possible de le faire avec du son ou d’autres types de flux.

Algorithme simple de stéganographie

Si vous souhaitez coder votre propre intégration d’une stéganographie à la Pico-8 cela passera nécessairement par une bonne connaissance du binaire et de ses opérateurs.

Concrètement, on récupère les pixels d’une image auxquelles on applique un masque sur chaque canal pour que les deux derniers bits passent à zéro. On récupère ensuite 2 bits de données que l’on fusionnera avec le canal en cours. Pour un canal cela donne un code de ce type :

byte data = 0xFF; // la data à écrire

byte B6bits = B8bits & ~0x03; // on nettoie les 2 derniers bits du canal B (blue)
byte data2bits = data & 0x03; // on conserve les deux derniers bits de data
byte RData8bits = R6bits | data2bits;

data = data >> 2; // on prépare la data pour le prochain canal puis on passe au canal G (Green)

Librairie de stéganographie

Heureusement si vous n’êtes pas à l’aise avec ce type de développement bas-niveau vous pouvez utilisez des librairies déjà toutes faites.

Par exemple en C# : https://github.com/paw3lx/StegoCore qui vous permet d’écrire du code de ce type :

byte[] secretData = System.IO.File.ReadAllBytes("secret.data");
using(var stego = new Stego("someimage.jpg"))
{
    stego.SetSecretData(fileBytes);
    Image<Rgba32> secretImage = stego.Embed(AlgorithmEnum.Lsb);
}

Facile et intuitif !

Référence

Plus d’information sur le format de la cartouche Pico-8 ?

https://pico-8.fandom.com/wiki/P8PNGFileFormat

Une liste de cartouche PICO-8 jouables :

http://picoscope101.fr/

Générer un contrôle réutilisable dans un projet UWP

Vous venez de créer un superbe contrôle et désirez que le monde entier puisse l’utiliser dans un package Nuget ou directement dans un autre projet UWP ?

Pour le partager vous avez pris soin de le construire à l’intérieur d’une librairie (DLL). Si vous pensez qu’il suffit simplement d’ajouter la DLL dans un projet pour que le contôle fonctionne, vous tomberez surement sur cette exception :

Impossible de copier le fichier “C:\Users\Sam.nuget\packages\pixelcontainer\1.1.1\lib\uap10.0\SamuelBlanchard.UI.Panels\SamuelBlanchard.UI.Panels.xr.xml”, car il est introuvable. App10

ou en anglais :

Could not copy the file “C:\Users\Sam.nuget\packages\pixelcontainer\1.1.1\lib\uap10.0\SamuelBlanchard.UI.Panels\SamuelBlanchard.UI.Panels.xr.xml” because it was not found. App10

Cette exception, pas très agréable, vous indique qu’il manque des fichiers à votre librairie. Il faut donc les générer en cochant “Generate library layout” dans l’onglet “Build”

Notez en Français, le petit problème d’affichage :

En buildant de nouveau la librairie avec cette option, de nouveaux fichiers et dossiers seront générés.

Le dossier SamuelBlanchard.UI.Panels vient d’être ajouté

Il suffit maintenant d’ajouter le fichier “.dll“,” .pri” ainsi que le dossier qui correspond au namespace du contrôle (ici SamuelBlanchard.UI.Panels) dans le nouveau projet ou le packet Nuget pour que tout fonctionne normalement !

PS : En WPF vous pouvez juste mettre la DLL et cela fonctionnera directement 😉

Donner vie à vos images avec PixelContainer !

lorsque l’on souhaite positionner un contrôle à une position précise, XAML nous offre plusieurs alternatives . Par exemple :

  • En utilisant les attributs classique du Layout : Margin + Alignment.
  • Par le panel Canvas et ses propriétés attachés Canvas.Left et Canvas.Top
  • En utilisant l’attribut RenderTransform et la classe TranslateTransform

Mais toutes ces positions sont exprimés en coordonnées spécifiques à XAML. Si l’on désire manipuler des contrôles se trouvant sur une image, il serait plus simple de les exprimer en pixels. Et c’est là que PixelContainer entre en scène !

Description de PixelContainer

PixelContainer fonctionne un peu comme un Canvas avec son Canvas.Top et son Canvas.Left mais possède quelques atouts supplémentaires.

Mais, avant tout de chose, PixelContainer nécessite la source d’une image pour commencer à travailler :

<l:PixelContainer Source="ms-appx:///Assets/City.png"/>

Une fois chargée, l’image s’affiche. Il est bien sur possible d’appliquer la déformation que l’on souhaite par l’attribut “Stretch” comme pour un contrôle Image classique.

<l:PixelContainer Source="ms-appx:///Assets/City.png" Stretch="Uniform"/>
City.png en mode Stretch à Uniform (notez les bords noirs)

Une fois chargée, les dimensions de l’images permettent de calculer les positions en pixels. On peut alors positionner des contrôles facilement dans PixelContainer à l’aide Pixel.X et Pixel.Y.

<l:PixelContainer Source="ms-appx:///Assets/City.png" Stretch="Uniform">
    <!-- On dessine une flèche --> 
    <FontIcon l:Pixel.X="571" l:Pixel.Y="153" Glyph="" Foreground="White" FontSize="50"/>
</l:PixelContainer>

Dans cet exemple on dessine une flèche à l’aide de FontIcon en la positionnant aux coordonnées 571, 153 exprimées en pixels.

Une flèche près du soleil !

Mais en regardant de plus près, on s’aperçoit que si la flèche est placée au bon endroit, c’est sa pointe qui devrait être positionner à ces coordonnées.

Pas de panique ! On va juste rajouter à notre FontIcon l’attribut Pixel.Origin=”0,0.5″ pour lui permettre de se recentrer convenablement.

Pixel.Origin fonctionne de la même façon que RenderTransformOrigin, c’est à dire qu’il normalise la taille de son contrôle (ici FontIcon) pour qu’elle soit représentée dans un carrée de 1×1. Ainsi une taille de “0.5,0.5” représentera le centre du contrôle et un “0,0.5” assigné à notre FontIcon centrera verticalement la flèche en conservant la position horizontale initiale.

<FontIcon l:Pixel.X="571" l:Pixel.Y="153" l:Pixel.Origin="0,0.5" Glyph="" Foreground="White" FontSize="50"/>
Tout est rentré dans l’ordre grâce à Pixel.Origin

Donner une taille en pixel

A l’instar des positions, PixelContainer est également capable de donner une taille exprimée en pixels. C’est très pratique pour les images ou les contrôles à taille dynamique (comme les boutons par exemple).

<Button l:Pixel.X="100" l:Pixel.Y="100" l:Pixel.Width="200" l:Pixel.Height="50" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Hello"></Button>

Attention à ne pas oublier de modifier les attributs Horizontal / VerticalAlignment car le Bouton possède ses propres valeurs par défaut. En les positionnant à “Stretch” vous êtes sur que le bouton prendra toute la place donnée par PixelContainer.

Le bouton est accroché à Gratte-Ciel

Il est possible de ne donner qu’une seul taille en pixel pour un contrôl donné. Par exemple, on affectera à un bouton uniquement l:Pixel.Width. Dans ce cas la hauteur sera déterminé automatiquement par le contrôle.

Attribut Stretch et changement de taille

PixelContainer prend en compte l’attribut Stretch de son image. Cela implique que les positions et tailles des contrôles seront recalculés en cas de changement de cet attribut ou de tout changement de taille de PixelContainer.

Les contrôles se positionnent et se re-taillent automatiquement (Stretch / et changement de taille)

Cette vidéo montre bien l’intérêt du système. D’un coté un positionnement simplifié, de l’autre on garde la qualité d’affichage des controles XAML qui ne seront jamais pixélisés.

Ajouter de l’interactivité

Comme il est possible de positionner facilement un élément sur une image, on va pouvoir ajouter de l’interactivité simplement. Par exemple si l’on souhaite rendre le soleil de notre image réactif, il suffit d’ajouter une ellipse de couleur transparente en position et taille du soleil puis de détecter les événements d’entrée et de sortie de l’ellipse. facile ! En remplaçant l’ellipse par un rectangle, un path ou encore une image cela offre beaucoup de possibilités d’interaction.

ItemTemplate et Binding

Le binding est également présent dans PixelContainer. Il est même plus simple à mettre en place que pour un Canvas puisqu’il est disponible dès le DataBinding.

<l:PixelContainer.ItemTemplate>
    <DataTemplate>
        <Button l:Pixel.X="{Binding X}" l:Pixel.Y="{Binding Y}" l:Pixel.Width="300" l:Pixel.Height="300" Background="Red" Content="{Binding Name}"></Button>
    </DataTemplate>
</l:PixelContainer.ItemTemplate>

Il ne reste plus qu’à ajouter des données dans ItemsSource de PixelContainer.

PixelContent

Pour les allergiques aux propriétés attachés (l:Pixel.X par exemple), un petit contrôle a été crée. Il s’agit de ContentControl qui permet d’avoir les attributs PixelX, PixelY, PixelWidth, PixelHeight et PixelOrigin disponibles

Transformer des coordonnées

Pour faciliter la vie du développeur, deux méthodes de transformations de coordonnées sont incluses dans PixelContainer :

  • ConvertToPixelLayout : convertit une valeur (position ou taille) du layout XAML en pixels
  • ConvertToXamlLayout : Méthode inverse de ConvertToPixelLayout
this.PixelContainer.ConvertToPixelLayout(CoordinateAlignment.Horizontal, 10.0)

le paramètre CoordinateAlignment.Horizontal ou Vertical permet d’obtenir l’axe des valeurs à convertir car les axes ne sont pas toujours uniformes (Stretch.Fill).

Storyboard (facile mais pas très rapide)

Bien que le Binding fonctionne comme un charme avec les propriétés attachées (par exemple l:Pixel.X), UWP ne permet pas leurs utilisations dans les Storyboards (et c’est bien dommage !).

Il est tout de même possible d’utiliser le contrôle PixelContent et ses attributs (PixelX,…) pour les animer. ATTENTION : ne pas oublier le EnableDependentAnimation à true pour que l’animation fonctionne.

<Storyboard x:Name="StoryboardPixel" RepeatBehavior="Forever">
  <DoubleAnimation EnableDependentAnimation="True" Storyboard.TargetName="PixelContent" 
  Storyboard.TargetProperty="PixelX" From="572" To="0" AutoReverse="True"/>
</Storyboard>

Storyboard (moins facile mais très rapide)

Malheureusement l’utilisation de EnableDependentAnimation à true implique de piètre performance pour l’animation. Mais on peut contourner le problème en utilisant un TranslateTransform et les méthodes ConvertToXamlLayout.

<!-- Le FontIcon à animer-->
<FontIcon l:Pixel.X="571" l:Pixel.Y="153" l:Pixel.Origin="0,0.5" Glyph="" Foreground="White" FontSize="50">
    <!-- On ajoute un TranslateTransform --> 
    <FontIcon.RenderTransform>
        <TranslateTransform x:Name="TranslateFontIcon"></TranslateTransform>
    </FontIcon.RenderTransform>
</FontIcon>
<!-- Le storyboard vise Le translateTransform -->
<Storyboard x:Name="StoryboardPixelTranslate" RepeatBehavior="Forever">
    <DoubleAnimation x:Name="AnimationTranslateX" Storyboard.TargetName="TranslateFontIcon" Storyboard.TargetProperty="X" From="0"/>
</Storyboard>

L’animation commence à From=”0″ c’est à dire relativement à la position en pixel du FontIcon (ici x=571). Pour la faire bouger à la position en pixel x=600 par exemple on prendra la différence entre le point de départ et d’arrivée (600-571) pour obtenir une distance exprimée en pixel. Puis on utilisera ConvertToXamlLayout pour convertir la distance en pixel en distance XAML. Il ne restera plus qu’à appliquer la valeur obtenue à l’animation AnimationTranslateX.

this.AnimationTranslateX.To = this.PixelContainer.ConvertToXamlLayout(CoordinateAlignment.Horizontal, 600 - 571);
this.StoryboardPixelTranslate.Begin();

Lire et écrire des pixels en code-behind

Pour manager les valeurs en pixel, on utilisera les méthodes suivantes :

Pixel.SetX(this.MonControl, 100);
Pixel.SetY(this.MonControl, 200);
Pixel.SetOrigin(this.MonControl, new Point(0, 0.5));
Pixel.SetWidth(this.MonControl, 100);

var x = Pixel.GetX(this.MonControl);
...

Clipper les enfants

A partir de la version 1.1.0, vous pouvez utiliser l’attribut ClipImageToBounds qui permet de clipper, c’est à dire de couper, les contrôles enfants de PixelContainer qui sont à l’extérieur de l’image.

ClipImageToBounds = “False”
ClipImageToBounds = “True”

Obtenir PixelContainer

Pour démarrer avec PixelContainer, il suffit d’intégrer son nuget à l’intérieur de votre projet et c’est parti ! PixelContainer est disponible pour UWP et WPF.

https://www.nuget.org/packages/PixelContainer

Pour information son namespace est :

xmlns:l=”using:SamuelBlanchard.UI.Panels”

Vous trouverez également le code source de PixelContainer sur GitHub :

https://github.com/samoteph/PixelContainer

Enjoy !

Récapitulatif des différents accès aux fichiers et dossiers dans une application UWP

L’un des griefs les plus important à l’encontre du développement d’application UWP est sa capacité limitée à accéder aux fichiers de son ordinateur. Pourtant il existe plusieurs méthodes, que nous allons passer en revue, qui permettent de manipuler facilement les fichiers et dossiers .

ApplicationData et les fichiers de l’applications

Au lancement d’UWP Les concepteurs ont pris le parti, très sécuritaire, de ne pouvoir accéder qu’à des répertoires du disque dédiés à l’application (LocalFolder, TemporaryFolder, …). Ceux-ci sont regroupés dans la classe ApplicationData :

https://docs.microsoft.com/en-us/uwp/api/windows.storage.applicationdata

Ce parti pris est, comme souvent pour UWP, lié à la nature sandboxée de l’application. Elle permet de s’assurer que le développeur ne pourra pas accéder à des données sensibles du système.

Les fichiers de l’application sont donc écris dans un répertoire par nature difficilement accessible par l’utilisateur car nécessitant des droits Administrateurs. Or, l’utilisateur à souvent besoin de récupérer des fichiers exportés par l’application.

Heureusement, le store autorise un accès complets à des répertoires particuliers.

Les KnownFolders

Ces répertoires sont regroupés dans la classe KnownFolders. Ils permettent un accès à des répertoires externes à l’application à partir du moment ou ils sont déclarés comme Capacité dans le manifeste de l’application :

On déclare le répertoire Images comme accessible à l’application

Vous pouvez retrouver l’intégralité de ces dossiers et leurs descriptions en suivant ce lien :

https://docs.microsoft.com/en-us/uwp/api/windows.storage.knownfolders

En attendant en voici quelques uns parmi les plus utilisés :

  • Picture (répertoire Images)
  • Music (répertoire Musique)
  • Videos (répertoire Vidéos)
  • Removable Storage (clé USB ou carte SD par exemple). Dans ce cas seul les types de fichiers déclarés dans le manifeste de l’application sont accessibles.
Déclaration d’un type de fichier .avi reconnu par l’application et permettant sa lecture à partir d’une clé USB

Le cas DownloadFolder

Il est possible d’accéder au répertoire Download sans avoir a déclarer une Capacité particulière. En revanche, on ne pourra accéder qu’aux fichiers et dossiers crées par l’application. Une autre application UWP ne pourra donc pas accéder à nos propres fichiers. Cela reste tout de même un moyen trivial et rapide d’écrire des fichiers récupérables facilement par l’utilisateur.

La classe static DownloadFolder ne contient que des méthodes permettant de créer des fichiers ou des dossiers. Une fois appelé on récupère un StorageFile ou un StorageFolder qui les rendent accessibles à l’application.

StorageFile newFile = await DownloadsFolder.CreateFileAsync("file.txt");

La classe DownloadFolder ne possède pas de méthode pour lire le contenu du dossier Downloads. C’est ennuyeux si l’on souhaite réutiliser le fichier crée après redémarrage de l’application. Heureusement on peut palier à ce problème en stockant le StorageFile ou StorageFolder dans une liste persistante : StorageApplicationPermissions.FutureAccessList

Documentation sur le DownloadFolder :

https://docs.microsoft.com/en-us/uwp/api/windows.storage.downloadsfolder

Documentation sur la FutureAccessList :

https://docs.microsoft.com/fr-fr/uwp/api/windows.storage.accesscache.storageitemaccesslist

Je sélectionne avec les Pickers

Si l’écriture ou la lecture de votre fichier ou dossier doit être sélectionnées par l’utilisateur, il est possible d’utiliser des boites de dialogues spécialisées : Les Pickers

Ouverture d’un fichier à partir d’un picker

Les Pickers permettent d’atteindre l’ensemble du système de fichiers accessible pas l’utilisateur de l’application. Pour être utilisés, ils n’ont pas besoin d’un ajout de capacité.

Il existe plusieurs types de pickers à utiliser selon les cas :

  • FileOpenPicker qui permet l’ouverture d’un ou plusieurs fichiers
  • FileSavePicker pour enregistrer un fichier
  • FolderPicker pour ouvrir un dossier

Comme dans le cas du DownloadFolder, il peut être intéressant de sauver les derniers fichiers ou dossiers utiliser pour un accès futur par l’utilisateur ou l’application. Vous pouvez utiliser FutureAccessList mais également StorageItemMostRecentlyUsedList (liste MRU) qui permet de conserver les 25 derniers fichiers ou dossiers récemment ouverts.

Documentation sur les pickers :

https://docs.microsoft.com/fr-fr/windows/uwp/files/quickstart-using-file-and-folder-pickers

Documentation sur la liste MRU :

https://docs.microsoft.com/en-us/uwp/api/windows.storage.accesscache.storageapplicationpermissions.mostrecentlyusedlist#Windows_Storage_AccessCache_StorageApplicationPermissions_MostRecentlyUsedList

Accèder à tous les fichiers sans Pickers

Les applications en provenance du store ont des nécessités en terme de sécurité que les applications dont la vocation est d’être utilisées dans une entreprise ou dans le cadre d’un kiosque n’ont pas.

Microsofts à donc récemment décidé d’assouplir les accès aux fichiers pour ces cas particuliers en ajoutant une nouvelle capacité : broadFileSystemAccess. Pour la déclarer il faut ouvrir le manifeste de l’application sous forme XML puis ajouter rescap dans IgnorableNameSpace et la rescap:Capability broadFileSystemAccess dans Capabilities.

<Package
  ...
  xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
  IgnorableNamespaces="uap mp uap5 rescap">
...
<Capabilities>
    <rescap:Capability Name="broadFileSystemAccess" />
</Capabilities>

Aucune autre capacité de type DocumentsPictures ou Videos ne doit être déclaré pour que cette capacité puisse fonctionner correctement.

IMPORTANT : Pour que l’accès aux fichiers soit effectif, l’utilisateur doit l’autoriser explicitement dans les settings de la machine. Par défaut la valeur est OFF (depuis Windows October 2018 update).

Office n’a pas accès aux fichiers !

Il est possible de lancer cette page de settings à partir de son application :

bool result = await Windows.System.Launcher.LaunchUriAsync(new Uri("ms-settings:privacy-broadfilesystemaccess"));

Conclusion

Même si l’accès aux fichiers est moins aisé qu’une application .NET de bureau, il reste par le biais des Picker et de la capacité broadFileSystemAccess relativement simple et puissant. On peut espérer que Microsoft ajoute un jour une capacité permettant à une application UWP d’avoir un accès complet à la machine, sans settings externes à configurer, comme le ferait une application .NET classique. en attendant le développeur est suffisamment armé pour faire face à la grandes majorités des demandes clients.

Simuler l’envoi d’une touche dans une app XAML !

Lorsque votre application XAML ne bénéficie pas d’un clavier physique comme périphérique de saisie, que vous voulez simuler la pression de touches, de raccourcis clavier voir de commander complètement une application à distance, il est indispensable de pouvoir simuler l’envoi de touche du clavier. On parle alors d’injection de touches. On parlera dans cet article les différentes manières d’implémenter cette injection dans des applications WPF et UWP .

Injection vs TextBox méthodes

Lorsque l’on pense injection de touche, on est souvent tenté de ne penser qu’au contrôle TextBox. On pourrait effectivement se servir des méthode de manipulation de texte/curseur de la TextBox pour insérer du texte. Malheureusement le résultat est souvent peu satisfaisant et ne fonctionnerait pas sur tous les contrôles. Alors comment faire pour injecter la touche TAB (tabulation) ou envoyer une chaîne de caractères complète ?

A la fin de l’envoi, je touche (en WPF)

Heureusement il existe une API spécifique dans WPF qui permet d’envoyer des clés de touche ou une chaîne de caractères à un contrôle. On est donc capable d’envoyer du texte dans une TextBox/PasswordBox mais également de naviguer de contrôle en contrôle (en envoyant la touche Tabulation).

Commençons par l’envoi de touche :

// Envoyons la clé Entrée
Key keyValue = Key.Enter;

// on crée l'eventArgs qui va accueilir la touche à envoyer
var eventArgs = new KeyEventArgs(Keyboard.PrimaryDevice, Keyboard.PrimaryDevice.ActiveSource, 0, keyValue);
eventArgs.RoutedEvent = Keyboard.KeyDownEvent;

// C'est parti !
InputManager.Current.ProcessInput(eventArgs);

Puis l’envoi d’une chaîne :

// la chaine à envoyer
string keyString = "Une chaine";

// argument contenant les informations sur le texte envoyé
var eventArgs = new TextCompositionEventArgs(
Keyboard.PrimaryDevice,
new TextComposition(InputManager.Current, Keyboard.FocusedElement, keyString)
);

// static RoutedEvent appartenant à UIElement et donnant le type d’événement routé
eventArgs.RoutedEvent = TextInputEvent;

// envoie de la chaine au contrôle en cours
InputManager.Current.ProcessInput(eventArgs);

On choisira l’envoi de touche pour injecter des touches tabulation, entrée, backspace, flêches… On préférera l’envoie de chaîne pour le reste (caractères alphanumériques) car cela permet de ne pas être dépendant de la langue du système.

Et en UWP ?

UWP permet également d’injecter des touches au système mais de part sa nature sand-boxée il doit déclarer cette capacité dans son Manifest. C’est une capacité réservée au SDK Anniversary Update (Windows 1607) et qui est encore en preview.

ATTENTION : N’espérez pas trop voir la capacité d’injection passer la certification du store. Cette capacité doit être justifiée auprès de Microsoft (qui dira NON! basiquement).

Dans le manifest en mode XML on ajoute un namespace rescap
puis ajouter la capacité inputInjectionBrokered

Une fois cette capacité activée dans notre application, il est possible d’utiliser la classe InputInjector capable de simuler l’envoi de touche à l’élément disposant du focus.

var inputInjector = InputInjector.TryCreate();
// liste des clés
var keyInfos = new List<InjectedInputKeyboardInfo>();

// On veut envoyer la clé "TAB" (tabulation)
VirtualKey keyCode = VirtualKey.Tab;

var info = new InjectedInputKeyboardInfo();
info.VirtualKey = (ushort)keyCode;
// la clé sera envoyé en tant qu'evenement Down
info.KeyOptions = InjectedInputKeyOptions.None;

keyInfos.Add(info);

// envoi
inputInjector.InjectKeyboardInput(keyInfos);

// le caractère sera envoyé en tant qu'evenement Up
info.KeyOptions = InjectedInputKeyOptions.KeyUp;

// envoi
inputInjector.InjectKeyboardInput(keyInfos);

Vous pouvez injecter simultanément plusieurs clés en rajoutant des nouveaux InjectedInputKeyboardInfo dans keyInfos. Il est possible alors d’injecter des raccourcis clavier (VirtualKey.Control et VirtualKey.V pour CTRL+V par exemple).

Astuce : pour envoyer la touche ALT qui n’existe pas en tant que VirtualKey, vous devez envoyer à la suite VirtualKey.Control et VirtualKey.Control.Menu

Il est également possible d’envoyer une chaine de caractères de cette manière :

var inputInjector = InputInjector.TryCreate();
// liste des clés
var keyInfos = new List<InjectedInputKeyboardInfo>();
            
// On veut envoyer la chaine "Bonjour"
string keyString = "bonjour";

foreach (char c in keyString)
{
    var info = new InjectedInputKeyboardInfo();
    info.ScanCode = c;
    // le caractère sera envoyé en tant qu'evenement Down
    info.KeyOptions = InjectedInputKeyOptions.Unicode;
    keyInfos.Add(info);
}

// envoi
inputInjector.InjectKeyboardInput(keyInfos);

foreach (var info in keyInfos)
{
    // le caractère sera envoyé en tant qu'evenement Up
    info.KeyOptions = InjectedInputKeyOptions.KeyUp | InjectedInputKeyOptions.Unicode;
}

// envoi
inputInjector.InjectKeyboardInput(keyInfos);

Il est à noter que l’injection ne concerne pas que le clavier mais également des shortcuts spécifiques (Back, Home, Search), le Touch, le Pen, la souris ainsi que les manettes de jeu.

Le saviez-vous ? Dans le cadre d’une application en mode kiosque (mode dans lequel une application UWP est démarrée à la place du shell Windows), le clavier virtuel n’est pas disponible. C’est un vrai handicap (bug?) si vous devez gérer un formulaire de saisie. L’injection de touche de clavier est alors la seul solution pour remplacer ce clavier qui ne s’affiche pas..

Problème de focus ?

Lorsque vous utilisez ces méthodes d’injection, il est important de comprendre qu’elles envoient leurs touches sur l’élément disposant du Focus. Si par exemple vous utilisez un bouton pour lancer ces méthodes, le bouton prendra le focus et les touches ne seront pas envoyés à la textbox que vous visiez mais au bouton. Pas vraiment l’objectif recherché.

En UWP on privilégiera l’attribut AllowFocusOnInteraction=”False” sur le bouton pour empêcher le focus de se déclencher tandis qu’en WPF on ajoutera simplement l’attribut Focusable=”False”

Rendre accessible un contrôle enfant en XAML

Lorsque l’on crée un UserControl en XAML (UWP ou WPF) il est parfois intéressant de laisser le développeur prendre la main sur les contrôle internes.

Dans l’exemple suivant le UserControl comporte un bouton.

<UserControl>
    <Grid>
        <Button x:Name="Button1"></Button>
    </Grid>
</UserControl>

Ce UserControl est ensuite instancié dans une page XAML :

<Page>
<local:ControlUtilisateur x:Name="ControlUtilisateur"/>
</Page>

On peut donc s’attendre à ce que le développeur de la page puisse appeler en C# le bouton Button1 à partir du UserControl de la manière suivante :

this.ControlUtilisateur.Button1.Content = "Hello";

Mais il n’en est rien.

En effet lorsque l’on crée un nouveau contrôle dans la page XAML à partir de l’éditeur et que celui-ci est nommé (x:Name=”Button1″), Visual Studio va générer automatiquement du code C# correspondant à ce bouton. Ce code généré est un raccourci qui nous évite de déclarer nos propres variables pour pouvoir accéder aux éléments XAML, ce qui est plutôt pratique.

/*exemple du code qu'il nous faudrait écrire (merci VS)*/
private Button Button1 = this.FindName("Button1") as Button;

Mais voila, le bouton “Button1” est généré en private par défaut. Il n’est donc pas accessible au delà de son parent, le UserControl “ControlUtilisateur”. Dans la plupart des cas, c’est le comportement attendu. Si l’on a besoin d’accéder à une propriété du Bouton, à partir du UserControl, on aura tendance à créer une propriété wrappant la propriété du bouton. Par exemple une propriété ButtonStyle dans le UserControl qui sera affectée à la propriété Style du bouton.

Mais parfois il peut arriver que l’accès complet au contrôle interne (Button1) soit nécessaire. Dans ce cas il existe deux solutions :

  • Créer une propriété de type Button nommée “Button1Bis” dans le contrôle et renvoyer l’instance de “Button1”. On doit avoir deux noms pour désigner le même contrôle :/
  • Ajouter une balise dans XAML permettant de changer l’accessibilité de l’objet “Button1” généré dans VisualStudio.

<UserControl x:Class="Blog.ControlUtilisateur">
    <Grid>
        <Button x:Name="Button1" x:FieldModifier="public"></Button>
    </Grid>
</UserControl>

Dans cette dernière solution “Button1” sera déclaré comme public et l’on pourra y accéder directement à partir de “ControlUtilisateur” mais seulement à partir du code behind C# (et pas de XAML).

Tous les modificateurs d’accès sont présents : public, protected, internal, private (par défaut) ce qui permet de définir un niveau d’accessibilité de votre contrôle enfant relatif au besoin des contrôles parents. Il semble intéressant par exemple d’éviter le modificateur public au profit d’internal/protected afin que le monde entier ne puisse pas accéder aux controles internes de vos UserControls.