Настраиваемый хром для окон в WPF

Приглашенная статья, Джо Кастро (Joe Castro), разработчик группы продуктов WPF

В этой статье рассматриваются разработка и некоторые детали реализации окон WPF, заключенных в настраиваемый хром. Сегодня WPF поддерживает стандартные окна, со значками, текстовым заголовком и значками в заголовке, а также окна без границ, которые, соединенные с прозрачностью, позволяют использовать произвольные фигуры верхнего уровня, но приложение утрачивает поддержку системы для поведения, связанного со стандартной областью заголовка, например для развертывания. Существует желание создавать приложения, заполняющие этот разрыв: обладающие внешним видом и поведением стандартного окна и одновременно создающие более полные фирменные ощущения.

Сценарии

Существует много приложений (как WPF, так и не-WPF), в которых хром до определенной степени настраивается. В этом документе обсуждаются некоторые типы изменений и эмуляция подобного поведения. Некоторые из этих сценариев также обсуждались в других блогах, и авторы приводимых примеров были заметно сильнее меня, поэтому в подобных случаях я буду просто указывать соответствующий URL-адрес вместо того, чтобы дублировать их код. К этому документу также прилагается проект, содержащий код для всех описываемых приемов.

Существует несколько приложений, изменяющих свой хром различным образом:

Office 2007. Святой Грааль настраиваемого хрома реализуется множеством способов — с использованием Aero, если эта возможность доступна, и с изящным ухудшением внешнего вида там, где Aero недоступен. Когда поддержка Aero Glass включена в таких программах, как Word, кажется, что в них используется стандартная область заголовка, хотя текст заголовка находится в центре, а в левом верхнем углу размещены настраиваемые рисунки. Он содержит жемчужину и "Панель быстрого доступа" в области, очевидно не являющейся клиентской. Он также содержит стандартные кнопки заголовка. Когда наложение отключено (т.е. поддержка Aero Glass выключена), неклиентский макет заголовка сохраняется, но для заголовка и его кнопок используется отдельный стиль, независящий от пользовательской темы Windows.

Хром в Office

Safari, iTunes и Quicktime для Windows. Apple сохраняет большую часть внешнего вида и ощущений OS X с помощью своих приложений для Windows. Визуальный переход между клиентской и неклиентской областями практически незаметен. В Safari верхние углы окна слегка закруглены, а нижние — прямоугольны, окно окружено границей толщиной один пиксель, и оно использует собственный стиль значков в заголовке. Текущая версия iTunes использует аналогичную границу, но закругленные углы со всех сторон. Она также содержит значки в заголовке, стиль которых больше похож на стиль Vista, но DWM для их рисования не используется. Так как DWM не задействован, вокруг рамки окна отсутствует свечение при наведении

Хром в iTunes

Проводник Vista и мастера Aero. В окнах Проводника Vista не отображается ни значок в заголовке, ни текст заголовка, так как эти же сведения показываются чуть ниже, в строке навигации. Размер области заголовка в этом случае не меняется, она остается пустой и при включении поддержки Aero Glass. Эта область особенно заметна в мастерах, написанных для Vista — например в мастере создания — которые ведут себя так же, как Проводник. В этом случае эффект отличается от того, что происходит, когда системный значок не задан и текст заголовка очищен: оба элемента, тем не менее, появляются в других контекстах, таких как панель задач и окно alt-tab.

Мастер создания DVD в Vista при включенном Aero

Internet Explorer 7 и Windows Media Player в Vista. Эти программы расширяют прозрачную рамку, чтобы привлечь внимание пользователя к контенту, а не к элементам управления. Фактически IE не меняет область заголовка, но расширение прозрачности на адресную строку визуально делает ее частью заголовка. WMP использует аналогичный эффект в своих элементах управления в нижней части окна, фактически создавая вторую область заголовка, содержащую элементы управления.

IE7 и WMP в Vista

Baby Smash! А теперь что-то совсем другое, www.babysmash.com. Это приложение захватывает весь пользовательский экран, намеренно закрывая панель задач. Когда эта программа выполняется, она оказывается единственным приложением, с которым, как предполагается, сможет работать пользователь.

Как они это делают?

В каждую версию Windows встроена поддержка создания программ, изменяющих неклиентскую область, но WPF не предоставляет эти возможности в объекте Window. Платформа WPF создана так, чтобы, обнаружив отсутствие функциональных возможностей, разработчик мог уйти ниже публичных интерфейсов API и напрямую работать с дескрипторами Win32, поверх которых реализована WPF.

К этой статье прилагается код, помогающий понять описания, но предполагающий знание читателем основ Win32. Фактически, достаточно, чтобы читатель знал, что WndProc — гигантская функция, обрабатывающая системные и пользовательские события, на которые должен реагировать объект Window, и понимал, что все слова, написанные заглавными буквами (например HWND и NCCALCSIZE_PARAMS), скорее всего, относятся к структурам Win32. Я также использую соглашение имя_dll!ИмяФункции, для обращения к собственным функциям Windows, экспортированным из DLL. В C# они также доступны с помощью P/вызова. В качестве источника дополнительных сведений можно обратиться к MSDN.

В C# разработчик может получить HWND для WPF Window после его появления на экране,

IntPtr hwnd = new WindowInteropHelper(_window).Handle;

а затем создайте свой производный вариант WndProc,

HwndSource.FromHwnd(hwnd).AddHook(_WndProc);

Самое интересное происходит внутри созданной реализации _WndProc.

Расширение прозрачной рамки в IE7

DWM позволяет приложениям расширить рамку на свое окно с помощью простого вызова P/Invoke, dwmapi!DwmExtendFrameIntoClientArea. Эта функция была введена в Vista, поэтому при ее использовании необходимо проверить ее доступность. Фактически эта функция не влияет на неклиентскую область, но для поддерживаемых тем она может уменьшить различие между клиентской и неклиентской областями. Эта функция также будет работать, только если включена композиция, а при выключении и включении композиции понадобится повторно вызвать эту функцию, чтобы включить ее повторно. Состояние композиции можно определить с помощью функции dwmapi!DwmIsCompositionEnabled.

Очень простой код, реализующий эти задачи, был написан Адамом Натаном (Adam Nathan), поместившим его в свой блог, https://blogs.msdn.com/adam_nathan/archive/2006/05/04/589686.aspx. Вместо копирования результатов его труда я просто ссылаюсь на них.

Кроме показанного Адамом, для правильной обработки расширения прозрачной рамки также понадобится определять случаи, когда изменение темы влияет на доступность прозрачности. При изменении WndProc получит сообщение DWM_COMPOSITIONCHANGED, поэтому приложение сможет отреагировать соответствующим образом.

Baby Smash! Развернутое окно, учитывающее панель задач

В этом случае вызов WindowStyle.None срабатывает предполагаемым образом. Развертывание окна со стилем None скроет панель задач. Часто при работе с настраиваемым хромом этот эффект является нежелательным, но в данном сценарии именно к нему и стремился разработчик.

Если для приложений, использующих стиль WindowStyle.None, такое поведение, реализуемое по умолчанию, нежелательно, приложение может обработать WM_GETMINMAXINFO. И снова я собираюсь сослаться на чужой код, показывающий, как это сделать.

Лестер Лобо (Lester Lobo) подробно описал реализацию этого приема здесь, https://blogs.msdn.com/llobo/archive/2006/08/01/Maximizing-window-_2800_with-WindowStyle_3D00_None_2900_-considering-Taskbar.aspx.

Удаление избыточных данных из строки заголовка в Проводнике Vista

Понятно, что использование строки заголовка нежелательно, если те же данные показываются более естественным образом в другом месте окна. Для решения этой проблемы в Vista существует функция uxtheme!SetWindowThemeAttribute. Она позволяет удалить системный значок и текст заголовка. Ее действие не сводится к простой очистке соответствующих полей, так как они, тем не менее, появляются в панели задач и окне alt-tab. Ее также можно использовать для отключения только одного или другого поля в рамке окна.

Этот эффект работает только в Vista и только для темы Aero, хотя в отличие от расширений прозрачности он работает и в Aero Basic. Соответствующий код действительно похож на расширение прозрачной рамки. Принципиальная версия кода, реализующая описанный эффект (в предположении, что необходимые собственные структуры и функции уже объявлены), выглядит следующим образом:

void SetWindowThemeAttribute(Window window, bool showCaption, bool showIcon)
{
    bool isGlassEnabled = NativeMethods.DwmIsCompositionEnabled();
 
    IntPtr hwnd = new WindowInteropHelper(window).Handle;
 
    var options = new WTA_OPTIONS
    {
        dwMask = (WTNCA.NODRAWCAPTION | WTNCA.NODRAWICON)
    };
    if (isGlassEnabled)
    {
        if (!showCaption)
        {
            options.dwFlags |= WTNCA.NODRAWCAPTION;
        }
        if (!showIcon)
        {
            options.dwFlags |= WTNCA.NODRAWICON;
        }
    }
 
    NativeMethods.SetWindowThemeAttribute(hwnd, WINDOWTHEMEATTRIBUTETYPE.WTA_NONCLIENT, ref options, WTA_OPTIONS.Size);
}

Как и в случае прозрачности, для правильной обработки изменений темы приложение должно отслеживать WM_DWMCOMPOSITIONCHANGED и повторно применять атрибуты темы.

Рисование неклиентской области с использованием прозрачности в Office 2007 с Aero

DWM отключает эффекты прозрачности, обнаруживая, что программа пытается рисовать в неклиентской области. Office решает эту проблему, удаляя неклиентскую область.

Обработка сообщения WM_NCCALCSIZE позволяет приложению настроить соответствующий размер клиентской области. Правильная обработка этой функции достаточно сложна. Для этого программе фактически может потребоваться обработать это сообщение, не меняя структуру lParam NCCALCSIZE_PARAMS (или RECT, в зависимости от значения wParam). При получении сообщения lParam содержит прямоугольник нового окна, но ожидается, что поле будет переопределено новым клиентским прямоугольником. Если значение lParam не менять, неклиентская область фактически будет удалена!

Это не повлияет на эффекты ранее описанного расширения прозрачной рамки, поэтому, расширяя прозрачную рамку вверху на нужную высоту заголовка, а другие границы на соответствующую ширину, приложение сохраняет внешний вид стандартного окна, но получает возможность свободно рисовать в любом его месте. Рамку можно расширить на произвольное значение, но, чтобы реализовать поведение Office, соответствующее пользовательским настройкам для размеров заголовка, можно использовать для получения данных класс SystemParameters. Расширение прозрачности, использующее значения ResizeFrameHorizontalBorderHeight, ResizeFrameVerticalBorderWidth и CaptionHeight, выполняет разумные действия по эмуляции этих настроек.

При первом появлении окна понадобится обработать системное сообщение WM_NCCALCSIZE. Вызов функции user32!SetWindowPos с SWP_FRAMECHANGED и SWP_NOSIZE после привязки WndProc решает эту задачу без каких-либо побочных эффектов.

В этот момент DWM ведет себя немного странно. Если прозрачность расширяется сверху, значки заголовка Aero продолжают рисоваться. Высота значков ограничена, но если места по вертикали не хватает, значки будут сплющены и все равно появятся. Они не реагируют на действия пользователя без небольшого толчка, но это не самое сложное. Чтобы заставить их реагировать на щелчки мыши и наведение курсора (включая свечение за границами окна), вызовите dwmapi!DwmDefWindowProc в процедуре обработки WM_NCHITTEST, отслеживая необходимость обработки этого сообщения в программе. Но DWM не будет рисовать значок или текст заголовка, предоставляя разработчику свободу действий в этом отношении. Все это несложно выполнить с помощью визуальных объектов WPF. Естественным способом добиться отображения текста окна с эффектом прозрачности и соответствующим изменением цвета при развертывании окна может быть вызов uxtheme!DrawThemeTextEx. Возможно, не стоит взаимодействовать с GDI для получения параметров, необходимых вызову, так как их легко можно подставить из WPF. Помните, что стиль заголовка при развертывании окна меняется: В обычном состоянии заголовок обычно содержит черный текст с белой подсветкой. При развертывании подсветка исчезает, а текст становится черным.

После всех этих манипуляций у окна не будет данных о том, что разработчик делает с клиентской областью, поэтому область представления содержимого сохранит свой размер, но будет смещена вверх и влево. Чтобы это исправить, используйте полотно Canvas в качестве содержимого окна и привяжите его свойства Width и Height к значениям ActualWidth и ActualHeight окна. Это также можно выполнить как часть замены ControlTemplate объекта Window.

При развертывании окна Windows обрежет границу от имени приложения, если отсеченная область не использовалась. В этом сценарии нет причин для описанных действий, но за этим нужно следить, потому что, если прозрачность не расширена на границы так, как это было бы сделано средствами Windows (SM_CXFRAME (+ SM_CXPADDEDBORDER в Vista)), то развернутое окно обрежет края клиентской области. Простейший путь обойти эту проблему — создать границу (Border) вокруг содержимого окна, толщина которой равна толщине отсекаемой области, и свертывать ее видимость (Visibility), когда окно не развернуто.

Эта проблема является более серьезной, когда прозрачность отключена, но есть причина не возиться с заполнением прозрачности при использовании DWM. Если глубина прозрачности (Glass) различна, ее понадобится учитывать при обработке сообщения WM_WINDOWPOSCHANGED, когда окно развернуто. Соответствующий метод рассматривается в следующем разделе.

Все, что осталось выполнить в этом сценарии — обработать сообщение WM_NCHITTEST. Это позволяет системе выполнить тяжелую работу по обработке изменения размера и перетаскивания заголовка. Для получения интуитивно понятного поведения верните соответствующее значение HT, в зависимости от того, находится ли указатель мыши (определяемый lParam) над рамкой прозрачности. Если интерактивные элементы управления рисуются в области заголовка, не нужно выполнять вторую проверку попадания WPF перед возвращением HTCAPTION вслепую. Если VisualTreeHelper.HitTest не возвращает значение null для положения мыши относительно окна, возвращение HTNOWHERE гарантирует, что визуальный элемент ведет себя как элемент управления, а не как область заголовка. Эта операция может быть ресурсоемкой, как и приложения, ведущие себя подобным образом. Обрабатывая WM_NCHITTEST вместо использования WPF для перетаскивания окна и эскизов (Thumbs) для изменения размера, приложение может сохранить больше элементов стандартного поведения системы, например, системное меню для изменения размера будет работать правильно, а щелчок правой кнопкой мыши в области заголовка выведет системное меню. Благодаря детальной обработке элементов управления, обеспечиваемой этим сообщением, область заголовка фактически может быть создана в любом месте окна. Но ее иное размещение, не у верхней границы и не растянутой по ширине окна, может запутать пользователей.

Стоит отметить, что Office отключает свой хром Aero, когда окно уменьшается до достаточно малых размеров. Этот прием могут применять и другие приложения, если элементы выходят за границы значков заголовка DWM. Элементы WPF будут рисоваться поверх, поэтому при вызове DwmDefWindowProc они все равно будут подсвечиваться при наведении курсора мыши. Иллюзия псевдо-стандартного хрома поддерживается не для всех условий, по крайней мере, в случае Office. При перетаскивании заголовка они иногда рисуются поверх значков заголовка. Существует множество системных метрик, взаимодействующих с областью заголовка и влияющих на размер, шрифт и стиль текста заголовка. Очень трудно точно имитировать поведение системы для всех случаев.

Office 2007 без Aero, или вы отвечаете за все

Существует более сложный вариант предыдущего сценария Office, в котором DWM не используется для обработки всех аспектов рамки. Он охватывает поведение Office без Aero, WMP 11 в XP, а также Apple Safari и iTunes. Необязательно, что эти приложение работают именно так, но это способ решения этой проблемы в WPF.

Приложения, поддерживающие настраиваемый хром при включенной поддержке Aero Glass, должны изящно переходить к этому сценарию. Даже если программа работает в операционной системе Vista, в пользовательской теме не обязательна включена поддержка композиции. Некоторые приложения, такие как iTunes и Safari, просто не интегрируются с высокоуровневыми возможностями Vista и одинаково работают со всеми темами.

Во-первых, применимо все, упомянутое в последнем разделе о WM_NCCALCSIZE. Это сообщение можно использовать для удаления неклиентской области, упрощая размещать элементы в произвольных местах. Но без DWM граница окна будет выглядеть пустой и прямоугольной. Так как полотно распространяется на все окно, его можно использовать для рисования любых границ.

Если не устраивают прямые углы, можно скруглить их, применяя HRGN к окну. Это нужно делать при каждом изменении состояния окна, например, когда оно показывается, скрывается, или когда изменяется его размер. Самое безопасное место для этих действий — обработка WM_WINDOWPOSCHANGED. Закруглить углы окна можно, вызывая gdi32!CreateRoundRectRgn с шириной и высотой окна, а также с нужным закруглением, а затем вызывая gdi32!SetWindowRgn с созданным HRGN. Если нужно закруглить только конкретные углы, например только верхние, как в Safari, можно использовать функцию gdi32!CreateRectRgn для любого угла или края, а затем функцию gdi32!CombineRgn для их объединения.

При развернутом окне Windows обычно будет выполнять отсечение HRGN от имени программы, чтобы удалить заполнение вокруг пользовательского контента. Если разработчик применяет собственный HRGN, то его настройки сохраняются, по существу требуя от разработчика обнаруживать, когда окно развертывается, и использовать соответствующую прямоугольную область, используя для развертывания размеры монитора. Это очень похоже на реализацию развертывания окна без границы с сохранением панели задач.

В целом, самой замысловатой частью этого сценария является то, что Windows часто пытается рисовать стандартную строку заголовка, даже при отсутствии неклиентской области. Обычно эти попытки предсказуемы и происходят в моменты, когда Windows пришлось бы выполнить перерисовку, чтобы обновить строку заголовка. К таким моментам относятся развертывание, восстановление размера, свертывание, задание значка или текста и активация/деактивация окна, а также все переключения разрешенных состояний значков заголовка. Во избежание последствий можно обнаруживать и перехватывать эти типы изменений.

Для таких сообщений, как WM_SETTEXT и WM_SETICON, можно избежать перерисовки, временно удаляя стиль WS_VISIBLE из объекта Window, и позволить изменению выполниться, а затем восстановить стиль.

Для сообщения WM_NCACTIVATE документация MSDN неполна. Перехват вызова и его передача функции user32!DefWindowProc со значением lParam равным -1 заставят систему не перерисовывать область заголовка. Такая обработка сообщения WM_NCACTIVATE избавит разработчика от необходимости обрабатывать WM_ACTIVATE.

Если целью также является сохранение местонахождения и размера значков заголовка, а возможно и стиля, следует использовать NONCLIENTMETRICS, чтобы определить их место и размер. Обычно поддержки темы WPF должно быть достаточно, чтобы обнаружить, какой стиль использовать для кнопок в зависимости от пользовательской темы.

Код!

Последние два сценария были перегружены жаргоном Win32 без использования реальных фрагментов кода. Реализация описанного оказывается достаточно сложна, чтобы привести в смятение, будучи вставленной в статью. Поэтому к этой статье прилагаются библиотека DLL + исходный код, которые можно использовать в существующем проекте для создания окон WPF, ведущих себя в стиле Office, а также простое приложение, показывающее использование интерфейса API.

https://code.msdn.microsoft.com/chrome

Предсказывать будущее нелегко — или "Как сделать некоторых людей несчастными и навсегда"

В конечном счете, замена хрома окна выполняет работу диспетчера окон. Подобная эмуляция Windows может привести к отсутствию поведения, ожидаемого пользователями, либо неправильно работать в некоторых условиях (например при высоких значениях точек на дюйм или использовании программы чтения с экрана). Нужно соблюдать осторожность, чтобы после замены гарантировать правильную работу функций, ожидаемых пользователями.

Сегодня стандартными являются, например, следующие возможности заголовка окна:

  • Щелчок значка левой кнопкой мыши выводит системное меню.
  • Двойной щелчок системного значка закрывает приложение (так же ведет себя и жемчужина Office).
  • Щелчок заголовка правой кнопкой открывает системное меню.
  • Двойной щелчок заголовка развертывает приложение.
  • При использовании DWM меняется стиль текста заголовка развернутого окна.
  • Изменение цветов в зависимости от активного/неактивного состояний (Используемые цвета соответствуют окрашиванию DWM при включенной поддержке Aero Glass, цветам темы, если не используется классический интерфейс Windows, и системные цвета в классическом интерфейсе Windows.)
  • Соблюдаются метрики системы для размеров и метрики, доступные для измерения и зависящие от версии Windows (например в Vista была добавлена метрика iPaddedBorderWidth).

Существует ряд действий, которые система перестает выполнять автоматически, когда разработчик использует собственный хром. И некоторые виды поведения различны в разных версиях Windows.

Замена хрома объекта Window — это очень заметный способ выделить свое приложение и придать ему фирменный вид, но есть вероятность, что при реализации что-то будет пропущено или что в будущих версиях Windows какие-то элементы поведения могут измениться, и внешний вид приложения окажется неуместным.

Но это не означает, что "этого делать не надо". Просто замена хрома должна быть осознанным решением.

Еще одно предупреждение

Описанное в этой статье понадобится WPF и для реализации этого вида функциональных возможностей в платформе. В обозримом будущем всем придется работать поверх Win32. Это означает, что приложения, встраивающие свои функции в WndProc под WPF, потенциально могут изменить состояние окна несовместимым образом. Приложения, использующие приемы, описанные в этой статье, возможно, придется изменять, чтобы использовать преимущества, которые предложат будущие версии платформы. Такое развитие событий необязательно, но помнить о возможных проблемах необходимо.