Comment puis-je trouver des contrôles WPF par nom ou type?

264

J'ai besoin de rechercher dans une hiérarchie de contrôles WPF des contrôles qui correspondent à un nom ou un type donné. Comment puis-je faire ceci?

alex2k8
la source

Réponses:

311

J'ai combiné le format de modèle utilisé par John Myczek et l'algorithme de Tri Q ci-dessus pour créer un algorithme findChild qui peut être utilisé sur n'importe quel parent. Gardez à l'esprit que la recherche récursive d'un arbre vers le bas peut être un processus long. J'ai seulement vérifié cela sur une application WPF, veuillez commenter les erreurs que vous pourriez trouver et je corrigerai mon code.

WPF Snoop est un outil utile pour regarder l'arborescence visuelle - je vous recommande fortement de l'utiliser pendant les tests ou d'utiliser cet algorithme pour vérifier votre travail.

Il y a une petite erreur dans l'algorithme de Tri Q. Une fois l'enfant trouvé, si le nombre d'enfants est> 1 et que nous réitérons, nous pouvons remplacer l'enfant trouvé correctement. J'ai donc ajouté un if (foundChild != null) break;dans mon code pour faire face à cette condition.

/// <summary>
/// Finds a Child of a given item in the visual tree. 
/// </summary>
/// <param name="parent">A direct parent of the queried item.</param>
/// <typeparam name="T">The type of the queried item.</typeparam>
/// <param name="childName">x:Name or Name of child. </param>
/// <returns>The first parent item that matches the submitted type parameter. 
/// If not matching item can be found, 
/// a null parent is being returned.</returns>
public static T FindChild<T>(DependencyObject parent, string childName)
   where T : DependencyObject
{    
  // Confirm parent and childName are valid. 
  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);
    // If the child is not of the request child type child
    T childType = child as T;
    if (childType == null)
    {
      // recursively drill down the tree
      foundChild = FindChild<T>(child, childName);

      // If the child is found, break so we do not overwrite the found child. 
      if (foundChild != null) break;
    }
    else if (!string.IsNullOrEmpty(childName))
    {
      var frameworkElement = child as FrameworkElement;
      // If the child's name is set for search
      if (frameworkElement != null && frameworkElement.Name == childName)
      {
        // if the child's name is of the request name
        foundChild = (T)child;
        break;
      }
    }
    else
    {
      // child element found.
      foundChild = (T)child;
      break;
    }
  }

  return foundChild;
}

Appelez ça comme ceci:

TextBox foundTextBox = 
   UIHelper.FindChild<TextBox>(Application.Current.MainWindow, "myTextBoxName");

La note Application.Current.MainWindowpeut être n'importe quelle fenêtre parente.

CrimsonX
la source
@CrimsonX: Peut-être que je fais ça mal ... J'avais un besoin similaire où j'avais besoin d'accéder à un contrôle (ListBox) à l'intérieur d'un ContentControl (Expander). Le code ci-dessus n'a pas fonctionné pour moi tel quel. J'ai dû mettre à jour le code ci-dessus pour voir si un nœud feuille (GetChildrenCount => 0) est un ContentControl. Si oui, vérifiez si le contenu correspond aux critères nom + type.
Gishu
@ Gishu - Je pense que cela devrait fonctionner à cette fin. Pouvez-vous copier et coller votre code pour montrer comment vous utilisez l'appel? Je m'attendrais à ce que ce soit FindChild <ListBox> (Expander myExpanderName, "myListBoxName").
CrimsonX
3
@CrimsonX Je pense avoir trouvé un autre cas d'angle. J'essayais de trouver le PART_SubmenuPlaceholder dans le RibbonApplicationMenuItem, mais le code ci-dessus ne fonctionnait pas. Pour le résoudre, j'ai dû ajouter ce qui suit: if (name == ElementName) else {foundChild = FindChild (child, name) if (foundChild! = Null) break; }
kevindaub
6
Attention, il y a un bug ou plus dans la réponse. Il s'arrêtera dès qu'il atteindra un enfant du type recherché. Je pense que vous devriez considérer / prioriser d'autres réponses.
Eric Ouellet
2
Ce code est génial, mais il ne fonctionnera pas si vous ne recherchez pas un type d'élément spécifique, par exemple si vous passez FrameworkElementen T, il retournera null dès que la première boucle se terminera. donc tu vas devoir faire quelques modifications.
Amir Oveisi
131

Vous pouvez également rechercher un élément par son nom à l'aide de FrameworkElement.FindName (chaîne) .

Donné:

<UserControl ...>
    <TextBlock x:Name="myTextBlock" />
</UserControl>

Dans le fichier code-behind, vous pouvez écrire:

var myTextBlock = (TextBlock)this.FindName("myTextBlock");

Bien sûr, car il est défini à l'aide de x: Name, vous pouvez simplement référencer le champ généré, mais vous souhaitez peut-être le rechercher de manière dynamique plutôt que statique.

Cette approche est également disponible pour les modèles, dans lesquels l'élément nommé apparaît plusieurs fois (une fois par utilisation du modèle).

Drew Noakes
la source
6
Pour que cela fonctionne, vous ne devez pas nécessairement ajouter le "x:" à l'attribut name.
brian buck
3
Cela ne semble pas toujours fonctionner. J'ai des UserControls qui sont combinés ensemble par programmation dans des grilles imbriquées en tant que contenu d'une fenêtre de propriétés. La réponse de CrimsonX fonctionne bien cependant.
Matt
4
Cela ne fonctionnera pas pour les éléments dans ItemControls, ListBoxes, etc.
Sorensen
67

Vous pouvez utiliser VisualTreeHelper pour rechercher des contrôles. Vous trouverez ci-dessous une méthode qui utilise VisualTreeHelper pour rechercher un contrôle parent d'un type spécifié. Vous pouvez également utiliser VisualTreeHelper pour rechercher d'autres contrôles.

public static class UIHelper
{
   /// <summary>
   /// Finds a parent of a given item on the visual tree.
   /// </summary>
   /// <typeparam name="T">The type of the queried item.</typeparam>
   /// <param name="child">A direct or indirect child of the queried item.</param>
   /// <returns>The first parent item that matches the submitted type parameter. 
   /// If not matching item can be found, a null reference is being returned.</returns>
   public static T FindVisualParent<T>(DependencyObject child)
     where T : DependencyObject
   {
      // get parent item
      DependencyObject parentObject = VisualTreeHelper.GetParent(child);

      // we’ve reached the end of the tree
      if (parentObject == null) return null;

      // check if the parent matches the type we’re looking for
      T parent = parentObject as T;
      if (parent != null)
      {
         return parent;
      }
      else
      {
         // use recursion to proceed with next level
         return FindVisualParent<T>(parentObject);
      }
   }
}

Appelez ça comme ceci:

Window owner = UIHelper.FindVisualParent<Window>(myControl);
John Myczek
la source
Comment obtenez-vous ou qu'est-ce que myControl?
Demodave
21

Je répète peut-être tout le monde, mais j'ai un joli morceau de code qui étend la classe DependencyObject avec une méthode FindChild () qui vous donnera l'enfant par type et nom. Il suffit d'inclure et d'utiliser.

public static class UIChildFinder
{
    public static DependencyObject FindChild(this DependencyObject reference, string childName, Type childType)
    {
        DependencyObject foundChild = null;
        if (reference != null)
        {
            int childrenCount = VisualTreeHelper.GetChildrenCount(reference);
            for (int i = 0; i < childrenCount; i++)
            {
                var child = VisualTreeHelper.GetChild(reference, i);
                // If the child is not of the request child type child
                if (child.GetType() != childType)
                {
                    // recursively drill down the tree
                    foundChild = FindChild(child, childName, childType);
                }
                else if (!string.IsNullOrEmpty(childName))
                {
                    var frameworkElement = child as FrameworkElement;
                    // If the child's name is set for search
                    if (frameworkElement != null && frameworkElement.Name == childName)
                    {
                        // if the child's name is of the request name
                        foundChild = child;
                        break;
                    }
                }
                else
                {
                    // child element found.
                    foundChild = child;
                    break;
                }
            }
        }
        return foundChild;
    }
}

J'espère que vous le trouverez utile.

Tri Q Tran
la source
2
Par mon article ci-dessus, il y a une petite erreur d'implémentation dans votre code: stackoverflow.com/questions/636383/wpf-ways-to-find-controls/…
CrimsonX
18

Mes extensions au code.

  • Surcharges ajoutées pour trouver un enfant par type, par type et critère (prédicat), trouver tous les enfants de type qui répondent aux critères
  • la méthode FindChildren est un itérateur en plus d'être une méthode d'extension pour DependencyObject
  • FindChildren parcourt également des sous-arbres logiques. Voir le post de Josh Smith lié dans le blog.

Source: https://code.google.com/p/gishu-util/source/browse/#git%2FWPF%2FUtilities

Article de blog explicatif: http://madcoderspeak.blogspot.com/2010/04/wpf-find-child-control-of-specific-type.html

Gishu
la source
-1 Exactement ce que j'allais implémenter (prédicat, itérateur et méthode d'extension), mais il y a un 404 sur le lien source. Deviendra +1 si le code est inclus ici ou si le lien source est corrigé!
cod3monk3y
@ cod3monk3y - La migration Git a tué le lien semble-t-il :) Voilà. code.google.com/p/gishu-util/source/browse/…
Gishu
18

Si vous souhaitez trouver TOUS les contrôles d'un type spécifique, cet extrait pourrait également vous intéresser

    public static IEnumerable<T> FindVisualChildren<T>(DependencyObject parent) 
        where T : DependencyObject
    {
        int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < childrenCount; i++)
        {
            var child = VisualTreeHelper.GetChild(parent, i);

            var childType = child as T;
            if (childType != null)
            {
                yield return (T)child;
            }

            foreach (var other in FindVisualChildren<T>(child))
            {
                yield return other;
            }
        }
    }
UrbanEsc
la source
3
Bon, mais assurez-vous que le contrôle est chargé sinon GetChildrenCount renverra 0.
Klaus Nji
@UrbanEsc, pourquoi lancez-vous childune deuxième fois? Si vous avez childTypedu type T, vous pouvez écrire à l'intérieur du if: yield return childType... non?
Massimiliano Kraus
@MassimilianoKraus Hé, désolé pour la réponse tardive, mais vous avez raison. Je l'attribue en réécrivant cet extrait plusieurs fois, et cela pourrait donc être un fragment d'une vérification différente
UrbanEsc
16

Cela supprimera certains éléments - vous devez l'étendre comme ceci afin de prendre en charge un plus large éventail de contrôles. Pour une brève discussion, jetez un œil ici

 /// <summary>
 /// Helper methods for UI-related tasks.
 /// </summary>
 public static class UIHelper
 {
   /// <summary>
   /// Finds a parent of a given item on the visual tree.
   /// </summary>
   /// <typeparam name="T">The type of the queried item.</typeparam>
   /// <param name="child">A direct or indirect child of the
   /// queried item.</param>
   /// <returns>The first parent item that matches the submitted
   /// type parameter. If not matching item can be found, a null
   /// reference is being returned.</returns>
   public static T TryFindParent<T>(DependencyObject child)
     where T : DependencyObject
   {
     //get parent item
     DependencyObject parentObject = GetParentObject(child);

     //we've reached the end of the tree
     if (parentObject == null) return null;

     //check if the parent matches the type we're looking for
     T parent = parentObject as T;
     if (parent != null)
     {
       return parent;
     }
     else
     {
       //use recursion to proceed with next level
       return TryFindParent<T>(parentObject);
     }
   }

   /// <summary>
   /// This method is an alternative to WPF's
   /// <see cref="VisualTreeHelper.GetParent"/> method, which also
   /// supports content elements. Do note, that for content element,
   /// this method falls back to the logical tree of the element!
   /// </summary>
   /// <param name="child">The item to be processed.</param>
   /// <returns>The submitted item's parent, if available. Otherwise
   /// null.</returns>
   public static DependencyObject GetParentObject(DependencyObject child)
   {
     if (child == null) return null;
     ContentElement contentElement = child as ContentElement;

     if (contentElement != null)
     {
       DependencyObject parent = ContentOperations.GetParent(contentElement);
       if (parent != null) return parent;

       FrameworkContentElement fce = contentElement as FrameworkContentElement;
       return fce != null ? fce.Parent : null;
     }

     //if it's not a ContentElement, rely on VisualTreeHelper
     return VisualTreeHelper.GetParent(child);
   }
}
Philipp
la source
5
Par convention, je m'attendrais à ce que n'importe quelle Try*méthode retourne boolet ait un outparamètre qui retourne le type en question, comme avec:bool IDictionary.TryGetValue(TKey key, out TValue value)
Drew Noakes
@DrewNoakes comment suggérez-vous Philipp de l'appeler, alors? Aussi, même avec une telle attente, je trouve son code à la fois clair et clair à utiliser.
ANeves
1
@ANeves, dans ce cas, je l'appellerais simplement FindParent. Ce nom pour moi implique qu'il pourrait revenir null. Le Try*préfixe est utilisé dans toute la BCL de la manière que je décris ci-dessus. Notez également que la plupart des autres réponses ici utilisent la Find*convention de dénomination. Ce n'est qu'un point mineur cependant :)
Drew Noakes
16

J'ai édité le code de CrimsonX car il ne fonctionnait pas avec les types de superclasse:

public static T FindChild<T>(DependencyObject depObj, string childName)
   where T : DependencyObject
{
    // Confirm obj is valid. 
    if (depObj == null) return null;

    // success case
    if (depObj is T && ((FrameworkElement)depObj).Name == childName)
        return depObj as T;

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
    {
        DependencyObject child = VisualTreeHelper.GetChild(depObj, i);

        //DFS
        T obj = FindChild<T>(child, childName);

        if (obj != null)
            return obj;
    }

    return null;
}
andresp
la source
1
Si vous passez cette méthode a DependencyObjectqui n'est pas a, FrameworkElementelle peut lever une exception. Utiliser également à GetChildrenCountchaque itération de la forboucle sonne comme une mauvaise idée.
Tim Pohlmann
1
Eh
Je viens de le mentionner, car je suis tombé dessus et d'autres pourraient aussi;)
Tim Pohlmann
13

Bien que j'aime la récursivité en général, ce n'est pas aussi efficace que l'itération lors de la programmation en C #, alors peut-être que la solution suivante est plus nette que celle suggérée par John Myczek? Cela recherche une hiérarchie à partir d'un contrôle donné pour trouver un contrôle ancêtre d'un type particulier.

public static T FindVisualAncestorOfType<T>(this DependencyObject Elt)
    where T : DependencyObject
{
    for (DependencyObject parent = VisualTreeHelper.GetParent(Elt);
        parent != null; parent = VisualTreeHelper.GetParent(parent))
    {
        T result = parent as T;
        if (result != null)
            return result;
    }
    return null;
}

Appelez-le comme ceci pour trouver le Windowcontenant contenant un contrôle appelé ExampleTextBox:

Window window = ExampleTextBox.FindVisualAncestorOfType<Window>();
Nathan Phillips
la source
9

Voici mon code pour trouver des contrôles par type tout en contrôlant la profondeur de la hiérarchie (maxDepth == 0 signifie infiniment profond).

public static class FrameworkElementExtension
{
    public static object[] FindControls(
        this FrameworkElement f, Type childType, int maxDepth)
    {
        return RecursiveFindControls(f, childType, 1, maxDepth);
    }

    private static object[] RecursiveFindControls(
        object o, Type childType, int depth, int maxDepth = 0)
    {
        List<object> list = new List<object>();
        var attrs = o.GetType()
            .GetCustomAttributes(typeof(ContentPropertyAttribute), true);
        if (attrs != null && attrs.Length > 0)
        {
            string childrenProperty = (attrs[0] as ContentPropertyAttribute).Name;
            foreach (var c in (IEnumerable)o.GetType()
                .GetProperty(childrenProperty).GetValue(o, null))
            {
                if (c.GetType().FullName == childType.FullName)
                    list.Add(c);
                if (maxDepth == 0 || depth < maxDepth)
                    list.AddRange(RecursiveFindControls(
                        c, childType, depth + 1, maxDepth));
            }
        }
        return list.ToArray();
    }
}
exciton80
la source
9

exciton80 ... J'avais un problème avec votre code qui ne se reproduisait pas via les contrôles utilisateur. Il atteignait la racine de la grille et lançait une erreur. Je crois que cela me corrige:

public static object[] FindControls(this FrameworkElement f, Type childType, int maxDepth)
{
    return RecursiveFindControls(f, childType, 1, maxDepth);
}

private static object[] RecursiveFindControls(object o, Type childType, int depth, int maxDepth = 0)
{
    List<object> list = new List<object>();
    var attrs = o.GetType().GetCustomAttributes(typeof(ContentPropertyAttribute), true);
    if (attrs != null && attrs.Length > 0)
    {
        string childrenProperty = (attrs[0] as ContentPropertyAttribute).Name;
        if (String.Equals(childrenProperty, "Content") || String.Equals(childrenProperty, "Children"))
        {
            var collection = o.GetType().GetProperty(childrenProperty).GetValue(o, null);
            if (collection is System.Windows.Controls.UIElementCollection) // snelson 6/6/11
            {
                foreach (var c in (IEnumerable)collection)
                {
                    if (c.GetType().FullName == childType.FullName)
                        list.Add(c);
                    if (maxDepth == 0 || depth < maxDepth)
                        list.AddRange(RecursiveFindControls(
                            c, childType, depth + 1, maxDepth));
                }
            }
            else if (collection != null && collection.GetType().BaseType.Name == "Panel") // snelson 6/6/11; added because was skipping control (e.g., System.Windows.Controls.Grid)
            {
                if (maxDepth == 0 || depth < maxDepth)
                    list.AddRange(RecursiveFindControls(
                        collection, childType, depth + 1, maxDepth));
            }
        }
    }
    return list.ToArray();
}
Shawn Nelson
la source
8

J'ai une fonction de séquence comme celle-ci (qui est complètement générale):

    public static IEnumerable<T> SelectAllRecursively<T>(this IEnumerable<T> items, Func<T, IEnumerable<T>> func)
    {
        return (items ?? Enumerable.Empty<T>()).SelectMany(o => new[] { o }.Concat(SelectAllRecursively(func(o), func)));
    }

Obtenir des enfants immédiats:

    public static IEnumerable<DependencyObject> FindChildren(this DependencyObject obj)
    {
        return Enumerable.Range(0, VisualTreeHelper.GetChildrenCount(obj))
            .Select(i => VisualTreeHelper.GetChild(obj, i));
    }

Trouver tous les enfants dans l'arbre hiararchique:

    public static IEnumerable<DependencyObject> FindAllChildren(this DependencyObject obj)
    {
        return obj.FindChildren().SelectAllRecursively(o => o.FindChildren());
    }

Vous pouvez appeler cela dans la fenêtre pour obtenir tous les contrôles.

Après avoir la collection, vous pouvez utiliser LINQ (c'est-à-dire OfType, Where).

VB Guy
la source
6

Puisque la question est suffisamment générale pour attirer des personnes à la recherche de réponses à des cas très triviaux: si vous voulez juste un enfant plutôt qu'un descendant, vous pouvez utiliser Linq:

private void ItemsControlItem_Loaded(object sender, RoutedEventArgs e)
{
    if (SomeCondition())
    {
        var children = (sender as Panel).Children;
        var child = (from Control child in children
                 where child.Name == "NameTextBox"
                 select child).First();
        child.Focus();
    }
}

ou bien sûr l'évidence pour l'itération de boucle sur les enfants.

El Zorko
la source
3

Ces options parlent déjà de la traversée de l'arborescence visuelle en C #. Il est également possible de parcourir l'arborescence visuelle dans xaml en utilisant l'extension de balisage RelativeSource. msdn

trouver par type

Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type <TypeToFind>}}}" 
Neeraj
la source
2

Voici une solution qui utilise un prédicat flexible:

public static DependencyObject FindChild(DependencyObject parent, Func<DependencyObject, bool> predicate)
{
    if (parent == null) return null;

    int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
    for (int i = 0; i < childrenCount; i++)
    {
        var child = VisualTreeHelper.GetChild(parent, i);

        if (predicate(child))
        {
            return child;
        }
        else
        {
            var foundChild = FindChild(child, predicate);
            if (foundChild != null)
                return foundChild;
        }
    }

    return null;
}

Vous pouvez par exemple l'appeler comme ceci:

var child = FindChild(parent, child =>
{
    var textBlock = child as TextBlock;
    if (textBlock != null && textBlock.Name == "MyTextBlock")
        return true;
    else
        return false;
}) as TextBlock;
Tim Pohlmann
la source
1

Ce code corrige simplement le bug de la réponse @CrimsonX:

 public static T FindChild<T>(DependencyObject parent, string childName)
       where T : DependencyObject
    {    
      // Confirm parent and childName are valid. 
      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);
        // If the child is not of the request child type child
        T childType = child as T;
        if (childType == null)
        {
          // recursively drill down the tree
          foundChild = FindChild<T>(child, childName);

          // If the child is found, break so we do not overwrite the found child. 
          if (foundChild != null) break;
        }
        else if (!string.IsNullOrEmpty(childName))
        {
          var frameworkElement = child as FrameworkElement;
          // If the child's name is set for search
          if (frameworkElement != null && frameworkElement.Name == childName)
          {
            // if the child's name is of the request name
            foundChild = (T)child;
            break;
          }

 // recursively drill down the tree
          foundChild = FindChild<T>(child, childName);

          // If the child is found, break so we do not overwrite the found child. 
          if (foundChild != null) break;


        else
        {
          // child element found.
          foundChild = (T)child;
          break;
        }
      }

      return foundChild;
    }  

Vous avez juste besoin de continuer à appeler la méthode de manière récursive si les types correspondent, mais pas les noms (cela se produit lorsque vous passez en FrameworkElementtant que T). sinon ça va revenir nullet c'est faux.

Amir Oveisi
la source
0

Pour trouver un ancêtre d'un type donné à partir du code, vous pouvez utiliser:

[CanBeNull]
public static T FindAncestor<T>(DependencyObject d) where T : DependencyObject
{
    while (true)
    {
        d = VisualTreeHelper.GetParent(d);

        if (d == null)
            return null;

        var t = d as T;

        if (t != null)
            return t;
    }
}

Cette implémentation utilise l'itération au lieu de la récursivité qui peut être légèrement plus rapide.

Si vous utilisez C # 7, cela peut être légèrement raccourci:

[CanBeNull]
public static T FindAncestor<T>(DependencyObject d) where T : DependencyObject
{
    while (true)
    {
        d = VisualTreeHelper.GetParent(d);

        if (d == null)
            return null;

        if (d is T t)
            return t;
    }
}
Drew Noakes
la source
-5

Essaye ça

<TextBlock x:Name="txtblock" FontSize="24" >Hai Welcom to this page
</TextBlock>

Code derrière

var txtblock = sender as Textblock;
txtblock.Foreground = "Red"
Jayasri
la source