A Delphi programozási nyelv

MDI (Multiple Document Interface) alkalmazások

Az  MDI (többdokumentumos felület) egy hasznos és elterjedt technika a Windows alkalmazások szerkezetének felépítésére.
Az MDI alkalmazások egy főformból és az azon belül megjelenő formokból állnak. A főform a szülő, míg a többit gyermek ablaknak nevezzük.




MDI alkalmazások általában a következők:  a szövegszerkesztők egy része, fejlesztő programok, internetes böngészők (az MS Word már nem ilyen egy ideje - az SDI).
Ahol több dokumentum megnyitására van lehetőség vagy szükség, ott érdemes lehet ezt a módszert használni.

MDI működése a Windowsban

Az MDI szerkezet több előnyt is nyújthat a programozónak. Ha Delphi MDI sablont használunk, akkor több funkció van, ami már eleve működik és nem kell leprogramozni. Például a gyermekablakok listája megtalálható a főform egy lenyíló menüjében, illetve az ablakok mozaikszerűen elrendezhetőek egy gombnyomás segítségével. A fenti pillanatkép egy olyan MDI alkalmazás futása közben készült, amelybe egy sor kódot sem írtam, alapban ezt adja a Delphi, ha elindítjuk egy új MDI alkalmazás fejlesztését (File/New/Other/MDI application).

Az MDI alkalmazások felépítése a Windowsban a következő:
- Az alkalmazás fő ablaka keretként vagy tárolóként viselkedik
- Van egy MDI ügyfélként ismert ablak, amely az MDI keretablak teljes területét befedi (sötétszürke rész a képen)
- Az MDI alkalmazásoknak több azonos vagy különböző típusú gyermekablaka van. Ezek nem közvetlenül a keretablakba helyeződnek, hanem az MDI ügyfél ablak gyermekei  (az pedig a kereté)

Keret- és gyermekablakok

A Delphi megkönnyíti az MDI alkalmazások készítését, még a Delphi által előre elkészített MDI alkalmazássablon nélkül is (File/New/Other/MDI application).
Elég csupán két formot létrehoznunk (minimum) az MDI alkalmazásunkhoz:
- az egyiknek a FormStyle tulajdonságát fsMDIForm-ra kell állítani
- a másikét pedig fsMDIChild-ra
Rögtön indíthatjuk is a programot, és a két ablak a várt módon jelenik meg:



Általában azonban a gyermekablakok nem jönnek létre a program indításakor, hanem futás közben dinamikusan hozzuk létre őket.
Ezt úgy oldhatjuk meg, hogy menüt készítünk, és a menüben csinálunk pl. egy New Element menüt, amelyhez a következő kódot rendeljük hozzá:

procedure TMainForm.NewElement1Click(Sender: TObject); var   ChildForm : TChildForm; begin   ChildForm := TChildForm.Create(Application);   ChildForm.Show; end;

Hogy a program helyesen működjön, a következőképpen kell kiegészíteni a fenti programkódot:

procedure TMainForm.NewElement1Click(Sender: TObject); var   ChildForm : TChildForm; begin   WindowMenu:=Window1;   Inc(counter);   ChildForm := TChildForm.Create(Self);   ChildForm.Caption:=ChildForm.Caption+ ' ' + IntToStr(counter);   ChildForm.Show; end;

Fontos még, hogy a kezdetkor csak a főform jelenjen meg, gyerek pedig ne. Ezt a Project / Options párbeszédablaka Forms oldalán állíthatjuk be.
megj: Előfordulhat, hogy a Window menüben nem jelenik meg a gyerekformok listája, amíg egy másik almenüt (Pl: itt Cascade) be nem teszük.



A gyermekablkakokat megnyithatjuk, kis méretre, ill. teljes méretre állíthatjuk, viszont a bezárás nem úgy működik, ahogy várnánk.
Bezárás helyett lekicsinyíti az ablakokat. Ez a Delphi alapvető működésével függ össze - a Delphi, ha lehet, nem zárja be az ablakokat, hanem elrejti, de MDI esetén ez nem működik.
Ahhoz, hogy valóban eltűnjön egy ablak, az OnClose esemény Action paraméterét caFree-re kell állítani, megsemmisítve ezzel az ablakot.

procedure TChildForm.FormClose(Sender: TObject; var Action: TCloseAction); begin     Action:=caFree; end;

Ablak menü felépítése TForm tagfüggvényekkel és egyéb hasznos MDI tagfüggvények

Hogy szabadon készíthessünk tetszőleges MDI alkalmazást és menüt hozzá, illetve hogy jobban átlássuk a működését, érdemes megnézni, hogyan készíthetünk saját Window menüt.
(Emlékeztetőül jegyzem meg, hogy a Delphinek van előre elkészített MDI sablonja File/New/Other/MDI application menü alatt, tehát nem muszáj nekünk megcsinálni mindig, csak ha testre akarjuk szabni.)

A Window menünek általában három eleme van: Cascade (lépcsőzetes elrendezés), Tile (mozaikszerű elrendezés), Arrange Icons (Ikonok elrendezése).
Erre a TFormnak beépített tagfüggvényei vannak a fenti menüknek megfelelően, amelyeket meghívhatunk : Cascade(), Tile(), ArrangeIcons().
Ezeket csak MDI keretek esetén lehet használni.

Van egy másik, jobb megoldás is.
Helyezzünk a formra egy ActionList komponenst, és rendeljük hozzá az előre meghatározott, kész MDI műveleteket. (Klikk ActionList-re, majd New Standard Action és a tree-be a Window alatt)
Majd hozzuk létre a Window menüben a megfelelő almenüket és azok action tulajdonságát állítsuk be a megfelelő action-ökre. Ezután már működnek is a parancsok.






További hasznos tagfüggvények és tulajdonságok, amelyek Delphiben kizárólag az MDI-hez kapcsolódnak:

- ActiveMDIChild tulajdonság - Az MDI keretform futási idejű csak olvasható tulajdonsága. Visszaadja az aktív gyerekablakot. Ez megváltozik, ha a felhasználó egy másik gyerekablakra kattint, illetve ha programban meghívjuk a Next, vagy a Previous eljárásokat, amelyek a következő ill. az előző gyermekablakra váltanak át.

- ClientHandle tulajdonság - az MDI ügyfélablak (lásd fent) Windows leíróját tartalmazza

- MDIChildren tulajdonság - a gyermekablakok tömbje, illetve ehhez tartozik a következő

- MDIChildCount - a gyermekablakok száma

A gyermekablakok belső sorrendje a működésbe hozatal sorrendjének fordítottja. Ez sorrend határozza meg az ablakok elhelyezkedését a képernyőn. A lista elején lévő ablak lesz legfelül.
Képzeljünk el egy geometriai tengelyt (a z tengely), amely a képernyőből kifelé mutat. Amelyik ablak magasabb koordinátával bír, az van feljebb (előrébb), így az takarja a többit (ez az ún. z-sorrend).

MDI alkalmazások készítése különböző gyermekablakokkal

Összetettebb MDI alkalmazások általában különböző fajtájú (vagyis eltérő gyermekformú) gyermekablakokból állnak.
Ezzel kapcsolatban felmerül néhány probléma, amire most kitérünk.

Hozzunk létre egy másik gyermek formot (ChildOther). Ez abban különbözzön az előzőtöl, hogy ennek lesz saját menüje, és ebbe teszünk egy AddCircle és egy Clear metódust.

Az AddCircle menü segítségével véletlenszerűen tudunk kirajzolni köröket, a Clear segítségével pedig töröljük az ablak tartalmát (a köröket).

procedure TChildOther.AddCircle1Click(Sender: TObject); var x,y:integer; begin     randomize;     x:=random(300);     y:=random(300);     Canvas.Ellipse(x-20,y-20,x+20,y+20); end; procedure TChildOther.Clear1Click(Sender: TObject); begin     Refresh; end;


Állítsuk be ezt is gyermekformnak (FormStyle tulajdonságát fsMDIChild-ra állítsuk).
Mivel MDI alkalmazásnál egy gyermekablaknak nem lehet saját menüje, ezért ez zavart okoz. Ilyenkor azt láthatjuk, hogy a gyermekablak menüje bekerül a főablak menüjének helyére, ami gond.
A gyermekablak és a keretablak menüjét egyesíthejük, ha beállítjuk a menükhöz tartozó GroupIndex tulajdonságokat, mondjuk így:
- File menü a MainFormon : 1
- Circle menü a ChildOther formon: 2
- Window menü a MainFormon : 3

Ekkor, ha egy TChildOther típusú gyermekablak aktív, akkor a menüben megjelenik a hozzá tartozó menü pluszban:



Míg ha a másik típus ablakra váltunk, akkor a menü eltűnik:




Az MDI ügyfélablakra mozaikszerű háttér rajzolása

A mozaikszerű háttérképek beállítását támogatja Delphi. A rendszer a Windows wm_EraseBKgnd üzenetkezelője segítségével festi a formra. Ehhez kellene írni egy kezelőt.
(Az üzenetek kezelésére egy külön utasítás van: a message kulcsszóval egyetlen var paraméterrel rendelkező eljárásokat írhatunk objektum tagfüggvényként.)
Ez a módszer itt nem működne azonban, mert a főform ablakát eltakarja a rajta lévő ügyfélablak (lásd fent).

Ezért egy alacsony szintű Windows programozási módszert használunk, az ún. alosztályozást (subclassing). Ennek az a lényege, hogy az ablak összes érkező üzenetét fogadó ablakeljárást egy új, általunk megírttal helyettesítjük. Erre a SetWindowLong API függvényt használjuk. Minden ablakhoz tartozik egy ilyen ún. ablakeljárás, amely fogadja az üzeneteket. A Delphi ablakok esetében ezek továbbítódnak a form megfelelő üzenetkezelő tagfüggvényeihez. A Delphi formok is rendelkeznek ablakeljárással, csak ez rejtve  marad - a WndProc virtuális függvényt hívják meg. Csak akkor kell a Windows ablakeljárásokat nekünk kezelni, ha nem Delphi ablakról van szó - mint pl. most, az MDI ügyfélablak esetén.
Itt azért nem részletezem az üzenetkezelés módszerét, mert elég nagy témakör, és ahhoz, hogy itt felhasználhassunk, most elegendő ennyit tudnunk róla.

A MainFormhoz vegyük hozzá a következőket:

private     OldwinProc,NewWinProc : Pointer; // az eredeti és az általunk kiegészített új ablakeljárásra mutató pointerek     procedure NewWinProcedure(var Msg: TMessage);   // az új ablakeljárásunk

Ahhoz, hogy a tagfüggvényünket ablakeljárásként lehessen használni, MakeObjectInstance függvényt meg kell rá hívni, hogy a rendszer függvényként kezelni tudja.

procedure TMainForm.FormCreate(Sender: TObject); begin     counter2:=0; NewWinProc:=MakeObjectInstance(NewWinProcedure);   // tagfüggvényünk a rendszer által kezelhető alakra hozza     OldWinProc:=Pointer (SetWindowLong(ClientHandle, gwl_WndProc,Cardinal(NewWinProc)));  // eredeti ablakeljárás     OutCanvas:=TCanvas.Create;    // egy OutCanvas nevű tagváltozót írjunk be a MainForm tagváltozói közé, amely segíti a gyorsabb kirajzolást end;

Az általunk kiegészített ablakkezelő eljárás pedig ez

procedure TMainForm.NewWinProcedure(var Msg: TMessage); var   BmpWidth, BmpHeight,i,j: Integer; begin   // az alapértelmezett feldolgozás elõször Msg.Result:=CallWindowProc(OldwinProc, //ClientHandle,Msg.Msg,MSG.WParam,Msg.LParam);   // kiegészítés - háttér újrafestés if Msg.Msg= wm_EraseBkgnd then    // - ha wm_EraseBkgnd üzenet jött, akkor fesse újra a hátteret   begin     BmpWidth:= MainForm.Image1.Width;     BmpHeight:= MainForm.Image1.Height;     if (BmpWidth<>0) and (BmpHeight<>0) then     begin       OutCanvas.Handle:=Msg.WParam;        for I:=0 to MainForm.ClientWidth div BmpWidth do         for J:=0 to MainForm.ClientHeight div BmpHeight do           begin            OutCanvas.Draw( I*BmpWidth, J*BmpHeight, MainForm.Image1.Picture.Graphic);   // kirajzolás, egy tetszőleges Image objektumból vehetjük a képet           end;     end;   endend;

Íme a végeredmény:



Példa program

Az fenti leírás alapján elkészült példa program letöltehető verziója megtalálható itt:
Programs/MDI_example_application.zip

A program Borland Delphi Enterprise 6.0-ban készült.
Szerző neve: Kovács István
Készítés éve: 2008