Растянуть Canvas и элементы внутри него по всей клиентской области

в 6:19, , рубрики: .net, C#, canvas, wpf, XAML

Во время работы над нашим десктопным приложением столкнулся с такой задачей: имеется элемент-график с некоторыми настройками для отображения (реализован в виде ControlTemplate подключаемого через привязку в ContentControl), к имеющимся настройкам нужно было добавить группу дополнительных. Чтобы не засорять уже имеющийся интерфейс, я решил поместить список этих настроек в элемент Expander, который, при необходимости, можно было бы развернуть, а в остальное время график занимал бы максимально возможное полезное пространство.

Один из способов размещения элементов — под катом.

В итоге была получена следующая иерархия элементов:

<TabItem>
  <Grid>
    <Grid.RowDefenitions>
      <RowDefenition Height="Auto" />
      <RowDefenition />
    </Grid.RowDefenitions>
    <Expander Header="Дополнительные настройки" Grid.Row="0" IsExpanded="False" MinWidth="270">
      <Элемент для настройки 1 />
      <Элемент для настройки 2 />
      <Элемент для настройки n />
    </Expander>
    <ContentControl <!--всякие настройки и привязки--> Grid.Row="1" />
  </Grid>
</TabItem>

Использую Grid, поскольку этот контейнер занимает пространство внутри вкладки полностью и сам способен растягивать элементы в строках по ширине.

Разместил Expander в верхней части вкладки, контрол для работы со спектром — сразу под ним. Однако, результат не оправдал ожиданий: при разворачивании Expander'a контрол графика сдвигается вниз, тем самым приходится закрывать Expander и проверять результат, либо скроллить оставшуюся для графика половинку экрана.

Возникла мысль взять тот же Expander и заставить его открываться как бы над настраиваемым контролом. Для этого идеально подходит контейнер Canvas, ведь именно в нем можно задавать ZIndex (глубину слоя) для каждого содержащегося в нем элемента. Я перепробовал несколько вариантов компоновки, ниже — один из них:

<TabItem>
  <Grid>
    <Canvas>
      <Expander Header="Дополнительные настройки" IsExpanded="False" Panel.ZIndex="1" MinWidth="270" Canvas.Right="10" >
        <Элемент для настройки 1 />
        <Элемент для настройки 2 />
        <Элемент для настройки n />
      </Expander>
      <ContentControl <!--всякие настройки и привязки--> Canvas.Top="25" />
    </Canvas>
  </Grid>
</TabItem>

Но тут меня опять ждало разочарование, Canvas либо вообще не отображался (поскольку по умолчанию его ширина и высота = 0), либо элементы внутри него то были слишком малы в размерах, то наооборот — выходили за границу видимости экрана.

Добиться нужно было следующего: растянуть Canvas по всей клиентской области вкладки; разместить в верхней правой части Canvas'а Expander фиксированной ширины, а под ним на всю оставшуюся область растянуть ContentControl.

Проведя некоторое время в поиске информации, её фильтрации и обобщении, пришел к следующему решению: внутри вкладки размещаем DockPanel. Этот контейнер также может автоматически задавать размеры элементам, находящимся внутри него. Устанавливаем для DockPanel свойство LastChildFill=«True», чтобы он растягивал последний из элементов на всю оставшуюся область. Задаем DockPanel имя, например, x:Name=«spectrumDock» и помещаем в него Canvas, который «прикрепляем» к верхней части панели (хотя размещение имеет особого значения). Внутри Canvas размещаем Expander и ContentControl следующим образом:

  <DockPanel x:Name="spectrumDock" LastChildFill="True">
    <Canvas>
      <Expander Header="Дополнительные настройки" IsExpanded="False" Panel.ZIndex="1" MinWidth="270" Canvas.Right="10" >
        <Элемент для настройки 1 />
        <Элемент для настройки 2 />
        <Элемент для настройки n />
        <Expander.Effect>
          <DropShadowEffect BlurRadius="6" Direction="270" ShadowDepth="1" Opacity="0.5"/>
        </Expander.Effect>
      </Expander>
      <ContentControl Content="{Binding Path=ControlTemplateName}" Canvas.Top="25" 
        Height="{Binding ElementName=spectrumDock, Path=ActualHeight, Converter={StaticResource SizeTrimmerConverter}, ConverterParameter='25'}"
        Width="{Binding ElementName=spectrumDock, Path=ActualWidth}" />
    </Canvas>
  </DockPanel>

… тень для Expander'а создает ощущение, что этот элемент находится сверху, над остальными. Для задания размеров ContentControl'у используется просто биндинг свойств ширины и высоты (именно Actual) DockPanel. Однако, было одно «НО» — ContentControl сдвинут на 25 пикселей вниз и имеет высоту, равную высоте DockPanel, следовательно, он выезжает за видимую границу на эти самые 25 пикселей, т.е. нужно забиндить к ContentControl высоту DockPanel минус 25 пикселей. Эта задача решается довольно просто — использованием конвертера, которому в качестве параметра передается требуемое количество пикселей для отступа:

namespace пространство_имен
{
  [ValueConversion(typeof(double), typeof(double))]
  public class SizeTrimmerConverter : IValueConverter
  {
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
      return (double)value - ConvertParameter(parameter);
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
      return (double)value + ConvertParameter(parameter);
    }
    private double ConvertParameter(object parameter)
    {
      string _stringValue = parameter.ToString();
      double _result = 0;
      double.TryParse(_stringValue, out _result);
      return _result;
    }
  }
}

В описании главного контрола (UserControl или форма) указываем ссылку на пространство имен:

xmlns:Converters="clr-namespace:пространство_имен;assembly=название_сборки"

… и описываем конвертер в ресурсах:

<Главный контрол.Resources>
  <Converters:SizeTrimmerConverter x:Key="SizeTrimmerConverter" />
</Главный контрол.Resources>

… конвертеры лежат в отдельной библиотеке, поэтому указывается название_сборки.

В результате всё получилось так, как и было в ожиданиях. Прошу не судить строго, возможно, данный метод и является «костыльным», но моего опыта в использовании технологий WPF + XAML ещё достаточно не много. Надеюсь, мой опыт окажется кому-то полезен.

Автор: ILepekhov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js