2010-06-09 9 views
9

Próbuję napisać niestandardowy Panel klasę dla WPF, nadrzędnymi MeasureOverride i ArrangeOverride, ale jednocześnie jest to głównie pracy mam przeżywa jeden dziwny problem nie mogę wyjaśnić.Niestandardowy automatycznego doboru WPF klasy panelu

W szczególności po tym, jak zadzwonić Arrange na moich elementów podrzędnych w ArrangeOverride po zastanawianie się, jakie są ich rozmiary powinny być, nie są one wielkości do wielkości daję im, i wydaje się być wielkości do wielkości przekazanego do ich Measure metoda wewnątrz MeasureOverride.

Czy brakuje mi czegoś, jak ten system powinien działać? Rozumiem, że wywołanie Measure po prostu powoduje, że dziecko ocenia jego DesiredSize na podstawie dostarczonej availableSize i nie powinno wpływać na jego rzeczywisty ostateczny rozmiar.

Oto mój pełny kod (Panel, btw, jest przeznaczony do zorganizowania dzieci w najbardziej efektywny sposób, dając mniej miejsca dla wierszy, które go nie potrzebują i dzieląc pozostałą przestrzeń równomiernie między resztę-- obecnie obsługuje tylko orientację pionową, ale planuję dodać poziomą, gdy tylko poprawi się jej działanie):

Edytuj: Dzięki za odpowiedzi. Za chwilę przyjrzę im się bliżej. Jednak pozwól mi wyjaśnić, jak działa mój zamierzony algorytm, ponieważ tego nie wyjaśniałem.

Po pierwsze, najlepszym sposobem na myślenie o tym, co robię, jest wyobrazić sobie Grid z każdym rzędem ustawionym na *. Równomiernie dzieli przestrzeń. Jednak w niektórych przypadkach element z rzędu może nie potrzebować całej tej przestrzeni; jeśli tak jest, chcę wziąć resztkę miejsca i oddać je do tych rzędów, które mogłyby wykorzystać tę przestrzeń. Jeśli żadne wiersze nie wymagają dodatkowej przestrzeni, staram się równomiernie rozmieścić rzeczy (to robi extraSpace, tylko w tym przypadku).

Robię to w dwóch podaniach. Ostatecznym punktem pierwszego przejścia jest określenie ostatecznego "normalnego rozmiaru" rzędu - tj. rozmiar rzędów, które zostaną zmniejszone (biorąc pod uwagę rozmiar mniejszy niż jego pożądany rozmiar). Robię to, przechodząc przez najmniejszy przedmiot do największego i dostosowując obliczony normalny rozmiar na każdym kroku, dodając resztki przestrzeni od każdego małego przedmiotu do każdego kolejnego większego przedmiotu, aż żadna pozycja "nie pasuje", a następnie pęknie.

W następnym przebiegu używam tej wartości normalnej do określenia, czy dany element może się zmieścić, czy nie, po prostu pobierając Min normalnego rozmiaru z pożądanym rozmiarem elementu.

(ja także zmieniła sposób anonimowy do funkcji lambda dla uproszczenia).

Edycja 2: Mój algorytm wydaje się doskonale przy określaniu właściwego rozmiaru dzieci. Jednak dzieci po prostu nie akceptują swojego rozmiaru. Próbowałem Goblina zasugerować MeasureOverride, przekazując PositiveInfinity i zwracając Size (0,0), ale to powoduje, że dzieci rysują się tak, jakby w ogóle nie było żadnych ograniczeń przestrzennych. Częścią tego nie jest oczywiste, że dzieje się tak z powodu połączenia z numerem Measure. Dokumentacja Microsoftu na ten temat nie jest wcale jasna, ponieważ kilkakrotnie przeczytałem każdą klasę i opis właściwości. Jednak teraz jest jasne, że wywołanie Measure w rzeczywistości wpływa na renderowanie elementu potomnego, więc spróbuję podzielić logikę między dwie funkcje, jak sugeruje BladeWise.

Rozwiązany! Mam to działa.Jak podejrzewałem, musiałem dwukrotnie wywołać funkcję Measure() na każdym dziecku (jeden raz, aby ocenić DesiredSize, a drugi, aby nadać dziecku odpowiednią wysokość). Wydaje mi się dziwne, że układ w WPF byłby zaprojektowany w tak dziwny sposób, w którym jest podzielony na dwie części, ale Przełęcz Miarka faktycznie robi dwie rzeczy: miary i rozmiarów dzieci i przepustka Rozmieść nie ma obok niczego oprócz faktycznie fizycznie pozycjonują dzieci. Bardzo dziwne.

Opublikuję działający kod u dołu.

pierwsze, oryginalne (uszkodzony) Kod:

protected override Size MeasureOverride(Size availableSize) { 
    foreach (UIElement child in Children) 
     child.Measure(availableSize); 

    return availableSize; 
} 

protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize) { 
    double extraSpace = 0.0; 
    var sortedChildren = Children.Cast<UIElement>().OrderBy<UIElement, double>(child=>child.DesiredSize.Height;); 
    double remainingSpace = finalSize.Height; 
    double normalSpace = 0.0; 
    int remainingChildren = Children.Count; 
    foreach (UIElement child in sortedChildren) { 
     normalSpace = remainingSpace/remainingChildren; 
     if (child.DesiredSize.Height < normalSpace) // if == there would be no point continuing as there would be no remaining space 
      remainingSpace -= child.DesiredSize.Height; 
     else { 
      remainingSpace = 0; 
      break; 
     } 
     remainingChildren--; 
    } 

    // this is only for cases where every child item fits (i.e. the above loop terminates normally): 
    extraSpace = remainingSpace/Children.Count; 
    double offset = 0.0; 

    foreach (UIElement child in Children) { 
     //child.Measure(new Size(finalSize.Width, normalSpace)); 
     double value = Math.Min(child.DesiredSize.Height, normalSpace) + extraSpace; 
      child.Arrange(new Rect(0, offset, finalSize.Width, value)); 
     offset += value; 
    } 

    return finalSize; 
} 

A oto kod roboczych:

double _normalSpace = 0.0; 
double _extraSpace = 0.0; 

protected override Size MeasureOverride(Size availableSize) { 
    // first pass to evaluate DesiredSize given available size: 
    foreach (UIElement child in Children) 
     child.Measure(availableSize); 

    // now determine the "normal" size: 
    var sortedChildren = Children.Cast<UIElement>().OrderBy<UIElement, double>(child => child.DesiredSize.Height); 
    double remainingSpace = availableSize.Height; 
    int remainingChildren = Children.Count; 
    foreach (UIElement child in sortedChildren) { 
     _normalSpace = remainingSpace/remainingChildren; 
     if (child.DesiredSize.Height < _normalSpace) // if == there would be no point continuing as there would be no remaining space 
      remainingSpace -= child.DesiredSize.Height; 
     else { 
      remainingSpace = 0; 
      break; 
     } 
     remainingChildren--; 
    } 
    // there will be extra space if every child fits and the above loop terminates normally: 
    _extraSpace = remainingSpace/Children.Count; // divide the remaining space up evenly among all children 

    // second pass to give each child its proper available size: 
    foreach (UIElement child in Children) 
     child.Measure(new Size(availableSize.Width, _normalSpace)); 

    return availableSize; 
} 

protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize) { 
    double offset = 0.0; 

    foreach (UIElement child in Children) { 
     double value = Math.Min(child.DesiredSize.Height, _normalSpace) + _extraSpace; 
     child.Arrange(new Rect(0, offset, finalSize.Width, value)); 
     offset += value; 
    } 

    return finalSize; 
} 

To nie może być super-wydajny, co z konieczności wzywania Measure dwukrotnie (i iteracji Children trzy razy), ale działa. Należy docenić wszelkie optymalizacje algorytmu.

+0

jeszcze jedno: Nie martwię się o wyjątek od powrotu 'availableSize' w' MeasureOverride' ponieważ panel ma sensu wewnątrz nieskończona przestrzeń jak "ScrollViewer". Ma sens tylko wtedy, gdy przestrzeń jest ograniczona. – devios1

+0

Ja też mam ten problem. Każda zmiana rozmiaru w ArrangeOverride po prostu klipuje. Nie tego oczekiwałem od czytania dokumentacji lub jakichkolwiek książek WPF. Również uciekam się do wywołania Measure dwa razy, aby rozwiązać ten problem. Dzięki. –

Odpowiedz

8

Zobaczymy, czy mam rację, jak Panel powinno działać:

  • Należy określić żądany rozmiar każdego UIElement dziecka
  • zależności w przypadku takich rozmiarów powinien określić, czy jest dostępna pewna ilość miejsca:
  • W takim przypadku istnieje przestrzeń, każdy rozmiar UIElement powinien być dostosowany tak, aby cała przestrzeń była wypełniona (tj. każdy rozmiar elementu zostanie zwiększony o część pozostałej przestrzeni)

Jeśli dobrze się sprawdzi, twoja obecna implementacja nie może wykonać tego zadania, ponieważ musisz zmienić pożądany rozmiar samych dzieci, nie tylko ich rozmiar renderowania (który jest wykonywany przez pomiary i aranżacje).

Należy pamiętać, że przejście Pomiar jest używane do określenia, ile miejsca będzie wymagać UIElement, biorąc pod uwagę ograniczenie rozmiaru (availableSize przekazana do metody). W przypadku parametru Panel wywołuje on także funkcję Przełęcz dla swoich dzieci, ale nie ustawia żądanego rozmiaru swoich dzieci (innymi słowy, rozmiar potomków to dane wejściowe dla przejścia dla miar panelu). Jeśli chodzi o przepustkę Rozmieść, jest ona używana do określenia prostokąta, w którym element interfejsu użytkownika będzie ostatecznie renderowany, niezależnie od zmierzonego rozmiaru.W przypadku parametru Panel wywołuje on również przekazanie dla jego dzieci, ale tak jak miara przechodzi dalej, nie zmieni pożądanego rozmiaru dzieci (po prostu zdefiniuje ich przestrzeń renderowania).

Aby osiągnąć wymaganą zachowanie:

  • rozszczepiony logiki między środkiem i Umów przepustkę (w kodzie, cała logika jest w przełęczy Arrange, natomiast kod używany do określenia, ile miejsca jest wymagana dla każdego dziecka powinno być umieszczone w podaniu środka)
  • stosowanie odpowiedniej AttachedProperty (tj RequiredHeight) zamiast pożądanej wielkości dzieci (nie masz kontroli nad wielkością dziecka, chyba że jest ustawiony na Auto, więc nie ma potrzeby przyjmowania DesiredSize)

Ponieważ nie jestem pewien, mam rozumieć cel panelu pisałem przykład:

a. Załóż nowe rozwiązanie WPF (WpfApplication1) i dodać nowy plik klasy (*) CustomPanel.cs

b. otworzyć CustomPanel.cs plik i wklej ten kod

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Windows.Controls; 
using System.Windows; 

namespace WpfApplication1 
{ 
public class CustomPanel : Panel 
{ 

    /// <summary> 
    /// RequiredHeight Attached Dependency Property 
    /// </summary> 
    public static readonly DependencyProperty RequiredHeightProperty = DependencyProperty.RegisterAttached("RequiredHeight", typeof(double), typeof(CustomPanel), new FrameworkPropertyMetadata((double)double.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure, new PropertyChangedCallback(OnRequiredHeightPropertyChanged))); 

    private static void OnRequiredHeightPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) 
    { 

    } 

    public static double GetRequiredHeight(DependencyObject d) 
    { 
    return (double)d.GetValue(RequiredHeightProperty); 
    } 

    public static void SetRequiredHeight(DependencyObject d, double value) 
    { 
    d.SetValue(RequiredHeightProperty, value); 
    } 

    private double m_ExtraSpace = 0; 

    private double m_NormalSpace = 0; 

    protected override Size MeasureOverride(Size availableSize) 
    { 
    //Measure the children... 
    foreach (UIElement child in Children) 
    child.Measure(availableSize); 

    //Sort them depending on their desired size... 
    var sortedChildren = Children.Cast<UIElement>().OrderBy<UIElement, double>(new Func<UIElement, double>(delegate(UIElement child) 
    { 
    return GetRequiredHeight(child); 
    })); 

    //Compute remaining space... 
    double remainingSpace = availableSize.Height; 
    m_NormalSpace = 0.0; 
    int remainingChildren = Children.Count; 
    foreach (UIElement child in sortedChildren) 
    { 
    m_NormalSpace = remainingSpace/remainingChildren; 
    double height = GetRequiredHeight(child); 
    if (height < m_NormalSpace) // if == there would be no point continuing as there would be no remaining space 
    remainingSpace -= height; 
    else 
    { 
    remainingSpace = 0; 
    break; 
    } 
    remainingChildren--; 
    } 

    //Dtermine the extra space to add to every child... 
    m_ExtraSpace = remainingSpace/Children.Count; 
    return Size.Empty; //The panel should take all the available space... 
    } 

    protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize) 
    { 
    double offset = 0.0; 

    foreach (UIElement child in Children) 
    { 
    double height = GetRequiredHeight(child); 
    double value = (double.IsNaN(height) ? m_NormalSpace : Math.Min(height, m_NormalSpace)) + m_ExtraSpace; 
    child.Arrange(new Rect(0, offset, finalSize.Width, value)); 
    offset += value; 
    } 

    return finalSize; //The final size is the available size... 
    } 
} 
} 

C. Otwórz projekt MainWindow.xaml plik i wklej ten kod

<Window x:Class="WpfApplication1.MainWindow" 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
xmlns:local="clr-namespace:WpfApplication1" 
Title="MainWindow" Height="350" Width="525"> 
<Grid> 
     <local:CustomPanel> 
      <Rectangle Fill="Blue" local:CustomPanel.RequiredHeight="22"/> 
      <Rectangle Fill="Red" local:CustomPanel.RequiredHeight="70"/> 
      <Rectangle Fill="Green" local:CustomPanel.RequiredHeight="10"/> 
      <Rectangle Fill="Purple" local:CustomPanel.RequiredHeight="5"/> 
      <Rectangle Fill="Yellow" local:CustomPanel.RequiredHeight="42"/> 
      <Rectangle Fill="Orange" local:CustomPanel.RequiredHeight="41"/> 
     </local:CustomPanel> 
    </Grid> 
</Window> 
+0

Dzięki za odpowiedź - uważam, że jest to krok w dobrym kierunku. Wydaje się, że działa dobrze, biorąc pod uwagę RequiredHeights. Jednakże, ponieważ celem panelu jest * auto-size *, posiadanie w sposób jawny każdego rzędu wymaganej wysokości pokonuje cel. To powiedziawszy, myślę, że nadal możliwe jest automatyczne wymiarowanie panelu, może będę musiał zadzwonić do Measure() dwa razy na każde dziecko, aby to zrobić (najpierw ocenić pożądaną wysokość, a po drugie "powiedzieć", jakiej wysokości to powinno być, ponieważ najwyraźniej jest to konieczne). Pozwól mi przetestować tę teorię ... – devios1

+0

Dam ci odpowiedź, ponieważ ta odpowiedź pomogła mi znaleźć rozwiązanie! – devios1

+0

Dzięki, rozumiem, co chcesz osiągnąć, ale obawiam się, że jest to dość trudne, ponieważ jeśli rozmiar dziecka jest ustawiony jako "Auto", pożądany rozmiar będzie (najprawdopodobniej) taki sam jak ograniczenie, które przechodzisz do Metoda pomiaru.Oznacza to, że podczas gdy automatyczne dopasowywanie może działać w przypadku obiektów, których rozmiar jest stały, zachowanie z automatycznymi rozmiarami nie będzie spójne. Czy rozważałeś możliwość użycia RequiredHeight jak w przykładzie i związania go z pewną właściwością (DesiredSize?) Poprzez konwerter? W ten sposób możesz zachować staranne wdrożenie panelu, próbując osiągnąć automatyczną zmianę rozmiaru. – BladeWise

2

starałem się uprościć kod nieco:

public class CustomPanel:Panel 
{ 
    protected override Size MeasureOverride(Size availableSize) 
    { 
     foreach (UIElement child in Children) 
      child.Measure(new Size(double.PositiveInfinity,double.PositiveInfinity)); 

     return new Size(0,0); 
    } 

    protected override Size ArrangeOverride(Size finalSize) 
    { 
     double remainingSpace = Math.Max(0.0,finalSize.Height - Children.Cast<UIElement>().Sum(c => c.DesiredSize.Height)); 
     var extraSpace = remainingSpace/Children.Count; 
     double offset = 0.0; 

     foreach (UIElement child in Children) 
     { 
      double height = child.DesiredSize.Height + extraSpace; 
      child.Arrange(new Rect(0, offset, finalSize.Width, height)); 
      offset += height; 
     } 

     return finalSize; 
    } 

} 

Kilka Uwagi:

  • Nie należy powrócić dostępny rozmiar w MeasureOverride - może to być dodatnia nieskończoność co spowoduje wyjątek . A ponieważ w zasadzie nie obchodzi cię, jaki jest rozmiar, po prostu zwróć nowy Rozmiar (0,0).
  • Co do twojego problemu z wysokością dzieci - myślę, że ma to związek z rzeczywistymi dziećmi - czy są one w jakiś sposób ograniczone pod względem stylu lub właściwości w odniesieniu do HorizontalAlignment?

EDIT: wersja 2.0:

public class CustomPanel : Panel 
{ 
    protected override Size MeasureOverride(Size availableSize) 
    { 
     foreach (UIElement child in Children) 
      child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); 

     return new Size(0, 0); 
    } 

    protected override Size ArrangeOverride(Size finalSize) 
    { 
     double optimumHeight = finalSize.Height/Children.Count; 
     var smallElements = Children.Cast<UIElement>().Where(c => c.DesiredSize.Height < optimumHeight); 
     double leftOverHeight = smallElements.Sum(c => optimumHeight - c.DesiredSize.Height); 
     var extraSpaceForBigElements = leftOverHeight/(Children.Count - smallElements.Count()); 
     double offset = 0.0; 

     foreach (UIElement child in Children) 
     { 
      double height = child.DesiredSize.Height < optimumHeight ? child.DesiredSize.Height : optimumHeight + extraSpaceForBigElements; 
      child.Arrange(new Rect(0, offset, finalSize.Width, height)); 
      offset += height; 
     } 

     return finalSize; 
    } 

} 
+0

Och, czekaj - myślę, że przegapiłem twoje zamiary panelu ... W jaki sposób ustalacie, które elementy dostają lewą przestrzeń? Teraz kod sprawia, że ​​wszystkie one są trochę wyższe, niż chcą. – Goblin

+0

Goblin, twój kod uwzględnia tylko jeden scenariusz, a mianowicie, że wszystkie przedmioty pasują naturalnie do danej przestrzeni i pozostawia im miejsce. Jest to tylko skrajny przypadek w zamierzonym zastosowaniu panelu, który oczekuje, że całkowita wysokość wszystkich elementów będzie większa niż wysokość panelu, ale jednocześnie oczekuje, że niektóre dzieci będą mniejsze niż 1/4 całkowitej przestrzeni, i dzieli tę "pustą" przestrzeń (która występuje w wierszach o równych rozmiarach) wyżej wśród wyższych dzieci. – devios1

+0

+1 za pomoc. – devios1

Powiązane problemy