Пользовательский класс WPF Panel для автоматического определения размера - PullRequest
9 голосов
/ 09 июня 2010

Я пытаюсь написать собственный класс Panel для WPF, переопределив MeasureOverride и ArrangeOverride, но, хотя он в основном работает, у меня возникла одна странная проблема, которую я не могу объяснить.

В частности, после того, как я позвонил Arrange на мои дочерние элементы в ArrangeOverride, после того как выяснил, какими должны быть их размеры, они не соответствуют размеру, который я им даю, и, кажется, измеряют размер к размеру. передал их Measure метод внутри MeasureOverride.

Я что-то упускаю из-за того, как эта система должна работать? Насколько я понимаю, вызов Measure просто заставляет ребенка оценить его DesiredSize на основе предоставленного availableSize и не должен влиять на его фактический конечный размер.

Вот мой полный код (кстати, Panel предназначена для того, чтобы расположить дочерние элементы максимально экономно, выделяя меньше места для строк, которые в этом не нуждаются, и равномерно распределяя оставшееся пространство между остальными. в настоящее время поддерживается только вертикальная ориентация, но я планирую добавить горизонтальную, как только она будет работать правильно

Редактировать: Спасибо за ответы. Я рассмотрю их более подробно через минуту. Однако позвольте мне уточнить, как работает мой предполагаемый алгоритм, поскольку я этого не объяснял.

Прежде всего, лучший способ подумать о том, что я делаю, это представить сетку с каждой строкой, установленной на *. Это делит пространство равномерно. Однако в некоторых случаях элементу в строке может не понадобиться все это пространство; если это так, я хочу взять любой оставшийся пробел и дать его тем строкам, которые могли бы использовать пробел. Если никакие строки не нуждаются в дополнительном пространстве, я просто пытаюсь распределить вещи равномерно (это то, что делает extraSpace, это только для этого случая).

Я делаю это в два прохода. Конечной точкой первого прохода является определение окончательного «нормального размера» строки, т.е. размер строк, которые будут сокращены (с учетом размера, меньшего, чем его требуемый размер). Я делаю это, переходя от самого маленького элемента к самому большому и корректируя вычисленный нормальный размер на каждом шаге, добавляя оставшееся пространство от каждого маленького элемента к каждому последующему большему элементу до тех пор, пока больше не будет «соответствовать» элементам, а затем сломаться.

В следующем проходе я использую это нормальное значение, чтобы определить, подходит ли элемент или нет, просто взяв Min нормального размера с желаемым размером элемента.

(я также изменил анонимный метод на лямбда-функцию для простоты.)

Редактировать 2: Кажется, мой алгоритм отлично работает для определения правильного размера детей. Тем не менее, дети просто не принимают их данного размера. Я попробовал предложенный Гоблином MeasureOverride, передав PositiveInfinity и вернув Size (0,0), но это заставляет детей рисовать себя так, как будто нет никаких ограничений пространства. Часть, которая не очевидна в этом, состоит в том, что это происходит из-за вызова Measure. Документация Microsoft по этому вопросу не совсем ясна, так как я несколько раз перечитывал описание каждого класса и свойства. Однако теперь ясно, что вызов Measure действительно влияет на рендеринг дочернего элемента, поэтому я попытаюсь разделить логику между двумя функциями, как предложил BladeWise.

Решено !! Я все заработал. Как я и подозревал, мне нужно было вызывать Measure () дважды для каждого ребенка (один раз, чтобы оценить DesiredSize, и второй, чтобы дать каждому ребенку его правильный рост). Мне кажется странным, что макет в WPF был бы спроектирован таким странным образом, когда он разделен на два прохода, но проход Measure фактически делает две вещи: измеряет дочерние элементы и размеров, и проход Arrange почти ничего кроме того, чтобы фактически физически расположить детей. Очень странно.

Выложу рабочий код внизу.

Во-первых, оригинальный (сломанный) код:

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 три раза), но это работает. Любые оптимизации алгоритма приветствуются.

Ответы [ 2 ]

8 голосов
/ 09 июня 2010

Давайте посмотрим, правильно ли я понял, как должен работать Panel:

  • Он должен определять желаемый размер каждого UIElement дочернего элемента
  • В зависимости от таких размеров, онследует определить, есть ли доступное пространство
  • . Если такое пространство существует, каждый UIElement размер должен быть отрегулирован так, чтобы заполнить все пространство (т. е. каждый размер элемента будет увеличиваться на часть оставшегося пространства)

Если я правильно понял, ваша текущая реализация не может выполнить эту задачу, так как вам нужно изменить желаемый размер самих дочерних элементов, а не только их размер рендеринга (который выполняется с помощью Measureи организовать проходы).

Имейте в виду, что проход Измерения используется для определения того, сколько места потребуется UIElement, учитывая ограничение размера (availableSize, передаваемый методу).В случае Panel он также вызывает проход Измерения для своих дочерних элементов, но не устанавливает желаемый размер своих дочерних элементов (другими словами, размер дочерних элементов является входом для мерыпроход панели).Что касается прохода Arrange, он используется для определения прямоугольника, в котором элемент UI будет окончательно визуализирован, независимо от измеренного размера.В случае Panel, он также вызывает проход Arrange для своих дочерних элементов, но точно так же, как передача меры не изменит желаемого размера дочерних элементов (он просто определит их пространство рендеринга).

Для достижения требуемого поведения:

  • Правильно разделить логику между проходом Measure и Arrange (в вашем коде вся логика находится в проходе Arrange, а код используется дляопределите, сколько места требуется для каждого ребенка, которое должно быть помещено в проход измерения)
  • Используйте правильное AttachedProperty (т. е. RequiredHeight ) вместо желаемого размера детей (выне контролируйте размер дочернего элемента, если он не установлен на Auto, поэтому нет необходимости брать DesiredSize)

, поскольку я не уверен, что понял назначение панелиЯ написал пример:

a. Создайте новое решение Wpf ( WpfApplication1 ) и добавьте новый файл класса (CustomPanel.cs *)

b. Откройте 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...
  }
 }
}

c. Откройте файл проекта 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>
2 голосов
/ 09 июня 2010

Я попытался немного упростить ваш код:

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).
  • Что касается вашей проблемы с ростом детей - я думаю, что это связано сфактические дети - они как-то ограничены в размерах с помощью стиля или свойств в отношении HorizontalAlignment?

РЕДАКТИРОВАТЬ: версия 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;
    }

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...