Mono és a Gtk# áttekintés

Bevezető: 

Eredeti cikk: http://hup.hu/cikkek/20080421/mono_es_gtk_sharp_attekintes

Szerző: Nagy Gábor

Nem olyan régen rászántam egy napomat és megismerkedtem a .NET csodáival, a C# nyelv rejtelmeivel, ezért szeretném megosztani a tapasztalataimat. Áttekintő jellegű leírást magyar nyelven sajnálatos módon nem találtam, pedig már nem olyan fiatal a terület.

Ha jól emlékszem 4 évvel ezelőtt kaptam egy C# könyvet. Konkrétan a C# mesteri szinten 21 nap alatt. El is kezdtem olvasni, viszont minden oldalon szerepelt az a mondat, hogy: Ez a funkció jelenleg csak a Microsoft .NET megvalósításban érhető el. Mivel már akkor is Linux-ot használtam desktopra, így gyorsan kedvemet szegte. Talán még egy HelloWorld-ot kipróbáltam az 1.0 alatti valamelyik béta Mono-val, viszont gyorsan halottnak könyveltem el a dolgok a Javaval szemben.

Jelenleg a C# nyelv erős szabványosítása miatt, valamit az erős háttérnek, továbbá a két párhuzamos implementációnak köszönhetően, úgy gondolom megállja a helyét. A tervezés és a megvalósítás utolérte, s talán mostanra le is előzte a Java lehetőségeit. Talán még a beágyazott rendszereken, és a mobil készülékeken láthatunk érdekes dolgokat a jövőben.

Feladatnak egy GPS Data logger meghajtóprogramjának megírását választottam. Az eszköz USB-re csatlakozik, egy soros átalakító van benne, ami PL2303-ként jelenik meg a rendszerben. Ezek után erre tudunk rácsatlakozni, és kommunikálni, letölteni a rögzített adatokat.

A döntésem azért a .NET-re esett, mivel Javaban már megtanultam régebben programozni, valamint ott csak külső class segítségével lehet elérni a soros portot (RxTx). A .NET viszont 2.0-s megjelenése óta támogatja az IO Port kezelést, így könnyedén kommunikálhatunk soros eszközeinkkel platformfüggetlenül. (Sportszerű nehezítés, hogy hiába érhető el driver Mac OS X alá, a Mono jelenlegi verziójában nem képest azt elérhető soros portként kilistázni...)

1. WinForms és Gtk#

Mivel először WinForms-szal próbálkoztam meg összerakni az alkalmazás kezelői felületét, rá kellett ébrednem, hogy bizony ennek a megvalósítás 1.9-es (2.0 beta) Mono változatban még közel sem teljes. A fejlesztést Mac OS X (Tiger) operációs rendszeren végeztem, ahol szerencsére már elkészült a natív GTK port, így már mindhárom főbb platformon lehet GTK#-ot használó alkalmazások futtatni. A döntés egyetlen hátránya, hogy a majdani kliens számítógépen nem elég a Microsoft .NET Framework telepítése (Windows esetén), hanem Mono-t igényel.

Fordításhoz szükséges beállítások:


export PKG_CONFIG_PATH=/Library/Frameworks/Mono.framework/Versions/1.9/lib/pkgconfig/

Esetemben így nézett ki. Ez arra a célra szolgál, hogy a rendszer megtalálja a gtk-sharp-2.0.pc fájlt. Más rendszereken ilyen hiba esetén célszerű locate segítségével felderíteni, hol található a fájl, majd exportálni a PATH-ben a könyvtárat. Tehát:

$ locate gtk-sharp-2.0.pc
/Library/Frameworks/Mono.framework/Versions/1.9/lib/pkgconfig/gtk-sharp-2.0.pc

Ha már így rendelkezésünkre áll, akkor a fordításánál pkg paraméter segítségével használhatjuk is.

$ gmcs -pkg:gtk-sharp-2.0 [gtk-t használó forrás fájl.cs]

Megjegyzés: mcs segítségével 1.1-es .NET platformra, még gmcs segítségével 2.0-sra tudunk fordítani

2. Az első GTK# alkalmazásunk

Mivel MonoDevelep Mac OS X-es változata nem támogatja a GTK# fejlesztést, valamint maga az IDE eszköz is hiányosnak és lassúnak tűnt, megmaradtam a Vim használatánál. Az XCode plugint szintén lustaság okán nem állítottam be, amúgysem használom, hiába hallottam róla jókat.

A fejlesztést továbbiakban két terminálban folytatjuk. Az egyikben a Vim, vagy kedvenc szerkesztőnk állandó jelleggel mutatja a forráskódot. A másikban a fent említett PATH beállítva, az ismertetett módon fordítjuk és futtatjuk az alkalmazást. Lelkesebbek erre írhatnak scriptet is...

Az első programunk kódja a következő lesz:

$ cat FirstGtkApp.cs

using System;
using Gtk;

public class GtkHelloWorld {

GtkHelloWorld() {
Application.Init();

Window myWin = new Window("My first GTK# Application!");
myWin.DeleteEvent += new DeleteEventHandler (OnWinDelete);
myWin.Resize(200,200);

Label myLabel = new Label();
myLabel.Text = "Hello World!!!!";

myWin.Add(myLabel);

myWin.ShowAll();

Application.Run();
}

public static void Main() {
new GtkHelloWorld();
}

private void OnWinDelete (object o, DeleteEventArgs args) {
Application.Quit();
}

}

Először két névteret kell használatba vennünk a grafikus alkalmazás elkészítéséhez. Ezek a Gtk, és System namespace. Első felelős a nekünk szükséges elemek bekerüléséért.

Az alkalmazás Main függvényében példányosítjuk az osztályunkat. Ez eredményezi a konstruktor lefutását, amiben ténylegesen megvalósítjuk a megjelenítést. Először az alkamazást inicializáljuk az Application.Init(); függvényhívással. Későbbiekben ehhez hasonlóan az Application.Run(); segítségével fogjuk futásra bírni.

Előtte azonban még létre kell hoznunk egy ablakot, amire rajzolni szeretnénk. Ez a Window osztály egy példányának létrehozásávál történik. Konstruktorába az alkalmazás neve kerül. Ezt a későbbiek során Window.Title publikus string változó módosításával tudjuk átállítani. A Window.DeleteEvent-hez rendelünk egy eseménykezelő függvényt, ami azért fog felelni, ha bezárjuk az ablakot, akkor az alkalmazás is lépjen, és visszakapjuk a konzolt.

Következő lépésben a Label osztály segítségével egy cimkét hozunk létre. Itt is használhatjuk a konstruktort a szöveg megadására, vagy az itt alkalmazott módon tudjuk módosítani.

Ezek után már csak rá kell helyeznünk a Window osztály Add() függvényével a Widget-ünket az ablakra. Majd beállítani, hogy minden megjelenjen a képernyőn. Az elemek láthatóságát külön is lehet állítani. Erre az egyes osztályok Show() függvénye szolgál. Jól jön akkor, ha bizonyos funkciók csak események hatására lesznek elérhetőek.

Most már csak fordítanunk és futtatnunk kell az alkalmazást.

$ gmcs -pkg:gtk-sharp-2.0 FirstGtkApp.cs
$ mono FirstGtkApp.exe FirstGtkApp.exe

Megjegyzés: Azért nem a Main függvénybe került az egész kód, mert későbbiek során is hajlamosak lennénk ott hagyni, és bizonyos esetekben a static definíció miatt a fordító különféle warningokkal ajándékozna meg minket. Természetesen a helyes megoldás, hogy minden ablakot külön függvényben írunk le, és igény szerint hívjuk meg őket.

3. Tárolók

Ezeket az osztályokat használjuk a képernyőn megjeleníteni kívánt elemeink elrendezésére. Ugyanazt a célt szolgálják, mint Javaban a layout-ok. Az ablakokat gyakorlatilag területekre osztjuk fel, amikbe belepakoljuk a látványelemeket.

Az elérhető alap típusok listája (nem teljes):

  • Fixed: rögzített kinézet hozható vele létre, pixelre pontosan meg tudjuk adni, hogy mi hova kerüljön
  • VBox: vertikális felosztása az adott területnek, amit hozzáadunk, az automatikusan függőleges oszlopba rendeződik
  • HBox: előzőhöz hasonlóan, csak horizontálisan történik az elhelyezés
  • Table: az általunk definált méretű táblazatot hozhatunk létre, aminek a rácspontjaira feszíthetjük ki az elemeinket
  • Frame: elemek keretbe foglalására szolgáló tároló
  • ScrolledWindow: amit belehelyezünk, az scrollozhatóvá válik, ha nem fér ki a képernyőre (pl.: TextView)

A tárolók méretét a Widget osztályból örökölt SetSizeRequest(int x, int y) metódus segítségével állíthatjuk be.

Az ablakhoz történő hozzáadásuk ugyanúgy történik, ahogy a többi elemé is, a Window.Add() függvényben paraméterként megadva.

Megjegyzés: Véleményem szerint érdemes először egy VBox tárolót elhelyezni a képernyőn. Ebbe helyezni a menu sort, az ablak középső részét, valamint a státus sort. Ezek után pedig a középső rész a megfelelő rétegekkel igény szerint feltölteni.

3.1 Fixed tároló

A tárolónak van paraméter nélküli konstruktora, legegyszerűbb azt használni a létrehozásnál.

Button testButton = new Button("Teszt");

Fixed fixArea = new Fixed();
fixArea.SetSizeRequest(100,100);

fixArea.Put(testButton,10,10);

Ezzel létrehoztunk egy 100x100 négyzetet, aminek a (10,10) pontjába helyeztük a Teszt feliratú gombunkat. Az elemek bal felső sarka kerül mindig az általunk megadott koordinátára.

3.2 VBox és HBox tároló

A következő példában három gombot fogunk létrehozni. Minden a Teszt felirat szerepel sorszámozva. Legfelül lesz az 1-es számú, és alatta a második sorban egymás mellett a 2-es és 3-as számú.

Button testButton1 = new Button("Teszt1");
Button testButton2 = new Button("Teszt2");
Button testButton3 = new Button("Teszt3");

VBox vbox = new VBox(false, 1);
HBox hbox = new HBox(false, 1);

hbox.Add(testButton2);
hbox.Add(testButton3);
vbox.Add(testButton1);
vbox.Add(hbox);

Láthatóan mindkét tároló konstruktora két paramétert vár. Az első egy logikai változó, ami azt adja meg, hogy a rendszer kikényszerítse-e a benne elhelyezett elemektől, hogy egyenlő méretekkel rendelkezzenek. A második paraméterben pedig az elemeket elválasztó terület nagyságát adhatjuk meg egy integer segítségével.

3.3 Table tároló

Button testButton1 = new Button("Teszt1");
Button testButton2 = new Button("Teszt2");
Button testButton3 = new Button("Teszt3");
Button testButton4 = new Button("Teszt4");

Table newTable = new Table(2,2,true)

newTable.Attach(testButton1, 0, 1, 0, 1);
newTable.Attach(testButton2, 1, 2, 0, 1);
newTable.Attach(testButton3, 0, 1, 1, 2);
newTable.Attach(testButton4, 1, 2, 1, 2);

A létrehozáshoz meg kell adnunk hány sort és oszlopot szeretnénk a táblázatban. A harmadik paraméter itt is a homogén méretezés kikényszerítését jelenti.

Ezek után a meglévő elemeinket csatolni kell a táblázathoz az Attach függvény segítségével. Első paraméterében várja a Widget osztályból öröklődött elemet, amit hozzá akarunk adni. A tovább négy paraméterben a helyet, hogy hova szeretnénk rakni. Az első két koordináta adja meg, hogy melyik két oszlop között tart az elem. A második két koordináta, hogy melyik két sor között. A táblázat bal felső saroktól számozódik, 0-tól kezdődően. Tehát a példában szereplő 2x2 táblázat bal felső celláját láthatóan a (0,1,0,1) paraméter négyessel tudjuk kijelölni.

3.4 Frame tároló

Button testButton = new Button("Teszt");

Frame labeledFrame = new Frame("Keret:");

labeledFrame.Add(testButton);

A példa nem túl életszerű, viszont látható, hogy a gombunk körül egy keret helyezkedik el, aminek a bal felső részébe található a címke. Az osztálynak van paraméter nélküli konstruktora is. Ekkor a Frame.Label publikus string változón keresztül tudjuk a címkét megváltoztatni.

3.5 ScrolledWindow tároló

Hasonlóan a Frame-hez, létrehozás után egyszűen hozzá kell adni a kívánt elem(ek)et. A konstruktor nem vár paramétereket, címkével nem rendelkezik. A TextView-val együtt szemléltetésre kerül a későbbiek folyamán.

4. Elemek

Néhány egyszerűbb elem, amit könnyedén a képernyőre lehet helyezni, és még hasznuk is van.

  • Label: a példában is szereplő címke osztály
  • Button: egyszerű nyomógomb
  • ComboBox: legördülő lista, elődje az OptionMenu, ám az elavultá vált, ez használandó helyette
  • TextView: szöveg megjelenítésre alkalmas mező, akár szerkeszthető is, szükséges mellé a TextBuffer osztály
  • MessageBox: üzenet ablak megjelenítése
  • RadioButton: ismert választó gomb
  • CheckButton: ismert jelölő négyzet
  • VSeparator: függőleges vonal elválasztásra
  • HSeparator: ugyanaz vízszintesen
  • StatusBar: státusz sor, amit az ablak alján használunk
  • Tree: listázott megjelenítés

4.1 TextView elem

Összetettebb példának hozzunk létre a képernyőn egy olyan mezőt, ahova az alkalmazás a továbbiakban loggolni fogja a tevékenységeit. Ez a mező legyen görgethető, és kijelölhető, az esetleges szöveg másoláshoz, viszont módosítani ne lehessen.

TextView view;
TextBuffer buffer;
Frame logFrame;
ScrolledWindow logWindow;

logFrame = new Frame();
logFrame.Label = "Log:";
view = new Gtk.TextView ();
view.Editable = false;
view.CursorVisible = true;
buffer = view.Buffer;
buffer.Text = "";
logWindow = new ScrolledWindow();
logWindow.SetSizeRequest(480,200);

logWindow.Add(view);
logFrame.Add(logWindow);

A megvalósításhoz a TextView elemet fogjuk használni. Ebben az elemben egy TextBuffer típusú változó tartalmát fogjuk megjeleníteni, amit működés közben folyamatosan írunk, jelen esetben hozzáfűzünk.

A naplózást tartalmazó mezőt egy Framebe ágyazzuk, amit felcimkézünk a "Log:" felirattal. Ezek után létrehozzuk a TextView-t és beállítjuk a kívánt paramétereket. Következőkben a TextBuffer változót összerendeljük a TextView osztály azonos típusú publikus változójával. Ezek után létrehozzuk a ScrolledWindow típusú változót, amibe bele fogjuk helyezni ezt az elemet, és ezt az ablakot ágyazzuk a Framebe.

4.2 ComboBox elem

Ezt az elemet csak azért emeltem ki, mivel talán kellően gyakran használt, és talán másik is belefutnának abba a hibába, hogy először az OptionMenu osztályt akarják használni. Aztán csodálkoznak a fordító által jelzett Warningokon, miszerint az elem elavult.

A használata egyszerű. Létre hozás során inicializálni kell a Text elemét, és ahhoz hozzá fűzni sorban a kívánt lista elemeket. Alapvetően a leghosszabb lista elem méretét veszi fel, ám véleményem szerint érdemes előre beállítani a már ismertett módon.

   
   ComboBox combo;

combo = ComboBox.NewText();

for (int i = 0; i < 5; i ++)
combo.AppendText ("item " + i);

4.3 StatusBar elem

A státusz sor hozzáadás hasonlóan történik az összes többi elem, egyszűen példányosítani kell, és utána hozzáadni a megfelelő Object leszármazott elemhez. Az érdekessége, hogy szöveget megjeleníteni rajta hasonlóan lehet, mint egy verem. Két hasznos függvénye van, a Push() és Pop(). Az elsővel látható módon írhatunk rá, a másodikkal eltávolíthatjuk azt. A stackre való lenyomásnál egy sorszámot is rendelhetünk az üzenethez, ez lesz első paraméter, míg a szöveg a második. A Pop(int id) függvénnyel, mert a kívánt azonosítójú string-et távolítjuk el. A példában még egy tulajdonságát állítottuk be a StatusBarnak, méghozzá azt, hogy megjelenítse az átméretező sarkot, vagy sem. Tapasztalataim szerint ez Windows alatt sikeresen működik is, míg Mac OS X alatt figyelmen kívül hagyja.


Statusbar sb;

sb = new Statusbar();
sb.HasResizeGrip = false;

sb.Push (1, "Welcome!");

4.4 Tree elem

Erről az elemről a GtkSharp hivatalos oldalán is található egy kellően részletes leírás. Én itt ezt egy kicsit leegyszerűsétettem. Demonstrálás szempontjából megfelelő, viszont a Tree erejét nem fejezi ki kellően, így érdemes elolvasni.

A következőkben létrehozunk egy elemet, amiben található egy Items cimkével rendelkező oszlop, és abban öt sort, amiben az itemek vannak felsorolva.

Először létrehozzuk magát a TreeView-t, amibe pakoljuk az elemeket. Ilyen az oszlop, amiből most csak egyet hozunk létre. Szükség van még két további változóra. Az egyik felel azért, hogy a listában látszódjanak az elemek, míg a másik magát a listát képezi. A listánál meg kell adnunk, hogy milyen típusú és mennyi elemet tárolunk benne, ezért a konstruktora változó hosszúságú paramétersort igényel.

Ezek után beállítjuk a tároló oszlopot. Adunk neki nevet, illetve magát a fejléc mezőt helyezzük el, és megmondjuk neki, hogy alatta text típusú elemek fognak sorakozni. Ezek után a TreeView elem Model objektumának megadjuk, hogy az általunk létrehozott ListStoret használja. Innentől már csak az itemListStore változót kell feltölteni, amit a for ciklus szemléltet.

   
   TreeView tree                   = new TreeView();
   TreeViewColumn itemColumn       = new TreeViewColumn();
   CellRendererText itemNameCell   = new CellRendererText ();
   ListStore itemListStore         = new ListStore(typeof (string));

itemColumn.Title = "Items";
itemColumn.PackStart (itemNameCell, true);
itemColumn.AddAttribute (itemNameCell, "text", 0);
tree.AppendColumn(itemColumn);
tree.Model = itemListStore;

for (int i = 0; i < 5; i ++)
portListStore.AppendValues("item " + i);

5. Eseménykezelés

Miután már szépen tele tudjuk rajzolni a képernyőt, ideje megismerkedni annak a módjával, hogyan is tudjuk életrekelteni az alkalmazásunkat.

Ennek a legjobb módja, hogy bizonyos elemekhez olyan függvényeket rendelünk, ami a rendszer által detektált eseményeknél lefutnak. Ilyen lehet például, egy gomb megnyomása, ablak átméretezése, TextBox szerkesztése, menu elem kiválasztása...

A megvalósítás menete, hogy a Widget eseményéhez hozzáadunk, egy új eseménykezelő osztályt.

helloButton.Clicked += new EventHandler(helloButton_Clicked);

private void helloButton_Clicked(object o, EventArgs args) {
Console.WriteLine("Kattintás...");
}

A gomb Clicked eseményez adtuk hozzá az általunk megírt helloButton_Clicked függvényt. Ez a függvény private, mivel nem szeretnénk, hogy az osztályunkon kívül bármi is meghívja, és void, mivel nincs visszatérési értke. A rendszer a függvénynek átadja, hogy melyik objektum hívta meg. Itt például, ha kattintás esetén át szeretnénk írni a gomb szövegét, akkor a Label módosítása előtt az objektumot vissza kell kasztolni Button típusúra. Az EventArgs osztály tárol az eseménykezelő számára használható adatokat. A példakódban kattintás esetén csak egy sort írunk ki a konzolba.


A Button osztály eseményei:

  • Activated: ha a gomb aktiválva lett
  • Clicked: ha rákattintunk
  • Entered: ha az egérmutató a gomb területére ér
  • Left: ha az egérmutató a gomb területét elhagyja
  • Pressed: ha a gombot lenyomomják
  • Released: ha a gombot felengedik

6. Néhány javaslat

Mint programozás során mindig, itt is érdemes követni egy struktúrális logikát. Aki évek óta foglalkozik ezzel a területtel, annak már biztosan megtörtént. Többieknek adnék néhány szerény tanácsot.

A C hagyományok szerint még mindig érdemes először a változókat definiálni a programkód elején, hogy lássuk, miket is akarunk felhasználni. Így kevesebb a valószínűsége, hogy valami feleslegeset is létrehozunk, és ott marad a kódban.

A következő részben érdemes az elemek példányosítását megcsinálni, és beállítani a megfelelő tulajdonságaikat, amiket később látni szeretnénk.

Harmadik lépésként építsük fel a konténerek elrendezését egymásban. Ha előállítottuk a kívánt struktúrát, akkor adjuk hozzá az elemeket a megfelelő, előre elképzelt helyekez. A tervezés segít elkerülni az elkavarodást a helyek között.

Végül adjuk hozzá az ablakhoz a konténereket, és jelenítsük meg a felhasználó felé.