Поддержка Windows Visual Styles (Themes) API в Ваших органах управления

Автор: Акжан Абдулин
Опубликовано: 04.12.2001
Версия текста: 1.06
Введение
Стандартные органы управления
Нестандартные органы управления
Среда разработки
С++
Delphi
Основные понятия
Проверка окружения
Обработка ошибок
Хэндлы данных темы
Классы, элементы и их состояния
Отрисовка
Клиентская и неклиентская области окна
Менеджер тем
Особенности
API низкого уровня
Обрамление органа управления
Динамические библиотеки
Дополнительные материалы
Ссылки
Благодарности
Особое спасибо

Введение

В операционных системах (OC) компании Microsoft, начиная с Microsoft Windows XP, появились так называемые визуальные стили (visual styles), которые определяют внешний вид органов управления (controls) и других окон (windows) интерфейса пользователя.

В отличие от более ранних ОС компании Microsoft, органы управления теперь могут иметь не только иные цветовую схему и пропорции, но также иные методы прорисовки отдельных своих элементов оформления (parts).

Сами методы отрисовки различных стандартных элементов были выделены в отдельный модуль с расширением mst, который поставляется в составе визуального стиля. В комплект поставки Windows XP входит только один визуальный стиль - Luna.

На любом визуальном стиле может быть основано несколько различных тем оформления (themes).

Естественно, что Visual Styles API поддерживает отрисовку всех стандартных органов управления Windows. Более того, необходимо отметить, что создание органов управления, имеющих визуальное представление, отличное от стандартного, теперь сопряжено с большими неудобствами. Впрочем, не будем забегать вперёд.

К сожалению, Visual Styles API является частью операционной системы, что не позволяет использовать его преимущества на более ранних платформах. Кроме того, даже в случае операционной системы, в принципе поддерживающей визуальные стили, пользователь может отказаться от их использования.

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

Замечания, приведённые выше, не позволяют назвать Visual Styles API хорошо продуманным проектом. Тем не менее, именно использование Visual Styles API придало приложениям Windows XP новый, весьма выгодный облик. Необходимо признать, что эстетически стиль Luna на голову превосходит все ранее выполненные разработки компании Microsoft.

Приложения, использующие нестандартные органы управления, в новом окружении могут выглядеть инородно, что может вызвать у пользователя реакцию отторжения. И наоборот, творческое использование возможностей Visual Styles API в Ваших компонентах может привлечь пользователя. Наличие качественного содержимого не гарантирует успеха, равно как и внешний лоск, но сочетание этих свойств, по моему личному мнению, удваивает эффект.

Именно поэтому поддержка Visual Styles API особенно важна для разработчиков коммерческого и условно-бесплатного программного обеспечения.

Данная статья объединяет как информацию, опубликованную в MSDN, так и опыт Ваших коллег.

В частности, Visual Styles API был изучен мной для дальнейшего использования в наборе компонент ElPack от компании EldoS, чьей целевой платформой является Borland Delphi/C++Builder. Многие блоки исходного текста, приведённые в статье, с некоторыми сокращениями, изменениями и дополнениями взяты из ElPack.

Большинство компонент пакета ElPack 3.0.1 теперь поддерживают Visual Styles API и могут быть использованы в Ваших приложениях для придания им современного облика. Кроме того, изучение исходных текстов этого пакета само по себе может оказаться увлекательным и полезным занятием.

Стандартные органы управления

Visual Styles API поддерживается новым поколением библиотеки общих органов управления (ComCtl32.dll версии 6 и выше). Необходимо отметить, что новое поколение этой библиотеки реализует поддержку как общих органов управления (common controls), таких, как treeview и listview, так и стандартных органов управления (standard controls), таких, как edit, listbox и combo box. Очевидно, что такое решение практически сделало ComCtl32.dll несовместимой с предыдущими ОС, так как поддержка стандартных органов управления в ранних ОС возложена на модуль user.dll.

Для обеспечения совместимости с унаследованными приложениями в Windows XP введено несколько изменений в механизм загрузки динамических библиотек. В частности, если Ваше приложение не отмечено, как спроектированное с использованием ComCtl32.dll версии 6, то оно сможет воспользоваться только предыдущим поколением библиотеки общих органов управления. Соответственно, все стандартные органы управления Вашего приложения в таком случае не будут использовать возможности Visual Styles API.

Пометить Ваше приложение, как спроектированное с использованием ComCtl32.dll версии 6, можно с помощью так называемого манифеста (manifest). Манифест представляет собой документ XML, описывающий требования модуля к операционному окружению.

Подробное рассмотрение манифестов выходит за рамки данной статьи, поэтому я ограничусь тем, что опубликую манифест, декларирующий необходимость использования библиотеки общих органов управления ComCtl32.dll версии 6 (см. примечание в конце статьи):

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?> 
   <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
      <assemblyIdentity processorArchitecture="*" version="5.1.0.0" type="win32"
        name="Microsoft.Windows.Shell.shell32" />
      <description>Windows Shell</description>
      <dependency>
         <dependentAssembly>
            <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls"
              version="6.0.0.0" publicKeyToken="6595b64144ccf1df" language="*"
              processorArchitecture="*" /> 
         </dependentAssembly>
      </dependency>
   </assembly>

Назначить манифест Вашему приложению можно двумя путями:

Если существует файл манифеста для данного модуля, то именно он будет использоваться даже в случае наличия манифеста, зашитого в ресурсы модуля.

Нестандартные органы управления

Всякий раз, когда мы вынуждены создавать свой орган управления, а не расширять функциональность существующего, мы должны самостоятельно реализовывать поддержку Visual Styles API.

Все методы Visual Styles API фактически реализованы в динамической библиотеке uxtheme.dll. Эта библиотека включает в себя менеджер тем, реализующий механизмы смены и уведомления о смене тем (theme manager), а также прокси-модуль, переадресующий вызовы приложения тематических методов к соответсвующим точкам входа модуля визуального стиля с расширением mst.

Среда разработки

В дальнейшем я буду иллюстрировать текст данной статьи примерами на Borland Delphi/VCL, но разработчики, использующие иные среды разработки приложений, без особого труда смогут прочитать эти примеры и переработать для своего окружения, например, для ATL/WTL или MFC, так как оные примеры основаны на использовании чистого Win32 API/Visual Styles API.

С++

Если Вы хотите работать на C++, то Вам необходим обновлённый Platform SDK (не ранее, чем июнь 2001 г.). Visual Styles API определён в заголовочных файлах:

При линковке Вам, возможно, потребуется подключить библиотеку импорта uxtheme.lib. Впрочем, по известным причинам (совместимость Ваших компонент с устаревшими платформами) Вы можете предпочесть реализовать позднее связывание с помощью LoadLibrary.

В разделе дополнительных материалов к данной статье Вы найдёте классы C++, облегчающие построение органов управления на С++, совместимых с Visual Styles API, от Владимира Романова.

Первый класс обеспечивает динамическую загрузку менеджера тем. Кстати, автор рекомендует обратить внимание на то, каким образом реализована динамическая загрузка (использование и переопределение макросов) - эта методика весьма удобна.

Второй класс (вернее, шаблон класса) содержит абстракции более высокого уровня, равно как и обработчик сообщений, и предназначен для использования совместно с ATL/WTL

/////////////////////////////////////////////////////////////////////////////
// CXpTheme - message handlers for theme support
// Chain to CXpTheme message map.
// Example:
// class CMyButton : public CWindowImpl<CMyButton, CButton>,
//                 public CXpTheme<CMyButton>
// 
// public:
//      BEGIN_MSG_MAP(CMyButton)
//              // your handlers...
//              CHAIN_MSG_MAP_ALT(CXpTheme<CMyButton>, 1)
//      END_MSG_MAP()
//      // other stuff...
// };
// Also you must call CXpTheme::SubclassWindow() from CMyButton::SubclassWindow() 

Кроме того, Владимир Романов предоставил полные исходные тексты органа управления, повторяющего функциональность стандартной кнопки Windows (themed pushbutton).

Delphi

Если Вы хотите работать на Delphi, то Вам необходим обновлённый Delphi-JEDI Complete Win32 API Header Convertion (не ранее, чем июль 2001 г.). На сегодня он доступен по адресу ftp://delphi-jedi.org/api/Win32API.zip. Visual Styles API определён в модулях:

Возможно, в этих модулях Вам придётся добавить директиву компиляции {$MINENUMSIZE 4}.

Впрочем, по известным причинам (совместимость Ваших компонент с устаревшими платформами) Вы можете предпочесть переписать декларации вызовов API для использования позднего связывания. Marcel van Brakel предполагает в ближайшем будущем добавить в эти модули опциональное позднее связывание (включаемое директивой компиляции).

Кроме того, я перечислю некоторые важные константы, которые определены в Platform SDK:

const
  // тип ресурсов для манифеста
  RT_MANIFEST = 24;
  // id манифеста для приложения
  CREATEPROCESS_MANIFEST_RESOURCE_ID = 1;
  // id для статического импорта модуля
  ISOLATIONAWARE_MANIFEST_RESOURCE_ID = 2;
  // id для динамического импорта модуля
  ISOLATIONAWARE_NOSTATICIMPORT_MANIFEST_RESOURCE_ID = 3;
  // сообщение, приходящее при смене темы
  WM_THEMECHANGED = $031A;

Игорь Кокарев предоставил примеры исходных текстов. Кроме того, в дополнительные материалы мною включен Visual Styles Explorer вместе с исходными текстами. Схема визуального стиля была определена мною не полностью, но Вы можете самостоятельно дополнить её, например, дополнив XML-документ, описывающий её. Возможно, в будущем я предприму шаги по расширению функциональности этого приложения.

Основные понятия

Проверка окружения

В случае, если мы проектируем компоненты для использования не только в операционном окружении, поддерживающим Visual Styles API, нам придётся реализовать проверку операционного окружения и динамическую загрузку библиотеки менеждера тем в случае подходящего окружения.

Нижеследующий код предполагает, что Visual Styles API может присутствовать только в операционных системах семейства Windows NT версии не ниже 5.1 (Windows XP):

var
  ThemesAvailable : Boolean;

implementation

uses
  SysUtils;

const
  themelib = 'uxtheme.dll';

var
  hThemeLib: HINST;

initialization

{$ifdef MSWINDOWS}

  if (Win32Platform  = VER_PLATFORM_WIN32_NT) and
     (((Win32MajorVersion = 5) and (Win32MinorVersion >= 1)) or
      (Win32MajorVersion > 5)) then
  begin
    hThemeLib := LoadLibrary(themelib);
    if hThemeLib <> 0 then
    begin
      IsThemeActive := GetProcAddress(hThemeLib, 'IsThemeActive');      
      // другие действия
      ThemesAvailable := True;
    end;
  end;
{$endif}

finalization

  ThemesAvailable := False;

end.

Обработка ошибок

Большинство методов Visual Styles API для информирования вызывающей стороны о своих действиях используют возвращаемое значение типа HRESULT, уже знакомое всем, кто имел дело с COM. Вы можете использовать макросы SUCCEEDED(hr) и FAILED(hr) для определения успешности того или иного вызова.

Кроме того, все методы Visual Styles API используют для информирования об ошибках метод Win32 API SetLastError. Вы можете использовать метод GetLastError. Вся информация об ошибках является потоко-зависимой.

Delphi/C++Builder предоставляет Вам удобные методы OleCheck(hr) и RaiseLastWin32Error(), выбрасывающие исключение при возврате неуспешного значения с текстом сообщения, полученным через FormatMessage. Аналогичную функциональность предлагает и Visual C++.

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

Таким образом, Вам может быть удобно создать метод ThemeCheck, выбрасывающий исключение при сбое в работе методов Visual Styles API (в большинстве случае достаточно использовать OleCheck).

unit ThemeSupport;

interface

uses
  SysUtils;

type
  EThemeException = class(Exception);

function ThemeCheck(hr: HRESULT): HRESULT;

implementation

uses
  Windows,
  ComObj,
  ElUxTheme;

function ThemeCheck(hr: HRESULT): HRESULT;
var
  LangId: DWORD;
  tecx: TThemeErrorContext;
  err: WideString;
begin
  if FAILED(hr) then
  begin
    if SUCCEEDED(GetThemeLastErrorContext(tecx)) then
    begin
      LangId := ConvertDefaultLocale(LOCALE_USER_DEFAULT);
      SetLength(err, 255); // try to get 255-char len string
      if SUCCEEDED(FormatThemeMessage(LangId, tecx, PWideChar(err), Length(err))) then
      begin
        raise EThemeException.Create(err); // raises and exits
      end;
    end;
    OleError(hr); // use system error message formatting services
  end;
  Result := hr; // if succeeded
end;

end.

Хэндлы данных темы

Visual Styles API является хэндл-ориентированным, как и большинство иных сервисов операционных систем Microsoft.

Хэндл данных темы (далее - хэндл темы) привязан к строго определённым теме и классу стандартных/общих органов управления Windows.

type
  HTheme = THandle;

Если Вы разрабатываете орган управления, использующий элементы различных стандартных органов управления (например, Page Control, который отрисовывает кнопки прокрутки, если закладки не помещаются в видимую часть органа управления), то Вам придётся открыть для использования несколько хэндлов тем, так как хэндл темы Page не умеет отрисовывать кнопки прокрутки, и придётся использовать дополнительно, например, хэндл темы Scrollbar.

Для получения и освобождения хэндлов тем используются методы OpenThemeData и CloseThemeData. Если OpenThemeData возвращает нулевое значение, то Вы обязаны использовать свой код отрисовки органа управления.

Последний открытый через OpenThemeData хэндл темы для данного окна может быть запрошен через GetWindowTheme. Тем не менее, рекомендую избегать использования данного метода.

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

Обычно хэндлы используемых тем запрашиваются при получении сообщения WM_CREATE и освобождаются при получении сообщения WM_DESTROY. Подобная практика желательна, так как вычисление размера клиентской области Вашего органа управления через WM_NCCALCSIZE обычно требует наличия хэндла темы.

Классы, элементы и их состояния

Visual Styles API включает в себя поддержку для стандартных и общих классов окон. Если Вы знакомы с Windows API, то помните, что класс окна в Windows идентифицируется строкой символов.

Для каждого класса окон в Visual Styles API определены один или несколько элементов. Фактически можно представить орган управления как простую плоскую мозаику из относительно крупных несоставных элементов. Элементы идентифицируются неотрицательным индексом. В модуле TmSchema все элементы определены через перечисления, и их идентификаторы имеют вид xxxP_yyy, где xxx указывает на класс ограна управления, а yyy - на элемент, определённый в классе. Типичный пример - идентификатор BP_GROUPBOX, определяющий "группу органов управления", как элемент, принадлежащий классу органов управления "button".

Основной (корневой, базовый) элемент всегда имеет индекс 0. Этот элемент часто носит также имя "заполнителя" (filler). Обычно этот элемент используется для отрисовки фона неклиентской области органа управления (если только речь не идёт о сложных вариациях) при получении сообщения WM_NCPAINT.

Кроме того, элемент управления может содержать ещё несколько дополнительных элементов. Так, строка состояния ("status") может иметь элементы SP_PANE, SP_GRIPPERPANE и SP_GRIPPER. Для каждого класса органов управления определены свои элементы. Их индексы для разных классов могут перекрываться.

Каждый элемент может иметь некое состояние. Например, кнопка прокрутки полосы прокрутки может быть в нормальном, запрещённом, нажатом и "горячем" (hot) состояниях (см. примечание). Каждое состояние идентифицируется неотрицательным индексом. Состояние 0 используется для информационных запросов. Кроме того, для разных классов и элементов состояния обычно определены одинаково (если они определены). В модуле TmSchema все состояния определены через перечисления, и их идентификаторы имеют вид xxxS_yyy, где xxx указывает на элемент, а yyy - на состояние, свойственное элементу. Типичный пример - идентификатор PBS_DISABLED, определяющий состояние "Запрещено" для элемента BP_PUSHBUTTON, принадлежащего классу органов управления "button".

Вы можете определить, определено ли в используемой теме наличие любого из элементов класса в том или ином состоянии вызовом IsThemePartDefined. Учтите, что при наличии состояния XXXS_NORMAL состояния XXXS_HOT и аналогичные считаются также определёнными, независимо от того, что возвращает IsThemePartDefined. Состояние XXXS_DISABLED может быть и не определено, и для проверки его определённости в стиле необходимо вызвать IsThemePartDefined. Более подробно в изучении этих тонкостей Visual Styles API Вам поможет Visual Styles (Themes) Explorer, включённый в дополнительные материалы данной статьи.

Кроме того, Вы можете определить размеры того или иного элемента данного класса в данной теме через вызов GetThemePartSize. Для каждого элемента определены минимально возможный (TS_MIN), оптимальный (TS_TRUE) и рисуемый (TS_DRAW) размеры. Рисуемый размер исчисляется на базе передавемой области для отрисовки. Минимально возможный размер определяет, когда элемент всё ещё будет рисоваться. Оптимальный размер - размер, на который ориентировались разработчики темы.

Этот вызов очень полезен, например, если мы хотим узнать, где расположить size grip для нашего компонента панели состояния.

Отрисовка

Сама по себе отрисовка элементов с использованием Visual Styles API выполняется весьма просто:

Если элемент содержит прозрачные или полупрозрачные фрагменты (что определяется через IsThemeBackgroundPartiallyTransparent), то сперва производится отрисовка окна, владеющего данным, через DrawThemeParentBackground. При необходимости Вы можете получить регион, который точно обрамляет границы, где элемент себя рисует, через GetThemeBackgroundRegion. Это может быть полезно для последующего использования этого региона в вызове SetWindowRgn.

Сам элемент отрисовывается с помощью метода DrawThemeBackground. Текст на элементе отрисовывается через DrawThemeText аналогично системному DrawText. Изображения из списков изображений отрисовываются через DrawThemeIcon. Все эти методы по возможности отражают состояние элемента, модифицируя выводимое изображение.

У Вас есть также возможность получить более подробную информацию об используемом тематическом шрифте (GetThemeTextExtent, GetThemeTextMetrics).

Кроме того, есть ещё несколько дополнительных методов.

Клиентская и неклиентская области окна

Необходимо отметить, что Visual Styles API не проводит никакого разграничения между элементами клиентской и неклиентской области окна. В то же время при использовании Visual Styles API Вам приходится самостоятельно выполнять отрисовку и клиентской, и неклиентской части окна средствами Visual Styles API.

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

Например, tab pane (страница) из page control (набор страниц) имеет чётко означенную рамку. Любые органы управления, размещаемые внутри этой страницы, не должны пересекать эту рамку. Фактически это означает необходимость формирования клиентской области окна, размещаемой внутри рамки.

Visual Styles API предоставляет нам метод GetThemeBackgroundContentRect, который позволяет получить расположение клиентской области окна. Важно учитывать, что отступы слева, справа, сверху и снизу необязательно будут хоть как-то симметричны. Вот типичный пример использования:

procedure TElTabSheet.WMNCCalcSize(var Message: TWMNCCalcSize);
var                            
  R, R1: TRect;
begin
  if IsThemeApplied then
  begin
    if PageControl.ShowBorder then    
    begin
      inherited;
      R := Message.CalcSize_Params.rgrc[0];      
      if Succeeded( GetThemeBackgroundContentRect(TabTheme, Canvas.Handle, TABP_PANE, 0, R, R1)) then      
      begin        
        Message.CalcSize_Params.rgrc[0] := R1;      
      end;    
    end;  
  end  
  else    
    inherited;
end;

Естественно, мы теперь должны переопределить отрисовку неклиентской области окна:

procedure TElTabSheet.WMNCPaint(var Message: TMessage);
var
  RC,
  R1,
  R2,
  RW : TRect;
  DC : HDC;
begin
  if not IsThemeApplied then
  begin
    Inherited;
  end
  else
  begin
    // попробуем получить контекст, ограниченный областью отсечения
    DC := GetDCEx(Handle, HRGN(Msg.wParam), DCX_WINDOW or DCX_INTERSECTRGN);    
    if DC = 0 then 
    begin
      // не получилось. возьмём весь контекст.      
      DC := GetWindowDC(Handle);    
    end;
    // получили клиентскую область в координатах клиентской области    
    Windows.GetClientRect(Handle, RC);
    // получили область окна в координатах экрана
    GetWindowRect(Handle, RW);
    // получили область окна в координатах клиентской области
    MapWindowPoints(0, Handle, RW, 2);
    // получили клиентскую область в координатах неклиентской области    
    OffsetRect(RC, -RW.Left, -RW.Top);
    // вырезали из контекста клиентскую область - там не рисуем
    ExcludeClipRect(DC, RC.Left, RC.Top, RC.Right, RC.Bottom);
    // получили неклиентскую область в координатах клиентской области    
    OffsetRect(RW, -RW.Left, -RW.Top);
    R2 := RW;

    // пропущена специфика TElTabSheet
    if IsThemeBackgroundPartiallyTransparent(TabTheme, DC, TABP_PANE, 0) then
    begin
        DrawParentThemeBackground(Handle, DC, RW);
    end;

    DrawThemeBackground(TabTheme, DC, TABP_PANE, 0, RW, @R2);
    ReleaseDC(Handle, DC);
  end;
end;

Отрисовка клиентской части окна также должна быть изменена таким образом, чтобы рисовать только ту часть "заполнителя", которая должна быть видна в клиентской области. Мы могли бы использовать алгоритм, дополняющий вышеприведённый (вычисления через GetWindowRect, MapWindowPoints и GetClientRect) с точностью до наоборот, но здесь показан ещё один вариант - повторное использование GetThemeBackgroundContentRect:

procedure TElTabSheet.Paint;
var R, Rect,
    R1     : TRect;
    R2     : TRect;
    ACtl   : TWinControl;
    BgRect : TRect;

begin
  R := ClientRect;
  if IsThemeApplied then
  begin
    R2 := BoundsRect;
    OffsetRect(R2, -R2.Left, -R2.Top);
    GetThemeBackgroundContentRect(TabTheme, Canvas.Handle, TABP_PANE, 0, R2, R1);

    R2.Left := - R1.Left;
    R2.Top := - R1.Top;
    R2.Right := R.Right + R2.Right - R1.Right - R2.Left + R1.Left;
    R2.Bottom := R.Bottom + R2.Bottom - R1.Bottom - R2.Top + R1.Top;

    R1 := Canvas.ClipRect;

    DrawThemeBackground(TabTheme, Canvas.Handle, TABP_PANE, 0, R2, @R1);
    exit;               
  end;

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

Кроме того, в случае прозрачности/полупрозрачности элемента-заполнителя нам нужно отрисовывать ту часть окна-владельца данного органа управления, которая перекрывается нашим органом управления.

Проверить, является какой-либо элемент темы в определённом состоянии прозрачным или полупрозрачным, можно с помощью метода IsThemeBackgroundPartiallyTransparent. Произвести отрисовку той части окна-владельца, что перекрывается нашим компонентом, можно с помощью метода DrawThemeParentBackground.

Вышеуказанные действия имеет смысл вынести в обработчик сообщения WM_ERASEBKGND (учтите, что подобную работу необходимо выполнять и в обработчике WM_NCPAINT для неклиентской области). Кстати, рисование элемента-заполнителя также можно вынести в этот обработчик.

{$ifndef CLX_USED}
procedure TElXPThemedControl.WMEraseBkgnd(var Message: TWMEraseBkgnd);
var
  RC: TRect;
  RW: TRect;
begin
  {$ifdef VCL_4_USED}
  if IsThemeApplied() then
  begin
    RC := ClientRect;
    GetThemeBackgroundExtent(Theme, Message.DC, 0, 0, RC, RW);
    if IsThemeBackgroundPartiallyTransparent(Theme, 0, 0) then
    begin
      DrawThemeParentBackground(Handle, Message.DC, RC);
    end;
    DrawThemeBackground(Handle, Message.DC, 0, 0, RW, @RC);
    Message.Result := 1;
  end
  else
  {$endif}
  begin
    Inherited;
  end;
end;
{$endif}

Для некоторых органов управления отрисовка фона с помощью элемента-заполнителя не является необходимым шагом (например, для CheckBox в режиме Transparent). В таких случаях этот обработчик необходимо переопределить.

Вышеприведённый исходный текст можно улучшить, чтобы производить отрисовку элемента-заполнителя согласно текущему состоянию органа управления. Так, в случае, если окно запрещено (not Enabled), и определена отрисовка для запрещённого состояния (IsThemePartDefined), отрисовку надо производить с использованием состояния "Запрещено" (код этого состояния для большинства элементов равен 4).

В наборе компонент ElPack используется иной подход. Обработчик WM_ERASEBKGND определён в виде пустышки, а вся отрисовка сосредоточена в обработчиках сообщений WM_NCPAINT и WM_PAINT. Кроме того, отрисовка происходит не напрямую в целевой дисплейный контекст, а с использованием буферизации через промежуточный дисплейный контекст в памяти (TBitmap, PixelFormat := pfDevice). Это позволяет избежать эффекта мерцания изображения.

Visual Styles API также предоставляет вызов HitTestThemeBackground для уточнения принадлежности точки к элементу класса окна темы. Возвращаемое значение инадлежит подмножеству значений, возвращаемых обработчиком события WM_NCHITTEST.

Менеджер тем

Всему рабочему столу операционной системы может быть назначена одна глобальная тема, исходя из предпочтений пользователя, работающего в данной сессии (global theme). Проверить, назначена ли глобальная тема, мы можем с помощью вызова IsThemeActive. Если глобальная тема не назначена, то визуальный стиль также не определён, и прорисовку органа управления Вам необходимо выполнять без использования Visual Styles API.

Каждое из запущенных приложений может как поддерживать темы, так и не поддерживать их (в той или иной степени). Таким образом, существует понятие темы, назначенной приложению (application theme). Проверить, назначена ли тема приложению, мы можем с помощью вызова IsAppThemed.

Более того, мы можем определять тему отдельно для каждого конкретного окна (органа управления). Фактически, орган управления имеет дело только с этой темой (control theme). При наличии глобальной темы Вы всегда имеете возможность работать с темами, даже если для приложения в целом тема не назначена. Общие органы управления всегда ориентируются на то, определена ли тема для приложения, но в своих органах управления я использую визуальные стили даже в случае отсутствия темы, назначенной приложению.

Тему окна можно изменить с помощью вызова SetWindowTheme. Так, если Вы хотите запретить использование тем для окна, заданного hwnd, то Вы можете написать нечто вроде

SetWindowTheme (hwnd, ' ', ' ');

А в случае, если Вам надо восстановить поведение по умолчанию, достаточно вызвать

SetWindowTheme (hwnd, nil, nil);

Каждый раз, когда менеджеру тем требуется изменить тему для Вашего компонента, он присылает компоненту сообщение WM_THEMECHANGED.

Таким образом, при получении данного сообщения наш орган управления должен освободить используемые хэндлы тем, а затем запросить новые хэндлы тем. Кроме того, поскольку смена темы обычно означает и смену пропорций элементов органа управления, в частности, расположения клиентской области окна, то необходимо уведомить компонент о необходимости перерасчёта расположения клиентской области.

Здесь я покажу полезные детали одного из базовых классов ElPack, который реализует большую часть требуемой нам функциональности (этот компонент совместим как с VCL, так и с CLX):

type
  TElXPThemedControl = class(TCustomControl)
  private
    FUseXPThemes: Boolean;
    FTheme: HTheme;
  protected
    procedure SetUseXPThemes(const Value: Boolean); virtual;
    function GetThemedClassName: WideString; virtual; abstract;
    // пропустим
{$ifdef MSWINDOWS}
{$ifndef CLX_USED}
    procedure WMThemeChanged(var Message: TMessage); message WM_THEMECHANGED;
    // пропустим
{$endif}
    procedure FreeThemeHandle; dynamic;
    procedure CreateThemeHandle; dynamic;
{$endif}
    property UseXPThemes: Boolean read FUseXPThemes write SetUseXPThemes default True;
  public
    constructor Create(AOwner : TComponent); override;
    function IsThemeApplied: Boolean;
    property Theme: HTheme read FTheme;
  end;


constructor TElXPThemedControl.Create(AOwner: TComponent);
begin
  inherited;
  FUseXPThemes := True;
end;


procedure TElXPThemedControl.CreateThemeHandle;
begin
  if ThemesAvailable then
    {$ifndef CLX_USED}
    FTheme := OpenThemeData(Handle, PWideChar(GetThemedClassName()))
    {$else}
    {$ifdef MSWINDOWS}
    FTheme := OpenThemeData(QWidget_winID(Handle), PWideChar(GetThemedClassName()))
    {$endif}
    {$endif}
  else
    FTheme := 0;
end;


procedure TElXPThemedControl.FreeThemeHandle;
begin
  {$ifdef MSWINDOWS}
  if ThemesAvailable then
    CloseThemeData(FTheme);
  {$endif}
  FTheme := 0;
end;

function TElXPThemedControl.IsThemeApplied: Boolean;
begin
  Result := UseXPThemes and (FTheme <> 0);
end;


procedure TElXPThemedControl.SetUseXPThemes(const Value: Boolean);
begin
  if FUseXPThemes <> Value then
  begin
    FUseXPThemes := Value;
    {$ifdef MSWINDOWS}
    if ThemesAvailable and HandleAllocated then
    begin
      if FUseXPThemes then
      begin
        CreateThemeHandle;
      end
      else
      begin
        FreeThemeHandle;
      end;
    end;
    {$endif}
  end;
end;

procedure TElXPThemedControl.WMThemeChanged(var Message: TMessage);
begin
  if ThemesAvailable and UseXPThemes then
  begin
    FreeThemeHandle;
    CreateThemeHandle;
    SetWindowPos(
      Handle,
      0,
      0, 0, 0, 0,
      SWP_FRAMECHANGED or SWP_NOMOVE or SWP_NOSIZE or SWP_NOZORDER
      );
    RedrawWindow(Handle, nil, 0, RDW_FRAME or RDW_INVALIDATE or RDW_ERASE);
  end;
  Message.Result := 1;
end;

Думаю, Вам не составит труда расширить этот компонент самостоятельно, добавив обработчики WM_NCCREATE и WM_NCDESTROY. Свойство UseXPThemes позволяет контролировать использование тем со стороны приложения.

Вышеприведённой информации вполне достаточно, чтобы начать корректно использовать Visual Styles API в своих продуктах.

Особенности

API низкого уровня

Кроме вышеприведённых высокоуровневых вызовов, Visual Styles API предоставляет набор API низкого уровня. С их помощью можно получить различную информацию о свойствах (таких, как размеры, положение, цвет и т.п.), используемых визуальным стилем для прорисовки того или иного элемента темы. Некоторые методы API нижнего уровня корректно работают даже в случае отсутствия глобального визуального стиля, возвращая необходимую системную информацию.

К сожалению, структура этого уровня API была реализована недостаточно хорошо. Так в стандартной теме для элемента GroupBox работает вызов

GetThemeColor(Theme, BP_GROUPBOX, 0, TMT_TEXTCOLOR, AColor);

В то время как для элемента CheckBox такой вызов работать не будет.

Обрамление органа управления

Visual Styles API предоставляет нам метод DrawThemeEdge для отрисовки обрамлений аналогично методу DrawEdge. К сожалению, фактически его отрисовка никак не вписывается в тематическое оформление.

Я рекомендую Вам для отрисовки обрамления органа управления, реализующего ввод данных, использовать элемент-заполнитель класса "edit" в необходимом состоянии (normal или disabled).

Отрисовка нестандартных элементов

Конечно, Visual Styles API не имеет никакого представления о нестандартных органах управления. В эту категорию попали и такие уже обычные для нас органы управления, как Outlook Bar и Splitter.

В этом случае программисту приходится самостоятельно подбирать нужное ассорти с использованием элементов оформления стандартных органов управления.

Например, так как сплиттер сам по себе вполне может быть отнесён к той же категории оформления, что и рамка окна, егоотрисовку можно выполнять с использованием элемента WP_FRAME класса "window". Таким образом реализуется сплиттер в Windows Media Player 8.

С другой стороны, некоторые авторы могут предпочесть отрисовку сплиттера с использованием элемента BP_PUSHBUTTON класса "button", как более привычного.

Возможность подобных разночтений я отношу к одной из самых больших проблем существующего Visual Styles API. Более того, это означает сырость данной реализации. Возможно, в будущем Microsoft каким-либо образом сможет исправить эту ситуацию.

Кроме того, предположим, что мы решили на практически стандартный по оформлению орган управления (window или scrollbar) повесить ещё одну дополнительную кнопку. Здесь нас подстерегает ещё один большой недостаток существующего Visual Styles API.

Дело в том, что Visual Styles API разбивает органы управления на большие плоские элементы. Невозможно нарисовать на полосе прокрутки просто фон для типичной кнопки, дополнив его потом каким-либо своим изображением над этим фоном, так как возможно нарисовать только кнопку целиком (и все они уже включают в себя некое предопределённое изображение на поверхности кнопки). В таких случаях нам приходилось идти на компромиссы, которые ещё неизвестно как скажутся на наших приложениях на новых темах от Microsoft. Так, фон для кнопки на полосе прокрутки мы рисовали, как thumb button. Если вдруг в новой теме от Microsoft thumb button окажется вдруг весьма сложным по художественному замыслу элементом, то наш компромисс пойдёт коту под хвост.

Эту непредусмотрительность создателей Visual Styles API я также отношу к сырости продукта. Да, идея хороша. Но реализация менеджера тем, по моему скромному мнению, была проведена некомпетентной командой и в сжатые сроки.

Динамические библиотеки

Как уже упоминалось выше, новое поколение операционных систем Microsoft, начиная с Windows XP, реализуют новый способ загрузки динамических библиотек, основанный на использовании манифестов. Например, теперь основное приложение может использовать новую библиотеку общих органов управления, а дополнительные модули - старую. Подобное поведение стало возможным благодаря появлению концепции политики изоляции версий модулей.

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

При статическом импорте динамической библиотеки используется манифест с кодом ISOLATIONAWARE_MANIFEST_RESOURCE_ID. При загрузке динамической библиотеки через вызов LoadLibrary(Ex) (динамический импорт библиотеки) используется манифест с кодом ISOLATIONAWARE_NOSTATICIMPORT_MANIFEST_RESOURCE_ID.

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

Кроме того, для модуля, совместимого с политикой изоляции операционной системы, переопределено целое подмножество API, например, таких, как IsolationAwareImageList_Create. Для того, чтобы эти API были переопределены, необходимо (для C/C++) перед включением windows.h определить константу препроцессора:

#define ISOLATION_AWARE_ENABLED 1

Дополнительные материалы

Ссылки

Большая часть информации доступна в MSDN Online.

Кроме того, заглядывайте изредка на мой уголок разработчика.

В случае обнаружения неточностей и ошибок в статье пишите мне.

Благодарности

Данная статья оказалась бы невозможной без участия сообщества разработчиков SWRUS.

Особое спасибо

Примечания
  • Важно отметить, что формат данных манифеста в Whistler RC1 несовместим с форматом данных манифеста Windows XP Release. В этой статье я ориентируюсь на Windows XP и последующие ОС. Алексей Попов, автор Ghost Installer, разработал файл манифеста, совместимый как с Whistler RC1, RC2, так и с Windows XP.
  • Если Вы сами формируете манифест для включения в ресурсы одного из Ваших модулей, то не используйте внутри XML-документа переводы каретки. Манифест должен располагаться на одной строке.
  • Элемент находится в "горячем" состоянии, когда он получает сообщения WM_MOUSEMOVE (курсор мыши находится над ним, и сообщения от мыши не захватываются любым другим окном).

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