Arbre logique et arbre visuel WPF en 2 min

En WPF, les éléments visuels sont organisés hiérarchiquement avec une structure en arbre: des éléments sont ajoutés au contenu d’autres éléments formant des nœuds dans un arbre d’objets. Le developpeur implémente les relations entre les objets formant ainsi un arbre logique (i.e. logical tree). Lorsque les objets sont dessinés puis affichés au runtime, les relations entres les objets peut différer de celles de l’arbre logique, ces relations utilisées pendant l’affichage est l’arbre visuel (i.e. visual tree).

Hiérarchie des classes d’objets

Avant de rentrer dans les détails des arbres d’objets, on peut rappeler la hiérarchie des classes des objets principaux en WPF. Il ne faut pas confondre la hiérarchie des classes avec les arbres logiques et visuels:

  • Hiérarchie des classes: arbre d’héritage des types d’objets.
  • Arbre logique et arbre visuel: structures de composition des objets valables seulement au runtime.

La hiérarchie des classes des objets est:

Les classes se décomposent de la façon suivante:

  • System.Object: toutes les classes dérivent de ce type.
  • System.Windows.Threading.DispatcherObject: ce sont des objets utilisant le Dispatcher WPF (boucle de messages WPF). L’accès à ces objets est limité au thread ayant créé l’objet.
  • System.Windows.DependencyObject: classe de base des objets supportant les i>dependency properties.
  • System.Windows.Freezable: classe de base des objets pouvant être figés dans un état en lecture seule pour des raisons de performance. Ce sont, le plus souvent, primitives graphiques comme les brushes, les pens, les classes géométriques ou les classes d’animation. Lorsque ces objets sont figés, ils peuvent être partagés entre plusieurs threads (contrairement aux objets de type DispatcherObject). Ces objets restent toujours figés et peuvent être cloné si besoin.
  • System.Windows.Media.Visual: classe de base des objets ayant une représentation visuel 2D.
  • System.Windows.UIElement: classe de base des objets 2D supportant les fonctionnalités WPF de routed events, des command bindings, du système de layout et de focus.
  • System.Windows.ContentElement: classe de base similaire à UIElement n’ayant pas un comportement de rendu visuel. Ces objets sont contenus par un objet de type Visual qui peut avoir un rendu graphique. Un objet de type ContentElement a besoin de plusieurs objets Visual pour avoir un rendu graphique complet.
  • System.Windows.FrameworkElement: ajoute au UIElement une gestion des styles, du data binding, des ressources, des mécanismes de tooltip et de menu contextuel.
  • System.Windows.FrameworkContentElement: similaire à FrameworkElement pour des objets ayant un contenu.
  • System.Windows.Controls.Control: classe de base des controls WPF. Cette classe permet de rajouter des propriétés comme Foreground, Background ou FontSize.

Arbre logique

Comme indiqué plus haut, les objets WPF sont organisés dans une structure en arbre c’est-à-dire que les instances d’objets sont créées et rangées dans d’autres objets suivant une relation de composition.

Il y a donc des éléments contenus dans d’autres éléments, une relation d’enfant entre un objet et un autre et inversement une notion de parent.

Le but de l’arbre logique est de gérer de façon uniforme les objets de type FrameworkElement et FrameworkContentElement suivant leurs relations d’appartenance.

Ainsi la notion d’arbre logique est responsable des fonctionnalités:

  • La propriété FrameworkElement.Parent: cette propriété présente dans tous les objets de type FrameworkElement permet d’avoir une relation Parent-Enfant pour tous ces objets, definissant ainsi la relation d’appartenance.
  • Héritage des valeurs des DependencyProperty: certaines propriétés sont héritables comme FontFamily ou DataContext c’est-à-dire que la valeur de ces propriétés est hérité du parent logique lorsqu’il existe (i.e. logical ancestor). L’héritage des valeurs se fait entre les objets de type FrameworkElement ou FrameworkContentElement.
  • Références {DynamicResource}: lorsque le code Xaml comporte une référence {DynamicResource}, la recherche de la ressource se fait suivant l’arbre logique sur les objets possédant une propriété Resources de type ResourceDictionary. Cette recherche se fait sur les “ancêtres logiques” (i.e. logical ancestors). Resources est une propriété des objets de type FrameworkElement ou FrameworkContentElement (mais aussi System.Windows.Application).
  • Recherche du nom: quand il faut chercher le nom d’un élément, par exemple, quand on écrit dans le code Xaml: {Binding ElementName=OtherElement}, la recherche se fait aussi suivant les “ancêtres logiques”.
  • Routed events: les “évènements routés” (i.e. routed events) lorsqu’ils sont déclenchés, suivent l’arbre logique du haut vers le bas pour les évènements “tunnel” (i.e. tunneling routed events) ou du bas vers le haut pour les évènements “bubble” (i.e. bubbling routed events).

L’ajout ou la suppression se fait par l’intermédiaire des fonctions FrameworkElement.AddLogicalChild ou FrameworkElement.RemoveLogicalChild de façon implicite lorsqu’on affecte une valeur aux objets, par exemple, par l’intermédiaire de la propriété ContentControl.Content.

L’ajout d’enfants se ne fait forcement de la même façon pour tous les objets mais varie suivant leurs spécifités:

Namespace Classe Propriétés
System.Windows.Controls AdornedElementPlaceholder Child
ContentControl Content
Decorator Child
Grid Children (hérité de Panel)
Columns
Rows
HeaderedItemsControl Items (hérité de ItemsControl)
Header
ItemsControl Items
Page Content
Panel Children
RichTextBox Document
TextBlock Text
TextBox Text
ViewBox Child
System.Windows.Controls.Primitives DocumentViewerBase Document
Popup Child
System.Windows.Documents PageContent Child
Table RowGroups
Columns
Span Inlines

Arbre visuel

L’arbre visuel est une notion de relations entre les objets lorsqu’ils ont un rendu graphique. Comme pour l’arbre logique, il existe une relation d’appartenance toutefois cette relation se fait sur les objets de type System.Windows.Media.Visual.

L’arbre visuel peut être similaire à l’arbre logique toutefois, d’une façon générale, il comporte plus d’éléments car à un élément de l’arbre logique correspondant un ou plusieurs éléments de l’arbre visuel. L’arbre logique peut ne pas exister mais l’arbre visuel existe toujours.

Par exemple quand on écrit:

<Window> 
    <StackPanel> 
        <Label Content="Label" /> 
        <Button Content="Button" /> 
    </StackPanel> 
</Window>

L’arbre visuel est:

L’arbre logique comporte les éléments écrits dans le code Xaml: Window, StackPanel, Label et Button alors que l’arbre visuel comporte d’autres éléments nécessaires à l’affichage.

Ainsi l’arbre visuel sert à:

  • Effectuer le rendu visuel des éléments,
  • Héritage des valeurs des propriétés: si un élément n’a pas de parent dans l’arbre logique alors la règle d’héritage de la valeur d’une propriété héritable se fait suivant l’arbre visuel au lieu de l’arbre logique.
  • Références {DynamicResource}: de même, s’il n’y a pas d’arbre logique, c’est l’arbre visuel qui est utilisé pour la recherche d’une ressource.
  • Routed events: les “évènements routés” parcourent aussi l’arbre visuel s’il est différent de l’arbre logique.
  • Propagation des propriétés UIElement.Opacity, UIElement.IsEnabled: la recherche de valeur de ces propriétés se fait suivant l’arbre visuel.
  • Transformations: les transformations UIElement.RenderTransform et UIElement.LayoutTransform sont prises en compte dans un élément suivant les transformations appliqués à son parent dans l’arbre visuel.

LogicalTreeHelper et VisualTreeHelper

Il est possible de circuler le long des arbres logiques et visuels en s’aidant des fonctions des classes statiques System.Windows.LogicalTreeHelper et System.Windows.Media.VisualTreeHelper.

La classe System.Windows.LogicalTreeHelper possède les fonctions:

  • GetChildren(): pour chercher les enfants logiques d’un élément de type DependencyObject, FrameworkContentElement ou FrameworkElement.
  • GetParent(): pour trouver le parent logique d’un élément de type DependencyObject.
  • FindLogicalNode(): pour un objet enfant suivant son nom dans l’arbre logique.

De même, le classe System.Windows.Media.VisualTreeHelper possède des fonctions similaires:

  • GetChild(): renvoie l’élément enfant d’un objet de type DependencyObject situé à un index donné dans une collection d’objets enfants.
  • GetChildCount(): renvoie le nombre d’éléments enfant.
  • GetParent(): renvoie le parent d’un objet de type DependencyObject.

Chercher un type particulier parmi les enfants

Pour chercher récursivement parmi les enfants d’un objet de type DependencyObject, un objet suivant son type:

public static T FindChild<T>(DependencyObject parent)  
    where T : DependencyObject 
{ 
    if (parent == null) return null; 
 
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) 
    { 
        var child = VisualTreeHelper.GetChild(parent, i); 
 
        var result = (child as T) ?? FindChild<T>(child); 
        if (result != null) return result; 
    } 
    return null; 
}

Cherche un élément avec un nom particulier parmi les enfants

De même on peut effectuer la recherche de façon récursive en cherchant un type et un nom particulier:

public static T FindChild<T>(DependencyObject parent, string childName) 
   where T : DependencyObject 
{     
  if (parent == null) return null; 
 
  T foundChild = null; 
 
  int childrenCount = VisualTreeHelper.GetChildrenCount(parent); 
  for (int i = 0; i < childrenCount; i++) 
  { 
    var child = VisualTreeHelper.GetChild(parent, i); 
 
    T childType = child as T; 
    if (childType == null) 
    { 
      foundChild = FindChild<T>(child, childName); 
 
      if (foundChild != null) break; 
    } 
    else if (!string.IsNullOrEmpty(childName)) 
    { 
      var frameworkElement = child as FrameworkElement; 
      if (frameworkElement != null && frameworkElement.Name == childName) 
      { 
        foundChild = (T)child; 
        break; 
      } 
    } 
    else 
    { 
      foundChild = (T)child; 
      break; 
    } 
  } 
 
  return foundChild; 
}

Chercher tous les enfants ayant un type particulier

Pour chercher tous les descendants ayant un type particulier:

public static IEnumerable<T> FindVisualChildren<T>(DependencyObject parent)  
    where T : DependencyObject 
{ 
    if (parent != null) 
    { 
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) 
        { 
            DependencyObject child = VisualTreeHelper.GetChild(parent, i); 
            if (child != null && child is T) 
            { 
                yield return (T)child; 
            } 
 
            foreach (T childChildren in FindVisualChildren<T>(child)) 
            { 
                yield return childChildren; 
            } 
        } 
    } 
}

Débugguer l’arbre visuel

Il est possible de voir l’arbre visuel des objets WPF d’une application au runtime avec des outils particuliers. Par exemple Snoop 2.8.0 est un outil très puissant:

  • Voir les objets de l’arbre visuel,
  • Voir et modifier les propriétés des objets de l’arbre visuel,
  • Voir le trajet des évènement routés dans le sens “tunnel” (généralement préfixés par Preview comme PreviewKeyDown) et dans le sens “bubble” (par exemple KeyDown),
  • Localiser un objet directement sur l’interface de l’application à partir de l’arbre visuel: cette fonctionnalité est particulièrement utile pour débugguer l’héritage des propriétés, l’application de styles…
One response... add one

Leave a Reply