Jak znaleźć formanty WPF według nazwy lub typu?

264

Muszę przeszukać hierarchię kontroli WPF w celu znalezienia formantów pasujących do podanej nazwy lub typu. W jaki sposób mogę to zrobić?

alex2k8
źródło

Odpowiedzi:

311

Połączyłem format szablonu używany przez Johna Myczka i powyższy algorytm Tri Q, aby utworzyć algorytm findChild, który można zastosować na dowolnym rodzicu. Pamiętaj, że rekurencyjne przeszukiwanie drzewa w dół może być długim procesem. Sprawdziłem to tylko w aplikacji WPF, skomentuj wszelkie błędy, które możesz znaleźć, a ja poprawię mój kod.

WPF Snoop to przydatne narzędzie do przeglądania drzewa wizualnego - zdecydowanie zalecam używanie go podczas testowania lub używania tego algorytmu do sprawdzania pracy.

W algorytmie Tri Q jest mały błąd. Po znalezieniu dziecka, jeśli childCount jest> 1 i iterujemy ponownie, możemy zastąpić poprawnie znalezione dziecko. Dlatego dodałem if (foundChild != null) break;do mojego kodu a, aby poradzić sobie z tym warunkiem.

/// <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;
}

Nazwij to tak:

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

Uwaga Application.Current.MainWindowmoże być dowolnym oknem nadrzędnym.

CrimsonX
źródło
@CrimsonX: Może robię to źle ... Miałem podobną potrzebę, gdy potrzebowałem dostać się do kontrolki (ListBox) w ContentControl (Expander). Powyższy kod nie działał dla mnie tak, jak jest. Musiałem zaktualizować powyższy kod, aby sprawdzić, czy węzeł liścia (GetChildrenCount => 0) jest ContentControl. Jeśli tak, sprawdź, czy treść odpowiada kryteriom nazwa + typ.
Gishu
@Gishu - Myślę, że to powinno działać w tym celu. Czy możesz skopiować i wkleić kod, aby pokazać, jak korzystasz z połączenia? Spodziewałbym się, że powinien to być FindChild <ListBox> (Expander myExpanderName, „myListBoxName”).
CrimsonX
3
@CrimsonX Myślę, że znalazłem inny przypadek narożny. Próbowałem znaleźć PART_SubmenuPlaceholder w RibbonApplicationMenuItem, ale powyższy kod nie działał. Aby rozwiązać ten problem, musiałem dodać: if (name == ElementName) else {foundChild = FindChild (child, name) if (foundChild! = Null) break; }
kevindaub
6
Uważaj, w odpowiedzi jest błąd lub więcej. Zatrzyma się, gdy tylko dotrze do dziecka typu wyszukiwania. Myślę, że powinieneś rozważyć / uszeregować pod względem ważności inne odpowiedzi.
Eric Ouellet,
2
Ten kod jest świetny, ale nie zadziała, jeśli nie szukasz określonego typu elementu, na przykład jeśli przejdziesz FrameworkElementjako T, zwróci null, gdy tylko pierwsza pętla się zakończy. więc będziesz musiał dokonać modyfikacji.
Amir Oveisi
131

Możesz również znaleźć element według nazwy, używając FrameworkElement.FindName (string) .

Dany:

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

W pliku za kodem możesz napisać:

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

Oczywiście, ponieważ definiuje się go za pomocą x: Nazwa, możesz po prostu odwoływać się do wygenerowanego pola, ale być może chcesz to sprawdzić dynamicznie, a nie statycznie.

To podejście jest również dostępne w przypadku szablonów, w których nazwany element pojawia się wiele razy (raz na użycie szablonu).

Drew Noakes
źródło
6
Aby to zadziałało, niekoniecznie musisz dodać „x:” do atrybutu name.
brian buck
3
To nie zawsze działa. Mam UserControls, które są programowo łączone razem w zagnieżdżonych siatkach jako zawartość okna właściwości. Odpowiedź CrimsonX działa jednak dobrze.
Matt
4
To nie zadziała w przypadku elementów w ItemControls, ListBoxes itp.
Sorensen
67

Za pomocą VisualTreeHelper można znaleźć formanty. Poniżej znajduje się metoda wykorzystująca VisualTreeHelper do znalezienia kontroli nadrzędnej określonego typu. Możesz użyć VisualTreeHelper, aby znaleźć formanty również na inne sposoby.

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);
      }
   }
}

Nazwij to tak:

Window owner = UIHelper.FindVisualParent<Window>(myControl);
John Myczek
źródło
Jak uzyskać lub czym jest myControl?
Demodave,
21

Być może powtarzam wszystkim innym, ale mam ładny fragment kodu, który rozszerza klasę DependencyObject za pomocą metody FindChild (), która da ci dziecko według typu i imienia. Wystarczy dołączyć i użyć.

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;
    }
}

Mam nadzieję, że uznasz to za przydatne.

Tri Q Tran
źródło
2
W moim poście powyżej jest mały błąd implementacji w twoim kodzie: stackoverflow.com/questions/636383/wpf-ways-to-find-controls/...
CrimsonX
18

Moje rozszerzenia do kodu.

  • Dodano przeciążenia, aby znaleźć jedno dziecko według typu, według typu i kryteriów (predykat), znaleźć wszystkie dzieci podrzędne, które spełniają kryteria
  • metoda FindChildren jest iteratorem oprócz tego, że jest metodą rozszerzenia dla DependencyObject
  • FindChildren spaceruje także w logicznych poddrzewach. Zobacz post Josha Smitha połączony z postem na blogu.

Źródło: https://code.google.com/p/gishu-util/source/browse/#git%2FWPF%2FUtilities

Objaśniający post na blogu: http://madcoderspeak.blogspot.com/2010/04/wpf-find-child-control-of-specific-type.html

Gishu
źródło
-1 Dokładnie to, co miałem zaimplementować (predykat, iterator i metoda rozszerzenia), ale na linku źródłowym jest 404. Zmieni się na +1, jeśli kod jest tu zawarty, lub link źródłowy jest naprawiony!
cod3monk3y
@ cod3monk3y - Wydaje się, że migracja Git zabiła link :) Oto proszę .. code.google.com/p/gishu-util/source/browse/…
Gishu
18

Jeśli chcesz znaleźć WSZYSTKIE formanty określonego typu, ten fragment kodu może Cię również zainteresować

    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
źródło
3
Dobry, ale upewnij się, że kontrola jest załadowana, w przeciwnym razie GetChildrenCount zwróci 0.
Klaus Nji
@ UrbanEsc, dlaczego rzucasz childpo raz drugi? Jeśli masz childTypetyp T, możesz pisać wewnątrz if: yield return childType... nie?
Massimiliano Kraus
@MassimilianoKraus Hej, przepraszam za spóźnioną odpowiedź, ale masz rację. Przypisuję go kilka razy przepisując ten fragment, a zatem może to być fragment innej kontroli
UrbanEsc
16

Spowoduje to odrzucenie niektórych elementów - powinieneś rozszerzyć go w ten sposób, aby obsługiwać szerszą gamę elementów sterujących. Krótka dyskusja znajduje się tutaj

 /// <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
źródło
5
Zgodnie z konwencją oczekiwałbym, że jakakolwiek Try*metoda zwróci booli będzie mieć outparametr, który zwraca dany typ, jak w przypadku:bool IDictionary.TryGetValue(TKey key, out TValue value)
Drew Noakes,
@DrewNoakes, jak więc nazwać to Philipp? Ponadto, nawet z takimi oczekiwaniami, uważam, że jego kod jest zarówno jasny, jak i przejrzysty w użyciu.
ANeves
1
@ANeves, w tym przypadku po prostu nazwałbym to FindParent. Ta nazwa sugeruje, że może wrócić null. Try*Prefiks jest używany na całym BCL w sposób opisuję powyżej. Zauważ też, że większość innych odpowiedzi tutaj używa Find*konwencji nazewnictwa. To tylko drobna uwaga :)
Drew Noakes
16

Edytowałem kod CrimsonX, ponieważ nie działał on z typami nadklasy:

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
źródło
1
Jeśli zdasz tę metodę, a DependencyObjectnie jest FrameworkElementto, może zgłosić wyjątek. Również używanie GetChildrenCountprzy każdej iteracji forpętli wydaje się złym pomysłem.
Tim Pohlmann
1
cóż, to jest sprzed 5 lat, więc nawet nie wiem, czy to już działa :)
andresp
Właśnie o tym wspomniałem, bo natknąłem się na to i inni też mogli;)
Tim Pohlmann
13

Chociaż ogólnie uwielbiam rekurencję, nie jest ona tak skuteczna jak iteracja podczas programowania w C #, więc może poniższe rozwiązanie jest fajniejsze niż to zaproponowane przez Johna Myczka? Przeszukuje hierarchię z danego elementu sterującego, aby znaleźć element nadrzędny określonego typu.

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;
}

Nazwij to tak, aby znaleźć Windowformant zawierający ExampleTextBox:

Window window = ExampleTextBox.FindVisualAncestorOfType<Window>();
Nathan Phillips
źródło
9

Oto mój kod, aby znaleźć formanty według typu, jednocześnie kontrolując, jak głęboko wchodzimy do hierarchii (maxDepth == 0 oznacza nieskończenie głęboką).

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
źródło
9

exciton80 ... Miałem problem z tym, że Twój kod nie powtarza się przez kontrolki użytkownika. Uderzał w korzeń siatki i generował błąd. Myślę, że to naprawia to dla mnie:

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
źródło
8

Mam taką funkcję sekwencji (która jest całkowicie ogólna):

    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)));
    }

Pierwsze natychmiastowe dzieci:

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

Znalezienie wszystkich dzieci w drzewie hiararchicznym:

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

Możesz to wywołać w oknie, aby uzyskać wszystkie elementy sterujące.

Po utworzeniu kolekcji możesz użyć LINQ (tj. OfType, Where).

VB Guy
źródło
6

Ponieważ pytanie jest na tyle ogólne, że może przyciągnąć ludzi szukających odpowiedzi na bardzo trywialne przypadki: jeśli chcesz po prostu dziecka, a nie potomka, możesz użyć 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();
    }
}

lub oczywiście oczywiste dla iteracji pętli nad dziećmi.

El Zorko
źródło
3

Te opcje mówią już o przechodzeniu przez Visual Tree w C #. Możliwe jest również przeglądanie drzewa wizualnego w Xaml przy użyciu rozszerzenia znaczników RelativeSource. msdn

znajdź według rodzaju

Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type <TypeToFind>}}}" 
Neeraj
źródło
2

Oto rozwiązanie wykorzystujące elastyczny predykat:

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;
}

Możesz na przykład nazwać to tak:

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
źródło
1

Ten kod naprawia tylko błąd odpowiedzi @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;
    }  

Musisz tylko wywoływać metodę rekurencyjnie, jeśli typy są zgodne, ale nazwy się nie zgadzają (dzieje się tak, gdy podajesz FrameworkElementjako T). inaczej to wróci nulli to źle.

Amir Oveisi
źródło
0

Aby znaleźć przodka danego typu na podstawie kodu, możesz użyć:

[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;
    }
}

Ta implementacja wykorzystuje iterację zamiast rekurencji, która może być nieco szybsza.

Jeśli używasz C # 7, można to zrobić nieco krócej:

[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
źródło
-5

Spróbuj tego

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

Kod za

var txtblock = sender as Textblock;
txtblock.Foreground = "Red"
Jayasri
źródło