В этой статье речь пойдёт о том, как использовать WPF Data Triggers в схеме разбиения монитора Stack для того, чтобы полностью автоматически отдавать приложению всё пространство рабочего стола, когда другие приложения не запущены. Stack — это тайловый менеджер окон для Windows, использующий WPF XAML в качестве языка разметки экрана.
Поставляющаяся из коробки схема разбиения экрана для больших мониторов (см. под катом) разбивает рабочий стол на 2 главные зоны — центральную (я обычно использую её для браузеров и IDE) и боковую (туда попадают RSS Reader, мессенжеры и окна терминалов). Эта схема фиксирована. Т.е. если у вас открыт только браузер, Stack оставит пустое место слева от него. В этой статье я покажу как использовать WPF Data Triggers и Data Binding, чтобы схопывать эту область автоматически, когда она пустая.
Вот так выглядит схема разбиения до начала работы над ней:
Тут можно взять исходный код последней версии.
Для начала придётся создать копию с другим именем, потому что в оригинале есть пометка
Собственно, работать в основном придётся с самым внешним Grid, т.к. он определяет левую колонку и всё остальное.
Как видим, колонки фиксированы по размерам как 1:3. Нам такое не надо, и мы попытаемся сжимать левую колонку, если в ней ничего нет.
К счастью, у нас уже есть табы в левой колонке, которые должны показывать все окна, находящиеся в ней. А это значит мы можем использовать их Items.Count в качестве источника для DataTrigger.
Чтобы это сделать, придётся дать им имя (они определены в начале схемы внутри первой колонки):
Тут вступает в бой магия DataTrigger'ов. Берём определение первой колонки (см. выше) и меняем фиксированный Width на стиль с DataTrigger:
Обратите внимание на то, что 1* используется как значение по умолчанию, а триггер вкючается при 0 элементов. Было бы логичнее указать триггер при > 0 элементах, но WPF XAML так не умеет.
Итак, сохраняем, выбираем в Stack'е новую схему и видим ..., что больше не можем ничего положить на левую сторону, т.к. она схлопывается при запуске из-за того, что в ней ничего нет (мы сами так написали), а значит и навести мышкой туда нельзя. Win + стрелочка, кстати, уже работает, так что если вы мышкой не пользуетесь, следующую часть можно будет пропустить.
Если вы раньше делали кастомыные схемы для Stack, вы уже знаете, что зона, куда можно положить окно не обязана совпадать с зоной, где это окно окажется. Поэтому мы сделаем очень просто — добавим зону слева, которая будет видима даже когда колонка будет свёрнута. Так можно сделать, если положить в Grid левой колонки Canvas — Canvas не обрезает элементы, которые в него не вмещаются.
Эта Canvas заменит собой Border, который в оригинальной схеме разметки использовался для того, чтобы растягивать элементы на всю левую колонку. Вот как это будет выглядеть:
Поскольку Canvas в общем-то предназначен для рисования, нет простого способа растянуть элементы во всю его ширину. Поэтому размеры элементов в нём привинчены к размерам самого Canvas через data-binding'и:
Также у них у всех указан MinWidth чтобы когда колонка вместе с Canvas'ом были свёрнуты, элементы всё равно имели бы ненулевую ширину.
В общем-то это уже выглядит как надо, но если вы сейчас сохраните и попробуете мышкой перенести что-нибудь налево, у вас ничего не получится. Из-за порядка элементов в XAML файле основная зона находится выше нашего DropOverlay в z-order, превращая его в бесполезный «DropUnderlay» :)
Исправить это очень просто — поменяйте местами Grid'ы, описывающие колонки. Было:
Стало:
Теперь левая колонка будет выше в z-order, чем центральная часть, и DropOverlay сможет принимать мышку.
Вот что мы увидим:
Полный код схемы разбиения тут: pastebin
С такой схемой разбивки экрана гораздо удобнее, чем с фиксированной. Однако не помешали бы дополнительные доработки. Например, не удобно, что при драге первого окна мышкой налево не показывается куда именно оно переедет, т.к. соответствующая зона свёрнута. Эту пробему можно решить c помощью MouseOver триггера, который бы разворачивал всю левую колонку при наведении мыши.
Вообще, я ещё не пробовал, но можно было бы использовать в триггерах и элементах разметки данные, полученные из внешних источников. Получилось бы что-то вроде кастомных виджетов на рабочем столе, например, с котировками курсов. Единственная проблема — WPF не умеет обновлять данные из источника по таймеру.
Вообще это очень круто, что можно использовать XAML для кастомизации рабочего стола в Windows. Открывает много возможностей.
Поставляющаяся из коробки схема разбиения экрана для больших мониторов (см. под катом) разбивает рабочий стол на 2 главные зоны — центральную (я обычно использую её для браузеров и IDE) и боковую (туда попадают RSS Reader, мессенжеры и окна терминалов). Эта схема фиксирована. Т.е. если у вас открыт только браузер, Stack оставит пустое место слева от него. В этой статье я покажу как использовать WPF Data Triggers и Data Binding, чтобы схопывать эту область автоматически, когда она пустая.
С чего начнём?
Вот так выглядит схема разбиения до начала работы над ней:
Тут можно взять исходный код последней версии.
Для начала придётся создать копию с другим именем, потому что в оригинале есть пометка
This file is overwritten after every update. Please, modify a copy!Я назвал свою схему Large Horizontal Left Autocollapse.
Собственно, работать в основном придётся с самым внешним Grid, т.к. он определяет левую колонку и всё остальное.
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="3*"/>
Как видим, колонки фиксированы по размерам как 1:3. Нам такое не надо, и мы попытаемся сжимать левую колонку, если в ней ничего нет.
Data Trigger
К счастью, у нас уже есть табы в левой колонке, которые должны показывать все окна, находящиеся в ней. А это значит мы можем использовать их Items.Count в качестве источника для DataTrigger.
Чтобы это сделать, придётся дать им имя (они определены в начале схемы внутри первой колонки):
<zones:WindowTabs>...
->
<zones:WindowTabs x:Name="SideTabs">
<zones:WindowTabs.ItemsSource>
<CompositeCollection>
<zones:ZoneElement Content="{Binding ViewModel, Source={x:Reference SideStack}}"/>
<CollectionContainer Collection="{Binding Windows, Source={x:Reference SideSingle}}"/>
</CompositeCollection>
</zones:WindowTabs.ItemsSource>
</zones:WindowTabs>
Тут вступает в бой магия DataTrigger'ов. Берём определение первой колонки (см. выше) и меняем фиксированный Width на стиль с DataTrigger:
<ColumnDefinition>
<ColumnDefinition.Style>
<Style>
<Setter Property="ColumnDefinition.Width" Value="1*"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Items.Count, ElementName=SideTabs}" Value="0">
<Setter Property="ColumnDefinition.Width" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ColumnDefinition.Style>
</ColumnDefinition>
Обратите внимание на то, что 1* используется как значение по умолчанию, а триггер вкючается при 0 элементов. Было бы логичнее указать триггер при > 0 элементах, но WPF XAML так не умеет.
Итак, сохраняем, выбираем в Stack'е новую схему и видим ..., что больше не можем ничего положить на левую сторону, т.к. она схлопывается при запуске из-за того, что в ней ничего нет (мы сами так написали), а значит и навести мышкой туда нельзя. Win + стрелочка, кстати, уже работает, так что если вы мышкой не пользуетесь, следующую часть можно будет пропустить.
Что же делать с мышкой
Если вы раньше делали кастомыные схемы для Stack, вы уже знаете, что зона, куда можно положить окно не обязана совпадать с зоной, где это окно окажется. Поэтому мы сделаем очень просто — добавим зону слева, которая будет видима даже когда колонка будет свёрнута. Так можно сделать, если положить в Grid левой колонки Canvas — Canvas не обрезает элементы, которые в него не вмещаются.
Эта Canvas заменит собой Border, который в оригинальной схеме разметки использовался для того, чтобы растягивать элементы на всю левую колонку. Вот как это будет выглядеть:
<!-- Canvas is visible even though its container is collapsed -->
<Canvas x:Name="DropOverlay"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
zones:Layout.IsHint="True">
<zones:Zone Target="{Binding ElementName=SideStack}" MinWidth="160"
Width="{Binding ActualWidth, ElementName=DropOverlay}"
Height="{Binding ActualHeight, ElementName=DropOverlay}"/>
<Grid MinWidth="160"
Width="{Binding ActualWidth, ElementName=DropOverlay}"
Height="{Binding ActualHeight, ElementName=DropOverlay}"
HorizontalAlignment="Stretch">
<Border Height="160" Width="160" Background="#44F">
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
FontFamily="Segoe UI Symbol" Foreground="White" Text="?" FontSize="80"/>
</Border>
<zones:Zone HorizontalAlignment="Center" VerticalAlignment="Center"
Height="160" Width="160"
Target="{Binding ElementName=SideSingle}"/>
</Grid>
</Canvas>
Поскольку Canvas в общем-то предназначен для рисования, нет простого способа растянуть элементы во всю его ширину. Поэтому размеры элементов в нём привинчены к размерам самого Canvas через data-binding'и:
Width="{Binding ActualWidth, ElementName=DropOverlay}"
Также у них у всех указан MinWidth чтобы когда колонка вместе с Canvas'ом были свёрнуты, элементы всё равно имели бы ненулевую ширину.
В общем-то это уже выглядит как надо, но если вы сейчас сохраните и попробуете мышкой перенести что-нибудь налево, у вас ничего не получится. Из-за порядка элементов в XAML файле основная зона находится выше нашего DropOverlay в z-order, превращая его в бесполезный «DropUnderlay» :)
Исправить это очень просто — поменяйте местами Grid'ы, описывающие колонки. Было:
...
</Grid.ColumnDefinitions>
<Grid>
...LEFT...
</Grid>
<Grid Grid.Column="1">
...CENTER...
</Grid>
...
Стало:
...
</Grid.ColumnDefinitions>
<Grid Grid.Column="1">
...CENTER...
</Grid>
<Grid Grid.Column="0">
...LEFT...
</Grid>
...
Теперь левая колонка будет выше в z-order, чем центральная часть, и DropOverlay сможет принимать мышку.
Вот что мы увидим:
Полный код схемы разбиения тут: pastebin
Послесловие
С такой схемой разбивки экрана гораздо удобнее, чем с фиксированной. Однако не помешали бы дополнительные доработки. Например, не удобно, что при драге первого окна мышкой налево не показывается куда именно оно переедет, т.к. соответствующая зона свёрнута. Эту пробему можно решить c помощью MouseOver триггера, который бы разворачивал всю левую колонку при наведении мыши.
Вообще, я ещё не пробовал, но можно было бы использовать в триггерах и элементах разметки данные, полученные из внешних источников. Получилось бы что-то вроде кастомных виджетов на рабочем столе, например, с котировками курсов. Единственная проблема — WPF не умеет обновлять данные из источника по таймеру.
Вообще это очень круто, что можно использовать XAML для кастомизации рабочего стола в Windows. Открывает много возможностей.