2010-06-09 35 views
9

Tôi đang cố gắng để viết một lớp tùy chỉnh Panel cho WPF, bằng cách ghi đè MeasureOverrideArrangeOverride nhưng, trong khi đó hầu hết là làm việc Tôi đang gặp một vấn đề kỳ lạ tôi không thể giải thích.Một tùy chỉnh tự động kích thước WPF Bảng điều chỉnh lớp

Cụ thể, sau khi tôi gọi Arrange trên các mặt hàng con trong ArrangeOverride sau khi xác định kích thước của chúng, chúng không định kích thước theo kích thước tôi cung cấp cho chúng và có kích thước Phương thức Measure bên trong MeasureOverride.

Tôi có thiếu thứ gì đó trong cách hệ thống này được yêu cầu hoạt động không? Sự hiểu biết của tôi là việc gọi số Measure chỉ đơn giản là làm cho trẻ đánh giá DesiredSize của nó dựa trên số liệu có sẵnSize và không nên ảnh hưởng đến kích thước cuối cùng thực tế của nó. Đây là mã đầy đủ của tôi (Panel, btw, được thiết kế để sắp xếp trẻ em theo cách hiệu quả nhất về mặt không gian, cho ít không gian hơn cho các hàng không cần nó và chia tách không gian còn lại lên một cách đồng đều giữa các phần còn lại-- nó hiện chỉ hỗ trợ định hướng dọc nhưng tôi có kế hoạch thêm chiều ngang khi tôi làm cho nó hoạt động bình thường):

Chỉnh sửa: Cảm ơn phản hồi. Tôi sẽ kiểm tra chúng chặt chẽ hơn trong giây lát. Tuy nhiên hãy để tôi làm rõ thuật toán dự định của tôi hoạt động như thế nào vì tôi không giải thích điều đó.

Trước hết, cách tốt nhất để nghĩ về những gì tôi đang làm là tưởng tượng một Grid với mỗi hàng được đặt thành *. Điều này chia không gian lên đồng đều. Tuy nhiên, trong một số trường hợp, phần tử trong một hàng có thể không cần tất cả không gian đó; nếu đây là trường hợp, tôi muốn lấy bất kỳ không gian còn sót lại nào và đưa nó cho những hàng có thể sử dụng không gian đó. Nếu không có hàng nào cần thêm bất kỳ không gian nào, tôi chỉ cố gắng để không gian mọi thứ đồng đều (đó là những gì extraSpace đang làm, nó chỉ dành cho trường hợp đó).

Tôi thực hiện điều này theo hai lần. Điểm cuối cùng của lần vượt qua đầu tiên là xác định "kích thước bình thường" cuối cùng của một hàng - nghĩa là. kích thước của các hàng sẽ bị thu nhỏ (với kích thước nhỏ hơn kích thước mong muốn của nó). Tôi làm điều này bằng cách bước qua vật nhỏ nhất lớn nhất và điều chỉnh kích thước bình thường được tính toán ở từng bước, bằng cách thêm khoảng trống còn lại từ mỗi mục nhỏ vào từng mục lớn hơn tiếp theo cho đến khi không có thêm mục nào "vừa" và sau đó ngắt. Trong lần tiếp theo, tôi sử dụng giá trị thông thường này để xác định xem một món đồ có thể vừa hoặc không, đơn giản bằng cách lấy số Min của kích thước bình thường với kích thước mong muốn của mặt hàng đó.

(Tôi cũng đã thay đổi phương pháp vô danh đến một hàm lambda cho đơn giản.)

Chỉnh sửa 2: thuật toán của tôi dường như làm việc tuyệt vời tại việc xác định kích thước thích hợp của trẻ em. Tuy nhiên, bọn trẻ không chấp nhận kích cỡ của chúng. Tôi đã cố gắng đề nghị của Goblin MeasureOverride bằng cách chuyển PositiveInfinity và trả về Kích thước (0,0), nhưng điều này khiến trẻ tự vẽ như thể không có ràng buộc về không gian nào cả. Phần không rõ ràng về điều này là nó đang xảy ra vì một cuộc gọi đến Measure. Tài liệu của Microsoft về chủ đề này không rõ ràng, vì tôi đã đọc qua từng lớp và mô tả đặc tính nhiều lần. Tuy nhiên, bây giờ rõ ràng là việc gọi Measure thực tế ảnh hưởng đến kết xuất của đứa trẻ, vì vậy tôi sẽ cố gắng chia logic thành hai hàm như BladeWise đề xuất.

Đã giải quyết !! Tôi nhận được nó hoạt động.Như tôi nghi ngờ, tôi cần phải gọi Measure() hai lần cho mỗi đứa trẻ (một lần để đánh giá DesiredSize và một giây để cho mỗi đứa trẻ có chiều cao thích hợp). Nó có vẻ kỳ lạ với tôi rằng bố trí trong WPF sẽ được thiết kế theo cách kỳ lạ, nơi nó được chia thành hai lượt, nhưng vượt qua Measure thực sự làm hai điều: các biện pháp kích cỡ trẻ em và vượt qua Arrange không bên cạnh gì ngoài thực sự thể chất vị trí của trẻ em. Rất kì lạ.

Tôi sẽ đăng mã hoạt động ở dưới cùng.

Đầu tiên, bản gốc (chia) mã:

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

Và đây là đoạn code làm việc:

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

Nó có thể không được siêu hiệu quả gì với việc phải gọi Measure hai lần (và lặp lại Children ba lần), nhưng nó hoạt động. Mọi sự tối ưu hóa cho thuật toán sẽ được đánh giá cao.

+0

Một điều nữa: Tôi không lo lắng về một ngoại lệ từ trở về 'availableSize' trong 'MeasureOverride' vì bảng điều khiển làm cho không có ý nghĩa bên trong một vô hạn không gian như một 'ScrollViewer'. Nó chỉ có ý nghĩa khi không gian bị giới hạn. – devios1

+0

Tôi cũng gặp vấn đề này. Thay vào đó, mọi thay đổi về kích thước trong ArrangeOverride chỉ là clip. Không phải những gì tôi mong đợi từ việc đọc tài liệu hoặc bất kỳ cuốn sách WPF nào. Ngoài ra, còn phải gọi đến Measure hai lần để giải quyết vấn đề này. Cảm ơn. –

Trả lời

8

Hãy xem nếu tôi có đúng như thế nào Panel nên làm việc:

  • Cần xác định kích thước mong muốn của tất cả các UIElement con
  • Tuỳ trên các kích thước như vậy, cần xác định xem có một số không gian có sẵn
  • Trong trường hợp đó không gian tồn tại, mỗi kích thước UIElement phải được điều chỉnh sao cho toàn bộ không gian được lấp đầy (tức là mỗi kích thước phần tử sẽ được tăng thêm bởi một phần không gian còn lại)

Nếu tôi làm đúng, việc triển khai hiện tại của bạn không thể thực hiện được công việc này, vì bạn cần thay đổi kích cỡ mong muốn của trẻ em, không chỉ kích thước hiển thị (được thực hiện bằng cách đo lường và sắp xếp vượt qua).

Hãy nhớ rằng vượt qua Đo lường được sử dụng để xác định khoảng trống UIElement sẽ yêu cầu, với giới hạn kích thước (availableSize được chuyển cho phương pháp). Trong trường hợp của Panel, nó cũng gọi một con số đo trên con của nó, nhưng không đặt kích thước mong muốn của con của nó (nói cách khác, kích thước của trẻ em là một đầu vào cho các biện pháp vượt qua của bảng điều khiển). Đối với thẻ Arrange pass, nó được sử dụng để xác định hình chữ nhật nơi phần tử giao diện người dùng cuối cùng sẽ được hiển thị, bất kể kích thước đo được là bao nhiêu.Trong trường hợp của Panel, nó cũng gọi một Arrange pass trên con của nó, nhưng cũng giống như số đo vượt qua nó sẽ không thay đổi kích thước mong muốn của trẻ em (nó sẽ chỉ xác định không gian render của chúng).

Để đạt được các hành vi yêu cầu:

  • Chia đúng logic giữa Đo lường và Sắp xếp qua (trong mã của bạn, tất cả các logic là trong Sắp xếp vượt qua, trong khi các mã được sử dụng để xác định bao nhiêu khoảng trống bắt buộc đối với mỗi trẻ em phải được đặt trong thẻ đo lường)
  • Sử dụng đúng AttachedProperty (ví dụ: Bắt buộcHeight) thay cho kích thước mong muốn của trẻ em (bạn không thể kiểm soát kích thước trẻ em trừ khi được đặt thành Auto, do đó không cần phải lấy DesiredSize)

Vì tôi không chắc là tôi đã hiểu được mục đích của bảng điều khiển, tôi đã viết một ví dụ:

a. Tạo một giải pháp WPF mới (WpfApplication1) và thêm một tập tin lớp mới (CustomPanel.cs *)

b. Mở CustomPanel.cs tập tin và dán mã này

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. Mở dự án MainWindow.xaml tập tin và dán mã này

<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

Cảm ơn bạn đã phản hồi - đây là một bước đi đúng hướng tôi nghĩ. Dường như nó hoạt động tốt với RequiredHeights. Tuy nhiên, vì mục đích của bảng điều khiển là * tự động kích thước *, phải cung cấp rõ ràng cho mỗi hàng một loại chiều cao yêu cầu đánh bại mục đích. Điều đó nói rằng, tôi nghĩ rằng vẫn có thể tự động kích thước bảng điều khiển, tôi có thể phải gọi Measure() hai lần trên mỗi đứa trẻ để làm điều đó (đầu tiên để đánh giá chiều cao mong muốn, và thứ hai để thực sự "nói" nó chiều cao nó nên, vì rõ ràng điều này là cần thiết). Hãy để tôi kiểm tra lý thuyết đó ... – devios1

+0

Tôi sẽ cung cấp cho bạn câu trả lời vì phản hồi này đã giúp tôi nhiều nhất để tìm ra giải pháp! – devios1

+0

Cảm ơn, tôi hiểu những gì bạn muốn đạt được, nhưng tôi sợ nó khá phức tạp, vì nếu kích thước con được đặt là 'Tự động', kích thước mong muốn sẽ (rất có thể) giống như ràng buộc bạn đang chuyển đến Phương pháp đo.Điều này có nghĩa là, trong khi kích thước tự động có thể hoạt động cho các đối tượng có kích thước cố định, hành vi với các kích thước tự động sẽ không nhất quán. Bạn đã xem xét khả năng sử dụng RequiredHeight như trong ví dụ và ràng buộc nó với một số thuộc tính (DesiredSize?) Thông qua một trình chuyển đổi? Bằng cách này, bạn có thể duy trì việc triển khai bảng gọn gàng, trong khi cố gắng đạt được kích thước tự động. – BladeWise

2

tôi đã cố gắng để đơn giản hóa mã của bạn một chút:

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

} 

Một vài lưu ý:

  • Bạn không nên trở lại kích thước có sẵn trong MeasureOverride - nó có thể là dương vô cực mà sẽ gây ra một ngoại lệ . Và vì bạn về cơ bản không quan tâm kích thước của nó là gì, chỉ cần trả về Kích thước mới (0,0).
  • Đối với vấn đề của bạn với chiều cao của trẻ em - Tôi nghĩ rằng nó đã làm với trẻ em thực tế - chúng bị giới hạn về kích thước bằng cách nào đó thông qua Style hoặc tài sản liên quan đến HorizontalAlignment?

EDIT: phiên bản 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

Ồ, chờ đã - Tôi nghĩ rằng tôi đã bỏ lỡ ý định của bạn về bảng điều khiển ... Làm cách nào để bạn xác định yếu tố nào có được dung lượng còn lại? Ngay bây giờ, mã của bạn làm cho chúng cao hơn một chút so với mong muốn của chúng. – Goblin

+0

Goblin, mã của bạn chỉ đưa vào tài khoản một kịch bản trường hợp, đó là tất cả các mục phù hợp tự nhiên vào không gian nhất định và có không gian còn sót lại. Đây chỉ là một trường hợp rìa trong mục đích sử dụng của bảng điều khiển, dự kiến ​​tổng chiều cao của tất cả các phần tử lớn hơn chiều cao của bảng điều khiển, nhưng cũng hy vọng một số trẻ nhỏ hơn 1/nth tổng diện tích và chia tách khoảng trống "trống" này (sẽ xuất hiện ở các hàng có kích thước bằng nhau) trong số các trẻ em cao hơn. – devios1

+0

+1 để được trợ giúp. – devios1

Các vấn đề liên quan