Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: 'Złośliwiec'
Biblioteki C++

Okna o nieregularnym kształcie

[lekcja] Artykuł opisuje w jaki sposób można utworzyć aplikację okienkową, która posiada samodzielnie zdefiniowany wygląd okna aplikacji.

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:
C/C++
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:
C/C++
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:

Zaokrąglone okienko (Windows XP)
Zaokrąglone okienko (Windows XP)

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:
C/C++
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.
C/C++
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:

Egzotyczne okienko na tle innej aplikacji (Windows XP)
Egzotyczne okienko na tle innej aplikacji (Windows XP)

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:
C/C++
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:
C/C++
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):

Okno - już pomalowane (Windows XP)
Okno - już pomalowane (Windows XP)
Poprzedni dokument Następny dokument
Obsługa podwójnego kliknięcia Tuning MessageBox-a