Formuláře v C++ Builderu – dokončení

V tomto článku se ještě budeme zabývat formuláři (třída TForm). Ukážeme si například, jak změnit některé jeho vlastnosti, které nelze nastavit v ObjectInspectoru, a jak zachytávat některé zprávy, které také nemají publikovanou příslušnou událost (Event).
Nejprve si řekneme o 3 důležitých metodách třídy TForm, které můžeme v naší třídě formuláře přepsat. První z nich je CreateParams:

virtual void __fastcall CreateParams(TCreateParams &Params);

Když se podíváme, jak vypadá struktura TCreateParams:

struct TCreateParams
{
  char *Caption;
  int Style;
  int ExStyle;
  int X;
  int Y;
  int Width;
  int Height;
  HWND WndParent;
  void *Param;
  tagWNDCLASSA WindowClass;
  char WinClassName[64];
};

zjistíme, že její prvky odpovídají parametrům funkce CreateWndowEx, což je API funkce, která je volána uvnitř VCL při vytvoření handle okna, představujícího formulář. Zvláště pokud alespoň trochu znáte WinAPI a říkají vám něco styly okna (konstanty WS_xxxx), pak tato funkce je pro vás ideálním místem pro změnu vlastností okna formuláře. Nemusíte se pak zabývat nějakými „borlandími“ vlastnostmi jako například property BorderStyle, Position apod. a dávat z nich dohromady požadovaný „vzhled“ okna. Naštěstí VCL má tolik slušnosti, že i když jsou uvedené property nastaveny jakkoli, respektuje zde zadané styly a nepřepíše je.

Ukažme si příklad, jak jednoduše vytvořit formulář, který má být zobrazen přes celou obrazovku (včetně pruhu úloh, samozřejmě) bez jakýchkoli rámečků či systémových ikon. K tomu nám stačí takto přepsat uvedenou virtuální funkci, a jsme hotovi:

void __fastcall TForm2::CreateParams(Controls::TCreateParams &Params)
{
  TForm::CreateParams(Params);
  Params.ExStyle = WS_EX_TOPMOST;
  Params.Style = WS_POPUP; // okno bez okrajů, titulku a sys. ikon
  Params.X = 0;
  Params.Y = 0;
  Params.Width = GetSystemMetrics(SM_CXSCREEN);
  Params.Height = GetSystemMetrics(SM_CYSCREEN);
}

No, a pokud chceme třeba uprostřed formuláře vycentrovat text, nemusíme používat nějaké TLabel s vypočítáváním souřadnic nebo něco podobného, ale stačí k tomu funkce DrawText (o které VCL bohužel nic neví):

void __fastcall TForm2::FormPaint(TObject *Sender)
{
  SetBkMode(Canvas->Handle, TRANSPARENT);
  DrawText(Canvas->Handle, "Full-screen formulář", -1,
    (LPRECT)&ClientRect,
    DT_SINGLELINE | DT_CENTER | DT_VCENTER); 
}

Rozšířený styl WS_EX_TOPMOST znamená, že naše okno, i když ztratí fokus, bude zobrazeno (jako neaktivní samozřejmě) nad ostatními okny s výjimkou těch, která tento styl mají také. Pak by záleželo na pořadí vytvoření těchto oken.

Nyní k další metodě, kterou můžeme využít při jejím přepsání. Jde o virtuální metodu CreateWindowHandle:

virtual void __fastcall CreateWindowHandle(const Controls::TCreateParams &Params);

Jakmile v této námi přepsané funkci zavoláme metodu předka, máme k disposici platný handle vytvořeného okna formuláře. Zde máme první příležitost jak aplikovat na okno třeba některé API funkce, které vyžadují jako parametr handle okna. Ale jak si hned ukážeme, VCL se často chová k programátorovi (zejména pokud chce využívat API funkce a očekává jejich korektní chování) značně nestandardně. Ukažme si příklad. Dejme si za úkol použít nějakou bitmapu jako výplň pozadí formuláře. Realizovat to lze samozřejmě různým způsobem. Mohli bychom třeba (jak to bylo nutně dělat ve Windows 95) v handleru OnPaint tuto bitmapu vykreslovat opakovaně tak, abychom jí zaplnili aktuální velikost formuláře. Od Windows 98 však existuje jednodušší a efektivnější způsob: vytvořit si štětec (Brush typu HBRUSH) a ten pak nastavit jako příslušnou vlastnost třídy okna pomocí funkce SetClassLongPtr, resp. SetClassLong (o té první uvedené totiž „dokumentace“ C++ Builderu taktně mlčí :-)). Jenom upřesním, že ve Windows 95 to šlo také, ale velikost bitmapy byla omezena na 8 x 8 pixelů, což většinou nebylo to „pravé ořechové“. Programátor vybaven těmito standardními vědomosti si nyní řekne: „Dejte mi handle okna a já vám nastavím jeho pozadí.“ Avšak je zde VCL.... Třída TForm (jako každý objekt odvozený od TWinControl) má totiž property Brush (typu objektu TBrush), který –jak název napovídá – určuje výplň pozadí okna. Tento Brush samozřejmě „přebije“ hodnotu nastavenou uvedeným způsobem pomocí SetClassLong. Je tedy potřeba nastavit handle získané bitmapy jako handle této property Brush. Samozřejmě že je to ještě „jednodušší“ než používat „nějaké SetClassLong“, nicméně pokud očekáváte standardní chování okna, můžete narazit a ztratit dost času, než přijdete na to, proč vám to nefunguje. Takže nyní již konkrétní ukázku. Mějme soubor s bitmapou, kterou si načteme pomocí funkce LoadImage a použijeme jako pozadí formuláře. Toto nastavení provedeme právě v přepsané funkci CreateWindowHandle:

HBITMAP hBitmap;

void __fastcall TForm1::CreateWindowHandle(const Controls::TCreateParams &Params)
{
  TForm::CreateWindowHandle(Params);
  hBitmap =  (HBITMAP)LoadImage(HInstance, "pozadi.bmp",
    IMAGE_BITMAP,  0, 0,
      LR_DEFAULTCOLOR | LR_DEFAULTSIZE | LR_LOADFROMFILE);
  if ( hBitmap == NULL )
    return;
  // Tohle u Borlandů fungovat nebude :-)
  SetClassLongPtr(Handle, GCLP_HBRBACKGROUND,
    (LONG_PTR)CreatePatternBrush(hBitmap));
  // Takže na to musíme takhle...
  Brush->Handle = CreatePatternBrush(hBitmap);
}

Výsledek vidíte na obrázku:

Nyní přejděme k jedné z nejvýznamnějších virtuálních metod, které můžeme přepsat, a tou je procedura okna, zde zapouzdřená do WndProc:

virtual void __fastcall WndProc(Messages::TMessage &Message);

V této metodě máme totiž možnost tím nejpřímějším způsobem zachytit jakoukoliv ze zpráv Windows, které okno dostane. Můžeme místo toho samozřejmě použít makra mapy zpráv (BEGIN_MESSAGE_MAP, END_MESSAGE_MAP a VCL_MESSAGE_HANDLER), ale kdo je zvyklý na proceduru okna, použije asi první způsob. Navíc pokud máme větší množství zpráv, které chceme takto zachytávat, vytváření maker dá více „psaní i klikání“.

Ukažme si například, jak zachytit 3 ze „systémových“ příkazů – minimalizaci, maximalizaci a zavření okna. Říkám zachytit, ale samozřejmě že kromě prostého zachycení máme možnost tuto zprávu zadržet nebo před jejím výchozím zpracováním nějak reagovat. Na zachycení zavření formuláře máme sice ve VCL událost OnClose, ale zachycení minimalizace či maximalizace v ObjectInspectoru nenajdete. Samozřejmě pokud si chcete kvůli takovéto „prkotině“ nabalit nějakou „skvělou komponentu“, která toho „umí mnohem více“, jak je libo, ale pak asi nemusíte číst tento seriál. Když jsem to nazval prkotinou, nechci se vůbec nějak dotknout někoho, pro něhož to prkotina není, proto je zde tento článek. Myslel jsem tím tolik, že když něco neumím, budu se to snažit naučit, a nikoli „nějak obejít“.

Takže k našemu problému. Uvedené systémové události vyvolají poslání zprávy WM_SYSCOMMAND s příslušným parametrem, uloženým ve wParam této zprávy. Konkrétně při požadavku na minimalizaci má wParam této zprávy hodnotu SC_MINIMIZE, při maximalizaci SC_MAXIMIZE a při zavření SC_CLOSE. Podívejme se tedy jak vypadá v naší ukázce procedura okna formuláře. Další handlery zpráv, které v ní vidíte, následně také vysvětlím.

void __fastcall TForm1::WndProc(Messages::TMessage &Message)
{
  switch ( Message.Msg )
  {
    case WM_SYSCOMMAND:
      switch ( Message.WParam )
      {
        case SC_CLOSE:
          MessageBox(Handle, "SC_CLOSE",
            "WM_SYSCOMMAND", MB_ICONINFORMATION);
          break;
        case SC_MINIMIZE:
          MessageBox(Handle, "Minimalizace zakázána",
            "WM_SYSCOMMAND", MB_ICONINFORMATION);
          return;
        case SC_MAXIMIZE:
          MessageBox(Handle, "Maximalizace zakázána",
            "WM_SYSCOMMAND", MB_ICONINFORMATION);
          return;
      }
      break;
    case WM_NCLBUTTONDOWN:
      MessageBeep(0);
      break;
    case WM_DISPLAYCHANGE:
      MessageBox(Handle, "Změna rozlišení obrazovky",
            "WM_DISPLAYCHANGE", MB_ICONINFORMATION);
      break;
  }
  TForm::WndProc(Message);
}

Jak si můžete všimnout, po „zpracování“ zpráv minimalizace a maximalizace je místo vyskočení z přepínače a následného volání výchozí procedury okna (prostřednictvím stejné metody předka, tedy třídy TForm) použito vyskočení z celé funkce. To znamená, že tato zpráva se nedostane do výchozího zpracování, a tedy se neprovede požadovaná akce. Jinak zachycení požadavku zavření formuláře můžeme realizovat také vytvořením události OnClose formuláře:

void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
  ShowMessage("OnClose");
}

Asi tušíte, že když spustíte program, nejdříve půjde tou procedurou okna (SC_CLOSE) a teprve po jeho zpracování voláním TForm::WndProc(Message) bude vyvolána událost OnClose. A právě jedno z těchto míst je tím nejlepším místem pro zpracování kódu typu „Dokument se změnil, chcete před ukončením aplikace uložit změny? ...“

Nyní zpět k funkci WndProc. Ve výpisu vidíte také handler zprávy WM_NCLBUTTONDOWN. Jde o další ze zpráv, které pomocí ObjectInspectoru, resp. událostí VCL nezachytíte. Tato zpráva oznamuje stlačení levého tlačítka myši v neklientské oblasti okna, což je „okrajová“ část okna, která zahrnuje titulkový pruh, menu a rámeček okna.

Dále je zde handler zprávy WM_DISPLAYCHANGE, kterou obdrží při změně rozlišení obrazovky všechna „top-level“ okna, tedy v podstatě okna, která nemají v rámci své aplikace svého vlastníka (rodiče – parent window). Aplikace má pak možnost reagovat příslušnou změnou velikosti okna přepočtenou na základě nových rozměrů obrazovky.

Zde si můžete stáhnout ukázkovou aplikaci: form_2.zip.

Diskuze (1) Další článek: GeForce 4 - 5. února

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