Mon premier blog

Aller au contenu | Aller au menu | Aller à la recherche

.Net en général

Fil des billets

mardi, octobre 27 2009

Le décodage des PathGeometry

WPF impose une nouvelle conduite de projets. En effet, maintenant avec cette technologie émergente, les designers et les programmeurs travaillent en commun. Un problème m'est survenu dans l'intégration du design. Prenons l'exemple d'un Canvas pour qui sa propriété Clip est passée entre les mains d'un designer afin de lui donner une forme bien précise, en utilisant des courbes de Béziers, des arcs ou encore des quadratiques de Béziers. Si on se penche sur la valeur de cette propriété, on peut remarquer que c'est une simple chaîne de caractères, avec une suite de chiffres et de lettres. Le designer a en fait généré un PathGeometry qui définit le contour du composant. La problématique était de rendre vectoriel quelques rectangles intégrant dans leur propriété Fill, un VisualBrush qui encapsulait une image. Ces rectangles, superposés les uns sur les autres, avaient tous un contour différents et par conséquent, leur propriété Clip n'avait pour valeur q'une chaîne de caractères du style :

M12.23254;65.324254A12;32 . . . L0.1453;9.345z

Par conséquent, impossible d'appliquer la méthode Transform sur la propriété. Cette chaîne à première vue complexe et alambiquée, ne l'ai qu'au premier abord. Il faut savoir que le PathGeometry possède une alternative plus légère : le StreamGeometry. Cette classe possède quelques contraintes, car elle ne prend pas en charge les liaisons de données, les animations ni les modifications. Cependant cette classe de type Geometry permet d'être tracée ou décrite facilement avec l'aide d'un StreamGeometryContext qui utilise des commandes de dessins. On peut assimiler un StreamGeometryContext comme une sorte de sérialiseur de tracés. Cette classe expose 5 principales méthode énoncée ci-dessous. Chacune de ces méthode permet de décrire un tracé précis, leur nom est assez explicite pour que je ne vous les détaille pas. L'appel à la méthode Freeze() est optionnel, il permet une optimisation en terme de performances.

StreamGeometryHelper_UsingStreamGeometryContex.jpg



Maintenant que nous avons posé les bases, nous allons rentrer dans la décomposition de cette chaîne de caractères qui semble s'étendre à l'infini :).

Pour commencer, on peut remarquer la présence très ponctuelle des lettres. En fait, elles sont considérées comme des marqueurs de type du tracé. On peut en dénombrer 5, tout comme le nombre de méthodes de tracés de la classe StreamGeometryContext. Voici les 5 marqueurs différents avec leur méthode respective :

  • A ArcTo().
  • C BezierTo().
  • L LineTo().
  • M BeginFigure().
  • Q QuadricBezierTo()

On pourra remarquer la présence d'un autre marqueur : z, qui indique simplement la fin de la valeur. Le reste de la chaîne de caractères correspond aux points, plus précisément aux paramètres, qui permettent de définir le tracé. La méthode BeginFigure(), prend en paramètre le point d'entré du tracé. Cette méthode et par extension son marqueur M est indispensable. S'il est absent le dessin ne pourra pas s'effectuer. Les marqueurs C, L et Q sont suivit d'une liste de points. Chaque point est sérialisé sous la forme : <double>X;<double>Y, avec un espace qui sépare chacun d'entre eux. Pour les arcs, les paramètres diffèrent un peut. C'est pour cela que l'on trouvera 1 marqueur A par arc de tracé contrairement aux trois autres qui sont unique dans toute la valeur. Voici le découpage de la sérialisation d'un arc : <double>Width;<double>Height;<double>Angle;<bool>IsLargeArc;<enum>SweepDirection;<double>X;<double>Y. L'ordre d'apparition des marqueurs et par conséquent de leurs paramètres est aléatoire, hormis le marqueur M qui sera toujours en tête de la string. On pourra trouver de temps à autre un marqueur F1 qui doit caractériser que l'on est passé par la création d'un PathGeometry plutôt q'un StreamGeometry, mais je n'en suis pas persuadé.

Passons maintenant coeur du sujet : voici le code

StreamGeometryHelper_Code.jpg

vendredi, octobre 9 2009

Comment trouver un élément dans l'Arbre logique WPF grâce à son nom.

Depuis que j'utilise le WPF, un problème récurrent m'est apparu. En effet, il n'y a aucune fonction de recherche dans l'arbre logique des composants d'un logiciel WPF. Il existe une classe, VisualTreeHelper, qui propose quelques méthodes statiques intéressantes mais pas assez poussées. Imaginons que nous désirons récupérer un élément particulier de l'arbre logique, à partir de n'importe quel autre composant. Par exemple, nous disposons d'une structure telle que:

Structure_test_FindTreeHelper.jpg

Il serait intéressant de pouvoir accéder à partir d'un événement déclenché sur le bouton1 au bouton2. Pour ce faire, la propriété Name d'un objet de type FrameworkElement ou dérivé, est unique dans toute une arborescence WPF, et sur cette propriété que nous allons faire notre recherche de FrameworkElement

Si on s'attarde un peut sur les composants de base que nous fournit WPF dans le namespace System.Windows.Controls, on s'aperçoit qu'il existe 4 conteneurs différents. J'entends par conteneur un objet de type primitif FrameworkElement qui peut contenir d'autres objets. Voici la liste des trois conteneurs ainsi que la propriété qui permet de référencer les éléments enfants.

  • Decorator : il permet de référencer un et un seul élément enfant dans sa propriété Child
  • ContentControl : il permet de référencer un et un seul élément enfant dans sa propriété Content
  • ItemsControl : il permet de référencer une liste d'éléments enfants dans sa propriété Items
  • Panel : il permet de référencer une liste d'éléments enfants dans sa propriété Children

Bien que dans ce listage on ne peut pas voir la différence entre les objets du type ItemsControl et Panel, elle est majeur. D'un point de vue architecture, ItemsControl tout comme ContentControl héritent de Control. Par contre Panel hérite directement de FrameworkElement tout comme Control. De plus les items d'un objet ItemsControl seront affichés les uns derrière les autres avec des styles qui peuvent être différents si vous utilisez un DataTemplateSelector. Cette liste d'items peut bénéficier des fonctionnalités des scrolls. Les objets hérités de Panel quant à eux, permettent un placement logique des éléments enfants comme un Grid qui nous permet de les organiser en ligne et/ou en colonne, ou encore un Canvas où le développeur est libre de choisir la position relative des enfants par rapport à sa taille et sa position. Voici comment se présente cette hiérarchie:

Hierarchie_ContentCtrl_ItemsCtrl_Panel.jpg

Maintenant entrons dans le vif du sujet. Les composants WPF s'organisent dans un arbre qui est du type Arbre n-aires, pour ce faire, il nous faut faire une recherche sur un string qui correspondra à l'élément cherché. Deux mots clé C# vont nous être très utile : as pour tout ce qui est cast et var qui permet de prendre n'importe quel type.

Pour initialiser une recherche, il est préférable de se positionner sur l'élément racine de l'arbre, c'est-à-dire l'élément qui à pour Parent la valeur null. Voici la méthode qui permet d'atteindre la racine à partir de n'importe quel élément de l'arborescence :

FindTreeHelper_GetRacine.jpg

Maintenant que nous pouvont atteindre la racine, à partir de n'importe quel élément, nous allons effectuer notre recherche. Nous devons parcourir tous les éléments enfants en prenant compte les quatre types de conteneurs du framework. Cet algorithme est un exemple de parcours en profondeur sur un arbre n-aires. C'est par ce même parcours que les navigateurs interprètent les applications Silverlight basées également sur du WPF. Voici la méthode de recherche :

FindTreeHelper_GetElementFromLogicalTree.jpg

Cet algorithme est efficace pour la recherche d'un seul élément. Mais imaginons maintenant que nous voulons effectuer une recherche sur une clé qui n'est pas unique. Dans ce cas, deux choix s'offre à nous. Soit on décide de récupérer le premier élément possédant la clé, ce qui n'est pas très judicieux si l'élément ciblé se trouve, par exemple, en troisième position. Soit on récupère la liste de tout les éléments concordants avec la clé. Pour se faire un nouveau mot clé nous sera utile : yield. Ce mot clé offert par .Net nous permet de remplir un IEnumerable<> dans une boucle. Prenons le cas où nous voulons récupérer tous les éléments d'un type donné. Par exemple si l'on veut récupérer l'ensemble des boutons d'un arbre logique WPF. Voici à quoi doit ressembler l'algorithme.

FindTreeHelper_GetElementsFromLogicalTree.jpg

Nous avons désormais deux parcours similaires mais avec un retour différent. Maintenant nous pouvons imaginer n'importe quelle recherche sur n'importe quelle clé. Il nous reste à faire un accès à ces nouvelles fonctionnalité par la méthode décrite ci-dessous.

FindTreeHelper_Find.jpg

Le parcours de l'arbre logique n'a plus de secret désormais mais qu'en est-il de l'arbre visuel, c'est-à-dire, une arborescence définie dans le Template d'un composant. L'extension à l'arbre visuel est aisée. En effet La classe VisualTreeHelper, grâce à sa méthode GetChild, nous permet d'accéder à l'élément racine dans le Template de l'élément ciblé. De plus, toute l'arborescence définie dans un Template n'est autre qu'un arbre logique. Donc il nous suffit d'atteindre cet élément racine au sein du Template et de lancer la recherche effectuée par les algorithmes précédents.

Je tiens à remercier Arnaud Auroux pour l'idée.

jeudi, septembre 3 2009

Surface - Synchroniser le scroll de n SurfaceScrollViewers

Le SurfaceScrollViewer est une surcharge de l'élément WPF ScrollViewer. Le framework 3.5 SP1, ne nous permet pas d'accéder, en code xaml, sur les propriétés HorizontalOffset et VerticalOffset de ce composant. Or ces 2 propriétés nous indiquent la position du scroll, respectivement horizontale et verticale, du ScrollViewer. Cette restriction vient du fait que l'on ne peut pas effectuer un binding en mode TwoWay sur ces propriétés. En effet, on peut récupérer la valeur, mais on neut peut la modifier. Afin de modifier ces 2 propriétés, on doit utiliser les méthodes ScrollToHorizontalOffset et ScrollToVerticalOffset respectivement. Dans cet article je vais vous détailler comment contourner cette carence de DataBinding. On va synchronizer différents SurfaceScrollViewers entre eux, selon leur groupe d'appartenance dont le développeur pourra définir.

Pour cela on créé une classe qui dérive de DependencyObject. Car, on sait que l'objet que l'on créé n'a pas de représentation graphique. Par contre, il doit être capable de fournir une propriété de dépendance et de s'attacher à un composant graphique, en l'occurence un SurfaceScrollViewer, d'où cet héritage.

declarer_class_scrollviewer_synchronizer.bmp

Dans cette classe nous allons créé une nouvelle propriété de dépendance, en utiilisant la méthode statique RegisteryAttached. Cette méthode permet d'inscrire une propriété jointe avec le système de propriétés de son appartenance. On lui spécifie le nom de la propriété dans un string, le type de la propriété, le type du propriétaire et les métadonnées de la propriété spécifiée. Voici la déclaration de la DependencyPorperty et son encapsulation.

dependency_property_srcollviewer_synchronizer.bmp

Les méthodes d'encapsulations sont statiques, ce qui leur permettent d'être partagées avec n'importe quel autre objet. Pour en savoir un peu plus sur les DependencyProperty je vous conseille de lire mon article sur le sujet.

Maintenant que nous avons notre propriété de dépendance sur laquelle on peu effectuer un binding, il nous faut stocker des données concernant les SurfaceScrollViewers afin de pouvoir synchroniser leurs scrolls. On définit 3 dictionnaires de données. Le premier permet de stocker les objets de type SurfaceScrollViewer en tant que clé du dictionnaire. Chaque clé a une valeur assignée de type string. Cette valeur correspond au groupe de synchronisation des scrolls dans lequel le SurfaceScrollViewer appartient. Le groupe correspond à la valeur de la propriété de dépendance ScrollGroupProperty que l'on aurra encré dans les propriétés, en code xaml, des SurfaceScrollViewers que l'on désire synchroniser.

dictionnaire1_scrollviewer_synchronizer.bmp

Les 2 dictionnaires suivant permettent de stocker la position du scroll horizontal et du scroll vertical respectivement. Ilsprennent comme clé un string qui est la valeur stockée dans le premier dictionnaire, c'est-à-dire le groupe de synchronisation.

dictionnaires_scrolls_scrollViewer_Synchronizer.bmp

On revient sur la méthode spécifiée dans les métadonnées de la propriété de dépendance ScrollGroupProperty. Elle est déclenchée suite à la modification de la propriété. Cette méthode va nous permettre de réagir à 2 choses :

  • On désabonne la méthode ScrollViewer_ScrollChanged de l'event ScrollChanged et on supprime le SurfaceScrollViewer du premier dictionnaire.
  • On réabonne la méthode ScrollViewer_ScrollChanged à l'event ScrollChanged, après avoir placé les scrolls selon les caractéristiques du groupe et l'insertion de la nouvelle KeyValuePair dans le dictionnaire.

OnScrollGroupChanged_ScrollViewer_synchronizer.bmp

La méthode ScrollViewer_ScrollChanged que l'on abonne à l'event ScrollChanged de chaque SurfaceScrollViewer ayant un groupe de définit, nous permet de positionner le scroll de tous les autres SurfaceScrollViewer du même goupe à la position de l'appelant. Merci aux références ^). C'est dans cette méthode que l'on pourra utiliser les méthodes ScrollToHorizontalOffset et ScrollToVerticalOffset.

ScrollViewer_ScrollChanged_ScrollViewer_Synchronizer.bmp

Pour utiliser cette nouvelle classe en xaml, on doit définir au préalable le namespace dans un xmlns. Voici une fois fait, comment uriliser la synchronisation.

SynchronizerScroll_SurfaceScrollViewer_xaml.bmp

lundi, août 31 2009

Les Dependency Properties

Le DataBinding est un outil très répandu et facilement utilisable en WPF. Cet outil permet de lier des données d'une couche métier à la couche de l'interface utilisateur. On peut aisément coupler une liste d'objets métiers aux items d'une ListBox et WPF s'occupe d'habiller le composant ainsi que chacune de ses items.

On peut demander à WPF de mettre à jour l'interface, à chaque modification de l'objet source. Pour cela on à 2 possibilités, soit on passe par la réflexion de .Net, soit en déclarant des Dependency Property. La réflexion s'utilise en implémentant l'interface INotifyPropertyChanged et déclenche l'event PropertyChanged à chaque fois qu'une propriété est modifiée.

Les Dependency Properties sont utilisables dans des Dependency Objects. Cette classe est la base d'un très grand nombre de classe de WPF. Les Dependency Properties permettent aux objets qui les détiennent, d'avoir des propriétés dont la valeur peut dépendre de nombreuses choses. En effet, elles peuvent dépendre d'un DataBinding ou d'une animation. De plus elles fournissent un support pour ce qui est de l'auto-validation ou de la gestion des valeurs par défaut. Les classes dérivées peuvent modifier très facilement le comportement d'une DependencyProperty héritée.

Déclarer une DependencyProperty

Avant toute chose, il faut savoir que la déclaration d'une DependencyProperty est soumise à quelques contraintes. En effet, elle doivent respectées cette synthaxe :

DependencyProperty_declaration_2.bmp

On peut déclarer de différentes manières une DependencyProperty. Nous allons voir la plus utilisée et la plus simple :

DependencyProperty_declaration_1.bmp

On enregistre dans un dictionnaire la propriété grâce à la méthode statique DependencyProperty.Register(). Cette méthode peut prendre de 3 à 5 paramètres.

  1. On donne la clé du dictionnaire sous forme de string qui sera le nom de la propriété tronquée de "Property". (string)
  2. On précise le type de la propriété. (Type)
  3. On précise le type de l'objet qui la déclare. (Type)
  4. (facultatif) On créé des métadonnées qui définirons le comportement de la propriété. (PropertyMetadata)
  5. (facultatif) On référence un rappel qui doit exécuter toute validation personalisé de la valeur de la propriété de référence, au delà de la validation de type standard. (ValidateValueCallBack).

Les deux derniers paramètres requièrent des méthodes d'extensions qui doivent être statiques, vue q'une Dependency Property l'est aussi. Voici à quoi ressemble la déclaration de ces 2 méthodes :

DependencyProperty_declaration_3.bmp

Afin de rendre les Dependency Porperties complètement transparentes dans une classe, on les encapsule dans des propriétés CLR. Cette encapsulation nous permet donc de pouvoir appliquer le DataBinding. La classe qui permet d'encapsuler les Dependency Properties, doit hériter de DependencyObject. En dérivant de cette classe on peut utiliser les méthodes GetValue et SetValue. Voici le code d'encapsulation :

Encapsulation d'une DependencyProperty

La méthode GetValue revoie un object on doit donc obligatoirement caster son retour par le type approprié, i.e. le 2e paramètre de la méthode statique Register. On note également que la propriété CLR doit respecter la synthaxe de la clé passé dans le dictionnaire lors de l'enregistrement.