Во время работы над нашим десктопным приложением столкнулся с такой задачей: имеется элемент-график с некоторыми настройками для отображения (реализован в виде 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