Pokud chce člověk programovat v systému X Window v jazycích C či C++, má k dispozici ohromné množství různých „toolkitů“ – tj. knihoven poskytujících tzv. „widgety“ neboli tlačítka, posuvníky, editační boxy pro vstup textu a další podobné věci. Jmenujme třeba knihovny Athena, GTK+ nebo Motif. Pokud chcete ale pouze programovat grafické aplikace v systému Linux a knihovna SVGALib vám připadá jako příliš nesystémové řešení (pokud se program pod ní napsaný „sekne“, může „zamrznout“ celý systém; dále SVGALib je specifikum Linuxu a programy nepůjdou přeložit pod jiným UNIX-like systémem), můžete chtít využít nízkoúrovňové grafické služby X Window Systému. Jestliže toužíte například napsat počítačovou hru nebo animaci, mohou pro vás být toolkity zbytečnou vrstvou navíc a můžete ocenit možnost použít přímo funkce typu „nakresli pixel“, „nakresli úsečku“, „věnuj pozornost klávesnici“, „reaguj na pohyb myši“ apod. bez prostředníka zvaného toolkit.
Pokud vám moje myšlenkové postupy v předchozím odstavci přišly povědomé, možná oceníte tento článek, který je úvodem do programování grafiku používajících aplikací s pomocí X Window Systemu a jeho knihovny XLib.
Rozhodl jsem se vás do problematiky uvést tím způsobem, že napíšu malý prográmek demonstrující použití XLibky v počítačové grafice a společně tento prográmek rozebereme. Ukázkový program je napsán v jazyce C++, ale nepoužívá žádné objektové vlastnosti tohoto jazyka – pouze mi připadala přehlednější možnost definovat proměnné kdekoli v kódu programu a ne jen na začátku funkcí či mimo funkce. Také se mi líbí „pluskové“ inline funkce, ale ty nejdou v našem příkladě použít.
Náš program bude velmi jednoduchá animace – kreslení náhodně umístěných čar náhodnou barvou přerušitelné stiskem libovolné klávesy. Najdete ho na konci tohoto článku. Přes jednoduchost algoritmu vypadá výsledná animace docela hezky. Program je možno přeložit a spustit mimo Linuxu i pod jinými UNIXovými systémy (ekvivalentní program využívající SVGALib místo XLib by fungoval pouze pod Linuxem; můj program byl zkompilován a spuštěn i na stroji Sun s operačním systémem SunOS a na stroji SGI Indy s Irixem).
Po nezbytném úvodu přejděme „k věci“:
Program pod X funguje díky spolupráci čtyř systémů:
- X Serveru, který se stará o „nízkoúrovňové“ služby jako komunikaci s grafickou kartou či s klávesnicí a myší. Plní příkazy naší aplikace a zpět jí posílá informace o událostech (events) v systému
- tzv. „toolkitu“ což je knihovna obsahující prvky uživatelského rozhraní
- Window Manageru, který se stará o věci, jako přesouvání oken, dekorace oken apod.
- a konečně vlastní aplikace, která využívá služeb X Serveru a může (ale nemusí) využívat služeb některého toolkitu a komunikovat s Window Managerem
V našem případě se budeme zajímat pouze o body (1) a (4) – přítomnost Window Manageru nás v podstatě nezajímá a žádný toolkit používat nebudeme.
Implementace mechanismu komunikace mezi aplikací a XServerem je „zapouzdřena“ knihovnou XLib a je pro nás v podstatě transparentní. Nebudeme se jí proto zabývat – pouze uvedeme, že umožňuje, aby náš program a XServer běžely každý na jiném stroji v síti.
Pokud chce aplikace začít využívat služeb XServeru, musí se s ním kontaktovat – to se zařídí funkcí XOpenDisplay s prototypem
Display *XOpenDisplay(char * display_name);
pokud místo display_name uvedeme NULLový ukazatel, vezme se hodnota proměnné prostředí DISPLAY. To je to, co nyní chceme, proto první „Xový“ příkaz naší aplikace zní
Display *dpy = XOpenDisplay(0);
Funkce vrátí ukazatel na strukturu Display, který budeme nadále při komunikaci s XServerem používat. Nebudeme se dívat „dovnitř“ této struktury – budeme s ukazatelem na ní tedy zacházet podobně jako s ukazatelem na strukturu typu FILE v I/O operacích. Pokud je dpy nulový ukazatel, znamená to, že se pokus o kontakt s XServerem nepodařil. Většinou to znamená, že uživatel spustil program na textové konzoli místo pod X. Můžeme na to zareagovat v podstatě jakkoli – často se používá hlášení chyby a potom exit(), ale to nemusí být pravidlem. Například některé verze EMACSu umí využívat služeb X, ale pokud je spustíme mimo X, editoru to nevadí – funguje v textovém režimu. Já jsem se rozhodl vypsat hlášku parodující jeden nejmenovaný operační a zároveň okenní systém a potom program ukončit s indikací chyby v návratové hodnotě.
if(!dpy) { fprintf(stderr,"This program requires X Windows.\n"); exit(1); }
Dále potřebujeme vytvořit okno, do kterého budeme kreslit. K tomu slouží funkce XCreateWindow() a XCreateSimpleWindow(). Ukážeme si prototyp druhé z nich:
Window XCreateSimpleWindow(Display * display, Window parent, int x, int y, unsigned int width, unsigned int height, unsigned int border_width, unsigned long border, unsigned long background);
Argumenty jsou většinou „samo-vysvětlující“, případně se můžete podívat do manuálové stránky. Poslední dva parametry jsou barvy – při alokaci barvy dostanete číslo typu unsigned long, kterým se potom na barvu odkazujete. My potřebujeme uvést číslo barvy černé, proto ho získáme k tomu uzpůsobenou funkcí BlackPixel():
int blackColor = BlackPixel(dpy, DefaultScreen(dpy));
Nyní již můžeme bez obav zavolat funkci XCreateSimpleWindow():
Window w = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, XSIZE, YSIZE, 0, blackColor, blackColor);
Kde XSIZE a YSIZE jsou konstanty určující velikost okna – jsou #definovány na začátku našeho programu a můžete si je zvolit takřka libovolně.
Naše okno ještě nemá určen titulek. To učiníme trochu složitější funkcí XChangeProperty(). Zájemce o studium dalších možností této funkce odkazuji na manuálové stránky – bylo by zbytečné je zde přepisovat.
const char * titulek = "Jde to i bez toolkitu!"; XChangeProperty(dpy, // display w, // ID okna XA_WM_NAME, // atom vlastnosti XA_STRING, // typ vlastnosti 8, // je osmibitová PropModeReplace, // budeme nahrazovat (Replace) (const unsigned char *) titulek, //data vlastnosti strlen(titulek) // počet elementů ve vlastnosti );
Vidíte, proč jsem použil C++ místo C ? Takhle můžu proměnnou „titulek“ deklarovat až v místě její potřeby. Tuto vlastnost C++ použijeme v našem kódu i jinde.
Okno zatím není na obrazovce – to musíme zařídit funkcí XMapWindow(), která okno zobrazí. Je zde menší problémek v tom, že okno se na obrazovce neobjeví hned – proto musíme počkat, až se objeví. K tomuto účelu nám poslouží mechanismus Xových událostí (events). Všechno se točí okolo datového typu XEvent – ten je unionem mnoha různých struktur. Všechny tyto struktury mají společnou pouze první položku – a to položku type typu int, která určuje typ události. Typ XEvent je deklarovaný v souboru /usr/include/X11/Xlib.h . My pouze X Serveru řekneme, které události nás zajímají a tyto pak budeme dostávat. To se zadává třetím parametrem funkce XSelectInput() – jsou to bitové masky všech možných událostí logicky sečtené dohromady. První dva parametry této funkce jsou display a okno. Ten třetí parametr může být bitovým ORem věcí jako KeyPressMask, EnterWindowMask, LeaveWindowMask, PointerMotionMask atd. , které jsou #definovány v souboru /usr/include/X11/X.h. V našem případě si „objednáme“, že chceme přijímat událost objevení okna, což zařídí StructureNotifyMask ve třetím parametru funkce XSelectInput() .
XSelectInput(dpy, w, StructureNotifyMask);
Jestliže jsme již zařídili, že nám systém řekne, kdy se okno skutečně zobrazí, můžeme konečně zobrazení okna přikázat.
XMapWindow(dpy, w);
Potom budeme ve smyčce čekat, dokud se okno neobjeví na obrazovce. K tomu nám poslouží funkce XNextEvent, kterým vybíráme následující událost z fronty událostí. Události budeme vybírat tak dlouho, dokud nenarazíme na událost typu MapNotify. To, že jsme na ní narazili, zjistíme pomocí složky type unionu XEvent.
XEvent e; do { XNextEvent(dpy, &e); } while( e.type != MapNotify);
Tak – okno je na obrazovce, můžeme začít kreslit. Napřed vytvoříme „grafický kontext“ – věc, na kterou se budeme odkazovat při všech „kreslících“ funkcích. Parametry této funkce – viz příslušná manuálová stránka.
GC gc = XCreateGC(dpy, w, 0, 0);
Abychom mohli rozumně kreslit, musíme si alokovat nějaké barvičky. Toho se týká struktura XColor, deklarovaná takto:
typedef struct { //identifikační číslo dané barvy v paletě unsigned long pixel; //barevné složky RGB 0 až 0xffff unsigned short red, green, blue; //tyto dva chary nás už nezajímají char flags; char pad; } XColor;
V našem programu nadefinujeme pole s těmito strukturami jako prvky. Jak je vidět z deklarace typu, na prvním místě je číslo barvy, které chceme naší barvě přidělit (ale XServer nám to může změnit – struktura je vstupní i výstupní).
Dále jsou RGB složky, parametry za nimi nás příliš nezajímají a mohou zůstat nulové.
XColor barvy[]= { {1,0xffff,0,0}, //červená {2,0,0xffff,0}, //zelená {3,0,0,0xffff}, //modrá {4,0xffff,0xffff,0}, //žlutá {5,0,0xffff,0xffff}, //jasně modrozelená {6,0xffff,0xffff,0xffff}, //bílá }; const int barev=sizeof(barvy)/sizeof(XColor);
Samozřejmě že hodnoty mohou být jiné než 0 a 0×ffff, klidně si přidejte další barvy.
Barvy nyní naalokujeme funkcí XAllocColor() a to samozřejmě v cyklu. Ve třetím parametru předáváme ukazatel na strukturu XColor, který bude vstupní (RGB hodnoty) i výstupní (systém může změnit číslo barvy, v tom případě jej ve struktuře přepíše).
for(int i=0;i<barev;i++) XAllocColor(dpy,DefaultColormap(dpy,DefaultScreen(dpy)), barvy+i );
Místo &(barvy[i]) jsem použil ekvivalentní, kratší zápis barvy+i , který snad každý Céčkař pochopí.
Protože budeme chtít program ukončit stiskem libovolné klávesy, řekneme XServeru, že nás zajímají události týkající se stisknutí klávesy. Použijeme funkci XSelectInput(), kterou jste zde již jednou viděli.
XSelectInput(dpy, w, KeyPressMask);
Teď přichází hlavní smyčka programu – inicializace máme již za sebou a můžeme se pustit do vlastního kreslení. Budeme v cyklu kreslit náhodné čáry náhodnou barvou, dokud nepřijde událost typu KeyPress.
Napřed funkcí XSetForeground() nastavíme číslo barvy, kterou budeme kreslit. Toto číslo je atribut unsigned long pixel struktury XColor. Vybereme proto náhodný prvek našeho pole a jeho atribut pixel předáme:
XSetForeground(dpy, gc,barvy[random()% barev].pixel );
Pokud nás v podstatě nezajímá, jaká barva to bude a chceme pouze náhodnou barvu ze všech barev v systému, můžeme to zkusit i jinak. Po zkompilování a spuštění programu zkuste odkomentovat následující řádek, který se snaží vybrat náhodnou barvu ze všech dostupných. Alokace barev je však důležitá a většinou nás v animacích, hrách apod. zajímá, která přesně barva to je, proto jsem v našem příkladě alokaci barev nemohl nepoužít. Ovšem jde to i takto:
XSetForeground(dpy,gc, random()%počet_barev_vašeho_grafického_režimu);
Kde za pseudokonstantu počet_barev_vašeho_grafického_režimu doplňte, kolik máte v systému barev. Nakreslíme poté náhodnou čáru funkcí XDrawLine, jejíž prototyp je následující:
XDrawLine(Display * display,Drawable d,GC gc, int x1,int y1,int x2,int y2);
„Drawable“ je například okno, ostatní parametry jsou jasné. Konkrétní volání této funkce v naší animaci vypadá takto:
XDrawLine(dpy, w, gc, random()%XSIZE, random()%YSIZE , random()%XSIZE , random()%YSIZE );
To, co jsme nakreslili, se v okně zobrazí teprve poté, co to „spláchneme“ funkcí XFlush():
XFlush(dpy);
Nyní přicházíme k závěru – k ukončující podmínce našeho do { } while kreslícího cyklu. Je to funkce s poměrně dlouhým názvem XCheckTypedWindowEvent(). Narozdíl od XNextEvent() neblokuje, ale pokud daná událost nenastala, vrátí nulu čili false. Cyklus tedy vypadá takto:
do { XSetForeground(...); XDrawLine(...); XFlush(dpy); } while(! XCheckTypedWindowEvent(dpy,w,KeyPress,&e) );
kde &e je ukazatel na strukturu XEvent e definovanou výše. Pokud bychom chtěli se stisknutou klávesou nějak dále pracovat, vrátí nám ji XServer v položce e.xkey.keycode . Není to však ASCII ani jiná rozumná hodnota – na něco srozumitelnějšího nám jí překonvertuje funkce XKeycodeToKeysym(), která v programu není použita. Ta vrátí hodnotu typu KeySym, což je věc definovaná v /usr/include/X11/keysymdef.h . Podívejte se tam a uvidíte.
To je vše, přátelé – nakonec pouze pro zajímavost zobrazíme keycode, který jsme dostali a vrátíme operačnímu systému nulu jako příznak normálního a bezchybného ukončení.
Zájemci si mohou kompletní zdrojový kód naší ukázkové animace stáhnout zde. Přeložíte jej příkazem
c++ animace.cc -L/usr/X11R6/lib -lX11
za předpokladu, že jste kód uložili do souboru animace.cc a že jsou Xové knihovny na vašem systému uloženy v adresáři /usr/X11R6/lib).