Добро пожаловать в форум, Guest  >>   Войти | Регистрация | Поиск | Правила | В избранное | Подписаться
Все форумы / WPF, Silverlight Новый топик    Ответить
 WPF: Шпаргалка для создания панели  [new]
Shocker.Pro
Member

Откуда: ->|<- :адуктО
Сообщений: 20286
Написал себе (и другим) шпаргалку, чтобы не забывать особенности работы с компоновкой с помощью панели. А то делаешь панели не так уж часто, и мелкие нюансы забываются.

public class MyPanel : Panel
{
	protected override Size MeasureOverride(Size availableSize)
	{
		// availableSize - содержит доступные размеры, предоставляемые контейнером.
		// Может быть бесконечность по любому или обоим измерениям, если панель находится в StackPanel, ScrollViewer и т.п.

		var size = new Size(100, 200);
		foreach (UIElement child in InternalChildren)
		{
			var elementSize = size;
			child.Measure(elementSize);
			var desiredSize = child.DesiredSize;
		}

		// В elementSize передаем максимальные размеры, которые может предоставить элементу панель, исходя из логики панели и availableSize.
		// Можно передавать бесконечные размеры, в частности availableSize.
		// Панель может и не использовать результат измерения своих элементов, но вызов child.Measure обязателен.
		// Каждый элемент формирует в child.DesiredSize минимальные требования по своим размерам, но не более elementSize (Margin будут учтены автоматически)
		// Элемент не может вернуть бесконечные размеры.

		return new Size(100, 200); ;

		// Необходимо вернуть минимальные требования по размерам панели, исходя из логики панели и полученных минимальных размеров элементов и не более availableSize.
		// Нельзя возвращать бесконечные размеры.
		// Нельзя возвращать availableSize, так как availableSize может иметь бесконечные размеры.
		// Не стоит возвращать размеры меньше минимально необходимых, так как в итоге именно эти размеры впоследствии могут быть предоставлены контейнером.
		// Не стоит возвращать размеры больше минимально необходимых, это просто нарушает смысл происходящего

	}

	protected override Size ArrangeOverride(Size finalSize)
	{
		var size = new Size(100, 200);
		foreach (UIElement child in InternalChildren)
		{
			var elementRect = new Rect(new Point(0, 0), size);
			child.Arrange(elementRect);
		}

		// Вызов child.Arrange обязателен, без него элементы не будут отрендерены.
		// В elementRect передаем финальное расположение и размер области, предоставленной для элемента, согласно логике панели, finalSize и child.DesiredSize.
		// Если у элемента есть Margin, система сама их учтет, дополнительно уменьшив предоставленную элементу область.
		// Если элемент меньше отведенной области, он будет располагаться в ней согласно Horizontal- и VerticalAlignment.
		// Если элемент больше отведенной области, он будет рендериться в полный размер, но выходящая за пределы elementRect часть видна не будет.
		// Можно указать область, выходящую за пределы области самой панели, видимость за границей панели будет определяться ClipToBounds самой панели.
		// Нельзя указать бесконечный размер.
		// После вызова child.Arrange можно прочитать отрендеренный реальный размер элемента из ActualHeight и ActualWidth

		return size;

		// Как правило, возвращается finalSize
		// Можно вернуть размер меньше finalSize, тогда панель будет позиционироваться в контейнере согласно своим Horizontal- и VerticalAlignment.
		// Можно вернуть размер больше finalSize (в том числе и бесконечный размер), но выходящая за пределы finalSize часть видна не будет
	}
}



Гляньте, может у кого дополнения есть, или я где-то не очень прав.
В частности по поводу обрезания выходящих за границы частей (на что-то влияет ClipToBounds, на что-то нет).

ЗЫ: В учебниках везде написано про измерения, что элемент возвращает желаемый размер. Но ведь это не совсем правильный термин, на мой взгляд, я употребляю слово "минимальный". Например тот же <Border /> (без настроек) затребует себе Size(0,0).
11 фев 19, 02:00    [21805978]     Ответить | Цитировать Сообщить модератору
 Re: WPF: Шпаргалка для создания панели  [new]
Roman Mejtes
Member

Откуда: г. Пермь
Сообщений: 3305
Shocker.Pro,

не знаю, чем вам термин желаемый размер не нравится. Так как это объект класса, с точки зрения объекта, этот как раз желаемый размер объекта, который зависит от его состояния . То есть размер который требуется элементу, для того, чтоб отобразиться в наилучшем виде (в идеале).

А так шпаргалка отличная, я правда это и так всё помню
11 фев 19, 10:58    [21806156]     Ответить | Цитировать Сообщить модератору
 Re: WPF: Шпаргалка для создания панели  [new]
Shocker.Pro
Member

Откуда: ->|<- :адуктО
Сообщений: 20286
Roman Mejtes
не знаю, чем вам термин желаемый размер не нравится. Так как это объект класса, с точки зрения объекта, этот как раз желаемый размер объекта, который зависит от его состояния .
Я трактую так: если у элемента стоит HorizontalAlignment=Stretch, то его желаемая ширина - занять всё доступное пространство. Но затребует он именно минимальное. То есть <Border BorderThickness="1" HorizontalAlignment="Stretch" /> затребует ширину в 2 дипа, даже если ему передают бесконечность в распоряжение.


Roman Mejtes
я правда это и так всё помню
- Доктор, у меня это. Я всё забываю!
- А я всё помню! Вы у меня уже были.
(с) Картинка с другого сайта.

С нюансами по обрезанию пришлось поэкспериментировать. Как-то делал панель, никак не мог понять, почему элементы подрезаются, тыкал везде ClipToBounds - не помогало. То, что надо резать желаемый размер по availableSize тоже наверняка не знал, посмотрел, как штатные контролы поступают.

+
Roman Mejtes
чем вам
Может в этом году, может в следующем... хочу проехать на машине от Питера до Владика. Проезжая через Пермь, отловлю тебя там, затащу в ресторан и заставлю выпить на брудершафт, чтобы уж наконец ты перестал мне вы-кать Картинка с другого сайта.Картинка с другого сайта.
11 фев 19, 12:22    [21806249]     Ответить | Цитировать Сообщить модератору
 Re: WPF: Шпаргалка для создания панели  [new]
Shocker.Pro
Member

Откуда: ->|<- :адуктО
Сообщений: 20286
Roman Mejtes,

Подскажи, InternalChildren заведомо неизменен между MeasureOverride и ArrangeOverride?

Смысл вопроса в следующем: так как зачастую основной расчет компоновки происходит уже на этапе измерения, логично было бы уже составить карту, которую просто использовать в ArrangeOverride, не обращаясь уже к коллекции InternalChildren заново.
16 фев 19, 15:07    [21811619]     Ответить | Цитировать Сообщить модератору
 Re: WPF: Шпаргалка для создания панели  [new]
Shocker.Pro
Member

Откуда: ->|<- :адуктО
Сообщений: 20286
И второй вопрос - нужно ли рендерить (да и измерять тоже) элементы, которые заведомо покинули область видимости при расстановке (да и при измерении)?
16 фев 19, 15:55    [21811657]     Ответить | Цитировать Сообщить модератору
 Re: WPF: Шпаргалка для создания панели  [new]
Shocker.Pro
Member

Откуда: ->|<- :адуктО
Сообщений: 20286
И третий вопрос, правомерен ли комментарий:
"finalSize не может быть больше availableSize"?
16 фев 19, 22:53    [21811876]     Ответить | Цитировать Сообщить модератору
 Re: WPF: Шпаргалка для создания панели  [new]
WinterGraveyard
Member

Откуда:
Сообщений: 59
Вроде как эта тема хорошо подходит для моего вопроса.
Ситуация: в листбоксе нужно отображать квадратные миниатюры неких объектов с подписью. Все квадраты должны быть одинакового размера, размер квадрата определяется длиной максимальной подписи (все подписи однострочные).
Сейчас я нахожу подпись с максимальной длиной (вот так), и привязываю к ней ширину и высоту border'а в DataTemplate. Схематично и в упрощенном виде это выглядит так:
namespace ListBoxTest
{
  public partial class MainWindow
  {
    public MainWindow()
    {
      InitializeComponent();
    }
  }

  public class MainModel
  {
    public MainModel()
    {
      Items = typeof(Type).GetProperties().Select(p => p.Name).ToList();
      var maxName = Items.OrderByDescending(n => n.Length).First();
      Console.WriteLine(maxName);
      var tb = new TextBlock();
      MaxItemWidth = new FormattedText(
        maxName,
        CultureInfo.CurrentCulture,
        FlowDirection.LeftToRight,
        new Typeface(tb.FontFamily, tb.FontStyle, tb.FontWeight, tb.FontStretch),
        tb.FontSize,
        Brushes.Black,
        new NumberSubstitution(),
        TextFormattingMode.Display
      ).Width + 16D;
    }
    public IReadOnlyCollection<string> Items { get; }
    public double MaxItemWidth { get; }
  }
}

<Window
  x:Class="ListBoxTest.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:ListBoxTest"
  Height="400"
  Width="600"
  WindowStartupLocation="CenterOwner">

  <Window.Resources>
    <local:MainModel x:Key="MainModel" />
    <DataTemplate x:Key="ItemsTemplate">
      <Border
        Margin="5"
        Width="{Binding Source={StaticResource MainModel}, Path=MaxItemWidth}"
        Height="{Binding Source={StaticResource MainModel}, Path=MaxItemWidth}"
        BorderThickness="1"
        BorderBrush="Gray"
        Background="Transparent">
        <TextBlock
          VerticalAlignment="Center"
          HorizontalAlignment="Center"
          Text="{Binding}"/>
      </Border>
    </DataTemplate>
  </Window.Resources>

  <Window.DataContext>
    <StaticResource ResourceKey="MainModel" />
  </Window.DataContext>

  <ListBox
    ItemsSource="{Binding Items}"
    SelectionMode="Single"
    ScrollViewer.HorizontalScrollBarVisibility="Disabled"
    ItemTemplate="{StaticResource ItemsTemplate}">
    <ListBox.ItemsPanel>
      <ItemsPanelTemplate>
        <WrapPanel IsItemsHost="True" Orientation="Horizontal"/>
      </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
  </ListBox>
</Window>

- всё нормально и без нареканий работает, но что-то мне подсказывает, что тут правильнее было бы сделать для ListBox.ItemsPanel свою панель. Но вот как - пока не могу сообразить.
24 апр 19, 08:40    [21870291]     Ответить | Цитировать Сообщить модератору
 Re: WPF: Шпаргалка для создания панели  [new]
Shocker.Pro
Member

Откуда: ->|<- :адуктО
Сообщений: 20286
Ну то есть ты хочешь избавится от вычисления общей ширины, которую сейчас ты вынужден вычислять вручную. Ну логично, в принципе, но в чем вопрос-то?
24 апр 19, 08:45    [21870292]     Ответить | Цитировать Сообщить модератору
 Re: WPF: Шпаргалка для создания панели  [new]
WinterGraveyard
Member

Откуда:
Сообщений: 59
Shocker.Pro
но в чем вопрос-то?

В том, что я вообще не представляю, с какого боку тут подступиться. Допустим, я наследуюсь от WrapPanel. Что тут нужно переопределить? MeasureOverride? ArrangeOverride? Каким образом? Допустим, из InternalChildren я получу элемент с максимальной шириной - в каком из методов (и как) мне задать для всех остальных элементов аналогичную ширину и высоту?
24 апр 19, 08:58    [21870301]     Ответить | Цитировать Сообщить модератору
 Re: WPF: Шпаргалка для создания панели  [new]
WinterGraveyard
Member

Откуда:
Сообщений: 59
Нашел еще вот такое решение:
<Window
  x:Class="ListBoxTest.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:ListBoxTest"
  Height="400"
  Width="600"
  WindowStartupLocation="CenterOwner">

  <Window.Resources>
    <local:MainModel x:Key="MainModel" />
    <DataTemplate x:Key="ItemsTemplate">
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="Auto" SharedSizeGroup="CellGroup" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto" SharedSizeGroup="CellGroup" />
        </Grid.RowDefinitions>
        <Border
          Margin="5"
          BorderThickness="1"
          BorderBrush="Gray"
          Background="Transparent">
          <TextBlock
            Margin="5"
            VerticalAlignment="Center"
            HorizontalAlignment="Center"
            Text="{Binding}"/>
        </Border>
      </Grid>
    </DataTemplate>
  </Window.Resources>

  <Window.DataContext>
    <StaticResource ResourceKey="MainModel" />
  </Window.DataContext>

  <ListBox
    Grid.IsSharedSizeScope="True"
    ItemsSource="{Binding Items}"
    SelectionMode="Single"
    ScrollViewer.HorizontalScrollBarVisibility="Disabled"
    ItemTemplate="{StaticResource ItemsTemplate}">
    <ListBox.ItemsPanel>
      <ItemsPanelTemplate>
        <WrapPanel IsItemsHost="True" Orientation="Horizontal"/>
      </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
  </ListBox>
</Window>

- и оно вполне работает без всяких предварительных вычислений. Теперь хотелось бы аналогичное всё-таки сделать через кастомную панель - чисто в целях самообразования, и общего развития.
24 апр 19, 09:49    [21870319]     Ответить | Цитировать Сообщить модератору
 Re: WPF: Шпаргалка для создания панели  [new]
Roman Mejtes
Member

Откуда: г. Пермь
Сообщений: 3305
В чем проблема то?
В методе MeasureOverride вычислите все размеры для элементов панели с помощью метода child.Measure(size).
Найдите максимальный размер высоты и ширины элементов.
Сохраните этот размер элементов в полях панели, чтоб воспользоваться им в методе ArrangeOverride.

Если в метод MeasureOverride был передан бесконечный размер, значит панель находится в прокручиваемой области. Размер будет бесконечным в том измерении, в котором разрешена прокрутка в элементе ScrollViewer. Если включена прокрутка по горизонтали, то нужно как то ограничить максимальное количество в 1 ряду, иначе все элементу будут в 1 ряд.
Всё это необходимо, чтоб вычислить предполагаемый размер самой панели, которую вам нужно вернуть в конце метода.
Предположим, что максимальное количество элементов в ряд 10 и панель бесконечна по вертикали и горизонтали. Тогда вы берете количество элементов делите на 10, округляете к верхней границе, получаете количество строк. Умножаете ширину элементов на 10 и высоту на количество строк, получаете размер панели.
Если прокрутка только по вертикали, тогда делите ширину на полученный размер элементов, получаете количество столбцов, умножаете на ширину элементов, получаете ширину панели, вычисляете количество рядов, вычисляете высоту.
Чтоб не образовывалось дыр слева, при таком раскладе, можно вычислить размер пустой области, поделить на количество столбцов и прибавить к размеру элементов.

Так вы получите размер всех элементов списка. Затем в методе ArrangeOverride расположите эти элементы плиткой с вычисленным размером с помощью метода child.Arrange(rect)
24 апр 19, 12:36    [21870435]     Ответить | Цитировать Сообщить модератору
 Re: WPF: Шпаргалка для создания панели  [new]
WinterGraveyard
Member

Откуда:
Сообщений: 59
Roman Mejtes
В чем проблема то?
В методе MeasureOverride вычислите все размеры для элементов панели с помощью метода child.Measure(size).
Найдите максимальный размер высоты и ширины элементов.
Сохраните этот размер элементов в полях панели, чтоб воспользоваться им в методе ArrangeOverride.

Если в метод MeasureOverride был передан бесконечный размер, значит панель находится в прокручиваемой области. Размер будет бесконечным в том измерении, в котором разрешена прокрутка в элементе ScrollViewer. Если включена прокрутка по горизонтали, то нужно как то ограничить максимальное количество в 1 ряду, иначе все элементу будут в 1 ряд.
Всё это необходимо, чтоб вычислить предполагаемый размер самой панели, которую вам нужно вернуть в конце метода.
Предположим, что максимальное количество элементов в ряд 10 и панель бесконечна по вертикали и горизонтали. Тогда вы берете количество элементов делите на 10, округляете к верхней границе, получаете количество строк. Умножаете ширину элементов на 10 и высоту на количество строк, получаете размер панели.
Если прокрутка только по вертикали, тогда делите ширину на полученный размер элементов, получаете количество столбцов, умножаете на ширину элементов, получаете ширину панели, вычисляете количество рядов, вычисляете высоту.
Чтоб не образовывалось дыр слева, при таком раскладе, можно вычислить размер пустой области, поделить на количество столбцов и прибавить к размеру элементов.

Так вы получите размер всех элементов списка. Затем в методе ArrangeOverride расположите эти элементы плиткой с вычисленным размером с помощью метода child.Arrange(rect)

Ну, по сути это всё - воспроизведение логики методов MeasureOverride/ArrangeOverride самой WrapPanel, только с заданными размерами элемента. Зачем всё это нужно, если это всё уже реализовано у WrapPanel (и реализация там, надо сказать, не очень проста). Плюс у WrapPanel есть свойства ItemWidth и ItemHeight, которые как раз определяют фиксированный размер элемента панели, и они используются внутри реализации MeasureOverride/ArrangeOverride, и они помечены флагом FrameworkPropertyMetadataOptions.AffectsMeasure. Поэтому я сделал проще:
namespace ListBoxTest
{
  public class MyWrapPanel : WrapPanel
  {
    public MyWrapPanel()
    {
      Loaded += (s, ea) =>
      {
        var maxWidth = InternalChildren.OfType<FrameworkElement>().Max(e => e.ActualWidth);
        if (maxWidth > 0)
        {
          ItemWidth = maxWidth;
          ItemHeight = maxWidth;
        }
      };
    }
  }
}

и в XAML сделал так:
<ListBox
  ItemsSource="{Binding Items}"
  SelectionMode="Single"
  ScrollViewer.HorizontalScrollBarVisibility="Disabled"
  HorizontalContentAlignment="Stretch"
  VerticalContentAlignment="Stretch"
  ItemTemplate="{StaticResource ItemsTemplate}">
  <ListBox.ItemsPanel>
    <ItemsPanelTemplate>
      <local:MyWrapPanel Orientation="Horizontal"/>
    </ItemsPanelTemplate>
  </ListBox.ItemsPanel>
</ListBox>

Но вариант с SharedSizeGroup мне нравится больше - остановлюсь на нём.
24 апр 19, 13:41    [21870517]     Ответить | Цитировать Сообщить модератору
 Re: WPF: Шпаргалка для создания панели  [new]
Roman Mejtes
Member

Откуда: г. Пермь
Сообщений: 3305
Событие Loaded, это не совсем то, что вы себе представляете, за время жизни объекта оно может быть вызвано более 1 раза.
Выше вы писали, что хотите, чисто в академических целях разобраться как работает макетирование в WPF (Measure\Arrange), по этому ваша отсылка в существующим реализациям, мне не совсем понятна. Используйте WrapPanel, я не против. Если поищите в интернетах, даже найдете реализацию этой панели с поддержкой виртуализации
24 апр 19, 13:52    [21870533]     Ответить | Цитировать Сообщить модератору
 Re: WPF: Шпаргалка для создания панели  [new]
Shocker.Pro
Member

Откуда: ->|<- :адуктО
Сообщений: 20286
Роман ответил подробно, я лишь немного общих слов
WinterGraveyard
Что тут нужно переопределить? MeasureOverride? ArrangeOverride?
Переопределять нужно оба метода в любом случае, потому что в базовом классе стоят заглушки

WinterGraveyard
в каком из методов (и как) мне задать для всех остальных элементов аналогичную ширину и высоту?
НЕ НАДО переопределять ширину и высоту дочерних элементов! Ты так зациклишься - будет бесконечное переизмерение и переразмещение.
Задача панели не задавать размер элемента, а размещать эти элементы, то есть выделить им координаты и пространство в соответствии с твоей логикой. А задача элемента - разместиться внутри этого пространства (например с HorizontalAlignment=Stretch)


WinterGraveyard
WrapPanel (и реализация там, надо сказать, не очень проста)
У Метью МакДональда в книжке есть реализация WrapPanel с возможностью добавления принудительного конца строки. Ничего сложного в коде панели у него нет.
24 апр 19, 15:50    [21870715]     Ответить | Цитировать Сообщить модератору
Все форумы / WPF, Silverlight Ответить