программирование :: разное

Chameleon Clock своими руками

Лирическое вступление
Многие видели довольно симпатичную программку Chameleon Clock. В свое время она мне сильно понравилась. Стало интересно, как подобную вещь можно сделать. В процессе изысканий пришлось вплотную столкнуться с программированием при помощи API Windows, и, замечу, полученные знания позволили мне в дальнейшем более уверенно себя чувствовать при программировании под эту ОС. Конечно, для создания полнофункциональной копии Chameleon Clock запала мне не хватило, но часики для таскбара с использованием скинов WinAmp сделать удалось.



Рис. 1. Вот такие вот часики.

Начнем с нуля
В начале любого проекта необходимо выбрать инструментарий, при помощи которого будет создаваться очередной шедевр. Первоначально данную программу я делал на Visual C++ 6, но вам представляю вариант, сделанный полностью на Delphi. Многие не очень опытные программисты думают, что на Delphi можно только интерфейсы писать, а ведь на нем можно програмировать все что угодно. Но для начала нужно определиться, что именно нам нужно сделать. А сделать нам нужно вот что: внедриться в процесс таскбара с помощью ловушки (Hook) и переопределить (сабклассировать) оконную функцию часиков на таскбаре таким образом, чтобы они рисовали то, что нам нужно. Ну, а нужны нам часики, нарисованные с помощью скинов WinAmp.

Ловушки и переопределение оконной функции

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

function SetWindowsHookEx(idHook: Integer; lpfn: TFNHookProc; hmod: HINST; dwThreadId: DWORD): HHOOK;

Данная функция устанавливает ловушку указанного типа (idHook) указанному потоку dwThreadId (если ноль, то всем запущенным потокам в системе), возвращает указатель на установленную ловушку, а если установить ловушку не удалось, возвращает ноль. Вот основные типы ловушек:
1.   WH_CALLWNDPROC — ловушка данного типа будет получать на обработку все сообщения, которые посылаются оконным функциям указанного потока до их отправки самой оконной функции.
2.   WH_CALLWNDPROCRET — данная ловушка получит на обработку сообщения, которые уже прошли обработку оконными функциями указанного потока.
3.   WH_CBT — служит для отслеживания следующих сообщений об активации, создании, уничтожении, получении фокуса ввода окном указанного потока.
4.   WH_DEBUG — необходима для отладки ранее установленных ловушек.
5.   WH_GETMESSAGE — отслеживает все сообщения из очереди сообщений для окон указанного потока.
6.   WH_KEYBOARD — предназначена для отслеживания нажатых клавиш активного окна указанного потока (используется, кстати, для написания клавиатурных шпионов=)).
7.   WH_MOUSE — как и предыдущая, только для мыши.

hmod — идентификатор модуля (dll), в котором находится функция-ловушка.
Lpfn — указатель на функцию-ловушку, которая и будет получать указанные в idHook сообщения. Функция имеет следующий вид:

function (code: Integer; wparam: WPARAM; lparam: LPARAM): LRESULT;

где code — тип отловленного события, а wparam и lparam содержат передаваемые ловушке данные. Значения этих параметров зависят от типа ловушки. Более подробное описание этих функций смотрите в MSDN.

function UnhookWindowsHookEx(hhk: HHOOK): BOOL;
Уничтожает указанную (hhk) ловушку. Если операция удалась, возвращает TRUE, если нет — FALSE.

Главное условие — функция — обработчик ловушки должна находиться в dll. Это связано с архитектурой Windows, где каждый процесс имеет свое изолированное адресное пространство. Это значит, что процессы не могут «видеть» данные и функции других процессов. Тут на помощь приходят динамически подключаемые библитеки (dll). При запросе установить ловушку dll, содержащая функцию-ловушку, будет пристыкована к нужным процессам. Получится, что наша функция внедрится в адресное пространство чужого процесса, что дает умелым программистам море возможностей. Вот тут настает подходящий момент рассказать об очень интересной возможности переопределения оконной функции из своего процесса (в 16-разрядной Windows можно было переопределить оконную функцию любого окна любого процесса, в 32-разрядной Windows это стало невозможным из-за разделения адресного пространства). Для этого используется функция:

function SetWindowLong(hWnd: HWND; nIndex: Integer; dwNewLong: Longint): Longint;

где hWnd — указатель окна, с которым будет работать функция, nIndex — идентификатор действия, которое необходимо выполнить над данным окном — в данный момент это будет GWL_WNDPROC — переопределение оконной функции. dwNewLong — новое значение. В нашем случае это будет указатель на новую оконную процедуру. В случае успеха данная функция возвращает указатель на старое значение — в нашем случае — на старую оконную процедуру. Этот указатель необходимо сохранить для того, чтобы можно было с помощью старой функции выполнять запросы системы, которые мы не хотим переопределять. После можно будет вернуть старую функцию на место — как будто никто ее и не переопределял.

Пишем простейшую программу на API


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

program TrayClock;

uses
Windows,SysUtils,Messages;
{$R *.res}
const
   TrayClock_dll = 'TrayClock.dll';

var
   mainWnd : HWND;//Указатель на окно программы
   wc : TWndClass; // Описание класса окна
   msgMain : TMsg;
   ExeName : string;
   ExeDir : string;
   SkinName_ : PChar;
   SkinDir_ : PChar;

function SetClockOnTray (h_wnd : HWND; nAction : BOOL; SkinDir : PChar; StartSkin : PChar) : BOOL; stdcall; external TrayClock_dll;
procedure RepaintClock(); stdcall; external TrayClock_dll;
function KillHooks(h_wnd : HWND):LRESULT; stdcall; external TrayClock_dll;

function TrayClockWndProc(HWindow: HWnd; Message, WParam: Longint; LParam: Longint): Longint;stdcall;
begin
   case Message of
      WM_TIMER:
         RepaintClock();
      WM_ENDSESSION,
      WM_CLOSE,
      WM_DESTROY,
      WM_USER + 100:
      begin
         KillTimer(mainWnd,100);
         KillHooks(mainWnd);
         PostQuitMessage(0);
         Halt;
      end;
   end;
   Result := DefWindowProc(HWindow, Message, WParam, LParam);
end;

function WeAreAlone (szName : string):boolean;
var
   hMutex : THandle;
begin
   Result := true;
   hMutex := CreateMutex (nil, true, PChar(szName));
   if GetLastError() = ERROR_ALREADY_EXISTS then
   begin
      CloseHandle(hMutex);
      Result := false;
   end;
end;

begin
   if not WeAreAlone('TrayClock-Odyssey-Soft') then
      Halt;
   SetLength(ExeName,MAX_PATH);
   GetModuleFileName(hInstance,PChar(ExeName),MAX_PATH);
   SetLength(ExeName,StrLen(PChar(ExeName)));
   ExeDir := ExtractFileDir(ExeName);
   GetMem(SkinName_,20);
   GetMem(SkinDir_,Length(ExeDir) + 20);
   FillChar(SkinDir_^,Length(ExeDir) + 20,0);
   FillChar(SkinName_^,20,0);
   lstrcpy(SkinName_,'BambooAmp');
   lstrcpy(SkinDir_,PChar(ExeDir + '\skins\'));

   wc.lpszMenuName := nil;
   wc.lpszClassName := 'TrayClockWnd';
   wc.hInstance := hInstance;
   wc.hIcon := 0;
   wc.hCursor := LoadCursor(0, IDC_ARROW);
   wc.hbrBackground := CreateSolidBrush(RGB(255,255,255));
   wc.style := 0;
   wc.lpfnWndProc := @TrayClockWndProc;
   wc.cbClsExtra := 0;
   wc.cbWndExtra := 0;

   if Windows.RegisterClass(wc) = 0 then
      Exit;

   mainWnd := CreateWindow( 'TrayClockWnd', 'TrayClock', 0, 0,0, 100, 100, 0, 0, hInstance, nil);
   if mainWnd = 0 then
      Exit;
   SetTimer(mainWnd,100,250,nil);

   SetClockOnTray(mainWnd,TRUE,SkinDir_,SkinName_);

   FreeMem(SkinDir_);
   FreeMem(SkinName_);

   while GetMessage(msgMain, mainWnd, 0, 0) do
   begin
      TranslateMessage(msgMain);
      DispatchMessage(msgMain);
   end;
end.

Довольно немного, правда? Теперь по некоторым ключевым моментам. Так как запуск нескольких копий часов может привести к забавным глюкам, необходима проверка на повторный запуск. Я предпочитаю использовать семафоры. См. функцию WeAreAlone. В ней пытаемся создать новый семафор (mutex) при помощи функции CreateMutex. Если такой семафор уже зарегистрирован в системе, значит, мы загружаемся второй раз и просто тихо-мирно выходим из программы. Данная функция пригодна к использованию в любой вашей программе. Только для каждой программы меняйте название семафора — параметр szName. Function fff();external ‘some.dll’; — это описание функции fff, которая находится в библиотеке some.dll, причем при запуске программы данная библиотека будет атоматически подгружена в память. Поэтому, если хотите запустить эту программу без указанной библиотеки, все описания и вызовы внешних функций необходимо поместить в коментарии. function TrayClockWndProc(HWindow: HWnd; Message, WParam: Longint; LParam: Longint): Longint; — это стандартный вид оконной функции (функция, в которой обрабатываются все сообщения, приходящие окну. Эта функция собственно и реализует всю функциональность окна в Windows). Она может иметь другое название, но типы параметров должны быть именно такими: HWindow — указатель на окно, для которого обрабатывается сообщение, Message — тип сообщения, полученного окном, WParam, LParam — параметры, переданные для обработки сообщения. DefWindowProc(HWindow, Message, WParam, LParam); — это функция Windows, которая обрабатывает стандартные сообщения для указанного окна. Т.е. делает самое основное: перемещение, изменение размера и обработку системных кнопок окна. Остальное нужно писать самим=). RegisterClass — регистрация класса вашего окна в системе. Именно в классе окна хранится указатель на оконную функцию! CreateWindow — создание окна. Ну и самое главное — цикл обработки полученных системных сообщений для нашего окна.
   while GetMessage(msgMain, mainWnd, 0, 0) do
   begin
      TranslateMessage(msgMain);
      DispatchMessage(msgMain);
   end;
Более подробное описание этих функций можно найти в MSDN.

Пишем библиотеку с ловушкой

К сожалению, полный текст библитеки занимает больше полутора тысяч строк, поэтому напечатать его целиком нет возможности (полный текст програмы на Delphi и VC++ можно найти на моем сайте http://odyssey.h12.ru, поэтому распишу только основные моменты.
library TrayClock;
uses Windows,SysUtils,Messages;
type
   ClockSize = array[1..2] of WORD;
   PClockSize = ^ClockSize;

var
   SaveDllProc: TDllProc;
   PrevTime : array[1..6] of byte;
   CurrTime : array[1..6] of byte;

procedure SubclassTray();
begin
   if Assigned(OldTrayClockWndProc) or
    Assigned(OldTrayNotifyWndProc) then
      Exit;

   OldTrayClockWndProc := TFNWndProc(SetWindowLong( hTrayClockWnd,    WL_WNDPROC, LongInt(Addr(NewTrayClockWindowProc))));
   OldTrayNotifyWndProc := TFNWndProc(SetWindowLong( hTrayNotifyWnd,    WL_WNDPROC, LongInt(Addr(NewTrayNotifyWindowProc))));
end;
procedure UnSubclassTray;
begin
   if (OldTrayClockWndProc <> nil) and (OldTrayNotifyWndProc <> nil) then
   begin
      SetWindowLong( hTrayClockWnd, GWL_WNDPROC, LongInt(OldTrayClockWndProc) );
      OldTrayClockWndProc := nil;
      SetWindowLong( hTrayNotifyWnd, GWL_WNDPROC, LongInt(OldTrayNotifyWndProc) );
      OldTrayNotifyWndProc := nil;
      DeleteSkin();
      DestroyOdyssClockMenu();
   end;
end;

function CallWndProc( code : integer; wp : WPARAM; lp : LPARAM ) : LRESULT; stdcall;
var
   mes : PCWPSTRUCT;
begin
   if code = HC_ACTION then
   begin
      mes := PCWPSTRUCT(lp);
      if mes^.message = SubclassMessage then
         SubclassTray();
   end;
   Result := CallNextHookEx( hhookCallWndProc, code, wp, lp );
end;

function SetClockOnTray(h_wnd : HWND; nAction : BOOL; SkinDir : PChar; StartSkin : PChar) : BOOL; stdcall;
var
   sd : _SkinData;
   cps : COPYDATASTRUCT;
begin
   if nAction then
   begin
      hOdyssClockWnd := h_wnd;
      hhookCallWndProc := SetWindowsHookEx( WH_CALLWNDPROC, @CallWndProc, hInstance, GetWindowThreadProcessId(hTrayWnd, nil));
      SendMessage(hTrayWnd,SubclassMessage,0,0);

      cps.cbData := sizeof(sd);
      cps.dwData := 0;
      cps.lpData := @sd;
      lstrcpy(PChar(@sd.DirName[1]),SkinDir);
      lstrcpy(PChar(@sd.SkinName[1]),StartSkin);
      sd.hOdyssClockWnd := hOdyssClockWnd;
      Result := (SendMessage(hTrayClockWnd, WM_COPYDATA, h_wnd, LongInt(@cps)) = 1);

      PostMessage(hTrayWnd,WM_EXITSIZEMOVE,0,0);
      PostMessage(hTrayWnd,WM_SIZE,0,0);
   end
   else
   begin
      DestroyOdyssClockMenu();
      SendMessage(hTrayClockWnd,UnsubclassMessage,0,0);
      UnhookWindowsHookEx( hhookCallWndProc );
      PostMessage(hTrayWnd,WM_EXITSIZEMOVE,0,0);
      PostMessage(hTrayWnd,WM_SIZE,0,0);
      hOdyssClockWnd := 0;

      Result := TRUE;
   end;
end;

function NewTrayClockWindowProc( h_wnd : HWND; msg : UINT; wp : WPARAM; lp : LPARAM ) : LRESULT; stdcall;
begin
   Result := 0;
   if msg = PaintClockMessage then
   begin
      RecalcCurrentTime();
      if(CurrTime[1] = PrevTime[1]) and
       (CurrTime[2] = PrevTime[2]) and
       (CurrTime[3] = PrevTime[3]) and
       (CurrTime[4] = PrevTime[4]) and
       (CurrTime[5] = PrevTime[5]) and
       (CurrTime[6] = PrevTime[6]) then
      Exit;
      hprgn := CreateOdyssClockPaintRgn;
      InvalidateRgn(h_wnd,hprgn,not IsWinXP and TransparentClock);
      PaintClock(hTrayClockWnd,FALSE);
      DeleteObject(hprgn);
      Exit;
   end;

   if msg = UnsubclassMessage then
   begin
      UnSubclassTray();
      DeleteSkin();
      InvalidateRect(h_wnd,nil,TRUE);
      Exit;
   end;

   case msg of
   ...
   WM_USER+100:
      if SkinIsExists then
      begin
         if CallWindowProc( OldTrayClockWndProc, h_wnd, msg, wp, lp ) <> 0 then
         begin
            CSize := PClockSize(@Result);
            CSize^[1] := GetOdyssClockWidth();
            CSize^[2] := GetOdyssClockHeight();
            Exit;
         end
         else
            Exit;
      end;
   WM_TIMER:
      if SkinIsExists then
         Exit;
   ...
   WM_PAINT:
      if SkinIsExists then
      begin
         PaintClock(h_wnd,TRUE);
         Exit;
      end;
   end;
Result := CallWindowProc(OldTrayClockWndProc, h_wnd, msg, wp, lp );
end;

procedure LibExit(Reason: Integer);
begin
   if Reason = DLL_PROCESS_DETACH then
   begin
      SetClockOnTray(0,FALSE,nil,nil);
   end;
   if Assigned(SaveDllProc) then
      SaveDllProc(Reason); // call saved entry point procedure
end;

//------------------------------------------------------------------------------

exports
   SetClockOnTray name 'SetClockOnTray',
   KillHooks name 'KillHooks';
begin
   ClockFlicker := 3;
   PrevFlickerSec := -1;
   InitClockParams(nil);
   SaveDllProc := DllProc; // save exit procedure chain
   DllProc := @LibExit; // install LibExit exit procedure
end.

SubclassTray переопределяет оконную функцию часов таскбара и сохраняет указатель на старую фукцию. UnSubclassTray соответственно возращает старый указатель на место. Вызывается при выходе из программы, чтобы Explorer не умер=). CallWndProc — это функция-ловушка для обработки сообщений, посылаемых в оконную процедуру часов. Используется она для перехвата нашего сообщения — SubclassMessage — и вызова функции SubclassTray. SetClockOnTray — функция, вызываемая из нашей программы для установки ловушки. NewTrayClockWindowProc — новая оконная функция часов таскбара. В ней 5 ключевых моментов:
·   Обработка сообщения PaintClockMessage. Это сообщение присылается из основной программы для отрисовки окна каждую четверть секунды. Программа сравнивает предыдущее значение времени с текущим. Если оно изменилось, идет перерисовка часов. Для вызова события WM_PAINT для окна часов используется функция InvalidateRgn. Причем до вызова этой функции создается описание области окна, которая и будет собственно перерисована. Например, изменилась только одна цифра — зачем перерисовывать все окно? Ведь нужно перерисовать только одну цифру! Тут на помощь и приходит функция InvalidateRgn. Все ее действие сводится к тому, что она помечает указанную область окна как «недействительную» и дает команду окну перерисовать эту область.
·   Обработка сообщения UnsubclassMessage. Это сообщение присылается из нашей программы как сигнал о том, что она завершает работу, и необходимо вернуть старую оконную функцию на место. Что и делает функция UnSubclassTray.
·   Обработка сообщения WM_USER+100. Это сообщение окну часов присылает таскбар. Обратно возвращается двойное слово (LRESULT), первая часть которого содержит ширину, вторая — высоту. Данное сообщение было вычислено путем долгих экспериментов. Без его обработки нормальная работа часов невозможна!
·   Обработка сообщения WM_TIMER. Это сообщение от таймера родных часов таскбара. Его нужно убить, чтобы не путался под нагами, пока работает наша программа.
·   Обработка сообщения WM_PAINT. Это сообщение сигнализирует, что пришла пора перерисовать часы. Что мы и делаем=).

Рисуем часы


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


Рис. 2. Скин больших цифр.


Рис. 3. Скин букв и спецсимволов.

Как видите, ничего сложного: все остальные скины имеют аналогичный размер и порядок следования символов. Т.е., имея картинку скина в памяти, мы можем высчитать размер каждого символа и его координаты. Вот текст функции, загружающей в память новый скин:

function SetSkin(SkinName : LPSTR) : BOOL;
var
   _hbmNumbers : HBITMAP;
   _hbmSymbols : HBITMAP;
bm : BITMAP;
   dFile : PChar;
   sFile : PChar;
   sDir : PChar;
   NameFile1 : array[1..MAX_PATH] of char;
   NameFile2 : array[1..MAX_PATH] of char;
begin
   Result := FALSE;
   if SkinName = nil then
      Exit;

   dFile := '\Numbers.bmp';
   sFile := '\Text.bmp';
   sDir := '\';

   lstrcpy(PChar(@NameFile1[1]),PChar(@SkinsDir[1]));
   lstrcat(PChar(@NameFile1[1]),sDir);
   lstrcat(PChar(@NameFile1[1]),SkinName);
   lstrcat(PChar(@NameFile1[1]),dFile);

   lstrcpy(PChar(@NameFile2[1]),PChar(@SkinsDir[1]));
   lstrcat(PChar(@NameFile2[1]),sDir);
   lstrcat(PChar(@NameFile2[1]),SkinName);
   lstrcat(PChar(@NameFile2[1]),sFile);

   _hbmNumbers := LoadImage(0,PChar(@NameFile1[1]), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);

   if GetObject(_hbmNumbers,sizeof(BITMAP),@bm) <> 0 then
   begin
      NumbersWidth := Trunc(bm.bmWidth/11);
      NumbersHeight := bm.bmHeight;
      bmDigiWidth := bm.bmWidth;
      bmDigiHeight := bm.bmHeight;
   end;
   _hbmSymbols := LoadImage(0, PChar(@NameFile2[1]), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
   
   if GetObject(_hbmSymbols,sizeof(BITMAP),@bm) <> 0 then
   begin
      SymbolsWidth := Trunc(bm.bmWidth/30);
      SymbolsHeight := Trunc(bm.bmHeight/3);
      bmSymbolWidth := bm.bmWidth;
      bmSymbolHeight := bm.bmHeight;
   end;

   if(_hbmNumbers = 0) or (_hbmSymbols = 0) then//failed set skin
   begin
      if _hbmNumbers <> 0 then
      begin
         DeleteObject(_hbmNumbers);
      end;
      if hbmSymbols <> 0 then
      begin
         DeleteObject(_hbmSymbols);
      end;
      if lstrcmp(PChar(@CurrentSkin[1]),PChar(@SkinName[1])) = 0 then
            CurrentSkin[1] := #0;

      SkinIsExists := FALSE;
   end
   else
   begin
      DeleteSkin();
      hbmNumbers := _hbmNumbers;
      hbmSymbols := _hbmSymbols;
      lstrcpy(PChar(@CurrentSkin[1]),SkinName);
      SkinIsExists := TRUE;
   end;
   Result := SkinIsExists;
end;

А вот функция, которая уничтожает текущий скин:
procedure DeleteSkin;
begin
   if hbmNumbers <> 0 then
   begin
      DeleteObject(hbmNumbers);
      hbmNumbers := 0;
   end;
   if hbmSymbols <> 0 then
   begin
      DeleteObject(hbmSymbols);
      hbmSymbols := 0;
   end;
end;

Здесь в принципе все прозрачно. Единственное, хочу заострить ваше внимание на двух важных моментах:
·   LoadImage — работает правильно только в Windows 2000, XP, еще, возможно, и под NT 4, но я не проверял. Поэтому для того, чтобы программа работала под Windows 9X, нужно использовать другой метод загрузки битмапов в память.
·   Количество обьектов GDI Windows ограничено! Нужно очень тщательно следить за тем, чтобы все созданные вами обьекты GDI (это шрифты, битмапы, регионы, кисти, палитры) уничтожались, когда в них нет необходимости. Т.е. на каждый вызов, который создает обьект GDI, должен существовать вызов DeleteObject.
Функции, которые копируют символы со скинов на указанный HDC (это указатель на область экрана, который используется в графических функциях API):

procedure DrawClockSymbol(h_dc : HDC; hdcSymbBits : HDC; Symbol : char; leftPoint : integer; topPoint : integer);
var
   Point : TPoint;
begin
   if GetSymbolCoordinates(Char(Symbol),Point) then
   begin
      if not TransparentClock then
         BitBlt(h_dc,leftPoint,topPoint, SymbolsWidth, SymbolsHeight, hdcSymbBits, Point.x, Point.y, SRCCOPY)
      else
         TransparentBlt(h_dc,leftPoint,topPoint, SymbolsWidth, SymbolsHeight, hdcSymbBits, Point.x, Point.y, SymbolsWidth, SymbolsHeight, GetPixel(hdcSymbBits, bmSymbolWidth-1, bmSymbolHeight-1));
   end;
end;

procedure DrawClockDigit(h_dc : HDC; hdcDigiBits : HDC; Digit : byte; leftPoint : integer; topPoint : integer; hdcSymbBits : HDC);
begin
   if not TransparentClock then
      BitBlt(h_dc,leftPoint,topPoint, NumbersWidth,NumbersHeight, hdcDigiBits, Digit*NumbersWidth, 0, SRCCOPY)
   else
      TransparentBlt(h_dc,leftPoint,topPoint, NumbersWidth,NumbersHeight, hdcDigiBits, Digit*NumbersWidth, 0, NumbersWidth, NumbersHeight, GetPixel(hdcSymbBits, bmSymbolWidth-1, bmSymbolHeight-1))
end;

procedure DrawClockDots(h_dc : HDC; hdcDigiBits : HDC; leftPoint: integer; topPoint : integer; hdcSymbBits : HDC);
begin
   if not TransparentClock then
   begin
      BitBlt(h_dc, leftPoint + Trunc(NumbersWidth/2), topPoint + Trunc(NumbersHeight/3), Trunc(NumbersWidth/4),Trunc(NumbersWidth/4), hdcDigiBits,Trunc(NumbersWidth/3), 0, SRCCOPY);

      BitBlt(h_dc, leftPoint + Trunc(NumbersWidth/2), topPoint + Trunc(NumbersHeight*2/3), Trunc(NumbersWidth/4),Trunc(NumbersWidth/4), hdcDigiBits,Trunc(NumbersWidth/3),0, SRCCOPY);
   end
   else
   begin
      TransparentBlt(h_dc,leftPoint+Trunc(NumbersWidth/2), topPoint + Trunc(NumbersHeight/3), Trunc(NumbersWidth/4),Trunc(NumbersWidth/4), hdcDigiBits,Trunc(NumbersWidth/3), 0, Trunc(NumbersWidth/4), Trunc(NumbersWidth/4), GetPixel(hdcSymbBits, bmSymbolWidth-1, bmSymbolHeight-1));

      TransparentBlt(h_dc,leftPoint+Trunc(NumbersWidth/2), topPoint + Trunc(NumbersHeight*2/3), Trunc(NumbersWidth/4), Trunc(NumbersWidth/4), hdcDigiBits,Trunc(NumbersWidth/3), 0, Trunc(NumbersWidth/4), Trunc(NumbersWidth/4), GetPixel(hdcSymbBits, bmSymbolWidth-1, bmSymbolHeight-1));
   end;
end;

Как видите, используется всего две API-функции: TransparentBlt и BitBlt. Рассмотрим их более подробно.

function TransparentBlt(DestDC: HDC; X, Y, Width, Height: Integer; SrcDC: HDC; XSrc, YSrc: Integer; crTransparent: UINT): BOOL;
Функция предназначена для копирования изображения с одного устройства вывода на другое с использованием эффекта прозрачности (Transparent).
DestDC — устройство (указатель на контекст устройства — HDC), на которое будет скопировано изображение.
X, Y, Width, Height — координаты области устройства, куда будет вставлено изображение.
SrcDC — устройство, с которого будет скопировано изображение.
XSrc, YSrc — левая верхняя точка области исходного изображения, которая будет скопирована.
crTransparent — цвет пикселей, которые будут прозрачными (пиксели с таким цветом просто не будут скопированы).
function BitBlt(DestDC: HDC; X, Y, Width, Height: Integer; SrcDC: HDC;
XSrc, YSrc: Integer; Rop: DWORD): BOOL;
Параметры такие же, как и у предыдущей функции, кроме последнего, который определяет тип операции. В нашем случае это SRCCOPY — скопировать.
Теперь о самой функции, которая будет рисовать часы. Вот ее основной код:
procedure PaintClock(h_Wnd : HWND; fErase : BOOL);
var
   h_dc : HDC;
   hdcDigiBits : HDC;
   hdcSymbBits : HDC;
begin
   if not SkinIsExists or
    not IsWindowVisible(h_Wnd) or
    IsClockPaintNow or
    (h_Wnd = 0) then
      Exit;
   IsClockPaintNow := TRUE;
   ...
   h_dc := BeginPaint(hTrayClockWnd,ps);
   if h_dc <> 0 then
   begin
      hdcDigiBits := CreateCompatibleDC(h_dc);
      hdcSymbBits := CreateCompatibleDC(h_dc);

      hOldNumBitmap := SelectObject(hdcDigiBits, hbmNumbers);
      hOldSymbBitmap := SelectObject(hdcSymbBits, hbmSymbols);

      SetMapMode (hdcSymbBits, GetMapMode (h_dc));
      SetMapMode (hdcDigiBits, GetMapMode (h_dc));

      hNaturalColorBrush:=CreateSolidBrush(GetPixel(hdcSymbBits, bmSymbolWidth-1, bmSymbolHeight-1));

      if not TransparentClock then
         FillRect(h_dc, Rect, hNaturalColorBrush)
      else
      begin
         if IsWinXP then
         begin
            iSavedID := SaveDC(h_dc);
            if not TrayIsVertical() then
               SetWindowOrgEx(h_dc, GetWindowWidth(hTrayNotifyWnd)- GetWindowWidth(hTrayClockWnd),0,nil)
            else
               SetWindowOrgEx(h_dc, 0,GetWindowHeight(hTrayNotifyWnd) –GetWindowHeight(hTrayClockWnd),nil);
CallWindowProc( OldTrayNotifyWndProc, hTrayNotifyWnd, WM_PRINTCLIENT, LongInt(h_dc), PRF_CLIENT or PRF_ERASEBKGND);
RestoreDC (h_dc, iSavedID);
         end;
      end;
      …
      DeleteObject(hNaturalColorBrush);
      EndPaint(hTrayClockWnd,ps);
      SelectObject(hdcDigiBits, hOldNumBitmap);
      SelectObject(hdcSymbBits, hOldSymbBitmap);
      DeleteDC(hdcDigiBits);
      DeleteDC(hdcSymbBits);
   end;
   IsClockPaintNow := FALSE;
end;

Теперь пройдемся по ключевым моментам кода этой процедуры:
·   В начале процедуры проверяется флаг IsClockPaintNow, и, если он равен TRUE, выходим из процедуры. Если нет, устанавливаем его значение равным TRUE, а в конце процедуры — равным FALSE. Это на тот случай, если во время рисования процедура будет вызвана еще раз. Попытка работать с одними и теми же ресурсами чревата различными глюками — вплоть до падения приложения Explorer.
·   BeginPaint и EndPaint — используются для того, чтобы получить указатель на контекст окна и его последующего освобождения.
·   CreateCompatibleDC — создает совместимый контекст.
·   SelectObject — заменяет битмап контекста тем, который нам нужен. В данном случае мы создаем два совместимых контекста: в один кладем битмап с цифрами, в другой — с символами. Эти контексты нам необходимы для передачи в функции BitBlt и TransparentBlt.
CallWindowProc( OldTrayNotifyWndProc, hTrayNotifyWnd, WM_PRINTCLIENT, LongInt(h_dc), PRF_CLIENT or PRF_ERASEBKGND); А вот это главная «плюшка» при рисовании часов с прозрачным фоном в Windows XP. Суть ее вот в чем. Как нам узнать, какой сейчас скин в системе? Как его нарисовать? Это довольно нетривиальная задача. Лишняя работа, одним словом. В Windows XP/2000 есть замечательное сообщение для оконной функции WM_PRINTCLIENT. Это сообщение как раз таки и говорит окну нарисовать свой фон. И в качестве параметров оно просит указатель на уже открытый девайс окна и указания, что делать. Флаг PRF_CLIENT указывает на необходимость перерисовать клиентскую часть окна (в нормальном окне это все, кроме рамки и заголовка), а флаг PRF_ERASEBKGND — что перед перерисовкой нужно содержимое окна очистить. Лично мне для этого решения пришлось посидеть не один день и провести кучу экспериментов=). Причем заметьте: это сообщение посылаем не самим часам, а окну-родителю. Это важно. Так как окно часов эту функцию не отрабатывает!
·   SaveDC — сохранение состояния контекста. RestoreDC — его восстановление из сохраненного.
·   Так как бедное окошко-родитель рисует свой фон для окна часов, то заодно нарисует и все иконки, которые он содержит. Да-да: именно окно с иконками является родительским для часиков. Но мы тоже не лыком шиты и просто-напросто обманем это окошко. Для этого и используется функция SetWindowOrgEx. Она просто смещает начальные координаты переданного контекста окна туда, куда мы ее попросим. В нашем случае мы высчитываем, где заканчиваются иконки, и именно эту точку делаем начальной! Иконки будут нарисованы на часах — но мы их так и не увидим: они будут за видимой границей окна.

Ну вот, в принципе, все основные моменты, которые лично у меня вызывали определенные затруднения. Все остальное можно почерпнуть из MSDN либо взять полный исходник на http://www.odyssey.h12.ru.



Сергей Лавриеня (Odyssey) 2 февраля 2005 г., Минск

© компьютерная газета