> GUI Çalışma Modelinin Temelleri : Eskiden monitörler yalnızca kalıp karakter basabiliyordu. Yani pixel temelinde bir grafik gösterim yoktu. Eğer ekranda görüntülenecek en küçük bir karakter ise bu çalıma moduna "text mode" ya da "console mode" denilmektedir. Grafik mod çalışmada ekranda kontrol edilebilecek en küçük birim bir karakter değil bir pixel'dir. (Pixel sözcüğü "picture element") sözcüklerinden kısaltma yapılarak uydurulmuştur. Grafik modda her şey küçük bir nokta olan pixel'lerin bir araya gelmesiyle oluşturulmaktadır. Bu nedenle text modda yalnızca yazılar görüntülenirken grafik modda resimler de görüntülenebilmektedir. Grafik modda her pixel Kırmızı, Yeşil ve Mavinin (kısaca RGB denilmektedir) tonal birleşimleriyle oluşturulmaktadır. Kırmızının, yeşilin ve mavinin 256 farklı tonal birleşimleri vardır. Böylece bukünkü modern grafik ekranlarda her pixel diğerlerin bağımsız olarak 2^24 (yaklaşık 16 milyon) (2^8 * 2^8 * 2^8) renkten biriyle boyanabilmektedir. İşte aslında grafik modda bir resim pixel'lerden oluşmaktadır. Her pixel uygun renge boyanınca biz onu bir resim gibi algılarız. Monitörler çeşitli çözünürlük modlarına da sahiptir. Bugün kullanılan en yaygın çözünürlük 1920x1080 (buna HD çözünürlük de denilmektedir) çözünürlüktür. Monitör bu çözünürlüğü kullandığında ekran adeta 1920x1080'lik bir pixel matrisi gibidir. Monitörler tamamen text modlarıda hala desteklemektedir. Monitör text moda geçirildiğinde artık pixel gösteremez yalnızca kalıp karakterleri gösterebilir. Eskiden monitörler hep text modda çalışırdı. Bu nedenle 80'li yıllarda bilgisayar ekranında film seyredemiyorduk, resim görüntüleyemiyorduk. (O zamanlarda monitörler ve grafik kartları grafik modlara yavaş yavaş sahip olsa da çok düşük çözünürlük sunuyordu.) Monitörün bir bölümü text bir bölümü grafik modda olamaz. Bu nedenle biz grafik ekranda çalıştığımız bilgisayarlarda bir terminal penceresi açtığımızda o pencere text modda olmaz. O pencere yalnızca text modu taklit eden bir emülasyon oluşturmaktadır. En basit, hızlı ve geleneksel çalışma modu text mod olduğu için Windows ve Linux gibi işletim sistemleri grafik arayüzünü kullanıyor olsa bile böyle text mod emülasyonunu sağlamaktadır. Pekiyi mademki GUI çalışma çok zengin olanaklar sunmaktadır neden o zaman hala konsol uygulamaları kullanılıyor? İşte GUI programların yazılması oldukça zordur. GUI programların yazılmasını kolaylaştırmak için pek çok yüksek seviyeli kütüphaneler oluşturulmuştur. Oysa text modda klasik konsol çalışma modeli oldukça basittir. Bazı sistemler ise hiç GUI arayüzüne sahip olmayabilmektedir. Örneğin UNIX/Linux sistemlerinde hala klasik konsol çalılma modeli çok yaygın biçimde kullanılmaktadır. (UNIX/Linux sistemleri özellikle sunucularda kullanıldığı için ve sunucuların grafik arayüze sahip olması onların hızını ve kaynak kullanımını yavaşlatacağı için bu sistemlerde klasik konsol çalışma modeli hala en yaygın modeldir.) GUI çalışma modeli ile klasik konsol çalışma modelini karşılaştırarısak şunları söyleyebiliriz: -> GUI çalışma modelinin uygulanması için bilgisayar sisteminin bazı gelişmiş özelliklere sahip olması gerekir. Halbuki konsol çalışma modeli DOS gibi basit işletim sistemlerinde ve onların çalıştığı eski donanımlarda bile oldukça verimli bir biçimde uygulanabilmektedir. -> GUI çalışma modelini kullanarak program yazmak oldukça zordur. Programcılar işlerini kolaylaştırmak için yüksek sebviyeli GUI kütüphaneler kullanmaktadır. -> GUI çalışma modeli konsol çalışma modeline göre yavaştır ve çok daha fazla sistem kaynağına gereksinimn duyar. -> GUI çalışma modeli modern grafiksel bir girdi ve çıktı ortamı sunmaktadır. Halbuki konsol çalışma ortamında her şey karakterlerin kalıp olarak konsol ekranına yazılmasıyla oluşturulmaktadır. Windows’un çekirdek (kernel) ile entegre edilmiş bir GUI alt sistemi vardır. Başka bir deyişle Windows’ta pencereli çalışma başka bir katman tarafından değil doğrudan işletim sisteminin çekirdeği tarafından sağlanmaktadır. Windows’u GUI alt sistemi olmadan kullanmak mümkün olsa da uygulamada çok zordur, Windows'ta GUI arayüz olmadan çalışma anlamlı değildir. UNIX/Linux sistemlerinde grafik arayüz çekirdeğin üzerine oturtulan ve ismine X11 (ya da XWindow) denilen bir alt sistem tarafından sağlanmaktadır. Yani örneğin Linux’un çekirdeğinin kaynak kodlarında pencere kavramına ilişkin hiçbir şey yoktur. Ancak Windows’ta vardır. Zaten Linux sistemlerinde doğal çalışma grafik arayüz ile değil text ekrandaki konsol arayüzü sağlanmaktadır. Server olarak kullanılan Linux sistemlerinde de genellikle sistemi yavaşlattığı gerekçesiyle grafik arayüz kullanılmamaktadır. Son yıllarda UNIX/Linux dünyasında klasik X11 GUI alt sistemine bir alternatif olarak "Wayland" isimli yeni bir alt sistem de tasarlanmıştır. Wayland alt sistemi X11'e göre daha modern ve hızlı bir tasarıma sahip olmakla birlikte henüz yaygınlaşmamıştır. X11 grafik sistemi client-server tarzda çalışmaktadır. Yani sanki X11 bir server program gibidir, pencere açmak ve pencereler üzerinde işlemler yapmak isteyen programlar da client programlar gibidir. X11 sisteminde işlem yapabilmek için oluşturulmuş temel kütüphaneye Xlib denilmektedir. Xlib'i X11’in API kütüphanesi olarak düşünebiliriz. Son yıllarda Xlib’in XCB isimli daha modern bir versiyonu da oluşturulmuştur. Xlib ve XCB temelde C Programlama Dilinden kullanılmak için tasarlanmıştır. Ancak bu kütüphaneler başka dillerden de kullanılabilmektedir. Grafik arayüze sahip pencereli sistemlerde genel olarak "mesaj tabanlı (message driven)" ya da "olay tabanlı (event driven)" denilen çalışma modeli kullanılmaktadır. Mesaj tabanlı çalışma modelinin ayrıntıları sistemden sisteme değişebilmekle birlikte burada biz her sistemde geçerli olan bazı temel bilgileri vermekle yetineceğiz. Mesaj tabanlı programlama modelinde klavye ve fare gibi aygıtlarda oluşan girdileri programcı kendisi almaya çalışmaz. klavye gibi, fare gibi girdi aygıtlarını işletim sisteminin (ya da GUI alt sistemin) kendisi izler. Oluşan girdi olayı hangi pencereye ilişkinse işletim sistemi ya da GUI alt sistem, bu girdi olayını “mesaj” adı altında bir yapıya dönüştürerek o pencerenin ilişkin olduğu (yani o pencereyi yaratan) programın “mesaj kuyruğu (message queue)” denilen bir kuyuk sistemine yerleştirir. Mesaj kuyruğu içerisinde mesajların bulunduğu FIFO prensibiyle çalışan bir kuyruk veri yapısıdır. Sistemin daha iyi anlaşılması için süreci maddeler halinde özetlemek istiyoruz: -> Her programın (thread'li sistemlerde her thread’in) “mesaj kuyruğu” denilen bir kuyruk veri yapısı vardır. Mesaj kuyruğu mesajlardan oluşmaktadır. -> İşletim sistemi ya da GUI alt sistem gerçekleşen girdi olaylarını “mesaj (message)” adı altında bir yapı formatına dönüşürmekte ve bunu pencerenin ilişkin olduğu programın (ya da thread’in) mesaj kuyruğuna eklemektedir. -> Mesajlar ilgili olayı betimleyen ve ona ilişkin bazı bilgileri barındıran yapı (structure) nesleridir. Örneğin Windows’ta mesajlar MSG isimli bir yapıyla temsil edilmişleridir. Bu yapının elemanlarında mesajın ne mesajı olduğu (yani neden gönderildiği) ve mesajın gönderilmesine neden olan olaya ilişkin bazı parametrik bilgiler bulunur. Görüldüğü gibi GUI programlama modelinde girdileri programcı elde etmeye çalışmamaktadır. Girdileri bizzat işletim sisteminin kendisi ya da GUI alt sistemi elde edip programcıya mesaj adı altında iletmektedir. GUI programlama modelinde işletim sisteminin (ya da GUI alt sistemin) oluşan mesajı ilgili programın (ya da thread’in) mesaj kuyruğuna eklemenin dışında başka bir sorumluluğu yoktur. Mesajların kuyruktan alınarak işlenmesi ilgili programın sorumluluğundadır. Böylece GUI programcısının mesaj kuyruğuna bakarak sıradaki mesajı alması ve ne olmuşsa ona uygun işlemleri yapması gerekir. Bu modelde programcı kodunu şöyle düzenler: "Bir döngü içerisinde sıradaki mesajı kuyruktan al, onun neden gönderildiğini belirle, uygun işlemleri yap, kuyrukta mesaj yoksa da blokede bekle”. İşte GUI programlarındaki mesaj kuyruğundan mesajı alıp işleyen döngüye mesaj döngüsü (message loop) denilmektedir. Bir GUI programının işleyişini tipik akışı aşağıdaki gibi bir kodla temsil edebiliriz: int main(void) { for (;;) { if (mesaj oencerinin x tuşuna basma mesajı mı) break; } return 0; } Bu temsili koddan da görüldüğü gibi tipik bir GUI programında programcı bir döngü içerisinde mesaj kuyruğundan sıradaki mesajı alır ve onu işler. Mesajın işlenmesi ise “ne olmuş ve ben buna karşı ne yapmalıyım?” biçiminde oluşturulmuş olan kodlarla yapılmaktadır. Pekiyi bir GUI programı nasıl sonlanmaktadır? İşte pencerenin sağındaki (bazı sistemlerde solundaki) X simgesine kullanıcı tıkladığında işletim sistemi ya da GUI alt sistem bunu da bir mesaj olarak o pencerenin ilişkin olduğu prosesin (ya da thread’in) mesaj kuyruğuna bırakır. Programcı da kuyruktan bu mesajı alarak mesaj döngüsünden çıkar ve program sonlanır. GUI ortamımız (framewok) ister .NET, ister Java, ister MFC olsun, isterse Qt olsun, işletim sisteminin ya da GUI alt sistemin çalışması hep burada ele açıklandığı gibidir. Yani örneğin biz .NET'te ya da Java'da işlemlerin sanki başka biçimlerde yapıldığını sanabiliriz. Aslında işlemler bu ortamlar tarafından aşağı seviyede yine burada anlatıldığı gibi yapılmaktadır. Bu ortamlar (frameworks) ya da kütüphaneler çeşitli yükleri üzerimizden alarak bize daha rahat bir çalışma modeli sunarlar. Ayrıca şunu da belirtmek istiyoruz: GUI programlama modeli özellikle nesne yönelimli programlama modeline çok uygun düşmektedir. Bu nedenle bu konuda kullanılan kütüphanelerin büyük bölümü sınıflar biçiminde nesne yönelimli diller için oluşturulmuş durumdadır. Örneğin Qt framework C++ ile, .NET Forms ve WPF framework'leri C# ile (ya da diğer nesne yönelimli .NET dilleri ile) kullanılmaktadır. Şimdi GUI programlama modelindeki mesaj kavramını biraz daha açalım. Yukarıda da belirttiğimiz gibi bu modelde programcıyı ilgilendiren çeşitli olaylara “mesaj” denilmektedir. Örneğin klavyeden bir tuşa basılması, pencere üzerinde fare ile tıklanması, pencere içerisinde farenin hareket ettirilmesi gibi olaylar hep birer mesaj oluşturmaktadır. İşletim sistemleri ya da GUI alt sistemler mesajları birbirinden ayırmak için onlara birer numara karşılık getirirler. Örneğin Windows’ta mesaj numaraları WM_XXX biçiminde sembolik sabitlerle kodlanmıştır. Programcılar da konuşurken ya da kod yazarken mesaj numaralarını değil, bu sembolik sabitleri kullanırlar. (Örneğin WM_LBUTTONDOWN, WM_MOUSEMOVE, WM_KEYDOWN gibi) Mesajların numaraları yalnızca gerçekleşen olayın türünü belirtmektedir. Oysa bazı olaylarda gerçekleşen olaya ilişkin bazı bilgiler de söz konusudur. İşte bir mesaja ilişkin o mesaja özgü bazı parametrik bilgiler de işletim sistemi ya da GUI alt sistem tarafından mesajın bir parçası olarak mesajın içerisine kodlanmaktadır. Örneğin Windows’ta biz klavyeden bir tuşa bastığımızda Windows WM_KEYDOWN isimli mesajı programın mesaj kuyruğuna bırakır. Bu mesajı kuyruktan alan programcı mesaj numarasına bakarak klavyenin bir tuşuna basılmış olduğunu anlar. Fakat hangi tuşa basılmıştır? İşte Windows basılan tuşun bilgisini de ayrıca bu mesajın içerisine kodlamaktadır. Örneğin WM_LBUTTONDOWN mesajını Windows farenin sol tuşuna tıklandığında kuyruğa bırakır. Ancak ayrıca basım koordinatını da mesaja ekler. Yani bir mesaj oluştuğunda yalnızca o mesajın hangi tür bir olay yüzünden oluştuğu bilgisini değil aynı zamanda o olayla ilgili bazı bilgileri de kuyruktaki mesajın içerisinden alabilmekteyiz. Windows'ta GUI programları en aşağı seviyede Windows API fonksiyonları kullanılarak yazılmaktadır. Ekrana boş bir pencere çıkartan iskelet bir GUI programı aşağıdaki gibi yazılabilir: #include LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow) { WNDCLASS wndClass; HWND hWnd; MSG message; if (!hPrevInstance) { wndClass.style = CS_HREDRAW | CS_VREDRAW; wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = hInstance; wndClass.hIcon = LoadIcon(NULL, IDI_QUESTION); wndClass.hbrBackground = GetStockObject(WHITE_BRUSH); wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); wndClass.lpszMenuName = NULL; wndClass.lpszClassName = "Generic"; wndClass.lpfnWndProc = (WNDPROC)WndProc; if (!RegisterClass(&wndClass)) return -1; } hWnd = CreateWindow("Generic", "Sample Windows", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); if (!hWnd) return -1; ShowWindow(hWnd, SW_RESTORE); UpdateWindow(hWnd); while (GetMessage(&message, 0, 0, 0)) { TranslateMessage(&message); DispatchMessage(&message); } return message.wParam; } LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } Windows sistemlerinde iskelet GUI programında şunların sırasıyla yapılması gerelmektedir: -> Önce WNDCLASS türünden bir yapı nesnesi tanımlanıp bunun içi doldurulur ve bu yapı RegisterClass APIO fonksiyonu ile sisteme register ettirilir. Bu WNDCKASS belirlemelerine "pencere sınıfı " denilmektedir. Örneğin: WNDCLASS wndClass; if (!hPrevInstance) { wndClass.style = CS_HREDRAW | CS_VREDRAW; wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = hInstance; wndClass.hIcon = LoadIcon(NULL, IDI_QUESTION); wndClass.hbrBackground = GetStockObject(WHITE_BRUSH); wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); wndClass.lpszMenuName = NULL; wndClass.lpszClassName = "Generic"; wndClass.lpfnWndProc = (WNDPROC)WndProc; if (!RegisterClass(&wndClass)) return -1; } -> Programın ana penceresi pencere sınıfı kullanılarak yaratılır: hWnd = CreateWindow("Generic", "Sample Windows", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); if (!hWnd) return -1; Ana pencere yaratıldıktan sonra pencerenin görünür hale getirilmesi gerekmektedir: ShowWindow(hWnd, SW_RESTORE); UpdateWindow(hWnd); -> Artık program mesaj döngüsüne girmelidir. Mesaj döngüsü kuyruktan sıradaki mesajı alıp bunu işleyen döngüdür. Mesaj döngüsü şöyle oluşturulmaktadır: while (GetMessage(&message, 0, 0, 0)) { TranslateMessage(&message); DispatchMessage(&message); } Burada GetMessage API fonksiyonu mesaj kuruğundan mesajı alır. TranslateMessage klavye mesajları için bazı dönüştürmeleri yapmaktadır. Mesajın işlenmesine yol açan fonksiyon DispatchMessage isimli API fonksiyonudur. Ancak DispatchMessage aslında pencere sınıfında belirtilen fonksiyonun çağrılmasına yol açmaktadır. Örneğimizde bu fonksiyon WndProc ismindedir. Yani DisptachMessage yapıldığında aslında WndProc fonksiyonu çağrılmaktadır. Buna "pencere fonsiyonu" denir. Programcı mesajı bu fonksiyon içerisinde işler. WndProc fonksiyonu şöyle yazılmıştır: LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } Windows'ta kuyruğa bırakılan bazı mesajların mutlaka işlenmesi gerekir. Bu işlem de çok sıkıcı olduğu için DefWindowProc isimli bir fonksiyon bulundurulmuştur. Programcı tarafından işlenmeyen mesajlar DefWindowProc fonksiyonuna verilir. Bu fonksiyon mesaj için gereken bazı default işlemler varsa onu yapar. Programın sonlanması pencerenin X simgesine tıklanarak yapılır. Bu durumda Windows kuyruğa WM_CLOSE isimli mesajı bırakır. DefWindowProc bu mesaj için DestroyWindow fonksiyonunu çağırır. Bu fonksiyon da WM_DESTROY mesajını oluşturur. Bu mesajda programcı PostQuitMessage API fonksiyonunu çağırır. Bu API fonksiyonu da kuyruğa WM_QUIT mesajını bırakır. WM_QUIT mesajını alan GetMessage fonksiyonu 0 ile geri döner. Böylece döngü sonlanır ve program da biter. Aşağıda bu konuya ilişkin kapsayıcı bir örnek verilmiştir: #include LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow) { WNDCLASS wndClass; HWND hWnd; MSG message; if (!hPrevInstance) { wndClass.style = CS_HREDRAW | CS_VREDRAW; wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = hInstance; wndClass.hIcon = LoadIcon(NULL, IDI_QUESTION); wndClass.hbrBackground = GetStockObject(WHITE_BRUSH); wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); wndClass.lpszMenuName = NULL; wndClass.lpszClassName = "Generic"; wndClass.lpfnWndProc = (WNDPROC)WndProc; if (!RegisterClass(&wndClass)) return -1; } hWnd = CreateWindow("Generic", "Sample Windows", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); if (!hWnd) return -1; ShowWindow(hWnd, SW_RESTORE); UpdateWindow(hWnd); while (GetMessage(&message, 0, 0, 0)) { TranslateMessage(&message); DispatchMessage(&message); } return message.wParam; } LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_DESTROY: PostQuitMessage(0); break; case WM_LBUTTONDOWN: MessageBox(hWnd, u8"Farenin sol tuşuna basıldı", "Mesaj", MB_OK); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } Daha önceden de belirttiğmiz gibi UNIX/Linux sistemlerinde XWindow ya da X11 denilen bir katman kullanılmaktadır. Bu katmanın API fonksiyonlarına XLIB ya da bunun modern biçimine XCB denilmektedir. XLib ya da XCB çok aşağı seviyeli bir kütüphanedir. Bu kütüphane kullanılarak GUI elemanlarını oluşturan X-Toolkit ya da kısaca Xt isimli ayrı bir kütüphane vardır. Bu Xt üzerine kurulan Motif gibi başka kütüphaneler de vardır. Ayrıca CLibb ya XCB üzerine kurulmuş olan iki önemli kütüphane de Qt ve GTK (GTK+ da denilmektedir) kütüphanelerdir. Qt kütüphanesi C++ ile yazılmıştır, dolayısıyla Qt için kodlar da C++ ile yazılmaktadır. GTK ise C'de yaaılmıştır. +----------------------------+ | GUI Uygulamaları | +----------------------------+ / | \ / | \ / | \ +--------+ +-----------+ +----------+ | GTK | | Qt | | Motif | +--------+ +-----------+ +----------+ | | | +----------+----+ +--+--+--+ +---+--+ | Xlib / Wayland| |Xlib/XCB| | Xt | +----------+----+ +--+--+--+ +---+--+ | | | | | +--+--+--+ | | |Xlib/XCB| | | +--+--+--+ | | | +-----------+----------------+-----------------+-----------------+ | X Window System (X11) | Wayland (alternatif) | +----------------------------------+-----------------------------+ | Donanım / OS (Çekirdek, GPU) | +----------------------------------------------------------------+ Şimdi de bu kütüphanelerin hiyeararşisini tek tek gösterelim. GTK kütüphanesini şöyle gösterebiliriz: GTK XlIb / Wayland X Window / Wayland Xt kütüphanesini şöyle gösterebiliriz: Xt XlIb X Window Motif kütüphanesini şöyle gösterebiliriz: Xt XlIb X Window Qt kütüphanesini de şöyle gösterebiliriz: Qt XlIb / Wayland X Window / Wayland Bugün artık Xt ve Motif yeni uygulamalar tarafından pek kullanılmamaktadır. Bu nedenle yüksek seviyeli kütüphaneler için önemli iki alternatif GTK ve Qt kütüphaneleridir. GTK kütüphanesi C ile kullanılabilir. Ancak Qt için C++ bilmek gerekir. Xlib ya da XCB kütüphaneleri oldukça düşük seviyeli kütüphanelerdir. Bunlar aslında X Window sistemlerinin aşağı seviyeli API kütüphanesi gibi düşünülebilir. Xlib ile GUI uygulmaları yazmak çok zordur. Çünkü Xlib içerisinde pencere yaratan öğeler olsa da GUI uygulamalarında kullanılan düğmeler (push buttons), listsmele kutuları (listbox), checkbox gibi grafik elemnlar bulunmamaktadır. Eğer C kullanılarak bu GUI elemanlar ile gelişmiş GUI programla roluşturmak istiyorsanız GTK kütüphanesini kullanabilirsiniz. Şimdi de UNIX/Linux sistemlerinde en düşük seviyede Xlib kullana iskelet bir GUI programını yazmaya çalışalım. Bu program ekrana boş bir pencere çıkartacaktır. Bunun öncelikle Xlib kütüphanesinin kurulması gerekmektedir. Bu işlem Debian tabanlı sistemlerde şöyle yapılabilir: $ sudo apt-get install libx11-dev Derleme işlemi sırasında X11 kütüphanesinin "-lX11" seçeneği ile belirtilmesi gerekmektedir. Örneğin: $ gcc -o generic-xlib generic-xlib.c -lX11 Ekrana boş bir pencere çıkartan iskelet GUI programı şöyle yazılabilir: /* generic-xlib.c */ #include #include #include int main(void) { Display *disp; Window w; XEvent e; int scr; disp = XOpenDisplay(NULL); if (disp == NULL) { fprintf(stderr, "Cannot open display\n"); exit(1); } scr = DefaultScreen(disp); w = XCreateSimpleWindow(disp, RootWindow(disp, scr), 10, 10, 100, 100, 1, BlackPixel(disp, scr), WhitePixel(disp, scr)); XSelectInput(disp, w, ExposureMask | KeyPressMask); XMapWindow(disp, w); for (;;) { XNextEvent(disp, &e); if (e.type == KeyPress) break; } XCloseDisplay(disp); return 0; } Burada sırasıyla şu işlemler yapılmıştır: -> XOpenDisplay fonksiyonu XWindow sunucusu ile bağlantı kurmak için kullanılmaktadır. Bu fonksiyon başarı durumunda bize Display türünden bir handle verir. -> Daha sonra biz bu handle’ı vererek bir ekran (screen) nesnesi yaratmamız gerekir. Bu işlem de DeafultScreen fonksiyonuyla yapılmaktadır. Bu fonksiyon bize ilgili ekranı betimleyen int türden bir değer vermektedir. -> Örnek programımızda daha sonra uygulamanın ana penceresi XCreateSimpleWindow fonksiyonuyla yaratılmıştır. Bu fonksiyon bize Window * türünden yaratılan pencereye ilişkin bir handle değeri vermektedir. -> Programda daha sonra mesaj döngüsüne girmeden önce hangi girdi olaylarının izleneceğini belirlemek için XSelectInput fonksiyonu çağrılmıştır. -> Mesaj döngüsünden sıradaki mesaj XNextEvent fonksiyonuyla elde edilmektedir. (Bu fonksiyonu Windows'ta GetMessage API fonksiyonuna benzetebilirsiniz. Bu fonksiyon bize kuyruktaki mesajı XEvent isimli bir yapı olarak verir. Örnek programımızda bir tuşa basıldığında mesaj döngüsünden çıkılmaktadır.) -> Mesaj döngüsünden çıkıldığında XCloseDisplay fonksiyonu ile daha önce alınmış olan ekran geri bırakılmıştır. Tabii ekran yok edildiğinde tüm pencereler de yok edilecektir. Ayrıca program sonlandığında X11 sistemi ile bağlantı da otomatik koparılmaktadır. GTK pek çok ayrıntıya sahip olan C tabanlı bir GUI kütüphanesidir. GTK kürüphanesinin son versiyonu GTK 4'tür. Bu version 2020'de oluşturulmuştur. Ancak önceki versiyon olan GTK 3 halen daha yoğun olarak kullanılmaktadır. GTK 3 Debian tabanlı sistemlerde şöyle kurulabilir: $ sudo apt-get install libgtk-3-dev GTK 4 ise şöyle kurulbailir: $ sudo apt-get install libgtk-4-dev GTK 3 ile GTK 4 birbirine çok benzemekle birlikte tam uyumlu değildir. Biz burada GTK 4 için bazı küçük örnekler vereceğiz. Ekrana boş bir pencere çıkartan iskelet GTK 4 programı şöyle oluşturulabilir: #include void activate(GtkApplication *app, gpointer user_data) { GtkWidget *window; // Yeni pencere oluştur window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Sample Window"); gtk_window_set_default_size(GTK_WINDOW(window), 400, 300); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char **argv) { GtkApplication *app; int status; // Uygulamayı nesnesini oluştur app = gtk_application_new("com.generic.application", G_APPLICATION_FLAGS_NONE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); // Uygulamayı çalıştır status = g_application_run(G_APPLICATION(app), argc, argv); // Hafızayı temizle g_object_unref(app); return status; } Bu program şöyle derlenebilir: $ gcc -o generic-gtk generic-gtk.c $(pkg-config --cflags --libs gtk4) Burada $(pkg-config --cflags --libs gtk4) ifadesi pkg-config programının çıktısının komut satırına yerleştirilmesini sağlamaktadır. GTK 4 programlarının derlenmesi için komut satırında çeşitli include dizinlerine ilişkin seçeneklerin ve birden fazla dinamik kütüphanenin devreye sokulması gerelmektedir. Bu $(pkg-config --cflags --libs gtk4) ifadesi aslında gereken komut satırı argümanlarını olutşurmaktadır. Yukarıdaki iskelet programın açıklamasını şöyle yapabiliriz: -> Bir GTK 4 uygulamasında önce bir GtkApplication nesnesinin oluşturulması gerekir. GtkApplication yapısı uygulama ile ilgili çeşitli bilgileri tutmaktadır. Bu işlem iskelet programda şöyle yapılmıştır: app = gtk_application_new("com.generic.application", G_APPLICATION_FLAGS_NONE); -> İskelet programda daha sonra uygulama çalıştırıldığında oluşan "activate" mesajı için activate isimli fonksiyonun çağrılması sağlanmıştır. GTK'da mesaj yerine "sinyal (signal)" sözcüğü kullanılmaktadır: g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); Bu sinyal bağlantısındna sonra artık activate isimli fonksiyon çağrılacaktır. Bizim bu fonksiyon içerisinde programın ana penceresini yaratmamız gerekir. -> activate fonksiyonu şöyle yazılmıştır: void activate(GtkApplication *app, gpointer user_data) { GtkWidget *window; window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Sample Window"); gtk_window_set_default_size(GTK_WINDOW(window), 400, 300); gtk_window_present(GTK_WINDOW(window)); } Uygulamanın ana penceresi gtk_application_window_new fonksiyonu ile yaratılmaktadır. Ana pencere diğer pencereler gibi GtkWidget yapısı ile temsil edilmektedir. gtk_window_set_title fonksiyonu yaratılan pencerenin başlık kısmına (caption) çıkacak yazının set edilmesini sağlamaktadır. Ana pencerenin default genişlik ve yüksekliği gtk_window_set_default_size fonksiyonuyla oluşturulmaktadır. Bu işlemlerden sonra iskelet programda gtk_window_present fonksiyonu ile ana pencere görünür hale getirilmiştir. -> Ana pencere yaratıldıktan sonra artık mesaj döngüsü oluşturulmalıdır. GTK 4'te mesaj döngüsü manuel oluşturulmaz. Mesaj döngüsü g_application_run fonksiyonu ile oluşturulmaktadır: status = g_application_run(G_APPLICATION(app), argc, argv); Program hayatını bu fonksiyon içerisinde oluşturulan mesaj döngüsünden geçirmektedir. -> Mesaj döngüsünden yine pencerenin X tuşuna basıldığında çıkılır. İskelet programımızda mesaj döngüsünden çıkıldığında heap'te tahsis edilen çeşitli nesnelerin yok edilmesi için g_object_unref fonksiyonu çağrılmıştır. GTK içerisinde pek çok GUI eleman hazır biçimde bulunmaktadır. Bu GUI elemanların yaratılması için fonksiyonlar vardır. Sinyal mekanizması yoluyla bu GUI elemanlarda birtakım olaylar gerçekletiğinde bizim belirlediğimiz fonksiyonun çağrılması sağlanabilmektedir. Örneğin: button_ok = gtk_button_new_with_label("Ok"); gtk_window_set_child(GTK_WINDOW(window), button_ok); g_signal_connect(button_ok, "clicked", G_CALLBACK(on_button_clicked), window); Burada bir düğme yaaratılmıştı. Bu düğmeye tıklandığında on_button_clicked isimli fonksiyon çağrılacaktır. Aşağıdaki örnekte biz fonksiyon çağrıldığında bir diyalog penceresinin çıkmasını sağladık: void on_button_clicked(GtkButton *button, gpointer user_data) { GtkWindow *parent_window = GTK_WINDOW(user_data); GtkWidget *dialog = gtk_message_dialog_new( parent_window, GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, "Düğmeye tıkladınız!" ); g_signal_connect(dialog, "response", G_CALLBACK(gtk_window_destroy), NULL); gtk_window_present(GTK_WINDOW(dialog)); } Aşağıda kapsayıcı bir örnek verilmiştir: #include void on_button_clicked(GtkButton *button, gpointer user_data) { GtkWindow *parent_window = GTK_WINDOW(user_data); GtkWidget *dialog = gtk_message_dialog_new( parent_window, GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, "Düğmeye tıkladınız!" ); g_signal_connect(dialog, "response", G_CALLBACK(gtk_window_destroy), NULL); gtk_window_present(GTK_WINDOW(dialog)); } void activate(GtkApplication *app, gpointer user_data) { GtkWidget *window; GtkWidget *button_ok; GtkWidget *box; window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Sample Window"); gtk_window_set_default_size(GTK_WINDOW(window), 320, 200); button_ok = gtk_button_new_with_label("Ok"); gtk_window_set_child(GTK_WINDOW(window), button_ok); g_signal_connect(button_ok, "clicked", G_CALLBACK(on_button_clicked), window); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char **argv) { GtkApplication *app; int status; app = gtk_application_new("com.generic.application", G_APPLICATION_FLAGS_NONE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; } Bugün çeşitli programalam dillerinden kullanılabilen pek çok GUI kütüphanesi ve GUI ortamları (GUI frameworks) bulunmaktadır. GUI kütüphaneleri ve GUI ortamları zamanla evrim geçirerek bugünkü durumlarına gelmiştir. Günümüzde GUI ortamlarında iki önemli tasarım seçeneği belirginleşmiştir: -> GUI elemanları için otomatik yerleştirme yapan nesnelerin kullanılması. -> GUI arayüzünü mümkün olduğunca koddan ayırma girişimleri. Eskiden kullanıcı arayüzlerindeki düğme gibi, edit alanları gibi, listeleme kutuları gibi GUI elemanları tek tek pixel temelinde programcı tarafından konumlandırılıyordu. Bu da ekran çözünürlüğü değiştiğinde görünümün bozulmasına yol açıyordu. Ancak son yıllarda artık GUI ortamlarında otomatik yerleştirme yapan nesneler bulundurulmaya başlanmıştır. Bu otomatik "yerleştirme nesneleri (layout objects)" çözürlük değişse bile yerleştirmeyi makul olarak kendisi yapmaktadır. Bir uygulamada GUI arayüzünün diğer kodlardan ayrılması uygulamanın "önyüz (frontend)" ve "arkayüz (backend)" kodlamasının farklı ekipler tarafından yapılabilmesine olanak sağlamaktadır. Son yıllarda artık GUI ortamları görsel arayüzle diğer kodları birbirinden ayırabilmek için mekanizmalar sunmaktadır. Böylece görsel arayüz XML ya da ona benzer bir dille bir text editörle oluşturulabilmekte ve uygulamaya kolayca dahil edilebilmektedir. Örneğin, GTK 3'te bu olanak daha sınırlıydı. GTK 4'te bu olanak güçlendirişmiştir. Aşağıda GTK 4 iel oluşturulmuş bir grid yerleştirme (layout) nesnesinin kullanımı verilmiştir. #include // Düğmeye basıldığında çalışacak fonksiyon static void on_button_clicked(GtkButton *button, gpointer user_data) { GtkEntry *entry = GTK_ENTRY(user_data); const char *text = gtk_entry_buffer_get_text(gtk_entry_get_buffer(entry)); // Ana pencereyi entry'den al (up-cast) GtkWindow *parent_window = GTK_WINDOW(gtk_widget_get_root(GTK_WIDGET(entry))); GtkWidget *dialog = gtk_message_dialog_new( parent_window, GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, "Editbox içeriği:\n%s", text ); gtk_window_set_transient_for(GTK_WINDOW(dialog), parent_window); g_signal_connect(dialog, "response", G_CALLBACK(gtk_window_destroy), NULL); gtk_window_present(GTK_WINDOW(dialog)); } // Uygulama başlatıldığında çağrılır static void activate(GtkApplication *app, gpointer user_data) { GtkWidget *window; GtkWidget *grid; GtkWidget *entry; GtkWidget *button; // Pencere oluştur window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Editbox + Düğme"); gtk_window_set_default_size(GTK_WINDOW(window), 400, 100); // Grid oluştur grid = gtk_grid_new(); gtk_grid_set_column_spacing(GTK_GRID(grid), 10); gtk_grid_set_row_spacing(GTK_GRID(grid), 10); gtk_window_set_child(GTK_WINDOW(window), grid); // Editbox entry = gtk_entry_new(); gtk_widget_set_margin_start(entry, 20); gtk_widget_set_margin_top(entry, 20); gtk_grid_attach(GTK_GRID(grid), entry, 0, 0, 1, 1); // (widget, col, row, width, height) // Düğme button = gtk_button_new_with_label("Göster"); gtk_widget_set_margin_start(button, 20); gtk_widget_set_margin_top(button, 20); gtk_grid_attach(GTK_GRID(grid), button, 1, 0, 1, 1); // Sinyal bağla g_signal_connect(button, "clicked", G_CALLBACK(on_button_clicked), entry); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char **argv) { GtkApplication *app; int status; app = gtk_application_new("com.ornek.editdugme", G_APPLICATION_FLAGS_NONE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; } > Hatırlatıcı Notlar: >> GUI üzerinden başlatılan terminal programına sahte ("pseudo") terminal, oturum açma ekranında "switch" yaptığımız terminal ise gerçek terminal olarak geçer. Fakat işlevsel olarak aralarında bir farklılık yoktur.