Pull to refresh

WPF: Нестандартное окно

Reading time 7 min
Views 159K
На днях, после долгого перерыва, надо было поработать на WPF, и возникло желание заменить поднадоевший стандартный вид окон Windows 7 на что-нибудь более вдохновляющее, скажем в стиле Visual Studio 2012:



Переходить на Windows 8 ради этого еще не хотелось, как и добавлять в проекты ссылки на метро-подобные библиотеки и разбираться с ними — это будет следуюшим шагом. А пока было интересно потратить вечер и добиться такого результата с минимальными изменениями рабочего кода. Забегая вперед, скажу что результат, как и планировалось, получился довольно чистым: фрагмент следующего кода, если не считать нескольких аттрибутов пропущенных для наглядности, это и есть окно с первого скриншота. Все изменения ограничились заданием стиля.

Обновление 3 декабря: в репозиторий добавлена альтернативная имплементация использующая новые классы в .Net 4.5 (проект WindowChrome.Demo), что позволило избежать существенной части нативного программирования с WinAPI.

<Window ... Style="{StaticResource VS2012WindowStyle}">
    <DockPanel>
        <StatusBar>
            <TextBlock>Ready</TextBlock>
            <StatusBarItem HorizontalAlignment="Right">
                <ResizeGrip />
            </StatusBarItem>
        </StatusBar>
        <TextBox Text="Hello, world!" />
    </DockPanel>
</Window>


Дальше я остановлюсь на ключевых моментах и подводных камнях при создания стиля окна. Демонстрационный проект доступен на github'е, если вы захотите поразбираться с исходниками самостоятельно или же просто использовать этот стиль не вдаваясь в подробности.

Основная проблема


WPF не работает с NC-area. NC, она же «Non-client area», она же «не-клиентская часть», она же хром, обрабатывается на более низком уровне. Если вам захотелось изменить какой-то из элементов окна — бордюр, иконку, заголовок или кнопку, то первый совет, который попадается при поиске — это убрать стиль окна и переделать все самому. Целиком.

<Window
    AllowsTransparency="true"
    WindowStyle="None"> ...

За всю историю развития WPF в этом отношении мало что изменилось. К счастью, у меня были исходники из старинного поста Алекса Яхнина по стилизации под Офис 2007, которые он писал работая над демо проектом по популяризации WPF для Микрософта, так что с нуля начинать мне не грозило.

В итоге нам надо получить один стиль, и по возможности, без дополнительных контролов: в дереве проекта XAML и код стиля расположились в директории CustomizedWindow, а основное окно в корне проекта.

Мне хотелось избежать добавления новых библиотек в проект, но сохранить возможность легко перенести стиль в другое приложение, что и определило такую структуру.

Создаем стиль


Стиль для окна, как и для любого другого контрола в WPF задается при помощи ControlTemplate. Содержимое окна будет показываться ContentPresenter'ом, а функциональность которую проще сделать в коде c#, подключится через x:Class атрибут в ResourceDictionary. Все очень стандартно для XAML'а.

<ResourceDictionary
    x:Class="Whush.Demo.Styles.CustomizedWindow.VS2012WindowStyle">
    <Style x:Key="VS2012WindowStyle" TargetType="{x:Type Window}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type Window}">
                    <!-- XAML хрома окна с отрисовкой бордюра, иконки и кнопок -->
                    <ContentPresenter />
                    <!-- еще XAML хрома окна -->
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Сразу же определим кнопки управления окном в стиле Студии 2012. Это будет единственный дополнительный глобальный стиль на случай если потом возникнет желание использовать такие кнопки в приложении.



Нам нужна функциональность обычной кнопки, но с очень примитивной отрисовкой — фактически только фон и содержимое.
XAML стиля кнопки
<Style x:Key="VS2012WindowStyleTitleBarButton" TargetType="{x:Type Button}">
    <Setter Property="Focusable" Value="false" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Button}">
                <Grid>
                    <Border x:Name="border" Background="Transparent" />
                    <ContentPresenter />
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter TargetName="border" Property="Background" Value="#FFF" />
                        <Setter TargetName="border" Property="Opacity" Value="0.7" />
                    </Trigger>
                    <Trigger Property="IsPressed" Value="True">
                        <Setter TargetName="border" Property="Background"
                            Value="{StaticResource VS2012WindowBorderBrush}"/>
                        <Setter TargetName="border" Property="Opacity" Value="1" />
                        <Setter Property="Foreground" Value="#FFF"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Изображения на кнопках проще всего сделать «в векторе». Например, вот так выглядит maximize:
<Path StrokeThickness="1"
    RenderOptions.EdgeMode="Aliased"
    Data="M0,0 H8 V8 H0 V0 M0,1 H8 M0,2 H8" />

Для текста заголовка используем стандартный шрифт Segoe UI. Единственная особенность здесь — убедиться, что текст отрисован без размытия, иначе заголовок окна будет выглядеть… плохо он будет выглядеть — как во второй строчке на скриншоте.



Кстати, для Path'а на кнопках с той же целью использовался EdgeMode=«Aliased», а
для текста в WPF 4+ появилась долгожданная возможность указать, что отображаться он будет на дисплее, а не на «идеальном устройстве», что и позволило добиться приемлимой четкости на наших неидеальных экранах.

<TextBlock
    TextOptions.TextRenderingMode="ClearType"
    TextOptions.TextFormattingMode="Display" > ...

Еще одна интересная особенность связана с «геометрией Windows 7» при распахивании окна на весь экран. Windows жульничает, масштабируя окно так, что бордюр целиком уходит за границу экрана, оставляя на мониторе только клиентскую часть окна. Естественно, что Windows при этом больше не отрисовывает бордюр и для стандартных окон все работает как ожидалось. WPF это никак не отрабатывает и, для таких окон как у нас, есть риск потерять часть изображения или начать рисовать на соседнем мониторе, если он подключен.

Остальные детали менее существенны, но если интересно, добро пожаловать в исходники.

Оживляем окно


.Net 4.0


Помимо реакции на кнопки и иконку, окно должно перемещаться и изменять размер при drag'е за заголовок, за края и уголки. Соответствующие горячие зоны проще всего задать при помощи невидимых контролов. Пример для левого верхнего (северо-западного) угла.

<Rectangle
    x:Name="rectSizeNorthWest"
    MouseDown="OnSizeNorthWest"
    Cursor="SizeNWSE" Fill="Transparent"
    VerticalAlignment="Top" HorizontalAlignment="Left"
    Width="5" Height="5" />

При наличие атрибута Class в ресурсах, методы этого класса можно вызывать просто по имени как обычные обработчики событий, чем мы и воспользовались. Сами обработчики, например MinButtonClick и OnSizeNorthWest, выглядят примерно так:

void MinButtonClick(object sender, RoutedEventArgs e) {
    Window window = ((FrameworkElement)sender).TemplatedParent as Window;
    if (window != null) window.WindowState = WindowState.Minimized;
}

void OnSizeNorthWest(object sender) {
    if (Mouse.LeftButton == MouseButtonState.Pressed) {
        Window window = ((FrameworkElement)sender).TemplatedParent as Window;
        if (window != null && window.WindowState == WindowState.Normal) {
            DragSize(w.GetWindowHandle(), SizingAction.NorthWest);
        }
    }
}

DragSize далее вызывает WinAPI (исходник) и заставляет Windows перейти в режим измененения размера окна как в до-дотнетовские времена.

.Net 4.5

В 4.5 появились удобные классы SystemCommands и WindowChrome. При добавлении к окну, WindowChrome берет на себя функции изменения размера, положения и состояния окна, оставляя нам более «глобальные» проблемы.

    <Setter Property="WindowChrome.WindowChrome">
        <Setter.Value>
            <WindowChrome
                NonClientFrameEdges="None"
                GlassFrameThickness="0"
                ResizeBorderThickness="7"
                CaptionHeight="32"
                CornerRadius="0"
            />
        </Setter.Value>
    </Setter>

При желании, можно использовать WindowChrome и на .Net 4.0, но придется добавить дополнительные библиотеки, например WPFShell (спасибо afsherman за подсказку).

Почти готово. Зададим триггеры для контроля изменений интерфейса при изменении состояния окна. Вернемся в XAML и, например, заставим StatusBar'ы изменять цвет в зависимости от значения Window.IsActive.
XAML для StatusBar'а
<Style.Resources>
    <Style TargetType="{x:Type StatusBar}">
        <Style.Triggers>
            <DataTrigger Value="True"
                Binding="{Binding IsActive, RelativeSource={RelativeSource AncestorType=Window}}">
                <Setter Property="Foreground"
                     Value="{StaticResource VS2012WindowStatusForeground}" />
                <Setter Property="Background"
                     Value="{StaticResource VS2012WindowBorderBrush}" />
            </DataTrigger>
            <DataTrigger Value="False"
                Binding="{Binding IsActive, RelativeSource={RelativeSource AncestorType=Window}}" >
                <Setter Property="Foreground"
                    Value="{StaticResource VS2012WindowStatusForegroundInactive}" />
                <Setter Property="Background"
                    Value="{StaticResource VS2012WindowBorderBrushInactive}" />
            </DataTrigger>
        </Style.Triggers>
    </Style>
</Style.Resources>

Обратите внимание, что этот стиль влияет не на темплэйт окна, а на контролы помещенные в наше окно. Помните самый первый фрагмент с пользовательским кодом?

<Window ... Style="{StaticResource VS2012WindowStyle}">
    ...
    <StatusBarItem HorizontalAlignment="Right">
    ...
</Window>

Вот стиль именно этого StatusBar'а мы сейчас и задали. При желании и времени так же можно задать и стиль для других классов контролов, например подправить ScrollBar, чтобы он тоже соответствовал нужному стилю. Но это уже будет упражнение на следующий свободный вечер.

Собираем все вместе


Все. Нам осталось только подключить стиль к проекту через ресурсы приложения:

<Application ... StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Styles/CustomizedWindow/VS2012WindowStyle.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

И можно использовать его в любом окне.


— Д.

P.S. Еще раз ссылка на исходники на github'е для тех кто сразу прокрутил вниз ради нее.
Tags:
Hubs:
+83
Comments 32
Comments Comments 32

Articles