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 !

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.