Formuláře v C++ Builderu III. (prozatím) naposled

V tomto článku se naposledy (alespoň pro nejbližších pár článků) podíváme ještě podrobněji na některé zajímavé záležitosti okolo formulářů. Naučme se jak používat průhlednost, jak nastavit formuláři vlastní tvar, třeba definovaný bitmapou apod.
Omezení velikosti a určení pozice při maximalizaci

Nejdříve se podívejme na problém omezení velikosti formuláře. Třída TForm nám nabízí property Constraints typu TSizeConstraints, kterou můžeme nastavit maximální a minimální povolené rozměry okna formuláře. Když nastavíme na nějakou nenulovou hodnotu všechny 4 property (tedy MaxWidth, MaxHeight, MinHeight, MinWidth), nelze formulář roztáhnout mimo tyto rozměry. Když formulář maximalizujeme, roztáhne se sice do velikosti specifikované těmi maximálními rozměry, ale „skočí“ na obrazovce do polohy 0,0. Bylo by samozřejmě možné odchytávat maximalizaci a „ručně“ formulář přesouvat do nějaké požadované pozice. Existuje však „čistší“ řešení – použití zprávy WM_GETMINMAXINFO. Tato zpráva totiž umožňuje kromě nastavení omezení velikosti, (které je implementováno pomocí property Constraints) specifikovat také rozměry a polohu okna ve stavu maximalizace. V této zprávě parametr lParam je adresa struktury MINMAXINFO:

typedef struct tagMINMAXINFO {
  POINT  ptReserved;
  POINT  ptMaxSize;
  POINT  ptMaxPosition;
  POINT  ptMinTrackSize;
  POINT  ptMaxTrackSize;
} MINMAXINFO;

V této struktuře, jak název napovídá, prvek ptMaxPosition určuje souřadnice levého horního rohu okna ve stavu maximalizace, ptMaxSize je pak velikost tohoto maximalizovaného okna. Další 2 prvky – ptMinTrackSize a ptMaxTrackSize – pak určují výše zmíněné minimální a maximální rozměry okna. Jak tuto zprávu použít? Jednoduše ji zachytíme (nejlépe přímo v proceduře okna) a před jejím posláním k dalšímu zpracování prostě tyto prvky naplníme požadovanými hodnotami. Ty, které chceme nechat, tak jak jsou, prostě ignorujeme. Praktický příklad je v ukázkové aplikace ve formuláři Form1, kde jsem takto přepsal metodu WndProc, tedy proceduru okna:

void __fastcall TForm1::WndProc(Messages::TMessage &Message)
{
  LPMINMAXINFO mmi;
  switch ( Message.Msg )
  {
    case WM_GETMINMAXINFO:
      mmi = (LPMINMAXINFO)Message.LParam;
      mmi->ptMaxPosition.x = 300;
      mmi->ptMaxPosition.y = 50;
      break;
  }
  TForm::WndProc(Message);
}

Nyní se maximalizovaný formulář přesune do specifikované pozice (konkrétně 300, 50), a nikoli do počátku souřadnic obrazovky.

Průhlednost formuláře

Windows 2000 a XP přímo podporují průhlednost oken (tzv. layered windows). Zaslechl jsem, že Delphi verze 6 již tuto průhlednost zapouzdřuje v příslušných property formuláře. Nic nám však nebrání použít tuto průhlednost v jakémkoli Win32 programu.

Jak na to? Abychom mohli aplikovat funkce průhlednosti, musíme nejdříve oknu nastavit příslušný rozšířený styl (ExStyle), kterým je WS_EX_LAYERED. V případě formuláře TForm to uděláme nejlépe v přepsané funkci CreateParams:

void __fastcall TForm1::CreateParams(Controls::TCreateParams &Params)
{
  TForm::CreateParams(Params);
  Params.ExStyle |= WS_EX_LAYERED;
}

Dále musíme „co nejdříve po vytvoření handle okna“ nastavit atributy průhlednosti. Tím ›co nejdříve‹ mám na mysli to, že pokud bychom neudělali nic dalšího, okno by bylo celé neviditelné. Atributy průhlednosti nastavíme pomocí funkce SetLayeredWindowAttributes:

BOOL SetLayeredWindowAttributes(
  HWND hwnd,    // handle okna
  COLORREF crKey,    // kód barvy
  BYTE bAlpha,    // alfa hodnota
  DWORD dwFlags    // příznaky použití
);

  • crKey – touto hodnotou máme možnost specifikovat barvu (kódem typu COLORREF), která má být průhledná. Znamená to, že všechny oblasti okna v této barvě jsou 100% průhledné i „průklepné“, tzn. nepřijímají zprávy myši, a když do tohoto průhledného místa klepneme, aktivujeme okno, které se nachází v tomto místě pod naším oknem.
  • bAlpha – hodnota určující stupeň průhlednosti okna (v rozsahu BYTE, tedy 0–255) jako celku, přičemž 0 znamená 100% průhlednost, 255 naopak nulovou průhlednost.
  • dwFlags – zde určíme, které z uvedených prvků jsou platné a mají se aplikovat. Může být kombinací hodnot LWA_COLORKEY a LWA_ALPHA.
V ukázkové aplikaci je formulář Form1, který toto demonstruje. Podívejme se nejprve na výsledek:

Je vidět, že celý formulář je poloprůhledný (konkrétně s alfa hodnotou 170) a uprostřed je zcela průhledná elipsa. Jak toho dosáhneme? Přepíšeme funkci CreateWindowHandle, což je ono místo „těsně po“ vytvoření handle okna, ve které takto nastavíme atributy průhlednosti:

void __fastcall TForm1::CreateWindowHandle(const Controls::TCreateParams &Params)
{
  TForm::CreateWindowHandle(Params);
  SetLayeredWindowAttributes(Handle, 0x0000FF00,
    170, LWA_ALPHA | LWA_COLORKEY);
}

Jako zcela průhlednou jsem za kód barvy zvolil například zelenou barvu (0x0000FF00). Nyní tedy zbývá nakreslit v této barvě onu elipsu. To musíme samozřejmě realizovat v handleru zprávy WM_PAINT, tedy ve VCL v události OnPaint třeba takto:

void __fastcall TForm1::FormPaint(TObject *Sender)
{
  Canvas->Brush->Color = 0x0000FF00;
  Ellipse(Canvas->Handle, 20, 20, 400, 250);
}

Regiony okna formuláře

Nyní se podívejme, jak lze vytvářet nepravidelná okna. Každému oknu lze nastavit takzvaný region; to je objekt (objekt, nikoli třída) GDI typu HRGN, což je handle tohoto objektu. Regiony se podrobněji můžeme zabývat jindy, zde jen tolik, že tyto regiony mohou být řekněme „geometrické“, definované nějakým jednoduchým geometrickým tvarem, nebo také složitější, vytvořené třeba na základě bitmapy (bitové masky). Když pak nějaký takový region nastavíme oknu, oblasti okna mimo tento region jsou 100% průhledné i „průklepné“.

Ukažme si dva příklady. V ukázkové aplikaci máme 2. formulář (který si nyní nastavíme jako hlavní). Nastavíme mu region tvořený obdélníkem se zaoblenými rohy (RoundRect), přičemž rozměr vezmeme z původního rozměru formuláře a poloměr zaoblení nastavíme třeba na 150. Styl formuláře nastavíme na WS_POPUP, tedy bez jakýchkoli okrajů a titulkového pruhu:

void __fastcall TForm2::CreateParams(Controls::TCreateParams &Params)
{
  TForm::CreateParams(Params);
  Params.Style = WS_POPUP;
}

Nastavíme si ještě nějakou vlastní barvu formuláře a přepíšeme opět funkci CreateWindowHandle.

void __fastcall TForm2::CreateWindowHandle(const Controls::TCreateParams &Params)
{
  TForm::CreateWindowHandle(Params);
  m_hrgn = CreateRoundRectRgn(0, 0 , Width, Height, 150, 150);
  SetWindowRgn(Handle, m_hrgn, TRUE);
}

A po spuštění vidíme výsledek:

Je vidět, že oblasti okna formuláře mimo definovaný region jsou 100% průhledné a ani nepřijímají žádné zprávy myši.

Formulář vlastního tvaru podle bitmapy

Na formuláři je jedno tlačítko, po jehož stisknutí změníme region na region definovaný bitmapovou maskou a okno vyplníme bitmapou. Podívejme se opět nejprve na výsledek po stisknutí tlačítka:

Opět tedy vlastní region, pozadí formuláře je navíc vyplněno bitmapou. Jak toho dosáhnout? Máme připravené 2 bitmapy, z nichž jedna tvoří pozadí formuláře a druhá masku pro vytvoření regionu:

Vytvořme si 2 členské proměnné typu HBITMAP a jednu typu HRGN:

HRGN m_hrgn;
HBITMAP m_hbitmapMask;
HBITMAP m_hbitmapBack;

Tyto bitmapy si nejprve (v konstruktoru) načteme ze souborů. Mohli bychom je samozřejmě dát i do zdrojů (resources) a načítat odtud:

__fastcall TForm2::TForm2(TComponent* Owner)
  : TForm(Owner)
{
  m_hbitmapMask = (HBITMAP)LoadImage(
    HInstance, "honza_mask.bmp", IMAGE_BITMAP, 0,0,
      LR_DEFAULTSIZE | LR_LOADFROMFILE);
  m_hbitmapBack = (HBITMAP)LoadImage(
    HInstance, "honza.bmp", IMAGE_BITMAP, 0,0,
      LR_DEFAULTSIZE | LR_LOADFROMFILE);
}

Nyní uvedu bez podrobnějšího rozboru (když se budeme podrobněji věnovat regionům, vrátíme se k tomu) funkci, která vytvoří region na základě bitové masky tak, že jej bude tvořit oblast, jež nemá bílou barvu:

void __fastcall TForm2::CreateFormRgn()
{
  if ( m_hbitmapMask == NULL )
    return;
  DeleteObject(m_hrgn);
  HRGN hrgn;
  COLORREF crColor;
  COLORREF crPixel;
  INT ConsecPix;
  INT x, y;
  HDC hdc;
  BITMAP bitmap;
  hdc = CreateCompatibleDC(::GetDC(::GetDesktopWindow()));
  GetObject(m_hbitmapMask, sizeof(BITMAP), &bitmap);
  SelectObject(hdc, m_hbitmapMask);
  m_hrgn = CreateRectRgn(0, 0, bitmap.bmWidth, bitmap.bmHeight);
  for ( y = 0; y < bitmap.bmHeight; y++ )
  {
    crColor = GetPixel(hdc, 0, y);
    ConsecPix = 1;
    for ( x = 0; x < bitmap.bmWidth; x++ )
    {
      crPixel = GetPixel(hdc, x, y);
      if ( crColor == crPixel )
        ConsecPix++;
      else
      {
        if ( crColor == 0x00FFFFFF )
        {
          hrgn = CreateRectRgn(x - ConsecPix, y, x, y + 1);
          CombineRgn(m_hrgn, m_hrgn, hrgn, RGN_DIFF);
          DeleteObject(hrgn);
        }
        crColor = crPixel;
        ConsecPix = 1;
      }
    }
    if ( (crColor == 0x00FFFFFF) && (ConsecPix > 0) )
    {
      hrgn = CreateRectRgn(x-ConsecPix, y, x, y+1);
      CombineRgn(m_hrgn, m_hrgn, hrgn, RGN_DIFF);
      DeleteObject(hrgn);
    }
  }
  if ( m_hrgn != NULL )
    SetWindowRgn(Handle, m_hrgn, TRUE);
  DeleteObject(hdc);
}

Pokud se vše podaří, na konci této funkce opět nastavíme vytvořený region oknu formuláře. Dále musíme nastavit (viz předchozí články) pozadí okna tvořené bitmapou a navíc skryjeme button a label. Vše v události OnClick onoho tlačítka na formuláři:

void __fastcall TForm2::OnBitmapRegion(TObject *Sender)
{
  CreateFormRgn();
  Brush->Handle = CreatePatternBrush(m_hbitmapBack);
  ShowWindow(Button1->Handle, SW_HIDE);
  Label1->Visible = false;
  Repaint();
}

Když máme takováto okna bez titulkového pruhu, můžeme chtít, aby je bylo možno táhnout také za klientskou oblast, což je v tomto případě celá viditelná a aktivní oblast okna. Využijeme zprávy WM_NCHITTEST, která je posílána spolu s jakoukoli myší událostí a ve své návratové hodnotě má hodnotu určující, v které oblasti okna došlo k události, jež zprávu vyvolala. Kompletní přehled možných hodnot i s jejich popisem naleznete v dokumentaci. Například zda to bylo v titulku, na rámečku, v klientské oblasti apod. To, že říkám v návratové hodnotě, znamená, že tuto hodnotu vrátí defaultní procedura okna, tedy API funkce DefWindowProc. Ve VCL aplikaci ji získáme prostě tak, že nejdříve zavoláme metodu WndProc předka formuláře, tedy TForm a pak tuto hodnotu máme v prvku Result struktury TMessage. Dále využijeme malého triku v tom smyslu, že pokud zjistíme, že jsme v klientské oblasti, nalžeme systému, že jsme v titulkovém pruhu, tím, že v proceduře okna vrátíme hodnotu HTCAPTION (odpovídající události v titulku). Ve VCL to opět uděláme tak, že nastavíme uvedený prvek Result na HTCAPTION. Celý kód procedury okna WndProc vidíte zde:


void __fastcall TForm2::WndProc(Messages::TMessage &Message)
{
  LPMINMAXINFO mmi;
  switch ( Message.Msg )
  {
    case WM_NCHITTEST:
      TForm::WndProc(Message);
      if ( (Message.Result == HTCLIENT) &&
        ( GetAsyncKeyState(MK_LBUTTON) < 0 ) ) // stisknuto levé tlačítko
      {
        Message.Result = HTCAPTION;
        return;
      }
      break;
  }
  TForm::WndProc(Message);
}

Navíc – jak vidíte – zde pro „větší čistotu“ testujeme, zda je současně stisknuto levé tlačítko myši pomocí funkce GetAsyncKeyState.

Zde je ke stažení ukázková aplikace forms_3.zip.

Diskuze (7) Další článek: Odklad slyšení v kauze Microsoftu se nekoná

Témata článku: , , , , , , , , , , , , , ,