Keywords: graphical editor, visualization, computer programming, VTK, Qt
Naj torej že obstaja geometrijska baza podatkov (GDB), ki definira računski prostor (Computational Domain=CD). CD je razdeljen na množico podprostorov, ki so po obliki kuboidi. Geometrija je definirana z vogali (V), robovi (E), površinami (S), volumni (VOL), njihovimi atributi ter medsebojnimi povezavami. Robovi so po matematičnem zapisu tipa B-spline, medtem ko so površine ali tipa B2-spline ali tipa bilinearna interpolacija BILIN(E1,E2,E3,E4). Vsak rob E je poleg kontrolnih točk E.BSPLINE.CP[i] dodatno določen s točkami E.POE[i], skozi katere načeloma poteka Bspline. Nadalje obstaja nabor geometrijskih pogojev (CONSTRAINTS= CON). Za vsak {V,P,E,S} je lahko predpisanih več CON. Npr.: Za Vi je predpisan CONi,j={Vi leži na površini Sk } ter pogoj CONi,j+1 ={Vi je periodičen z Vm}. GDB mora biti konsistentno sestavljena, da so njeni višji topološki elementi odvisni od nižjih. Ob spremembi nekega elementa GDB se mora baza konsistentno popraviti (vse odvisnosti izslediti in ustrezno popraviti). GDB se pri zagonu programa prebere iz datoteke, ki se predhodno formira s generatorjem šablone (Template Generator=TG) za izbran tip problema (šablona za oblopatične CD turbinskih strojev, za CD okoli trupa ladij, ...)
Izdelava programa z grafičnim vmesnikom (graphical user interface - GUI) je pogojena z izbiro knjižnice gradnikov (widgets) kot so gumbi, drsniki, menuji ipd. Takih knjižnic obstaja kar nekaj, ločijo pa se po prenosljivosti (eno ali več platformne), možnosti uporabe različnih programskih jezikov, licenci in ne nazadnje po zahtevnosti uporabe. Naslednja tabela prikazuje neketere najbolj razširjene knjižnice (povzeto po Programming Languages mini-HOWTO).
Knjižnica | Uporaba | Licenca | Jezik | Povezave z drugimi jeziki | Platforma |
---|---|---|---|---|---|
Tk | enostavna | Free | Tcl | Perl, Python, drugi | Unix, MS Win |
GTK+ | zahtevna | Free | C | Perl, C++, Python, drugi? | Unix, MS Win v razvoju? |
Qt | zahtevna | Free za Open Source | C++ | C, Perl, Python, drugi? | Unix, MS Win |
Motif | zahtevna | Non-Free | C/C++ | Python, drugi? | Unix |
Obe knjižnici -grafična in GUI- morata biti dovolj fleksibilni, da se ju da integrirati v eno aplikacijo. Celotno interakcijo z uporabnikom običajno prevzame GUI knjižnica, zato je potreben še nek vezni člen med grafično in GUI knjižnico. Ta člen je lahko del ene ali druge knjižnice ali pa je kar integriran v program. Nadalje potrebuje grafična knjižnica okno v katerega izrisuje. Tako okno je skoraj vedno del grafične knjižnice in postavi se vprašanje kako ga integrirati v grafični vmesnik.
VTK kot grafična knjižnica ne nudi nobene GUI podpore, zato sem moral izbrati GUI
knjižnico s katero bi jo lahko čim enostavneje povezal. Na prvi pogled najboljša
rešitev je Tk knjižnica, vendar nam uporaba interpreterskega jezika
izniči večji del fleksibilnosti VTK-ja, saj smo omejeni le na že s Tcl-jem povezane
objekte. Poleg tega tuje izkušnje navajajo, da je kombinacija Tcl+Tk zelo primerna
za enostavne aplikacije, medtem ko postane pri velikih projektih implementacija
komaj obvladljiva. Od omenjenih GUI knjižnic ostaneta še GTK+ in Qt, ki obe že
vsebujeti podporo za OpenGL (gradnik, ki ga lahko uporabimo kot platno za risanje).
Ker je VTK open-source, sem na internetu hitro našel več različnih objektov, ki
povežejo Qt in VTK, medtem ko za GTK+ iskanje ni bilo uspešno. GTK+ poleg tega tudi (še) nima
podpore za MS Win platformo. Končna izbira je torej
postala Qt knjižnica, katere edina slaba lastnost je precej draga licenca za
razvoj komercialnih programov.
Spletna stran: http://www.trolltech.com
Osnova objektnega programiranja so objektno orientirani programski jeziki. Najvidnejši med njimi so Perl, Python, Java in C++. Prvi trije so interpreterski in namenjeni vsak svojemu področju, medtem ko je C++ klasični prevajalni jezik namenjen predvsem pisanju aplikacij.
Qt in VTK knjižnici sta obe napisani v jeziku C++, zato je bila uporaba jezika C++
za razvoj programa logična izbira. C++ se je razvil iz jezika C, ki so mu najprej
dodali možnost uporabe razredov -C z razredi-, z nadaljnim razvojem pa se je standardiziral
kot C++. Objekti v C++ so spremenljivke določenega tipa. Tipi se definirajo z razredi
(class) v katerih je zapisano, katere spremenljivke - člane (members) vsebuje objekt in
katere funkcije - metode (methods) ima na voljo. Člani so lahko tudi drugi objekti.
Jezik C++ omogoča tudi več stopenj skrivanja članov in metod, ki so na ta način nedosegljivi
objektom drugega tipa. Za kratek primer vzemimo točko v 2D prostoru. Definirajmo nov tip Point
:
// deklaracija razreda class Point { public: void SetX( float xx ) { x = xx; CalcDistance(); }; void SetY( float yy ) { y = yy; CalcDistance(); }; float GetX() { return x; }; float GetY() { return y; }; float GetD() { return d; }; void Print(); private: float x, y; // koordinati točke float d; // razdalja do izhodišča void CalcDistance(); }; // definicija ostalih metod void Point::CalcDistance() { d = sqrt( x*x + y*y ); } void Point::Print() { cout << "x = " << x << endl << "y = " << y << endl; }Deklaracijo razreda sestavlja rezervirana beseda
class
, ki ji sledi ime
razreda. V zavitih oklepajih nato sledi telo razreda, kjer se nahajajo objekti (spremenljivke),
metode (funkcije) in informacije o zaščiti. V programu dostopamo do članov razreda tako,
da pred njim napišemo ime objekta in oboje ločimo s piko, npr.:
Point p; // deklariramo objekt "p" tipa "Point" p.x = 1.05; // tu prevajalnik javi napako p.SetY( 2.3 ); p.CalcDistance(); // tudi tu prevajalnik javi napako p.Print();Razlog, da prevajalnik v zgornjih dveh vrsticah javi napako, je v zaščiti. Člana
x
in CalcDistance()
stojita pod kvantifikatorjem private
, kar
pomeni, da lahko do njiju dostopajo samo člani istega razreda in navzven nista vidna. Obseg
kvantifikatorjev je do naslednjega kvantifikatorja oziroma do konca telesa razreda.
Kvantifikator public
pomeni, da so člani dosegljivi tudi zunaj razreda,
poleg njega pa obstaja še protected
, ki pomeni da so člani dosegljivi v lastnem in
v izpeljanih razredih. Zakaj je zaščita pomembna, lahko vidimo iz zgornjega primera:
vsakič, ko spremenimo položaj točke se spremeni tudi njena oddaljenost od središča (vsaj načeloma).
Če bi lahko zapisali p.x = 1.05
in nato pozabili klicati še
p.CalcDistance()
, bi razdalja ostala nepremenjena t.j. napačna.
Poglejmo si še dedovanje. Točko lahko prestavimo tudi v prostor, za kar moramo razredu Point
dodati še tretjo koordinato. To najlaže storimo tako, da izpeljemo nov razred, ki mu v glavi
razreda za osnovo - bazo podamo razred Point
:
class Point3D : public Point { public: void SetZ( float zz ) { z = zz; CalcDistance(); }; float GetZ(); { return z; } void Print(); private: float z; // tretja koordinata točke void CalcDistance(); }; void Point3D::CalcDistance() { d = sqrt( x*x + y*y + z*z ); } void Point3D::Print() { Point::Print(); // kličemo metodo baznega razreda // ker ima metoda isto ime, pišemo "razred::metoda()" cout << "z = " << z << endl; }Če nato v programu napišemo:
Point3D pt; pt.SetX( 1 ); pt.SetY( 1 ); pt.SetZ( 1 ); pt.Print(); cout << "d = " << pt.GetD() << endl;dobimo sledeč izpis:
x = 1 y = 1 z = 1 d = 1.73205Kot vidimo je izpeljani razred podedoval vse člane baznega razreda in zato lahko celo nastopa namesto njega. Dedovanje je izredno pomembno za pisanje knjižnic kot sta Qt in VTK, saj nam ni potrebno pisati delov iste kode. Z dedovanjem lahko vzpostavimo komunikacijo med objekti, ki dedujejo od znanega baznega razreda. C++ omogoča tudi deklaracijo abstraktnih razredov, katerih objekti ne morejo obstajati. Abstraktni razred samo določa zunanjo podobo, ki je skupna vsem njegovim dedičem, katerih objekti lahko obstajajo.
To je le delček sposobnosti jezika C++, ki pa že pokaže njegovo uporabnost. Omenim naj še možnost
preobložitve operatorjev, ki močno prispevajo k preglednosti kode. Recimo
da smo definirali razred complex
, ki ponazarja kompleksna števila.
Če poleg razreda definiramo še operator seštevanja '+
' za kompleksna
števila, lahko v programu pišemo:
complex a( 10,0 ); // a.re = 10, a.im = 0 complex b( 1,2 ); complex c; c = a + b; // c.re = 11, c.im = 2Še nekaj kar nisem omenil: zapis
complex a( 10,0 );
pomeni, da smo deklarirali
objekt a
razreda complex
in hkrati inicializirali dva njegova člana.
Zapis namreč predstavlja klic konstruktorja - metode, ki nosi isto ime kot razred in je
kot vse ostale metode deklarirana v telesu razreda. Definicija konstruktorja pa določa
katere člane inicializira in na kakšen način.
Gledano poenostavljeno, imamo na voljo nekaj osnovnih tipov gradnikov, ki jih med seboj
povežemo v grafični cevovod. Ti osnovni tipi so Source, Filter, Mapper in Actor.
Med seboj si podajajo podatkovne objekte in jih povežemo na način
Filter->SetInput( Source->GetOutput() )
.
Grafični cevovd se prične s Source objekti. Ti priskrbijo podatke, ki jih vizualiziramo,
tako da jih generirajo ali
pa jih preberejo iz datoteke. Filter objekti podatke prečistijo, poenostavijo, jim kaj
dodajo ali odvzamejo in nam na ta način omogočijo kontrolo nad podatki. Filter objekte
lahko tudi kar izpustimo iz grafičnega cevovoda. Mapper objekti (med drugim) predstavljajo
geometrijo, ki ponazarja podatke. Pri Actor objektih se cevovod zaključi. Actor objekti
združujejo informacije o transformacijah in videzu predmeta, ki ga prikazujejo.
Običajno vzpostavimo grafični cevovod za vsak predmet, ki ga prikazujemo. Cevovodi si lahko delijo objekte, lahko jih združujemo ali vejimo. Način izvajanja cevovoda je naslednji: vsak objekt, skozi katerega potujejo podatki, ima tudi notranji zapis zadnjega časa izvajanja in čas zadnje spremembe objekta. Preden se podatki prikažejo na zaslonu, vsak objekt preveri ali se je kaj spremenil od zadnjega časa izvajanja. Če se je, potem podatke še enkrat procesira. Npr. nekemu filtru smo vključili drugačen način obdelave podatkov. Naslednjič, preden se bodo podatki začeli prikazovati na zaslon, se bo filter ponovno izvedel, prav tako pa tudi vsi objekti do konca cevovoda, ker bodo vsi dobili nove podatke. V tem primeru se edino Source objektu ne bo potrebno ponovno izvajati, ker se je sprememba zgodila v cevovodu za njim.
Oglejmo si kratek primer uporabe knjižnice:
int main() { // ustvarimo risarja, risarsko okno in interaktorja za uporabnika vtkRenderer *ren = vtkRenderer::New(); vtkRenderWindow *renWindow = vtkRenderWindow::New(); renWindow->AddRenderer( ren ); vtkRenderWindowInteractor *iren = vtkRenderWindowInteractor::New(); iren->SetRenderWindow( renWindow ); // ustvarimo igralca z geometrijo osemstrane piramide vtkConeSource *cone = vtkConeSource::New(); cone->SetResolution( 8 ); vtkPolyDataMapper *coneMapper = vtkPolyDataMapper::New(); coneMapper->SetInput( cone->GetOutput() ); vtkActor *coneActor = vtkActor::New(); coneActor->SetMapper( coneMapper ); coneActor->GetProperty()->SetColor( 0,1,1 ); // zeleno modra barva piramide // risarju povemo katerega igralca naj riše ren->AddActor( coneActor ); ren->SetBackground( 1,1,1 ); // bela barva ozadja // uporabniku omogočimo interakcijo z igralcem iren->Start(); // počistimo za seboj ren->Delete(); renWindow->Delete(); iren->Delete(); cone->Delete(); coneMapper->Delete(); coneActor->Delete(); return 0; }Na sredini je lepo vidno kako zgradimo grafični cevovod. V njem ni nobenega Filter objekta, ker ga samo za prikaz piramide ne potrebujemo. Ko program prevedemo in poženemo, se nam odpre naslednje okno:
Z miško lahko piramido interaktivno rotiramo, prestavljamo in povečujemo ali pomanjšujemo. Kakšno interakcijo in na kakšen se bo le-ta izvajala, določa tip interaktorja, ki ga uporabimo v programu.
Eden od razlogov za tako uspešnost knjižnice je gotovo signal/slot mehanizem, ki omogoča komunikacijo med najrazličnejšimi objekti. Na vsak slot lahko pripeljemo enega ali več signalov, ki so posledica uporabnikovih akcij. Če npr. kliknemo na nek gumb, potem bo gumb oddal signal, ki bo morebitnemu sprejemniku signala povedal, da je bil gumb pritisnjen. Sprejemnik lahko nato primerno odgovori na uporabnikov klik. Vsakemu objektu oz. razredu, ki ga na novo deklariramo, lahko definiramo poljubno mnogo signalov in/ali slotov.
Drugi razlog za uspešnost knjižnice pa je veliko število že narejenih objektov, ki jih moramo samo še uporabiti. Recimo, da želimo uporabnika vprašati za ime datoteke, potem ko klikne na gumb "Open". Vse kar moramo storiti je, da gumb povežemo z naslednjo funkcijo:
void openFile() { QString filename; // ime datoteke filename = QFileDialog::getOpenFileName( QString::null, "*.cpp" ); if ( filename.isEmpty() ) { // uporabnik je pritisnil "Cancel" ali Esc return; } open( filename ); // funkcija, ki datoteko prebere }Ko uporabnik klikne na gumb, se odpre naslednje okno:
Uporabnik lahko sedaj poišče željeno datoteko in če bo kliknil na gumb "Open", se bo datoteka tudi prebrala.
Naslednji razlog za priljubljenost je verjetno podpora internacionalizaciji. Prej omenjeno namizno okolje KDE je že prevedeno v več jezikov, med drugim tudi v slovenščino. Če bi to uporabili pri zgornjem primeru, bi npr. na gumbih namesto "Open" in "Cancel" pisalo "Odpri" in "Prekliči".
Če pogledamo dokumentacijo VTK knjižnice, vidimo da sta vtkRenderWindow in vtkRenderWindowInteractor edina objekta (razreda), ki neposredno komunicirata z okenskim okoljem (oz. njuna naslednika vtkXRenderWindow in vtkXRenderWindowInteractor za X11 standarden Xt-GUI). Prvi priskrbi risarsko okno, drugi pa sprejema uporabnikove akcije (premikanje miške, pritiski tipk na tipkovnici). Ker bomo mi uporabili Qt-GUI, potrebujemo torej nekaj v smislu vtkQtRenderWindow in vtkQtRenderWindowInteractor, ki bosta komunicirala s Qt objekti, ki bodo predstavljali ogrodje aplikacije. Prav to pa je napisal Matthias Koenig. Razred vtkQtRenderWindow nasledi QGLWidget, ki je Qt-jevo risarsko okno za OpenGL, in vtkRenderWindow, da ga VTK objekti lahko uporabljajo. vtkQtRenderWindowInteractor ima ravno tako dve bazi, to sta QObject, ki je bazni razred vseh Qt objektov, in vtkRenderWindowInteractor, spet da ga VTK objekti lahko uporabljajo. Ker v Qt sprejemajo uporabnikove akcije samo QWidget objekti (in njegovi nasledniki - kar je tudi QGLWidget, ni pa QObject), vtkQtRenderWindow samo posreduje te akcije vtkQtRenderWindowInteractor-ju. QWidget objekti so namreč vidni deli grafičnega vmesnika (zato tudi edini lahko sprejemajo uporabnikove akcije), kar pa vtkRenderWindowInteractor po svoji vlogi ni. Na ta način dobimo kar najmanjšo kodo, ki je pregledna in popolnoma združljiva z obema knjižnicama. Vse kar se spremeni je, da namesto vtkRenderWindow in vtkRenderWindowInteractor uporabimo njuni Qt različici in vključimo vtkQtRenderWindow med ostale vidne objekte uporabniškega vmesnika. Rešitve drugih avtorjev so bile precej manj pregledne (najmanj to) in so običajno kar združile funkcionalnost več VTK objektov v en skupen VTK-Qt objekt. Taka rešitev pa je s stališča vzdrževanje kode in nadgradnje knjižnic popolnoma nesprejemljiva. Sicer tudi Koenigova rešitev ni bila popolna. Manjkala je podpora tekstnim anotacijam, kar je standardna možnost VTK-ja. Z nekaterimi popravki mi je uspelo to pomankljivost odpraviti.
Pisanje aplikacij s Qt uporabniškim vmesnikom izgleda nekako takole: definiramo nov
QWidget objekt (gradnik), ki bo nosilec vmesnika in bo aplikacijo s svojimi metodami
pravzaprav poganjal; v njem
definiramo ostale gradnike (menuje, gumbe itd.) in jih razporedimo; povežemo vse gradnike
preko njihovih signalov in slotov z metodami našega gradnika oz. med seboj; definiramo še
glavno funkcijo main()
v kateri ustvarimo objekt QApplication, ki skrbi za
glavno izvajalno zanko (main event loop), ustvarimo naš gradnik in ga podamo prvemu kot
glavni gradnik aplikacije.
Glavnemu gradniku naše aplikacije sem dal ime GEgui. V njem sem definiral tri gradnike, ki sestavljajo uporabniški vmesnik:
Naslednje VTK objekte sem moral prilagoditi našemu namenu oziroma jih napisati na novo:
Geometrijo lahko s pritiskom na tipko 'F1' in hkratnim premikanjem miške poljubno vrtimo, s tipkama 'F2' in 'F3' ter premikanjem miške, pa transliramo in zoomiramo. Tipka 'r' resetira pogled. Gumb v zgornjem desnem kotu prikaže geometrijo s prosojnimi površinami, tako da se lepo vidijo tudi notranje površine. Gumb zgoraj levo pa nam odpre dodatne izbire, s katerimi lahko označimo vse primitive na geometriji. Če z miško kliknemo na enega izmed primitivov (vozlišče, rob ali ploskev) se nam v spodnjem oknu izpišejo vse informacije ki so o njem znane, na geometriji pa se prikaže oznaka z imenom. Na zgornji sliki je tako označeno vozlišče. Vozlišča lahko premikamo po površini, ki je zapisana v GDB. Na vozlišče kliknemo dvakrat, da se nam prikaže ročaj za premikanje - poln rdeč kvadrat. Ročaj predstavlja vozlišče in ga lahko z miško povlečemo na drugo lokacijo. Pri tem se bo vozlišče premikalo po predpisani površini. Med premikanjem lahko uporabimo tipke F1-3, da spremenimo pogled. Vozlišče ostane označeno z ročajem vse dokler ne kliknemo drugam. Preden bomo zapustili program, nas bo le-ta opozoril na morebitne neshranjene spremembe na geometriji.
S Qt knjižnico pravzaprav ni bilo težav. Je odlično dokumentirana, z veliko primeri in celo učbenikom. Enostavna je za uporabo, saj nam ni potrebno skrbeti niti za čiščenje objektov, potem ko jih več ne potrebujemo. Objekti se namreč sami uničijo, če opazijo, da jih nihče več ne potrebuje.
[1] M. Prtenjak: C++ za velike in male, Desk 1995
[2] W. J. Schroeder, K. M. Martin, L. S. Avila, C. C. Law: The VTK User's Guide, Kitware, Inc. 2000