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

    На днях, после долгого перерыва, надо было поработать на 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'е для тех кто сразу прокрутил вниз ради нее.
    Поделиться публикацией

    Похожие публикации

    Комментарии 32

    • НЛО прилетело и опубликовало эту надпись здесь
        +2
        Ваша правда, для липучих заметок стиль идеально подходит, а поменять кнопки или привязаться к клавишам при полном доступе к имплементации — не проблема

        Для сравнения — вот окно 2012 студии без открытых проектов. Поначалу непривычно, но после нескольких месяцев становится более-менее естественным и начинает ассоциироваться со средствами разработки в целом, а не только со стикерами.

          0
          Меня после нескольких месяцев как бесил капслок в меню, так и бесит
            0
            PowerShell, но можно и просто в реестре:

            Set-ItemProperty -Path HKCU:\Software\Microsoft\VisualStudio\11.0\General -Name SuppressUppercaseConversion -Type DWord -Value 1
              0
              О, спасибо
        +2
        Есть одно отличие от «дефолтового» окна WPF — отсутствие обработки логики типа: дернули за верхнюю панель мышкой вниз (при условии что окно развернуто на весь экран) — окно свернулось до нормального размера. А так, спасибо, за статью.
          0
          Спасибо. Я раньше и не знал про такую возможность — очень удобно.
          Добавил эту функцию к коду на github'е (надо будет только посмотреть потом как улучшить производительность).
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              Как раз планировал взглянуть на наиболее интересные из метрошных WPF библиотек на выходных — Elysium и MahApps.Metro.

              Спасибо за подсказку. Для «родного» решения на .Net 4.5 — WindowsChrome более правильный подход. Хотя, для нестандартной формы окна совсем без костылей там, похоже, тоже не обойтись. Поищу решения для проблемы с тулбаром, посмотрю на ваш код и обновлю статью.
            –12
            > Мне хотелось избежать добавления новых библиотек в проект, но сохранить возможность легко перенести стиль в другое приложение, что и определило такую структуру.

            А можно установить экстеншен NuGet, через него установить MahApps.Metro (+ Resources — дело вкуса), чтобы не изобретать велосипед и не постить азы WPF на хабрахабре ;)

            Лично я считаю, что такие статьи заметки для начинающего WPF'щика не нужны на хабре. Этого полно в сети.
            Вот к примеру поисковый вывод. И это только на русском.
              0
              over 60 человек с вами не согласны.
                0
                на момент [11/15/2012:10:48] вижу только 30 из 35
                В фавор добавленные не считаю, ибо недостоверно. Добавить можно просто «последить за тредом для».

                Я сам много раз подумывал подобные статьи писать. Вот к примеру тема биндингов в WPF — RelativeSource, AncestorType, Mode.TwoWay по дефолту, IValueConverter и прочие ключи. Тоже можно описать, но ведь это все как минимум в MSDN достаточно хорошо описано. Не вижу смысл наводнять хабр поверхностными постами. Уж лучше написать про создание кастомного эффекта на PixelShader — это и тема посерьезней и популярнее, потому как близка к GPGPU.
                  0
                  Тут не просто элементарная статья, тут конкретный пример выполнения конкретной задачи. То что она решается просто, это же хорошо?

                  З.Ы. Когда я просто хочу следить за комментами, я ставлю галочку «в трекере»
                    0
                    По поводу «написать про создание кастомного эффекта» — на моем примере могу сказать что подобные статьи тоже считаются на хабре поверхностными.
                      0
                      > Уж лучше написать про создание кастомного эффекта на PixelShader
                      Потому и поставил префикс «Уж лучше» в том смысле, что есть шанс получить плюсов просто за модный тренд.
                      Как бы то нибыло, я давно хотел эту штуку попробовать и лично мне написание статьи было бы как минимум полезно с точки зрения глубины усвоения и упорядочивания мыслей. Пока пишешь материал он усваивается куда эффективнее, нежели просто чтение чужого.
                  +1
                  Статьи любые нам нужны, статьи любые нам важны...)

                  Я к примеру, совершенно не от мира десктопной разработки теперь получил в мозгу засечку, что можно вот в этом направлении что то сделать, и стал немного умнее, а вдруг мне завтра это понадобится и я вместо траты времени на гуглеж и курение доков получу порцию первоначальной информации отсюда (более вдумчиво прочитав статью) и сразу получу вектор дальнейшего гуглежа.
                    +1
                    Да и вообще, тема давно была раскрыта: blogs.msdn.com/b/wpfsdk/archive/2008/09/08/custom-window-chrome-in-wpf.aspx
                      0
                      А в карму-то нагадили будто высказывать свое мнение есть смертный грех.

                      Правду видимо писали:
                      > Получается, за плохие комментарии мы люто бешено минусуем в карму, за хорошие комментарии мы ставим плюсик комменту.

                      Ну извините, коль кого-то ненароком обидел или возмутил своими коментариями :)
                        +2
                        Забейте вы на эту карму. хабр — не пуп света.
                          0
                          Тут дело не в самой карме. Специально введены два инструмента: рейт каммента — для случая когда с ним несогласен; карма юзера — для карания или поощрения за поведение. И вот когда Начинают использовать второй инструмент, значит юзер (в данном случае я) сделал что-то достаточно негативное. Но при этом ни от кого не поступило в явном виде претензий.

                          Поэтому можно сделать опять вывод, что в статье про «Сублимацию на хабре» есть основания для размышлений.
                            0
                            На мой взгляд та статья несколько неконструктивна, этакий крик души.
                            Корень проблемы математический — когда на Stackoverflow, к примеру, карма (рейтинг) привязана к вопросам/ответам, то на хабре она ни с связана ни с чем. Ну и культура, конечно — тут очень много мелочных людей, которые любят поплевать в карму, просто потому что могут. На самом деле система (отсутствие комментария к плюсу/минусу, вышеупомянутая «оторванность» кармы) поощряет такие анонимные плевки и необоснованные плюсы (в праздничных топиках, например).
                            Поскольку царьки из администрации высокомерны (чего только стоят баны за обоснованную критику), изменений врят-ли стоит ждать. Пипл хавает, бабки за рекламу идут.
                            Давно перестал обращать внимание на карму. Чего и вам желаю.
                              0
                              Да… крик… карма врозь… культура… плевки ЧСВ потешить для… пипл схавает… Со всем этим я согласен. С высокомерием олимпа-жителей не столкнулся пока, слава Богу.

                              Задевает не карма оплеванная, а обостренное чувство несправедливости. Дааа, надо лечить недуг.
                                0
                                Минусовая или просто низка карма не позволяет постить топики куда надо. Например, я нашёл концепт-док для двигателя VASIMIR на английском языке. На хабре заветиться не успел, но перевод я запостить немогу, ибо карма ниже 5. Посему, за перевод я даже не принимался, думаю как поднимать карму.
                        0
                        Как раз стоит задача — поменять дизайн окон.
                        Сижу и изучаю тонны кода и думаю что нужно очень сильно эволюционировать для человека, который в этом ничего не понимает… И не совсем есть возможность свалить на других ибо растраты уже совсем не на том уровне… Ситуации разные бывают у абсолютно разных людей…
                        Итого:
                        Статья мне подошла очень очень кстати — я как будто желал ее увидеть на хабре…
                        Собственно и вам не хворать)
                        +2
                        Что подразумевается под «идеальным устройством/экраном»?
                          +1
                          «Идеальное устройство» это или беспиксельное устройство, или экран с очень высокой (бесконечной) плотностью пикселов и, соответственно, с возможностью показывать шрифт без искажений вызванных особенностями устройства. Из реальных к этому, наверное, ближе всего принтеры.

                          «Идальный» режим рендерит текст без учета существования пикселов.
                          Если линия в символе по толщине совпадает с одним пикселом, она может быть спозиционирована точно на пиксел или с дробным смещением, например в пол-пиксела. В первом случае будет четкая линия на экране, во втором — две линии в соседних пиксельных колонках, как в буквах N, l, m во второй строке на скриншоте.

                          MS долго настаивала, что вся отрисовка такста в WPF должна быть «идеальной», так что на мониторах на шрифты среднего и мелкого размера было страшно смотреть.
                          В WPF 4 добавили поддержку четкой отрисовки на обычных дисплеях и возможность выбора режима через TextFormattingMode с двумя возможными значениями — Display и Ideal.
                          +6
                          Мне нравятся кастомные окна, однако сильно раздражает то, что они не умеют вести себя как полноценные окна — я часто пользуюсь логикой, которая работает при перетаскивании окна за заголовок — потянул заголовок в самый верх экрана — окно развернулось на весь экран, потянул вниз — окно стало нормальным, и т.д.
                          Я уже и забыл когда нажимал на среднюю кнопку в заголовке окна — все окна таскаю за заголовок.
                          Без реализации этого поведения я не стал бы включать кастомный стиль окна в свой коммерческий проект.
                          Кстати, DevExpress для WinForms кастомизирует окно, но делает это путем перерисовки родного заголовка окна, поэтому окно хоть и выглядит кастомным, но ведет себя как нужно.
                            +1
                            А мне статья понравилась, пусть и ничего сверхъестественного не описывает. Сам программирую на WPF и учитывая не совсем ясные перспективы данного фреймворка, рад любым статьям на эту тему, автору спасибо. А солюшен MahApps.Metro, между прочим, занимает 20 мб, и не факт что в большинстве проектов он нужен полностью.
                              0
                              У меня скомпиленное приложение с MahApps.Metro + MahApps.Resources + Xceed.DataGrid + WPFToolkit Extended весит примерно 4 МБ. В них 2.41 МБ приходится на DataGrid и WPFToolkit. Сколько весит солюшен не важно.
                              Мой солюшен весит 19+ МБ, из которых 4 МБ — собранное приложение (папка bin), 13 МБ папка packages от NuGet. В packages только 1+ МБ приходится на MahApps.

                              Так что не вводите в заблуждение. Вот если б Вы сказали, мол, «не всегда лицензионная политика проекта допускает использование некоторых сторонних либ», то я бы, пожалуй, согласился.

                              PS: а за провокацию «учитывая не совсем ясные перспективы данного фреймворка» минусовать все равно не стану :)
                                0
                                Good for you. Но мои слова все-таки означают именно то, что написано, а именно если нужно решить какую-то небольшую задачу в проекте, попутно при этом немного размяв мозг прочтением документации и написанием живого кода, не надо этого бояться. А если есть желание и возможность сформулировать и оформить красиво свои исследования для других в виде подобной статьи — это вообще отлично. Просто нужно четко давать себе отчет, где творчество, а где изобретение велосипеда (например, собственный MVVM фреймворк или ORM пилить я никого не призываю). Мериться чем-бы то ни было я тем более не собирался, тут не магабайты важны, а сам принцип. Кстати, на репозиторий MahApps я подписался сразу как узнал о нем, и в одном из будущих проектов возможно буду его использовать.
                                Касаемо перспектив WPF — никакая это не провокация, просто мысли вслух. Уж очень активно в последнее время Microsoft продвигает HTML5/CSS3, и несмотря на чистосердечные обещания, что на WPF разработку это не повлияет, начинают возникать сомнения. C# то точно ничего не грозит хотя бы из-за ASP.NET/MVC, но судьба десктопной разработки в далекой перспективе уже как минимум не однозначна. Мое субьективное мнение, как человека, который сейчас работает в данном направлении и непосредственно в нем заинтересован. Вообщем, подводя итог, не надо искать черную кошку в темной комнате, особенно если ее там нет. Удачи.
                                  0
                                  > не надо искать черную кошку в темной комнате
                                  Не искал. Читал Ваш коммент буквально

                                  > А солюшен MahApps.Metro, между прочим, занимает 20 мб, и не факт что в большинстве проектов он нужен полностью.
                                  По факту ответил по реальные размеры и про то, что неприменим он может быть совсем по другим причинам, о которых Вы не написали.

                                  > Касаемо перспектив WPF — никакая это не провокация, просто мысли вслух.… Мое субьективное мнение, как человека, который сейчас работает в данном направлении
                                  и
                                  > Сам программирую на WPF и учитывая не совсем ясные перспективы данного фреймворка, рад любым статьям на эту тему
                                  Довольно разные формулировки. В предыдущем комменте не было написано, что это только ваше субъективное мнение/впечатление.

                                  > Но мои слова все-таки означают именно то, что написано, а именно если нужно решить какую-то небольшую задачу в проекте, попутно при этом немного размяв мозг прочтением документации и написанием живого кода, не надо этого бояться.

                                  Во-первых, написано у Вас было в другом ключе. Во-вторых, в реальном проекте всегда приходится взвешивать время на изобретение «таких вот» как раз велосипедов и профит от использования сторонних решений.

                                  > Просто нужно четко давать себе отчет, где творчество, а где изобретение велосипеда
                                  Ну в данном-то случае мы видим именно велик с кастомным протектором шин? Или Вы все же считаете это новым решением?
                                  Я так понял процитированную фразу должно было составить малось иначе. Универсальных решений сторонних не бывает. Всегда стоит выбор: взять готовое решение и выкинуть лишнее да обтесать острые углы ЛИБО написать свое нативное решение, четко отвечающее требованиям данного проекта. Т.е. проблема «Fast Implementation vs Best Fit». Это вы имели в виду?
                              0
                              Была как-то статья про плеер Meridian — музыкальный плеер в стиле Metro для Вконтакте. Интерфейс также сделан под metro style. Доступны исходники первой версии приложения. При желании можно покопаться в исходниках и посмотреть как там реализовано.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое