Regiony
Zapewne nie każdemu podobają się kanciaste okienka, jakie domyślnie serwuje nam system. Przy odrobinie zachodu można zmieniać dowolnie ich kształt. Niektóre aplikacje (zwłaszcza wszelkie odtwarzacze multimediów) miewają różne fantazyjne kształty. Czy takie coś jest ładne – kwestia gustu. Na pewno jednak warto wiedzieć, jak się takie sztuczki robi.
Kluczem do tej "tajemnicy" są regiony. Jest to jedno z pojęć charakterystycznych dla WinAPI i mówiąc krótko, region wyznacza pewien kształt. Jeśli zdefiniujemy kształt, jaki sobie wymarzyliśmy, w postaci regionu, to możemy łatwo sprawić, by okno naszej aplikacji miało ten właśnie kształt zamiast standardowego prostokąta.
Na początek zajmijmy się tworzeniem regionów. Mamy tu niezły arsenał środków, trzeba przyznać. Oprócz podstawowej funkcji
CreateRectRgn, tworzącej region prostokątny, mamy jeszcze
CreateRoundRectRgn (zaokrąglony prostokąt),
CreateEllipticRgn (region w kształcie elipsy),
CreatePolygonRgn (dowolny wielokąt) oraz
CreatePolyPolygonRgn (region złożony z kilku wielokątów). Dla większej wygody stworzono też funkcje
CreateRectRgnIndirect i
CreateEllipticRgnIndirect, które przyjmują tylko po jednym argumencie – wskaźniku do struktury
RECT (ich odpowiedniki bez końcówki
Indirect mają po kilka argumentów).
To jeszcze nie wszystko. Możemy dwa lub więcej gotowych regionów połączyć w jeden za pomocą
CombineRgn, co w wielu przypadkach będzie znacznie łatwiejsze, niż żmudne wklepywanie współrzędnych wierzchołków wielokąta. Możemy również dokonywać na regionach transformacji przy użyciu
ExtCreateRgn.
Dość gadania, zrobimy sobie teraz zaokrąglone okienko. Najpierw odpowiedni region:
HRGN hrZaokr = CreateRoundRectRgn( 0, 0, 300, 200, 20, 20 );
W przypadku tej funkcji, oprócz wierzchołków prostokąta, podajemy jeszcze promienie elips wyznaczających zaokrąglenia – im będą one większe, tym bardziej zaokrąglony kształt otrzymamy. Teraz możemy już wykorzystać nowo zdefiniowany kształt:
SetWindowRgn( hwnd, hrZaokr, TRUE );
Powyższa funkcja, jak sama nazwa wskazuje, przypisuje podany region do okna. Dzięki tej operacji kanty naszego okienka uległy wyraźnemu stępieniu:
Warto zwrócić uwagę, że jeśli nasze okno było mniejsze niż przypisany mu region, to część okna przestanie być widoczna. Jeśli więc mieliśmy okno o wymiarach 800x600 pikseli, to po przypisaniu powyższego regionu nie zobaczymy już m.in. przycisków na pasku tytułowym okienka. Nie będzie też widać ramki na dole i z prawej strony okna. Dlatego też jeśli bawimy się regionami, to najlepiej używać okienka z najprostszym możliwym stylem, np.
WS_POPUP (bez paska tytułowego, ramki i pseudo-3D wyglądu).
Spróbujmy teraz otrzymać jakiś ciekawszy kształt. Proponuję zacząć jednak nie od tworzenia regionów, lecz od przygotowania samego okienka. Przekonaliśmy się już bowiem przed chwilą, że im dziwniejszy kształt wymyślimy, tym mniej okienko będzie przyjazne użytkownikowi: obszary "nie-klienta" (
non-client area) okna znikną częściowo lub w całości. Najlepiej więc od razu je wyłączyć, ustawiając prosty styl. Zmieniamy kod tworzący okno na mniej więcej następujący:
hwnd = CreateWindowEx( 0, ClassName, "Nieregularne okno", WS_POPUP,
300, 200, 300, 200, HWND_DESKTOP,
NULL, hThisInstance, NULL );
Teraz dopiero możemy z czystym sumieniem przystąpić do niczym nieskrępowanej zabawy regionami.
HRGN hrKolo1, hrKolo2, hrKolo3, hrPasek;
hrKolo1 = CreateEllipticRgn( 0, 50, 100, 150 );
hrPasek = CreateRectRgn( 90, 75, 190, 115 );
hrKolo2 = CreateEllipticRgn( 180, 0, 380, 200 );
hrKolo3 = CreateEllipticRgn( 270, 90, 290, 110 );
CombineRgn( hrKolo1, hrKolo1, hrPasek, RGN_OR );
CombineRgn( hrKolo2, hrKolo1, hrKolo2, RGN_OR );
CombineRgn( hrKolo3, hrKolo2, hrKolo3, RGN_DIFF );
SetWindowRgn( hwnd, hrKolo3, TRUE );
Zamiarem naszym było zrobienie dwóch kółek, mniejszego i większego, połączenie ich prostokątnym paskiem i na koniec wycięcie malutkiego kółka w środku dużego koła. Najpierw robimy więc pojedyncze figury; następnie używamy funkcji
CombineRgn, by połączyć średnie koło z paskiem (wynik zapisujemy do regionu
hrKolo1). Ostatni argument funkcji
CombineRgn oznacza rodzaj operacji, którą wykonujemy na dwóch podanych regionach,
RGN_OR oznacza dodawanie,
RGN_AND – część wspólną (iloczyn),
RGN_DIFF – różnicę. Kompletną listę stałych znajdziesz oczywiście w dokumentacji.
Dalsze kroki to: połączenie pierwszego rezultatu z dużym kołem i zachowaniem tego
hrKolo2, no i wreszcie wycięcie małego koła i zapisanie ostatecznego kształtu w
hrKolo3. Ostatnia instrukcja to oczywiście nadanie oknu kształtu, nad którym tak się napracowaliśmy. No i mamy:
Przenoszenie okna
Jak już wspomnieliśmy, przykrym następstwem szaleństw z kształtami jest najczęściej utrata dość istotnych elementów okna, jak np. pasek tytułowy. Właściwie nie ma tutaj innej rady, jak tylko zaimplementować niezbędne elementy samodzielnie. Jak będą wyglądały u ciebie przyciski do minimalizowania czy zamykania okna, to już twoja sprawa. Ich zaprogramowanie również jest raczej proste. Ja ograniczę się tylko do stosunkowo trudnego problemu, jakim jest przemieszczanie okna po pulpicie. Ponieważ nie mamy już paska tytułowego (i nie chce go się nam robić ;-)), więc możemy dla uproszczenia przyjąć, że okno będzie można przenosić, "łapiąc" je gdziekolwiek (tak zresztą robi większość autorów "dziwnokształtnych" aplikacji).
Podstawą będzie oczywiście obsłużenie komunikatu
WM_MOUSEMOVE:
static bool lmbDown = false;
static POINT lastPos = { 0, 0 };
case WM_LBUTTONDOWN:
lmbDown = true;
SetCapture( hWnd );
break;
case WM_LBUTTONUP:
lmbDown = false;
ReleaseCapture();
break;
case WM_MOUSEMOVE:
if( lmbDown )
{
POINT pos;
RECT rc;
pos.x = GET_X_LPARAM( lParam );
pos.y = GET_Y_LPARAM( lParam );
ClientToScreen( hWnd, & pos );
GetWindowRect( hWnd, & rc );
SetWindowPos( hWnd, NULL, rc.left + pos.x - lastPos.x,
rc.top + pos.y - lastPos.y,
0, 0, SWP_NOSIZE | SWP_NOZORDER );
lastPos = pos;
}
else
{
GetCursorPos( & lastPos );
}
break;
Użyliśmy dwóch zmiennych statycznych (ponieważ nie są nam one potrzebne nigdzie poza procedurą okna, gdzie wklejamy powyższy kod). Pierwsza przechowuje stan lewego przycisku myszy (w gruncie rzeczy można by się obyć bez tego, ale w ten sposób jest czytelniej), a druga ostatnią pozycję kursora myszy.
Gdy użytkownik wciśnie przycisk myszy, aktualizujemy stan w zmiennej
lmbDown oraz przechwytujemy mysz – jest to konieczne, abyśmy od tej pory dostawali komunikat
WM_MOUSEMOVE również wtedy, gdy użytkownik wyjedzie poza obszar naszego okna. Gdy w końcu puści przeciągane okno, wołamy
ReleaseCapture, czyli mysz znowu można użyć do czegoś innego, niż przeciąganie okna.
Sama obsługa
WM_MOUSEMOVE jest nieco bardziej skomplikowana. Musimy tutaj sprawdzić stan przycisku myszy. Jeśli jest on wciśnięty, to pobieramy aktualne współrzędne kursora i przeliczamy je na współrzędne ekranowe (w
lParam dostajemy je względem obszaru klienckiego okna). Następnie pobieramy aktualne współrzędne okna (tym razem mamy je od razu w jednostkach ekranowych) i wykorzystując te dane przesuwamy okno o różnicę między aktualną (
pos) a ostatnią pozycją (
lastPos). Jeśli przycisk myszy nie jest wciśnięty, czyli po prostu przesuwamy mysz nad oknem, nie robimy nic oprócz uaktualnienia ostatniej pozycji – korzystamy tym razem z
GetCursorPos, bo tak jest prościej (nie trzeba przeliczać jednostek). Teraz można wreszcie przeciągać okno, klikając je w dowolnym miejscu.
W ten sposób przywróciliśmy część funkcjonalności, którą "zabrały" nam regiony. Przydałyby się jeszcze przyciski do zamykania, minimalizacji i maksymalizacji okna oraz pozostałe z jego standardowych właściwości, ale nie chcemy, by artykuł został zdominowany przez takie informacje :-).
Malowanie okna
Nasze nietypowe okienko na razie wygląda jakoś nijak, więc dobrze byłoby je jeszcze pomalować. Z tym może być pewien problem, ponieważ współrzędne regionów będą się mniej więcej pokrywać ze współrzędnymi. Mniej więcej, czyli niedokładnie. Dlatego dobrym pomysłem mogłoby być zrezygnowanie z rysowania ramek (piórko ustawione na
PS_NULL). W kodzie bez większych niespodzianek:
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hDC = BeginPaint( hWnd, & ps );
HBRUSH hBrush1 = CreateHatchBrush( HS_CROSS, RGB( 0, 255, 0 ) );
HBRUSH hBrush2 = CreateSolidBrush( RGB( 255, 128, 0 ) );
HBRUSH hBrushOld = SelectBrush( hDC, hBrush2 );
Rectangle( hDC, 90, 75, 190, 115 );
SelectBrush( hDC, hBrush1 );
Ellipse( hDC, 0, 50, 100, 150 );
Ellipse( hDC, 180, 0, 380, 200 );
SelectBrush( hDC, hBrushOld );
DeleteBrush( hBrush1 );
DeleteBrush( hBrush2 );
EndPaint( hWnd, & ps );
}
break;
Zauważ, że używamy tutaj makr z
windowsx.h. Efekt jest następujący (czarne tło maskuje tu krzywe ramki ;-P):