19. MDI
  1. Každá aplikace může být implementována jako vícedokumentové rozhraní (MDI) nebo jednodokumentové rozhraní (SDI). V aplikaci MDI může být otevřeno více dokumentů nebo podřízených oken v jednom nadřízeném okně. V aplikaci SDI může být pouze jedno okno dokumentu. Začneme novou aplikací. Pro formulář vytvoříme nabídku:
  2. Okno
    Nové
    a nastavíme tyto vlastnosti formuláře: Caption na MDI rám, FormStyle na fsMDIForm a WindowMenu na Okno1. K aplikaci přidáme další formulář, kde nastavíme: Caption na Podřízené okno, FormStyle na fsMDIChild a Position na poDefaut. Obsluha volby Nové bude tvořena příkazy (deklaraci instance formuláře Form2 můžeme v jednotce druhého formuláře vynechat a vyřadíme Form2 z automaticky vytvářených formulářů; je také zapotřebí  hlavičkový soubor druhého formuláře vložit do jednotky prvního formuláře):
    static int Citac;
    TForm2* Form2 = new TForm2(this);
    Citac++;
    Form2->Caption = Form2->Caption + ' ' + IntToStr(Citac);
    Form2->Show();
    Po spuštění aplikace můžeme otevřít několik podřízených oken. Povšimněte si, že se jejich seznam objevuje v nabídce Okno (je to určeno vlastností WindowMenu). Zjistěte, co se stane, otevřeme-li jich více než 9. Jestliže některé z podřízených oken zavřeme, pak v seznamu oken stále existuje (není zrušeno). Toto vyřešíme v další verzi aplikace.
  3. Většina MDI aplikací obsahuje v nabídce Okno volby pro kaskádovité a dlaždicovité uspořádání otevřených podřízených oken. Pro obsluhu těchto voleb můžeme použít některé vlastnosti a metody, které jsou u formuláře s vlastností FormStyle nastavenou na fsMDIForm. Jsou to: metoda Cascade (kaskádovité řazení), metoda Tile (dlaždicovité řazení), vlastnost TileMode (určuje jak bude pracovat Tile) a metoda ArrangeIcons (uspořádání ikon minimalizovaných oken). Toto jsou metody a vlastnosti odvozené od Windows. Builder definuje některé další: vlastnost ActiveMDIChild (určuje aktivní okno; tato vlastnost je určena pouze pro čtení), metoda Next (aktivuje následující okno), metoda Previous (aktivuje předchozí okno), vlastnost MDIChildCount (současný počet podřízených oken) a vlastnost MDIChildren (pole podřízených oken; pomocí cyklu můžeme procházet všemi podřízenými okny).

  4. Na závěr předchozího zadání jsme se zmínili o nedostatku (nezrušení uzavřeného podřízeného okna). Tuto závadu odstraníme vytvořením obsluhy události OnClose podřízeného formuláře s příkazem:
    Action = caFree;
    Toto je jediná metoda podřízeného okna. Rám MDI aplikace nyní vybavíme následující nabídkou:
    Soubor Okno
    Nové Kaskáda
    ---- Dlaždice
    Konec Uspořádat ikony
    Obsluhy voleb v nabídce již vytvořte sami.
  5. Nyní se pokusíme vytvořit další MDI aplikaci. Začneme vývoj nové aplikace. U formuláře změníme tyto vlastnosti: Name nastavíme na MainForm, Caption na Prohlížeč obrázků, Height na 450, Width na 575 a FormStyle na fsMDIForm. K formuláři přidáme také nabídku. Na formulář vložíme komponentu MainMenu, změníme její vlastnost Name na MainMenu a dvojitým kliknutím na této komponentě zobrazíme Návrhář nabídky. V místní nabídce Návrháře zvolíme Insert from Template a ze seznamu nabídek vybereme MDI Frame Menu. Naše nabídka bude v angličtině. Okno Návrháře nabídky již můžeme uzavřít. Na formulář přidáme dále komponentu OpenPictureDialog (změníme Name na OpenDialog a Title na Otevření obrázku) a komponentu SavePictureDialog (změníme Name na SaveDialog a Title na Uložení obrázku.

  6. Dále se budeme zabývat obsluhou voleb File | Open a File | Save As. Obsluha volby File | Open je tvořena příkazy:
    if (OpenDialog->Execute()) {
      TChild* child = new TChild(this);
      child->Image->Picture->LoadFromFile(OpenDialog->FileName);
      child->ClientWidth = child->Image->Picture->Width;
      child->ClientHeight = child->Image->Picture->Height;
      child->Caption = ExtractFileName(OpenDialog->FileName);
      child->Show();
    }
    Nejprve je provedeno dialogové okno otevření souboru. Pokud jej uzavřeme stiskem tlačítka OK, pak je vytvořen nový objekt TChild (třída, kterou vytvoříme později). Soubor obrázku je zaveden do komponenty Image na podřízeném formuláři, klientská oblast je změněna na rozměry obrázku a vlastnost Caption je nastavena na jméno zobrazovaného souboru. Funkce ExtractFileName je použita k extrakci samotného jména souboru obsaženého ve vlastnosti FileName komponenty OpenDialog. K získání jiných částí specifikace souboru existují další funkce: ExtractFilePath, ExtractFileDir, ExtractFileDrive a ExtractFileExt.
    Nesmíme zapomenout na naši předchozí diskusi o volání delete pro všechny objekty vytvořené pomocí new. Je předchozí kód výjimkou z tohoto pravidla? Ve skutečnosti není, protože VCL přebírá zodpovědnost za uvolňování paměti alokované pro podřízené okno MDI. Konstruktor TChild přebírá parametr this, který říká VCL, že vlastníkem podřízeného okna MDI je okno hlavního formuláře MDI. Při uzavření aplikace je také zajištěno zrušení všech podřízených objektů MDI.
    Obsluha volby File | Save As bude vypadat takto:
    TChild* child = dynamic_cast<TChild*>(ActiveMDIChild);
    if (!child) return;
    if (SaveDialog->Execute()) {
      child->Image->Picture->SaveToFile(SaveDialog->FileName);
    }
    První dva řádky tohoto kódu zjistí, zda podřízené okno MDI je aktivní. Pokud ano, pak je zobrazeno dialogové okno uložení souboru a obrázek je uložen. V předchozím kódu je použit speciální operátor C++ dynamic_cast. Tento operátor se používá k přetypování ukazatele třídy předka na ukazatel na odvozenou třídu. Vlastnost ActiveMDIChild vrací ukazatel na objekt TForm. V našem případě, pokud se jedná o ukazatel na TChild (typ uvedený ve špičatých závorkách; naše třída podřízeného okna MDI je odvozená od TForm), pak zpřístupníme vlastnost Image podřízeného formuláře. Pokud není možno provést přetypování, je vráceno NULL. Přetypování pomocí dynamic_cast je lepší (bezpečnější) než starý způsob přetypování.
    Obsluhy voleb Tile, Cascade a Arrange All vytvořte sami.
    Nyní již můžeme vytvořit podřízený formulář MDI. K aplikaci přidáme nový formulář a změníme u něj vlastnosti Name na Child, Caption necháme beze změny (je nastavován za běhu) a FormStyle na fsMDIChild. Na tento formulář vložíme komponentu Image, její vlastnost Name změníme na Image, vlastnost Stretch na true, Align na alClient.
    Do jednotky hlavního formuláře je také nutno přidat hlavičkový soubor podřízeného okna. Aplikace je hotova, ale má zatím několik nedostatků. Některé volby v nabídce nejsou obslouženy, tím se již ale nebudeme zabývat. Další nedostatek je automatické vytvoření podřízeného formuláře při spuštění aplikace. Podřízený formulář je nutno vyřadit ze seznamu podřízených formulářů (tím přebíráme odpovědnost za jeho vytvoření). Problémy také vznikají, když otevřeme soubor, který není obrázkem. Těmito problémy se již nebudeme zabývat.
  7. Pro MDI aplikace (např. textový editor) potřebuje hlavní nabídka získávat prvky nabídky z jiných (podřízených formulářů). Tomuto procesu říkáme slučování nabídek. Při práci s nabídkami používáme vlastnost Menu formuláře a vlastnosti GroupIndex prvků nabídky. Vlastnost Menu určuje aktivní nabídku formuláře (můžeme ji měnit i za běhu aplikace). Vlastnost GroupIndex určuje pořadí, ve kterém slučujeme prvky nabídky. Slučované prvky mohou nahradit prvky na hlavním řádku nabídky (stejná hodnota GroupIndex) nebo mohou být vloženy.

  8. Slučování nabídek si ukážeme na jednoduché aplikaci (nejedná se o MDI aplikaci). Hlavní formulář bude obsahovat nabídku (v závorkách jsou uvedeny hodnoty vlastností GroupIndex):
    Soubor (0) E-Mail (1)  Nápověda (2)
    Nový Zobraz modálně  O aplikaci
    Otevřít Zobraz nemodálně
    Uložit Zavři nemodální
    -----
    Konec
    Volbu Zavři nemodální zakážeme. K aplikaci přidáme další formulář, který bude mít nabídku:
    E-Mail Soubor (1)
    Otevřít
    Uložit
    ------
    Zavřít
    Abychom si ukázali, že tento přístup funguje s modálním i nemodálním formulářem, jsou v nabídce E-Mail volby pro zobrazení okna oběma způsoby. Obsluha volby Zobraz modálně je tvořena příkazem:
    Form2->ShowModal();
    Formuláře tohoto programu jsou vytvořeny při spuštění aplikace. Když program zobrazí druhý formulář jako modální, pak není možno používat nabídku hlavního formuláře. Pokud formulář zobrazíme nemodálně, měli bychom zakázat ručně volby Zobraz modálně a Zobraz nemodálně (v tomto případě není vhodné umožnit vícenásobné zobrazení formuláře). Obsluha volby Zobraz nemodálně tedy bude tvořena příkazy:
    Zobrazmodln1->Enabled = false;
    Zobraznemodln1->Enabled = false;
    Zavinemodln1->Enabled = true;
    Form2->Show();
    Poslední volba v nabídce E-Mail provádí zavření nemodálního formuláře. Obsluha bude tvořena příkazem:
    Form2->Close();
    Po uzavření nemodálního formuláře, bychom ale měli vrátit nabídku do původního stavu. Formulář lze ale uzavřít mnoha způsoby. Místo opakování kódu pro povolení voleb nabídky na mnoha místech je můžeme napsat pouze do obsluhy události OnClose pro druhý formulář:
    Form1->Zobrazmodln1->Enabled = true;
    Form1->Zobraznemodln1->Enabled = true;
    Form1->Zavinemodln1->Enabled = false;
    Nyní se ale programové jednotky obou formulářů odkazují na sebe navzájem. Do jednotky druhého formuláře musíme tedy přidat vložení hlavičkového souboru prvního formuláře. Pro druhý formulář vytvoříme ještě obsluhu volby Zavřít s příkazem Close();. Nyní již aplikaci můžeme vyzkoušet (zatím se ještě nejedná o slučování nabídek). Na tomto příkladě vidíme, že obsluha modálních formulářů je jednodušší než nemodálních formulářů.
    Aby naše aplikace měla nějaký význam, umístěte na hlavní formulář komponentu ListBox s telefonními čísly svých známých a na druhý formulář také ListBox, tentokrát s E-Mail adresami. Vytvořte také ostatní obsluhy voleb v nabídce (ukládání seznamů do souborů, otevírání souboru atd.).
  9. V předchozí aplikaci měly oba formuláře vlastní nabídky. C++ Builder ale podporuje slučování nabídek. Je to prováděno takto. Hlavní formulář má stále nabídku. Ostatní formuláře mají nabídky s nastavenou vlastností AutoMerge na true (jejich nabídka potom nebude zobrazována ve formuláři, ale bude spojena s hlavní nabídkou). Slučování proběhne podle hodnot vlastností GroupIndex. V našem příkladě nabídka E-Mail Soubor nahradí nabídku E-Mail (obě mají stejnou hodnotu GroupIndex). Slučování nabídek má význam pouze pro nemodální formuláře (u modálních formulářů se k nabídce nedostaneme, takže C++ Builder slučování neprovádí). Po uzavření nemodálního formuláře se ale neobnoví původní hlavní nabídka (formulář není zrušen, pouze se skryl). Problém vyřešíme zásadní změnou přístupu k obsluze druhého formuláře. Místo vytváření objektu formuláře při spouštění aplikace jej musíme vytvořit a zrušit pokaždé, když má být zobrazen nebo skryt. V dialogovém okně Project Options na stránce Forms zrušíme automatické vytváření druhého formuláře. Abychom mohli formulář zobrazit jako modální, musíme jej vytvořit, nastavit jeho vlastnosti a později jej zrušit. Obsluha volby Zobraz modálně bude tedy tvořena příkazy:

  10. Form2 = new TForm2(this);
    Form2->MainMenu1->AutoMerge = false;
    Form2->ShowModal();
    Form2->Free();
    Pro Zobraz nemodálně to bude:
    Form2 = new TForm2(this);
    Form2->MainMenu1->AutoMerge = true;
    Form2->Show();
    Při této volbě formulář zrušen není (není zde již nutné zakazovat volby v nabídce). Aby při uzavření formuláře byl formulář opravdu zrušen, musíme ještě u události OnClose druhého formuláře nastavit hodnotu parametru Action na caFree. Nyní je již aplikace hotova.
  11. Zatím vytvořené MDI aplikace neprováděly nic rozumného. Obdrželi jsme program s typickým chováním MDI, ale s žádným praktickým užitkem. S podřízeným formulářem můžeme ale provádět cokoliv. Můžeme přidat různé komponenty, vytvořit editory apod. Kterýkoli z programů, které jsme zatím vytvořili můžeme převést na MDI aplikaci (u některých to ale nedává smysl). Naší skutečnou první MDI aplikací bude jednoduchá verze grafického programu. Tento program na místě kliknutí myší zobrazí kruh. Podřízené okno obsahuje tuto nabídku (v aplikaci již použijeme slučování nabídek):
  12. Kruh
    Barva výplně ...
    Barva okraje ...
    Šíře okraje ...
    ---------------
    Získej pozici
    Důležité je to, že při programování podřízeného formuláře nemusíme brát v úvahu existenci jiných formulářů (včetně formuláře rámu). Jediný speciální případ je slučování nabídek. Vezmeme rám ze druhého zadání této kapitoly s jedinou změnou, kterou v něm provedeme je nastavení vlastností GroupIndex pro nabídku Soubor na 1 a pro nabídku Okno na 3. To je vše, co na tomto formuláři změníme. GroupIndex u nabídky Kruh v podřízeném formuláři nastavíme na 2. U podřízeného formuláře nastavíme tyto vlastnosti: Caption na Podřízené okno, Color na clTeal, FormStyle na fsMDIChild, Menu na MainMenu1 a Position na poDefault. Do soukromé části podřízeného formuláře přidáme deklarace:
    int XStred, YStred;
    int SireOkraje;
    TColor BarvaOkraje, BarvaVyplne;
    Nyní již zbývá vytvořit několik obsluh událostí. Obsluha OnCreate formuláře je tvořena příkazy:
    XStred = -200;
    YStred = -200;
    SireOkraje = 1;
    BarvaOkraje = clBlack;
    BarvaVyplne = clYellow;
    Obsluhu OnPaint vytvoříme příkazy:
    Canvas->Pen->Width = SireOkraje;
    Canvas->Pen->Color = BarvaOkraje;
    Canvas->Brush->Color = BarvaVyplne;
    Canvas->Ellipse(XStred-30, YStred-30, XStred+30, YStred+30);
    Obsluhu OnMouseDown tvoří příkazy:
    XStred = X;
    YStred = Y;
    Repaint();
    Obsluha OnClose je stejná jako v předcházející MDI aplikaci. Zbývá ještě vytvořit několik obsluh voleb v nabídce. Např. obsluhu volby Šířeokraje tvoří příkazy:
    AnsiString VstupniRetezec = IntToStr(SireOkraje);
    if (InputQuery("Okraj", "Zadej šířku", VstupniRetezec)){
      SireOkraje = VstupniRetezec.ToIntDef(SireOkraje);
      Repaint();
    }
    Ostatní obsluhy voleb vytvořte sami. Výpis informací volbou Získej pozici provádějte funkcí ShowMessage. Aplikaci můžeme vyzkoušet. Každý podřízený formulář může obsahovat pouze jeden kruh. Podřízených formulářů můžeme otevřít více.
  13. Obecně mohou MDI aplikace obsahovat více druhů podřízených formulářů. Ukážeme si to na rozšíření předchozí aplikace. Budeme muset vytvořit nový podřízený formulář. Tentokrát vytvoříme formulář s pohybujícím se čtvercem, který se bude odrážet od okrajů. K předchozí aplikaci přidáme další formulář, přidáme k němu následující nabídku:
  14. Čtverec Pohyb
    Barva výplně ... Start
    --------------- Stop
    Získej pozici
    U obou těchto nabídek nastavíme GroupIndex na 2 a volbu Start zakážeme. U formuláře nastavíme tyto vlastnosti: AutoScroll na False, Caption na Pohybující se čtverec, Color na clAqua, FormStyle na fsMDIChild, Menu na MainMenu1, Position na poDefault a Visible na True. Na formulář vložíme dále komponentu Shape a nastavíme pro ní vlastnosti: Left na 40, Top na 48, Width a Height na 30, Brush.Pen na clFuchsia, Pen.Color na clBlue, Pen.Width na 2 a Shape na stSquare. Na formulář přidáme ještě komponentu Timer a nastavíme jeho vlastnost Interval na 200. Nyní se již můžeme zabývat pohybem čtverce. Nejprve nadefinujeme výčtový typ Smery s možnými směry pohybu čtverce. Deklaraci tohoto typu umístíme do hlavičkového souboru před deklaraci typu formuláře:
    enum Smery {nahoru_vpravo, dolu_vpravo, dolu_vlevo, nahoru_vlevo};
    Jako soukromou položku třídy formuláře přidáme:
    Smery Smer;
    Obsluhu OnCreate bude tvořit příkaz:
    Smer = dolu_vlevo;
    a obsluha OnClose je tvořena obvyklým příkazem. Obsluhu volby nabídky Start tvoří příkazy:
    Timer1->Enabled = true;
    Start1->Enabled = false;
    Stop1->Enabled = true;
    Obsluhy ostatních voleb v nabídce vytvořte sami. Zbývá ještě vytvořit obsluhu události OnTimer časovače. V této obsluze budeme měnit souřadnice čtverce. Začátek obsluhy je tvořen příkazy:
    switch (Smer){
      case nahoru_vpravo:
        Shape1->Left += 3;
        Shape1->Top -= 3;
        if (Shape1->Top <= 0) Smer = dolu_vpravo;
        if (Shape1->Left+Shape1->Width>=ClientWidth) Smer = nahoru_vlevo;
        break;
      case nahoru_vlevo:
      ...
    }
    Zbytek obsluhy vytvořte sami. Tím je hotov druhý podřízený formulář. Musíme ještě změnit hlavní formulář. Změníme zde nabídku:
    Soubor Okno
    Nový kruh Kaskáda
    Nový čtverec Dlaždice vodorovně
    Uzavřít vše Dlaždice svisle
    --------------- Uspořádat ikony
    Konec Počet
    V nabídce Soubor si ukážeme obsluhu volby Uzavřít vše (ostatní vytvořte sami):
    for (int I = MDIChildCount - 1; I >= 0; I--) MDIChildren[I]->Close();
    Volba Dlaždice vodorovně bude obsloužena příkazy:
    TileMode = tbHorizontal;
    Tile();
    Obdobně vytvořte obsluhu volby Dlaždice svisle (tbVertical). Obsluhu volby Počet tvoří příkazy:
    int NCtverec = 0, NKruh = 0;
    for (int I = 0; I < MDIChildCount; I++)
     if (MDIChildren[I]->ClassNameIs("TForm3")) NCtverec++;
     else NKruh++;
    ShowMessage("Jsou zde "+IntToStr(MDIChildCount)+" podřízené formuláře.\n"
                +IntToStr(NKruh)+" je podřízených oken kruhů a \n" +
                IntToStr(NCtverec) + " podřízených oken čtverců.");
    Ostatní obsluhy vytvořte sami a aplikaci vyzkoušejte.
19. MDI