В этой статье речь пойдёт о том, как использовать WPF Data Triggers в схеме разбиения монитора Stack для того, чтобы полностью автоматически отдавать приложению всё пространство рабочего стола, когда другие приложения не запущены. Stack — это тайловый менеджер окон для Windows, использующий WPF XAML в качестве языка разметки экрана.

Поставляющаяся из коробки схема разбиения экрана для больших мониторов (см. под катом) разбивает рабочий стол на 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. Открывает много возможностей.

Комментарии (0)