2010-06-09 144 views
9

我試圖寫一個自定義Panel類WPF,通過覆蓋MeasureOverrideArrangeOverride但是,雖然這是大多工作我遇到了一個奇怪的問題,我無法解釋。自定義自動調整大小WPF Panel類

我特別呼籲後,我的子項ArrangeArrangeOverride搞清楚它們的大小應該是什麼,他們沒有大小的尺寸,我給他們後,和似乎上漿傳遞給他們的大小Measure方法MeasureOverride

我錯過了這個系統應該如何工作?我的理解是,撥打Measure只會導致孩子根據提供的可用尺寸評估其DesiredSize,並且不應影響其實際最終尺寸。

這裏是我的完整代碼(面板,順便說一句,旨在以最節省空間的方式安排兒童,給不需要它的行留出更少的空間,並將餘下的空間均勻地分開 - 它目前只支持垂直方向,但我計劃加水平一旦我得到它正常工作):

編輯:感謝您的答覆。我會稍後仔細檢查它們。不過,讓我澄清我的預期算法是如何工作的,因爲我沒有解釋這一點。

首先,想想我正在做的事情的最佳方式是想象每行設置爲*的網格。這將均勻分隔空間。但是,在某些情況下,連續的元素可能不需要所有的空間;如果是這種情況,我想把剩下的空間放到那些可以使用空間的行上。如果沒有行需要任何額外的空間,我只是嘗試平均分配東西(這就是extraSpace正在做的事情,只是爲了這種情況)。

我這樣做了兩次。第一遍的終點是確定一排的最終「正常尺寸」 - 即,將縮小的行的大小(給定比所需大小更小的大小)。我這樣做是通過將最小項目逐步增加到最大並在每個步驟調整計算的正常大小,方法是將每個小項目的剩餘空間添加到每個後續較大項目,直到沒有更多項目「合適」然後中斷。

在下一回閤中,我使用這個正常值來確定一個物品是否適合,只需將正常尺寸的Min與物品的所需尺寸對齊即可。

(我也改變了匿名方法爲簡單lambda函數)。

編輯2:我的算法似乎在確定孩子的適當大小的工作很大。但是,孩子們並沒有接受他們的尺寸。我通過傳遞PositiveInfinity並返回Size(0,0)來嘗試Goblin的建議MeasureOverride,但是這會讓孩子們自己畫出來,好像根本沒有空間限制。對此不明顯的部分是由於致電Measure而發生。微軟關於這個主題的文檔並不完全清楚,因爲我已經多次閱讀過每個類和屬性描述。但是,現在很明顯,調用Measure實際上會影響孩子的渲染,因此我將嘗試按照BladeWise建議的方式將這兩個函數中的邏輯分開。

解決!我得到它的工作。正如我所懷疑的,我需要每個孩子兩次調用Measure()(一次評估DesiredSize,第二次給每個孩子適當的身高)。在我看來,WPF中的佈局將被設計成一種奇怪的方式,在這裏它被分成兩個傳球,但測量傳球實際上做了兩件事:措施大小的孩子和排列傳球旁邊沒有任何東西實際上對孩子進行身體定位非常奇怪。

我會在底部發布工作代碼。

首先,原來的(碎)代碼:

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

而這裏的工作代碼:

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

它可能不是超高效什麼用不必調用Measure兩次(和迭代Children三次),但它的工作原理。任何優化算法將不勝感激。

+0

一兩件事:我不擔心異常從'MeasureOverride'返回'availableSize'因爲面板使內無限毫無意義像ScrollViewer一樣的空間。只有空間狹窄時纔有意義。 – devios1 2010-06-09 15:22:46

+0

我也有這個問題。 ArrangeOverride中的任何尺寸變化只是代替剪輯。不是我期望閱讀文檔或任何WPF書籍。也曾採取兩次措施來解決這個問題。謝謝。 – 2010-09-20 17:50:20

回答

8

讓我們來看看,如果我得到的權利如何Panel應該工作:

  • 應該確定每一個UIElement孩子
  • 期望的大小取決於在這樣的尺寸上,它應該確定是否有一些可用空間
  • 如果這樣存在空間,應調整每個UIElement大小,以便填滿整個空間(即,每個元素的大小將由剩餘空間的一部分)遞增

如果我得到它的權利,你的當前實現不能完成這個任務,因爲你需要改變孩子們自己的想要的尺寸,不僅是他們的渲染大小(通過「測量」和「排列」傳遞完成)。

請記住,如果大小限制(availableSize傳遞給方法),則使用度量通過來確定UIElement需要多少空間。在Panel的情況下,它也會對其子項調用Measure傳遞,但未設置子項的所需大小(換句話說,子項的大小是面板的度量傳遞的輸入)。 至於Arrange通道,它用於確定最終呈現UI元素的矩形,無論測量的大小如何。在Panel的情況下,它也會爲子項調用排列傳遞,但就像測量通過它不會更改所需的子尺寸(它只會定義它們的渲染空間)。

爲了達到所要求的行爲:測度之間

  • 拆分適當的邏輯和安排通(在你的代碼,所有的邏輯是在編配通,而所使用的代碼,以確定有多少空間要求每個孩子都應該在地方的孩子們所需的大小(你對孩子的大小無法控制的,除非它被設置爲Auto的放置在測量處理)
  • 使用適當AttachedProperty(即RequiredHeight)所以沒有必要拿DesiredSize

因爲我不知道我理解了面板的目的,我寫了一個例子:

一個。創建一個新的WPF解決方案(WpfApplication1),並添加一個新的類文件(* CustomPanel.cs)

打開CustomPanel.cs文件並粘貼此代碼

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

℃。打開項目MainWindow.xaml文件並粘貼此代碼

<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

感謝您的回覆 - 這是我認爲的正確方向邁出的一步。鑑於RequiredHeights,它似乎工作得很好。然而,由於面板的目的是*自動調整大小,因此必須明確地給每一行提供一個所需的高度,以此來達到目的。也就是說,我認爲仍然可以自動調整面板大小,但我可能必須在每個孩子身上調用兩次Measure()來完成它(首先要評估所需的高度,其次要實際「告訴」它是什麼高度應該是,因爲顯然這是必要的)。讓我測試一下這個理論...... – devios1 2010-06-09 14:58:21

+0

我會給你答案,因爲這個回答最能幫助我找到解決方案! – devios1 2010-06-09 15:19:16

+0

謝謝,我明白你想達到什麼目的,但我擔心它很棘手,因爲如果孩子的大小設置爲'自動',所需的大小將(最有可能)與您傳遞給測量方法。這意味着,雖然自動調整大小可以適用於大小固定的對象,但自動調整對象的行爲將不一致。 您是否考慮過在示例中使用RequiredHeight的可能性,並通過轉換器將其綁定到某個屬性(DesiredSize?)?通過這種方式,您可以保持整潔的面板實現,同時嘗試實現自動調整大小。 – BladeWise 2010-06-10 07:20:01

2

我試圖簡化代碼位:

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

} 

的幾個注意事項:

  • 中的MeasureOverride應該不會返回可用大小 - 也可能是正無窮大,這將導致異常。因爲你基本上不在乎它的大小,只需返回新的大小(0,0)。
  • 至於你的問題與兒童的身高 - 我認爲它與實際的孩子有關 - 他們有限的大小以某種方式通過風格或屬性關於Horizo​​ntalAlignment?

編輯: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

哦,等等 - 我想我錯過了你的面板的意圖... 你如何確定哪些元素得到剩餘空間?現在你的代碼使它們比想要的要高一點。 – Goblin 2010-06-09 08:23:56

+0

地精,你的代碼只考慮了一種情況,即所有的物品自然地適合於給定的空間並且有剩餘的空間。這僅僅是面板預期用途的一個附帶案例,它預計所有元素的總高度大於面板的高度,但也希望有些兒童小於總空間的1/n,並且將這個「空」空間(這將發生在同樣大小的行中)分裂成較高的兒童。 – devios1 2010-06-09 14:34:10

+0

+1幫助。 – devios1 2010-06-09 15:19:44