Formuláře a aplikace v C++ Builderu

V tomto článku se trochu podrobněji podíváme na problematiku formulářů, objektu aplikace a řekneme si něco o tom, jak jsou vlastnosti komponent ukládány do zdrojů programu (resources)
Zdroje (resources)

V předchozích článcích jsem se již zmínil o tom, že VCL aplikace (týká se to i Delphi) většinou nepoužívá standardní zdroje. Jako standardní zdroj je většinou do zdrojů ukládána pouze hlavní ikona programu (pod názvem „MAINICON“). Další standardní zdroje jsou pak ty, které přidáme „ručně. Jak konkrétně, k tomu se samozřejmě dostaneme později. Dále pokud sestavíte program staticky, tedy tak, aby v exe souboru byly obsaženy i balíčky a runtimová knihovna RTL, pak se v exe souboru objeví další standardní zdroje, jako asi 7 kurzorů které VCL používá a dále tabulka řetězců (String Table), obsahující různé texty používané především jako chybové a jiné zprávy, dále jsou tam například názvy měsíců a dní. Především tam naleznete zdroje typu Data (ve Win API označované typem RT_RCDATA), ve kterých jsou uloženy vlastnosti formulářů a dalších komponent na těchto formulářích. Již jsem se zmiňoval o způsobu uložení ve formě textových řetězců. Nyní se zmíním o jedné věci, která mnohdy zbytečně zvětšuje velikost zdrojů a tím celého programu. Mnohým se to asi bude zdát zanedbatelné, ale pro ty, kteří chtějí vidět trochu více pod povrch, to může být nová informace. Ještě poznámku k tomu, co myslím tím, „když se podíváte do zdrojů programu“: Pokud máte k disposici nějaký slušný editor zdrojů, který umí otevřít exe soubor a zobrazit, popřípadě i editovat všechny v něm obsažené zdroje, můžete ho použít. Ideální je editor Visual C++ (pokud jste skalními odpůrci Microsoftu – vaše smůla).

Nyní tedy konkrétní příklad. Vytvoříme si novou VCL aplikaci, na formulář dáme 1 TButton. Když se nyní podíváme do souboru DFM, kde je jakýsi textový skript pro vytvoření zdrojů najdeme v něm informace o tomto buttonu:

object Button2: TButton
  Left = 160
  Top = 40
  Width = 75
  Height = 25
  Caption = `Button2`
  TabOrder = 1
End

Jak je vidět jsou zde pouze ty vlastnosti, které nemají žádnou výchozí (default) hodnotu. Nyní pomocí ObjectInspectoru změníme hodnotu property ShowHint tohoto button na true. Samozřejmě že tato vlastnost se po uložení objeví ve zmíněném DFM souboru. Nyní vrátíme ShowHint zpět na výchozí hodnotu false. Podíváme se opět do DFM souboru a zjistíme, že tato property je tam uvedená, i když nyní má opět výchozí hodnotu. Podstatnější je že když program sestavíme, tato hodnota je přidaná i do zdrojů výsledného exe souboru. Zajímavé je že tato vlastnost se týká jen některých property (alespoň něco…). Například když změníte hodnotu property Cursor, poté jí vrátíte na crDefault, již se ve zdrojích vůbec neobjeví. Jaký z toho plyne závěr? Pokud chcete maximálně optimalizovat, projděte před sestavením finální verze programu soubor DFM a ručně (textově) v něm odstraňte ty property, které mají ve skutečnosti svoji výchozí hodnotu.

Ještě k formátu jak jsou v těchto zdrojích uložena data běžných typů. Například bitmapy jsou ve zdrojích uloženy „tak jak jsou“, tedy stejně jako v bitmapovém souboru, tj. včetně hlavičky s informacemi o bitmapě a pokud nejde bitmapu v plných barvách (true-color) tak také tabulka barev. Při této příležitosti ještě upozorním, že pokud vizuálně vyberete stejný bitmapová soubor třeba pro několik tlačítek, ve zdrojích se pak data této bitmapy zcela zbytečně opakují. Je lepší načít tuto bitmapu jednou při startu programu (ať už ze zdrojů nebo ze souboru) a pak ji dynamicky přiřadit všem těmto tlačítkům. Má to jedinou vadu, nemůžete se již ve fázi vizuálního návrhu kochat pohledem na své dílo v plné kráse.

Okno aplikace a hlavního formuláře

Když spustíte VCL aplikaci (s jedním „hlavním“ formulářem) a podíváte se nějakým programem (např. Spy++ nebo WinSight32) na existující okna v systému, zjistíte, že kromě okna formuláře, je zde ještě okno třídy TApplication, které je neviditelné (má rozměry 0x0 pixelů) a představuje to, co je zapouzdřeno třídou TApplication. Zde je třeba upozornit na jednu chybu nebo přinejmenším nepřesnost (záleží na výkladu) v nápovědě. Když se podíváte v nápovědě na property Handle třídy TApplication, stojí zde doslova:

Provides access to the window handle of the main form (window) of the application.

Pro ty méně znalé angličtiny přeložím: „Poskytuje přístup k handle hlavního formuláře (okna) aplikace.“

Pokud je tím hlavním oknem míněno to okno 0x0 pixelů, je to pravda, ale obávám se, že je to silně matoucí. Pokud je tím hlavním formulářem míněn skutečný hlavní formulář (TForm), jak by z toho každý asi usuzoval, je to samozřejmě nesmysl, který může být velice matoucí. Pro ozřejmění této problematiky jsem v ukázkové aplikaci na hlavní formulář umístil 3 tlačítka. První z nich („Skrýt aplikaci“) skryje okno aplikace, což se projeví tím, že jeho reprezentace na pruhu úloh zmizí:

void __fastcall TForm1::OnHideApplication(TObject *Sender)
{
  ShowWindow(Application->Handle, SW_HIDE);
}

Dalším tlačítkem toto okno opět zviditelníme, aby se opět objevilo na pruhu úloh:

void __fastcall TForm1::OnShowApplication(TObject *Sender)
{
  ShowWindow(Application->Handle, SW_RESTORE);
}

Třetím tlačítkem skryjeme okno formuláře. Formulář zmizí z obrazovky, avšak aplikace na pruhu úloh zůstane. Aby se nám formulář zase zobrazil, spustíme po jeho skrytí timer, po jehož vypršení formulář opět zobrazíme.

void __fastcall TForm1::OnFormHide(TObject *Sender)
{
  ShowWindow(Handle, SW_HIDE);
  SetTimer(Handle, 1, 3000, NULL);
}

Pro zachycení timeru jsem vytvořil handler zprávy WM_TIMER, který vypadá takto.

void __fastcall TForm1::OnWM_TIMER(TMessage& Msg)
{
  TForm::Dispatch(&Msg);
  KillTimer(Handle, 1);
ShowWindow(Handle, SW_RESTORE);
}

Více formulářů – dynamické vytváření

Pokud máte v aplikaci více formulářů, při jejich vytváření jsou tyto automaticky přidány do seznamu formulářů vytvářených při spuštění aplikace. Když se podíváme na funkci WinMain, umístěnou ve zdrojovém souboru aplikace (tedy třeba project1.cpp), vidíme jak se formuláře vytvářejí:

WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
  try
  {
    Application->Initialize();
    Application->CreateForm(__classid(TForm1), &Form1);
    Application->CreateForm(__classid(TForm2), &Form2);
    Application->Run();
  }
  catch (Exception &exception)
  {
    Application->ShowException(&exception);
  }
  return 0;
}

Takto vytvořené formuláře, včetně všech na nich umístěných komponent, jsou v paměti po celou dobu běhu aplikace. Některé z nich, například nějaké dialogy, nemusí v některých případech být vůbec použity. Proto bývá většinou výhodnější vytvářet formuláře až když je opravdu potřebujeme a „po použití“ zase odstranit z paměti. Jedinou daní je malá (v závislosti na složitosti formuláře) prodleva navíc před zobrazením formuláře. Ale na druhou stranu, kromě již zmíněného šetření pamětí, je start aplikace rychlejší. A to je většinou pro uživatele významnější. Čekat při startu aplikace třeba několik vteřin navíc, než se vytvoří někdy i desítky formulářů, otráví uživatele více než občas při otevření nového okna čekat nějakou tu desetinku sekundy navíc.

Pokud tedy chceme další formuláře vytvářet dynamicky, nejprve jejich „automatické“ vytvoření odstraníme z výše uvedené funkce WinMain. Pokud toto chcete dělat „vizuálně“, máte tuto možnost v dialogu „Project – Options“. Zde pak ze seznamu „Auto-create forms“ přesuneme požadovaný formulář do seznamu „Available forms, jak je vidět na obrázku:

Klepněte pro větší obrázek

Nyní si musíme samozřejmě říci, jak ty formuláře vyvářet za běhu, chcete-li „na požádání“. Můžeme použít způsob, který je ve zmíněné funkci WinMain, tedy metodu CreateForm třídy TApplication:

void __fastcall CreateForm(System::TMetaClass* InstanceClass, void *Reference);

Konkrétní použití, jak vytvořit a modálně zobrazit formulář na stisk tlačítka může vypadat třeba takto:

void __fastcall TForm1::OnCreateDynamic(TObject *Sender)
{
  Application->CreateForm(__classid(TForm2), &Form2);
  Form2->ShowModal();
  delete Form2;
}

Další možností je samozřejmě klasickým způsobem, pomocí operátoru new vytvořit instanci třídy formuláře. Samozřejmě není podmínkou že musíte použít tu proměnnou, kterou vám ke každé nové třídě (formuláře) IDE vygeneruje. Tu (Form2) jsem použit v uvedené ukázce. Také je samozřejmé, že můžete mít více instancí stejného formuláře (tedy jeho třídy) současně. Zde je tedy ukázka druhého způsobu, s výsledkem stejným jako v pevním případě:

void __fastcall TForm1::OnCreateNew(TObject *Sender)
{
  TForm2* myForm = new TForm2(this);
  myForm->ShowModal();
  delete myForm;
}

Ještě poznámku k volání delete takto v jednom handlenu. Toto můžete použít, pokud formulář zobrazujete modálně, tedy metodou ShowModal. V tom případě se totiž kód volajícího formuláře přeruší právě na volání ShowModal a zrušení instance třídy (delete) je voláno až po zavření formuláře, tedy jeho okna. Pokud bychom použili pro zobrazení nemodální metodu Show, došlo by k runtimové chybě, neboť instance třídy by byla odstraněna, ale okno a všechny komponenty na něm by stále existovaly.

Modal result

Jako poslední téma tohoto článku si ještě řekneme, jak vytvářet dialogové formuláře, tedy jak po zavření modálního okna zjistit například, kterým tlačítkem byl formulář-dialog zavřen.

Jednou z možností je nastavit již v designu (v ObjectInspectoru) tlačítkům na dialogu property na požadovanou hodnotu. Tato hodnota je pak návratovou hodnotou funkce ShowModal, v případě, že uživatel kline na toto tlačítko. Navíc již nemusíme vytvářet událost OnClick pro toto tlačítko, tedy nemusíme v této události volat metodu Close. Formulář se automaticky zavře pokud stiskneme tlačítko které má property ModalResult nastavenou na hodnotu jinou než mrNone.

Další možností je v události OnClick příslušného tlačítka místo volání Close() nastavit property ModalResult formuláře. Formulář se pak automaticky zavře (i bez následného volání Close) a nastavený ModalResult je pak návratovou hodnotou metody ShowModal tohoto formuláře.

Vytvořme si tedy formulář (Form3) fungující jako dialog. Nastavíme mu BorderStyle na bsDialog. Tím se změní jeho rámeček na styl odpovídající běžným dialogovým dialogovým oknům (nepůjde měnit jeho rozměry) a ze systémových ikon zůstane jen ikonka zavření okna. Umístíme na něj nějaký text (dotaz) a 3 tlačítka. Jednomu z nich nastavíme ModalResult v ObjectInspectoru na mrRetry. U zbývajících dvou vytvoříme události OnClick třeba takto:

void __fastcall TForm3::OnYes(TObject *Sender)
{
  ModalResult = mrYes;
}

void __fastcall TForm3::OnNo(TObject *Sender)
{
  ModalResult = mrNo;
}

Když už se zabýváme podrobněji „dialogem“ ukažme si jak můžeme zabránit zavření dialogu pomocí Alt-F4 nebo kliknutí na systémovou ikonku „zavřít“. Samozřejmě možností je víc. Ta obecnější, nespoléhající se na události VCL spočívá v přímém zachycení zprávy WM_SYSCOMMAND. To můžeme realizovat například pomocí makra mapy zpráv. Popíšu také „ruční“ přidání, neboť si nejsem jist, zda verze Standard C++ Builderu má „ClassExplorer“, pomocí kterého vytvoříme handler zprávy příkazem „New Method“ z kontextové nabídky třídy formuláře a vyplněním zobrazeného dialogu nějak takto:

Klepněte pro větší obrázek

„Wizard“ nám poté přidá následující kód, který bez použití ClassExploreru můžete zapsat přímo do hlavičkového a zdrojového souboru. V následujícím výpisu je již uvedena i vlastní realizace handleru WM_SYSCOMMAND:

// kód v deklaraci třídy
protected:
  BEGIN_MESSAGE_MAP
    VCL_MESSAGE_HANDLER(WM_SYSCOMMAND, TMessage, OnWM_SYSCOMMAND)
  END_MESSAGE_MAP(TForm)
  void __fastcall OnWM_SYSCOMMAND(TMessage& Msg);

void __fastcall TForm3::OnWM_SYSCOMMAND(TMessage& Msg)
{
  if ( Msg.WParam == SC_CLOSE )
  {
    MessageBox(Handle, "Čekám na jasnou odpověď !!!", "Dialog", MB_ICONERROR);
    return;
  }
  TForm::Dispatch(&Msg);
}

Jak je vidět, jde o to v případě zjištění parametru SC_CLOSE (wParam) zprávu WM_SYSCOMMAND zadržet, tedy nevolat metodu Dispatch, která zprávu pošle k výchozímu zpracování. Můžete se přesvědčit, že tento parametr zahrnuje jak zavření pomocí systémového menu tam pomocí klávesy Alt-F4.

Nyní jsme tedy „donutili“ uživatele aby zvolil jednu ze 3 nabízených možností a zbývá si ukázat, jak výsledek zpracovat voláním metody ShowModal. Podívejme se do zdrojového kódu hlavního formuláře:

void __fastcall TForm1::OnDialog(TObject *Sender)
{
  TForm3* myDialog = new TForm3(this);
START:
  int modalResult = myDialog->ShowModal();
  switch ( modalResult )
  {
    case mrYes:
      MessageBox(Handle, "Odpověd byla Ano", "Dialog", MB_ICONINFORMATION);
      break;
    case mrNo:
      MessageBox(Handle, "Odpověd byla Ne", "Dialog", MB_ICONINFORMATION);
      break;
    case mrRetry:
      MessageBox(Handle, "Jak chceš, zeptám se tedy znova ...",
        "Dialog", MB_ICONEXCLAMATION);
      goto START;
  }
  delete myDialog;
}

Myslím, že je zřejmé, že klíčovou částí kódu je zjištění návratové hodnotu metody ShowModal.

Tolik pro dnešek o formulářích. Vzhledem k tomu že toto téma je poměrně rozsáhlé a nabízí řadu dalších rad i zajímavostí, v příštím pokračování u nich ještě zůstaneme.

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

Diskuze (4) Další článek: Diskuze: na co se nejvíce těšíte v roce 2002?

Témata článku: Software, Programování, Stejná metoda, Toto, True Color, Dialog, Code, Timer, Catch, Textový řetězec, Formulář, Okno, Stejná úloha, Tito, Automatický návrat, Hlavní ikona, Zdroje


Určitě si přečtěte

Geniální programátor Edsger Dijkstra: hledal nové postupy a zavrhoval GoTo

Geniální programátor Edsger Dijkstra: hledal nové postupy a zavrhoval GoTo

** Edsger Dijkstra je osobností historie programování ** Vynalezl algoritmus pro nalezení nejkratší cesty v grafu a dostal Turingovu cenu ** Zasloužil se o blízké propojení programování a matematiky

Jiří Nahodil | 13

Proč byste měli rozmazávat SPZ aut na fotkách, které vystavujete na web

Proč byste měli rozmazávat SPZ aut na fotkách, které vystavujete na web

** Na fotkách aut nahraných na web je dobré rozmazat SPZ ** Značku dokáže z obrázku přečíst Google i Facebook ** SPZ může naplnit podstatu osobního údaje

Karel Kilián | 73


Aktuální číslo časopisu Computer

Megatest 24 PC zdrojů

Jak využít umělou inteligenci

10 špičkových sluchátek s ANC

Playstation 5 vs Xbox Series X