Skip to content

19. Γραφικό περιβάλλον διεπαφής

Σύνοψη Διεπαφές γραμμής εντολής (CLIs), γραφικά περιβάλλοντα διεπαφής (GUIs), πλεονεκτήματα και μειονεκτήματα των GUIs, βιβλιοθήκες για κατασκευή GUIs στη C, παράδειγμα εφαρμογής GUI με τη βιβλιοθήκη Nuklear σε Windows, Linux, MacOS, απλές αλληλεπιδράσεις προγράμματος με τον χρήστη με τη βιβλιοθήκη “tinyfiledialogs”, εναλλακτικές προσεγγίσεις για τη δημιουργία γραφικού περιβάλλοντος αλληλεπίδρασης με τον χρήστη.

Προαπαιτούμενη γνώση Τύποι δεδομένων, είσοδος/έξοδος, δομές επιλογής και επανάληψης, συναρτήσεις, πίνακες, δείκτες, αλφαριθμητικά, διαμερισμός κώδικα, ορίσματα γραμμής εντολών

19.1 Εισαγωγή

Τα παραδείγματα κώδικα που έχουν παρουσιαστεί στα προηγούμενα κεφάλαια είναι όλα εφαρμογές κονσόλας (console applications). Ο χρήστης καλείται να εισάγει τιμές εισόδου σε ένα τερματικό και τα αποτελέσματα εκτέλεσης του προγράμματος εμφανίζονται επίσης στο ίδιο τερματικό. Αν και βολικός, αυτός ο τρόπος αλληλεπίδρασης του χρήστη με το πρόγραμμα που εκτελείται έχει περιορισμούς καθώς δεν συμβαδίζει με τα σύγχρονα γραφικά περιβάλλοντα διεπαφής (GUIs, Graphical User Interfaces) που χρησιμοποιούν παράθυρα, λίστες επιλογής τιμών, πεδία εισαγωγής τιμών, πλήκτρα και άλλα στοιχεία ελέγχου που ο χρήστης μπορεί εύκολα να χειριστεί με το ποντίκι και το πληκτρολόγιο του υπολογιστή του. Στο 1 παρουσιάζεται η ιστορία της ανάπτυξης των διεπαφών υπολογιστικών συσκευών για τους χρήστες από το 1968 μέχρι το 2005, ενώ η περαιτέρω εξέλιξή τους σε συσκευές όπως τα κινητά τηλέφωνα, οι ταμπλέτες, οι φορέσιμες συσκευές και οι έξυπνες συσκευές (τηλεοράσεις, μετρητές κ.λπ.) γίνεται αντιληπτή βιωματικά στην καθημερινότητα των περισσότερων ανθρώπων. Στην εικόνα 19.1 φαίνεται η μορφή που έχει μια εφαρμογή κονσόλας ή αλλιώς εφαρμογή διεπαφής γραμμής εντολών (εφαρμογή CLI=Command Line Interface) και στην εικόνα 19.2 φαίνεται ένα παράδειγμα εφαρμογής GUI.

Σημαντικά πλεονεκτήματα των GUIs έναντι των CLIs είναι η φιλικότητα προς τον χρήστη και η παρουσίαση πλουσιότερου περιεχομένου (π.χ. εικόνες, διαγράμματα, γραφικά με δυνατότητα αλληλεπίδρασης με τον χρήστη). Εφαρμογές όπως επεξεργαστές κειμένου, λογιστικά φύλλα, λογισμικά παρουσιάσεων, παιχνίδια, λογισμικά επεξεργασίας εικόνας, λογισμικά προσομοιώσεων που ο χρήστης αλληλεπιδρά μαζί τους (π.χ. προσομοιωτής πτήσης) καθώς και άλλες εφαρμογές είναι συνυφασμένες με γραφικά περιβάλλοντα. Ωστόσο, τα GUIs έχουν και μειονεκτήματα, καθώς οι εφαρμογές που χρησιμοποιούν GUIs απαιτούν περισσότερους πόρους υπολογιστή (π.χ. κύκλους ΚΜΕ, μνήμη) για την εκτέλεσή τους έναντι των CLI εφαρμογών, ενώ μπορεί να καθυστερούν τον χειρισμό τους από έμπειρους χρήστες. Επιπλέον, είναι λιγότερο κατάλληλα για αυτοματοποίηση εργασιών και μπορεί η εκμάθηση των προχωρημένων χαρακτηριστικών εφαρμογών GUIs να είναι δύσκολη, γεγονός που δεν διευκολύνεται από συχνές ενημερώσεις που ενδεχομένως αλλάζουν δραστικά τη μορφή της διεπαφής.
Η ανάπτυξη εφαρμογών GUIs μπορεί να είναι μια απαιτητική εργασία, ειδικά αν πρόκειται για σύνθετες εφαρμογές. Η ύπαρξη εξειδικευμένων βιβλιοθηκών διευκολύνει τη διαδικασία και η χρήση τους είναι ο ενδεδειγμένος τρόπος για την κατασκευή GUIs. Ωστόσο, προκύπτουν θέματα επιλογής των πλέον κατάλληλων βιβλιοθηκών ανάλογα με το είδος της εφαρμογής (π.χ. εμπορική εφαρμογή, επιστημονική εφαρμογή, παιχνίδι και άλλα), θέματα φορητότητας (portability) της εφαρμογής από το ένα σύστημα σε άλλο (π.χ. από Windows σε Linux), θέματα εμφάνισης σύμφωνα με τη μορφή (look and feel) κάθε συστήματος και άλλα. Ορισμένες γλώσσες προγραμματισμού όπως η Python και η Java καθιστούν την κατασκευή GUIs ευκολότερη λόγω της ενσωματωμένης υποστήριξης που διαθέτουν για τέτοιου είδους εφαρμογές. Στη C δεν υπάρχει ενσωματωμένη στη γλώσσα υποστήριξη για κατασκευή εφαρμογών GUIs, αλλά όπως συμβαίνει και για πολλές άλλες ανάγκες (π.χ. επεξεργασία εικόνας, επεξεργασία ήχου, επεξεργασία βίντεο, συμπίεση, επικοινωνίες, βάσεις δεδομένων) η υποστήριξη παρέχεται μέσω εξωτερικών βιβλιοθηκών.
Οι βιβλιοθήκες που μπορούν να χρησιμοποιηθούν για ανάπτυξη GUI εφαρμογών στη C είναι πολλές. Ένα χαρακτηριστικό που τις διαφοροποιεί αναφέρθηκε ήδη και είναι η υποστήριξη ή μη, εγγενούς (native) look and feel. Native look and feel σημαίνει ότι ακολουθείται ο τρόπος σχεδιασμού παραθύρων και στοιχείων ελέγχου (πλήκτρων, λιστών, πεδίων εισαγωγής τιμών κ.λπ.) που χρησιμοποιεί το σύστημα. Ορισμένες από τις δημοφιλέστερες βιβλιοθήκες της C για GUIs παραθέτονται στην ακόλουθη λίστα:

  • Nuklear (https://github.com/Immediate-Mode-UI/Nuklear). Εύχρηστη, μικρή βιβλιοθήκη που θα περιγραφεί στη συνέχεια και θα δοθεί παράδειγμα χρήσης της στην παράγραφο 19.2.

  • UP (http://webserver2.tecgraf.puc-rio.br/iup/). Επιτρέπει το ίδιο πρόγραμμα να μεταγλωττίζεται και να εκτελείται σε διάφορα συστήματα χωρίς αλλαγές, διαθέτει σχετικά απλό API και προσφέρει native look and feel.

  • GTK+ (https://www.gtk.org/). Η πληρέστερη από τις βιβλιοθήκες GUI που αναφέρονται εδώ, αλλά και η περισσότερο σύνθετη. Προσφέρει native look and feel. Η βιβλιοθήκη GTK+ (GIMP Toolkit) παρέχει μεγάλο αριθμό εργαλείων, στοιχείων ελέγχου, και συναρτήσεων για τη σχεδίαση GUIs. Είναι ευέλικτη και επεκτάσιμη και υποστηρίζει την ανάπτυξη εφαρμογών και σε άλλες γλώσσες προγραμματισμού πέρα από τη C (π.χ. JavaScript, Perl, Python, Rust, Vala).

  • cimgui (https://github.com/cimgui/cimgui). Βιβλιοθήκη wrapper για τη βιβλιοθήκη Dear imGui (https://github.com/ocornut/imgui) που έχει υλοποιηθεί σε C++. Δεν προσφέρει native look and feel.

  • raygui (https://github.com/raysan5/raygui). Βιβλιοθήκη που έχει αναπτυχθεί ως βοηθητική για τη βιβλιοθήκη δημιουργίας παιχνιδιών raylib. Δεν προσφέρει native look and feel.

  • tinyfiledialogs (https://github.com/native-toolkit/libtinyfiledialogs). Μικρή βιβλιοθήκη που παρέχει τη δυνατότητα χρήσης πλαισίων διαλόγου (modal windows) για επικοινωνία με τον χρήστη σε προγράμματα C. Θα δοθεί παράδειγμα χρήσης της στην παράγραφο 19.3.

Στο παρόν κεφάλαιο θα χρησιμοποιηθεί η βιβλιοθήκη Nuklear καθώς είναι σχετικά απλή στη χρήση, ενώ όλη η υλοποίησή της βρίσκεται σε ένα «μονολιθικό» αρχείο επικεφαλίδας, το nuklear.h που περιέχει μάλιστα ως σχόλια ενσωματωμένες χρήσιμες αναλυτικές οδηγίες χρήσης της. Η Nuklear μπορεί να χρησιμοποιήσει τις cross-platform βιβλιοθήκες απόδοσης γραφικών (rendering) OpenGL2/3, SDL και Allegro, ενώ στα Windows για rendering μπορεί να χρησιμοποιήσει την D3D ή την GDI και στο Linux την Χ11. Το look and feel των εφαρμογών που δημιουργούνται με την Nuklear δεν είναι native. Παραδείγματα εμφάνισης οθονών που έχουν δημιουργηθεί με τη Nuklear φαίνονται στην εικόνα 19.3.

Σχήμα 19.3

Σχήμα 19.3: Παραδείγματα οθονών που έχουν δημιουργηθεί με τη βιβλιοθήκη Nuklear. Πηγή: https://github.com/Immediate-Mode-UI/Nuklear

19.2 Κατασκευή GUIs με τη βιβλιοθήκη Nuklear

Στα παραδείγματα που θα ακολουθήσουν θα χρησιμοποιηθεί η βιβλιοθήκη rendering OpenGL3 και οι βιβλιοθήκες GLFW και GLEW. Η OpenGL παρέχει ένα τυποποιημένο API γραφικών που επιτρέπει στους προγραμματιστές να σχεδιάζουν διδιάστατα και τριδιάστατα γραφικά. H GLFW επιτρέπει τη δημιουργία παραθύρων, τον χειρισμό εισόδου από τον χρήστη και τη ρύθμιση των λεγόμενων OpenGL contexts. Η GLEW επιτρέπει τον χειρισμό διαφόρων επεκτάσεων της OpenGL για πρόσβαση σε προχωρημένα χαρακτηριστικά της. Παρατηρήστε ότι αν χρησιμοποιηθεί κάποια άλλη βιβλιοθήκη rendering όπως η D3D11 ή η SDL, τότε ο κώδικας αλλά και οι απαιτούμενες εγκαταστάσεις επιπλέον λογισμικών αλλάζουν.

19.2.1 Εγκατάσταση της Nuklear σε Windows

Ο κώδικας της Nuklear μεταφορτώνεται από το github αποθετήριο https://github.com/Immediate-Mode-UI/Nuklear σε κάποια θέση του συστήματος, π.χ. C:\Nuklear. Στη συνέχεια μεταφορτώνεται η βιβλιοθήκη GLFW ως προ-μεταγλωττισμένη (pre-compiled binary) από τη σελίδα https://www.glfw.org/download.html επιλέγοντας “64-bit Windows binaries”. Το αρχείο που μεταφορτώνεται έχει όνομα της μορφής glfw‐3.3.8.bin.WIN64.zip (η έκδοση μπορεί να διαφέρει από τη 3.3.8) και αποσυμπιέζεται σε κάποια θέση του συστήματος, π.χ. C:\glfw‐3.3.8.bin.WIN64. Τα αρχεία που πρόκειται να χρησιμοποιηθούν βρίσκονται στον κατάλογο C:\Nuklear\lib‐mingw‐w64. Στη συνέχεια, μεταφορτώνεται η βιβλιοθήκη GLEW από το https://glew.sourceforge.net/ επιλέγοντας “Binaries Windows 32-bit and 64-bit”. Το αρχείο που μεταφορτώνεται έχει όνομα της μορφής glew‐2.1.0‐win32.zip (η έκδοση μπορεί να διαφέρει από τη 2.1.0) και αποσυμπιέζεται σε κάποια θέση, π.χ. C:\glew‐2.1.0.ß

19.2.2 Εγκατάσταση της Nuklear σε Windows

Ο κώδικας της Nuklear μεταφορτώνεται από το github αποθετήριο https://github.com/Immediate-Mode-UI/Nuklear σε κάποια θέση του συστήματος, π.χ. ~/Nuklear. H εγκατάσταση των βιβλιοθηκών GLFW, GLEW καθώς και του βοηθητικού εργαλείου pkg-config που εντοπίζει διαδρομές του συστήματος όπου βρίσκονται αρχεία επικεφαλίδων και αρχεία βιβλιοθηκών για λογισμικά που είναι εγκατεστημένα στο σύστημα γίνεται με τις ακόλουθες εντολές (π.χ. για Ubuntu 22.04 LTS):

$ sudo apt update
$ sudo apt install pkg‐config
$ sudo apt install libglfw3 ‐dev
$ sudo apt install libglew ‐dev

19.2.3 Εγκατάσταση της Nuklear σε MacOS

Παρόμοια με την εγκατάσταση για Linux είναι και η εγκατάσταση των απαιτούμενων λογισμικών σε MacOS. Αρχικά, μεταφορτώνεται ο κώδικας της Nuklear από το github αποθετήριο https://github.com/Immediate-Mode-UI/Nuklear σε κάποια θέση του συστήματος, π.χ. ~/Nuklear. Στη συνέχεια χρησιμοποιείται το brew που είναι ένα πρόγραμμα διαχείρισης εφαρμογών για το MacOS (https://brew.sh/index_el). H εγκατάσταση των βιβλιοθηκών GLFW, GLEW και του pkg-config γίνεται με τις ακόλουθες εντολές:

$ brew update
$ brew install pkg‐config
$ brew install glfw
$ brew install glew

19.2.4 Παράδειγμα GUI εφαρμογής - ένας απλός μετρητής

Στο παράδειγμα που ακολουθεί θα υλοποιηθεί μια απλή GUI εφαρμογή, με ένα πεδίο κειμένου και ένα πλήκτρο. Αρχικά, το πεδίο κειμένου θα περιέχει την τιμή 0 και κάθε φορά που θα πιέζεται το πλήκτρο, η τιμή στο πεδίο κειμένου θα αυξάνεται κατά 1. Η εφαρμογή θα μπορεί να μεταγλωττιστεί και να εκτελεστεί και στα τρία πλέον διαδεδομένα λειτουργικά συστήματα (Windows, Linux, MacOS), θα είναι δηλαδή cross-platform. Τα αρχεία της εφαρμογής θα βρίσκονται σε έναν κατάλογο και φαίνονται στο Σχήμα 19.4.

Σχήμα 19.4

Σχήμα 19.4: Αρχεία απλού GUI μετρητή, που χρησιμοποιούνται για μεταγλώττιση και εκτέλεση σε Windows, Linux και MacOS, με τη βιβλιοθήκη Nuklear.

Παρατηρήστε ότι για καθένα από τα λειτουργικά συστήματα Windows, Linux (Ubuntu 22.04 LTS) και MacOS υπάρχει ένα makefile που μπορεί να χρησιμοποιηθεί για τη μεταγλώττιση του κώδικα. Τα αρχεία nuklear.h και nuklear_glfw_dl3.h περιλαμβάνονται στον κώδικα της βιβλιοθήκης Nuklear, που έχει μεταφορτωθεί από το GitHub, στον κατάλογο ρίζας και στον υποκατάλογο demo\glfw_opengl3, αντίστοιχα. Στη λίστα των αρχείων συμπεριλαμβάνεται και το αρχείο glew32.dll που απαιτείται για εκτέλεση της εφαρμογής στα Windows και που μπορεί να αντιγραφεί από τον υποκατάλογο εγκατάστασης της GLEW, π.χ. C:\glew‐2.1.0\lib\Release\x64. Στη συνέχεια παρατίθεται ο κώδικας 19.1 του αρχείου main.cmain.c:

Κώδικας 19.1: ch19_p1/main.c - υλοποίηση GUI μετρητή με τη βιβλιοθήκη Nuklear.
#include <GL/glew.h>
#define MAX_VERTEX_BUFFER 512 * 1024
#define MAX_ELEMENT_BUFFER 128 * 1024
#define NK_INCLUDE_FIXED_TYPES
#define NK_INCLUDE_STANDARD_IO
#define NK_INCLUDE_STANDARD_VARARGS
#define NK_INCLUDE_DEFAULT_ALLOCATOR
#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
#define NK_INCLUDE_FONT_BAKING
#define NK_INCLUDE_DEFAULT_FONT
#define NK_IMPLEMENTATION
#define NK_GLFW_GL3_IMPLEMENTATION
#define NK_KEYSTATE_BASED_INPUT
#include "nuklear.h"
#include "nuklear_glfw_gl3.h"

GLFWwindow *win;
struct nk_context *ctx;
struct nk_glfw glfw = {0};

void init(void) {
  int p_width = 200, p_height = 60;
  if (!glfwInit()) {
    fprintf(stderr, "Failed to init GLFW!\n");
    exit(EXIT_FAILURE);
  }
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
  glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
  win = glfwCreateWindow(p_width, p_height, "Counter", NULL, NULL);
  glfwMakeContextCurrent(win);
  if (!win) {
    glfwTerminate();
    fprintf(stderr, "Failed to create GLFW window\n");
    exit(EXIT_FAILURE);
  }
  glewExperimental = 1;
  if (glewInit() != GLEW_OK) {
    fprintf(stderr, "Failed to setup GLEW\n");
    exit(EXIT_FAILURE);
  }
  ctx = nk_glfw3_init(&glfw, win, NK_GLFW3_INSTALL_CALLBACKS);
  struct nk_font_atlas *atlas;
  nk_glfw3_font_stash_begin(&glfw, &atlas);
  nk_glfw3_font_stash_end(&glfw);
}

void cleanup(void) {
  nk_glfw3_shutdown(&glfw);
  glfwTerminate();
}

void main_loop(void) {
  int w_width = 200, w_height = 60, counter = 0;
  char counter_txt[10] = "0";
  while (!glfwWindowShouldClose(win)) {
    glfwPollEvents();
    nk_glfw3_new_frame(&glfw);
    /* Σχεδίαση του GUI */
    if (nk_begin(ctx, "Counter window", nk_rect(0, 0, w_width, w_height), 0)) {
      nk_layout_row_dynamic(ctx, 30, 2);
      snprintf(counter_txt, 9, "%d", counter);
      nk_label(ctx, counter_txt, NK_TEXT_CENTERED);
      if (nk_button_label(ctx, "Count")) {
        counter++;
        fprintf(stdout, "Counter=%d\n", counter);
      }
    }
    nk_end(ctx);
    /* Απεικόνιση στην οθόνη */
    glViewport(0, 0, w_width, w_height);
    glClear(GL_COLOR_BUFFER_BIT);
    nk_glfw3_render(&glfw, NK_ANTI_ALIASING_ON, MAX_VERTEX_BUFFER,
                    MAX_ELEMENT_BUFFER);
    glfwSwapBuffers(win);
  }
}

int main(void) {
  init();
  main_loop();
  cleanup();
  return 0;
}

Ο κώδικας έχει διαμεριστεί σε 3 συναρτήσεις που καλούνται από τη main(), την init(), τη main_loop() και την cleanup(). Η init() αρχικοποιεί τη GLFW και τη GLEW και δημιουργεί το αντικείμενο context. Η σχεδίαση του GUI γίνεται στη main_loop() στις γραμμές 63 έως και 72 και περιλαμβάνει τη δημιουργία μιας γραμμής υποδοχής στοιχείων ελέγχου με ύψος 30 pixels και θέσεις για 2 στοιχεία ελέγχου. Τα στοιχεία ελέγχου που προστίθενται είναι μια ετικέτα (nk_label) και ένα πλήκτρο (nk_button_label). Στις γραμμές 67 έως και 70 υπάρχει ο έλεγχος και οι εντολές που εκτελούνται για κάθε φορά που πατιέται το πλήκτρο. Τέλος η cleanup() απελευθερώνει όλους τους πόρους που έχουν δεσμευθεί.

Μεταγλώττιση και εκτέλεση σε Windows Η μεταγλώττιση και εκτέλεση της εφαρμογής στα Windows με τον μεταγλωττιστή MinGW γίνεται με τις ακόλουθες εντολές:

> gcc main.c ‐std=c99 ‐pedantic ‐IC:\glew ‐2.1.0\include
    ↪ ‐IC:\glfw ‐3.3.8.bin.WIN64\include ‐LC:\glfw ‐3.3.8.bin.WIN64\lib‐mingw‐w64
    ↪ ‐LC:\glew ‐2.1.0\lib\Release\x64 ‐lglfw3 ‐lopengl32 ‐lgdi32 ‐lm ‐lGLU32 ‐lGLEW32
    ↪ ‐o counter.exe
> counter.exe

Εναλλακτικά, εφόσον έχει εγκατασταθεί το εργαλείο make, η μεταγλώττιση και εκτέλεση μπορεί να γίνει χρησιμοποιώντας το αρχείο Makefile_win.mk (κώδικας 19.2), όπως στη συνέχεια:

> make ‐f Makefile_windows.mk
> counter.exe
Κώδικας 19.2: ch19_p1/Makefile_win.mk, makefile για Windows.
CC = gcc
BIN = counter
SRC = main.c
CFLAGS = -std=c99 -pedantic -IC:\glew-2.1.0\include -IC:\glfw-3.3.8.bin.WIN64\include 
LIBS = -LC:\glfw-3.3.8.bin.WIN64\lib-mingw-w64 -LC:\glew-2.1.0\lib\Release\x64
LIBS += -lglfw3 -lopengl32 -lgdi32 -lm -lGLU32 -lGLEW32

$(BIN): $(SRC)
    del $(BIN).exe
    $(CC) $(SRC) $(CFLAGS) $(LIBS) -o $(BIN)

clean:
    del $(BIN).exe

Μεταγλώττιση και εκτέλεση σε Linux Η μεταγλώττιση και εκτέλεση της εφαρμογής στο Linux γίνεται με τις ακόλουθες εντολές:

$ make ‐f Makefile_linux.mk
$ ./counter
Τα περιεχόμενα του αρχείου Makefile_linux.mk είναι τα ακόλουθα:
```{.mk title="Κώδικας 19.3: ch19_p1/Makefile_linux.mk - makefile για Linux." linenums="1"}
CC = gcc
BIN = counter
SRC = main.c
CFLAGS = -std=c99 -pedantic `pkg-config --cflags glew glfw3` 
LIBS = -lGL -lm -lGLU `pkg-config --libs glew glfw3` 

$(BIN): $(SRC)
    $(CC) $(SRC) $(CFLAGS) $(LIBS) -o $(BIN)

clean:
    rm -f $(BIN)

Μεταγλώττιση και εκτέλεση σε MacOS Η μεταγλώττιση και εκτέλεση της εφαρμογής στο MacOS γίνεται με τις ακόλουθες εντολές:

$ make ‐f Makefile_osx.mk
$ ./counter

Τα περιεχόμενα του αρχείου Makefile_osx.mk είναι τα ακόλουθα 19.4:

Κώδικας 19.4: ch19_p1/Makefile_osx.mk - makefile για MacOS.
CC = clang
BIN = counter
SRC = main.c
CFLAGS = -std=c99 -pedantic `pkg-config --cflags glew glfw3` 
LIBS = -framework OpenGL -framework Cocoa -framework IOKit -framework CoreVideo -lm `pkg-config --libs glew glfw3` 

$(BIN): $(SRC)
    $(CC) $(SRC) $(CFLAGS) $(LIBS) -o $(BIN)

clean:
    rm -f $(BIN)

Tο αποτέλεσμα της εκτέλεσης φαίνεται στο Σχήμα 19.5. Κάθε φορά που πιέζεται το πλήκτρο Count στην οθόνη της εφαρμογής η τιμή που αναγράφεται στην ετικέτα αυξάνεται κατά ένα. Αν και το παράδειγμα έχει εκτελεστεί σε MacOS, η εμφάνιση στο εσωτερικό των παραθύρων θα είναι η ίδια και σε Linux και σε Windows, καθώς, όπως ήδη αναφέρθηκε, η Nuklear δεν υποστηρίζει native look and feel.

Σχήμα 19.5: GUI για την εφαρμογή Counter.

19.3 Η βιβλιοθήκη tinyfiledialogs

Μια απλή λύση στην περίπτωση που η αλληλεπίδραση με τον χρήστη αφορά πλαίσια διαλόγου (π.χ. εμφάνισης μηνυμάτων, λήψης απαντήσεων από τον χρήστη, επιλογής διαδρομής και αρχείου στο σύστημα αρχείων) είναι η βιβλιοθήκη tinyfiledialogs. Στο παράδειγμα που ακολουθεί παρουσιάζονται μερικές από τις βασικές δυνατότητες της βιβλιοθήκης καθώς ζητείται από τον χρήστη να απαντήσει σε ένα ερώτημα και η απάντησή του καταγράφεται σε ένα αρχείο κειμένου. Στην πορεία χρησιμοποιούνται πλαίσια διαλόγου εμφάνισης μηνυμάτων (tinyfd_messageBox), ένα πλαίσιο διαλόγου εισόδου τιμής από τον χρήστη (tinyfd_inputBox), μια ειδοποίηση (tinyfd_notifyPopup) και ένα πλαίσιο επιλογής θέσης αποθήκευσης αρχείου στο σύστημα αρχείων. Τα αρχεία που θα χρειαστούν για το παράδειγμα φαίνονται στο Σχήμα 19.6.

ch19.6.png

Σχήμα 19.6: Αρχεία που χρησιμοποιούνται στο παράδειγμα με τη βιβλιοθήκη tinyfiledialogs.

Τα αρχεία tinyfiledialogs.c και tinyfiledialogs.h μπορούν να μεταφορτωθούν από το https://github.com/native-toolkit/libtinyfiledialogs. Στη συνέχεια παρατίθεται στον κώδικα 19.5 ο κώδικας του αρχείου main.c.

Κώδικας 19.5: ch19_p2/main.c - υλοποίηση διαλόγου με τον χρήστη μετρητή με τη βιβλιοθήκη tinyfiledialogs.
#include "tinyfiledialogs.h"
#include <stdio.h>
#include <string.h>

int main(void) {
  int mbox_response;
  char const *ibox_response;
  char const *lTheSaveFileName;
  FILE *lIn;
  char lBuffer[1024];

  /*ορίσματα της tinyfd_messageBox: α) τίτλος παραθύρου διαλόγου, β) μήνυμα
   * παραθύρου διαλόγου, γ) τύπος παραθύρου διαλόγου ("ok" "okcancel" "yesno"
   * "yesnocancel"), δ) τύπος εικονιδίου ("info" "warning" "error" "question"),
   * ε) αριθμός προκαθορισμένου πλήκτρου (0 για cancel/no, 1 για ok/yes,  2 για
   * no στο yesnocancel)*/
  mbox_response = tinyfd_messageBox("Question", "Do you want to continue?",
                                    "yesno", "question", 0);
  if (mbox_response == 0) {
    tinyfd_beep();
    /*ορίσματα της tinyfd_notifyPopoup: α) τίτλος παραθύρου διαλόγου, β) μήνυμα
     * παραθύρου διαλόγου, γ) τύπος εικονιδίου ("info" "warning" "error")*/
    tinyfd_notifyPopup("Bye", "The program was terminated!", "info");
    return 0;
  }

  /*ορίσματα της tinyfd_inputBox: α) τίτλος παραθύρου διαλόγου, β) μήνυμα
   * παραθύρου διαλόγου, γ) προκαθορισμένη είσοδος */
  ibox_response =
      tinyfd_inputBox("An important question",
                      "What is the best age to start learning C?", "0");
  if (ibox_response == 0) {
    tinyfd_beep();
    tinyfd_notifyPopup("Bye", "The program was terminated!", "info");
    return 0;
  }

  /*ορίσματα της tinyfd_saveFileDialog: α) τίτλος παραθύρου διαλόγου, β)
   * προκαθορισμένη διαδρομή και όνομα αρχείου, γ) πλήθος προτύπων αρχείων ή 0,
   * δ) πρότυπα αρχείων π.χ. {"*.txt","*.doc"} ή NULL αν το προηγούμενο όρισμα
   * είναι 0, ε) "text files" για αρχεία κειμένου ή NULL*/
  lTheSaveFileName = tinyfd_saveFileDialog(
      "Your answer deserves to be saved in the filesystem!", "./answer.txt", 0,
      NULL, "text files");
  if (!lTheSaveFileName) {
    tinyfd_messageBox("Error", "Save file name is NULL", "ok", "error", 1);
    return 1;
  }
  lIn = fopen(lTheSaveFileName, "w");
  if (!lIn) {
    tinyfd_messageBox("Error", "Can not open this file in write mode", "ok",
                      "error", 1);
    return 1;
  }
  sprintf(lBuffer, "The best age to start learning C is %s!", ibox_response);
  fputs(lBuffer, lIn);
  fclose(lIn);
  return 0;
}

Η μεταγλώττιση και εκτέλεση σε Windows με τον μεταγλωττιστή MinGW γίνεται με τις ακόλουθες εντολές (1).

  1. Αν η μεταγλώττιση επιστρέψει σφάλμα ότι δεν αναγνωρίζει το wchar_t, τότε θα πρέπει να συμπεριληφθεί το αρχείο επικεφαλίδας stddef.h στην αρχή του tinyfiledialogs.h
> gcc ‐o main.exe main.c tinyfiledialogs.c ‐LC:/mingw/lib ‐lcomdlg32 ‐lole32
> main.exe

Η μεταγλώττιση και εκτέλεση σε Linux και σε MacOS γίνεται με τις ακόλουθες εντολές:

$ gcc ‐o main main.c tinyfiledialogs.c
$ ./main

Ένα παράδειγμα εκτέλεσης του κώδικα (σε MacOS) φαίνεται στις εικόνες 19.7, 19.8 και 19.9. Η απάντηση του χρήστη στην ερώτηση “What is the best age to start learning C?”, που εισάγει σε ένα πλαίσιο διαλόγου εισαγωγής κειμένου, αποθηκεύεται σε ένα αρχείο, με το όνομα και τη διαδρομή του στο σύστημα αρχείων να προσδιορίζονται από τον χρήστη.

Σχήμα 19.7: Πλαίσιο διαλόγου εμφάνισης μηνύματος με δυνατότητα απάντησης (Yes/No) από τον χρήστη.
Σχήμα 19.8: Πλαίσιο διαλόγου εισαγωγής απάντησης από τον χρήστη.
Σχήμα 19.9: Πλαίσιο επιλογής διαδρομής και εισαγωγής ονόματος αρχείου για αποθήκευσή του στο σύστημα αρχείων.

19.4 Εναλλακτικές προσεγγίσεις στη δημιουργία περιβαλλόντων αλληλεπίδρασης με το χρήστη

Μια εναλλακτική προσέγγιση στην ανάπτυξη περιβάλλοντος αλληλεπίδρασης με τον χρήστη αποτελεί η βιβλιοθήκη ncurses (https://invisible-island.net/ncurses/ncurses.html). Παρόμοια με τις εφαρμογές CLI, δεν χρησιμοποιεί γραφικά στοιχεία ελέγχου και εκτελείται σε περιβάλλον τερματικού. Ωστόσο, προσφέρει τη δυνατότητα κατασκευής εφαρμογών με παράθυρα, μενού και πλήκτρα όπως στην εικόνα 19.10.

ch19_ncurses.png

Σχήμα 19.10: Παράδειγμα οθόνης εφαρμογής που έχει κατασκευαστεί με τη βιβλιοθήκη ncurses. Πηγή: By Attys - Own work, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=15696679

Ένα απλό παράδειγμα χρήσης της ncurses παρουσιάζεται στη συνέχεια. Ο κώδικας 19.6 εμφανίζει στο κέντρο της οθόνης, μέσα σε πλαίσιο, ένα χρονόμετρο που μετρά αντίστροφα μέχρι να εξαντληθεί ο χρόνος (σε λεπτά) που έχει περάσει ως όρισμα γραμμής εντολών κατά την εκτέλεση. Καθώς το χρονόμετρο μετρά αντίστροφα, όταν ο μετρητής δείχνει 30 ή λιγότερα δευτερόλεπτα, το χρώμα του αλλάζει σε κόκκινο.

Κώδικας 19.6: ch19_p3.c - υλοποίηση χρονομετρητή με τη βιβλιοθήκη ncurses.
#include <ncurses.h>
#include <stdlib.h>
#include <time.h>

bool isPaused = false;
void togglePause(void) { isPaused = !isPaused; }

int main(int argc, char *argv[]) {
  if (argc > 2) {
    printf("Usage for 1 minute timer: %s\n", argv[0]);
    printf("Usage for <minutes> timer: %s <minutes>\n", argv[0]);
    return 1;
  }
  int duration = 60;
  if (argc == 2) {
    duration = atoi(argv[1]) * 60;
  }

  initscr();     // αρχικοποίηση της οθόνης
  start_color(); // έναρξη λειτουργικότητας χρωμάτων
  cbreak();      // αρχικοποίηση της οθόνης
  noecho(); // απόκρυψη της εισόδου που πληκτρολογεί ο χρήστης
  nodelay(stdscr, TRUE); // κάνε τη getch() non-blocking

  init_pair(1, COLOR_WHITE, COLOR_BLACK);
  init_pair(2, COLOR_RED, COLOR_BLACK);

  int win_height = 4, win_width = 50;
  int starty = (LINES - win_height) / 2; // κεντράρισμα κατακόρυφα
  int startx = (COLS - win_width) / 2; // κεντράρισμα οριζόντια
  WINDOW *timer_win = newwin(win_height, win_width, starty, startx);
  box(timer_win, 0, 0); // σχεδίαση ενός πλαισίου γύρω από το παράθυρο
  time_t startTime = time(NULL); // η ώρα έναρξης του μετρητή
  int elapsed = 0;
  printw("\nPress 'p' to pause/resume or 'q' to exit the timer.");
  refresh();
  while (elapsed < duration) {
    int ch = getch();
    if (ch == 'p' || ch == 'P') { // p για παύση/επανεκκίνηση
      togglePause();
    } else if (ch == 'q' || ch == 'Q') { // q για έξοδο
      break;
    }
    if (!isPaused) {
      wclear(timer_win);
      box(timer_win, 0, 0); // ξανασχεδίασε το πλαίσιο
      int remaining = duration - elapsed;
      int color_pair =
          remaining <= 30 ? 2 : 1; // αλλαγή χρώματος σε κόκκινο για τα
                                   // τελευταία 30 δευτερόλεπτα
      wattron(timer_win, COLOR_PAIR(color_pair));
      mvwprintw(timer_win, 1, 1, "Timer: %d out of %d seconds remaining.",
                duration - elapsed, duration);
      wattroff(timer_win, COLOR_PAIR(color_pair));
      wrefresh(timer_win);
      napms(100); // περίμενε 1 δευτερόλεπτο
      elapsed = time(NULL) - startTime;
    } else {
      wrefresh(timer_win);
      mvwprintw(timer_win, 2, 1, "Paused - Press 'p' to resume.");
      napms(100); // Περίμενε 0.1 δευτερόλεπτα
    }
  }
  endwin();
  return 0;
}

Η μεταγλώττιση και εκτέλεση του προγράμματος γίνεται ως εξής:

$ gcc ‐o ch19_p3 ch19_p3.c ‐lncurses
$ ./ch19_p3 2

Ο χρήστης μπορεί να ορίσει το χρονικό διάστημα μέτρησης σε λεπτά (στο παραπάνω παράδειγμα εκτέλεσης η τιμή αυτή είναι 2 που αντιστοιχεί σε 120 δευτερόλεπτα). Κατά την εκτέλεση του προγράμματος, πατώντας το πλήκτρο p ο μετρητής τίθεται σε παύση ή ξεκινά αν είναι ήδη σε παύση, ενώ με το πλήκτρο q το πρόγραμμα τερματίζει την εκτέλεσή του πρόωρα. Ένα παράδειγμα εκτέλεσης φαίνεται στα Σχήματα 19.11 και 19.12.

Σχήμα 19.11: Αντίστροφη μέτρηση χρόνου.
Σχήμα 19.12: Αλλαγή χρώματος σε κόκκινο στα τελευταία 30 δευτερόλεπτα.

Μια άλλη εναλλακτική προσέγγιση δημιουργίας γραφικού περιβάλλοντος αλληλεπίδρασης με τον χρήστη είναι να χρησιμοποιηθεί η Python ως γλώσσα με την οποία θα κατασκευαστεί το GUI (το frontend) και η «λογική» της εφαρμογής (το backend) να υλοποιηθεί σε C. Η Python διαθέτει πολλά GUI frameworks (π.χ. Tkinter, wxPython, PyQt5, Kivy, PySide2) που επιτρέπουν την εύκολη δημιουργία GUIs. Στο Κεφάλαιο 20 περιγράφονται τρόποι κλήσης κώδικα C από την Python.

19.5 Ασκήσεις

Άσκηση 1
Η μικρή εφαρμογή GUI μετρητή που υλοποιήθηκε στην παράγραφο 19.2.4, αποτελεί την πρώτη και απλούστερη από τις 7 εργασίες κατασκευής παραθυρικών εφαρμογών που περιγράφονται στην ιστοσελίδα 7GUIs 2. Υλοποιήστε τη δεύτερη από τις εργασίες (Temperature Converter) με τη C και τη βιβλιοθήκη Nuklear.

Άσκηση 2
Κατασκευάστε μια εφαρμογή που το σύστημα να επιλέγει έναν τυχαίο ακέραιο αριθμό στο διάστημα [1,100] και ο χρήστης να προσπαθεί να τον μαντέψει. Να ζητείται επαναληπτικά από τον χρήστη μέσω ενός παραθύρου διαλόγου εισαγωγής τιμής να μαντέψει την τιμή. Αν πετύχει την τιμή, να εμφανίζεται παράθυρο μηνύματος με το μήνυμα «συγχαρητήρια» και το πλήθος των προσπαθειών που χρειάστηκε. Αν δεν πετύχει την τιμή, να εμφανίζει μήνυμα που να τον πληροφορεί για το εάν η τιμή που εισήγαγε είναι μικρότερη ή μεγαλύτερη από τη ζητούμενη τιμή. Για τα παράθυρα διαλόγου με τον χρήστη, να χρησιμοποιηθεί η βιβλιοθήκη tinyfiledialogs.

Άσκηση 3
Xρησιμοποιώντας τη βιβλιοθήκη ncurses γράψτε ένα πρόγραμμα που να εμφανίζει ένα μενού επιλογών όπως στο Σχήμα 19.13. ch19_ncurses_ex3.png

Σχήμα 19.13: Επιθυμητή έξοδος του προγράμματος που θα προκύψει ως λύση της Άσκησης 3.

Ο χρήστης να μπορεί να κινείται στο μενού με τα πλήκτρα πάνω και κάτω και πατώντας το πλήκτρο enter να εμφανίζει μήνυμα σχετικά με την επιλογή που έκανε ή αν η επιλογή είναι το Exit το πρόγραμμα να τερματίζει. Μάθετε περισσότερα για το ncurses στο 3.

Άσκηση 4
Υλοποιήστε μια GUI εφαρμογή που να επιτρέπει τη δημιουργία, διαγραφή και προβολή εργασιών. Για κάθε εργασία να καταγράφεται ο τίτλος της και η ημερομηνία στην οποία θα πρέπει να έχει ολοκληρωθεί. Η προβολή των εργασιών να τις εμφανίζει σε μια λίστα, σε φθίνουσα ημερολογιακή σειρά. Χρησιμοποιήστε τη βιβλιοθήκη Nuklear.


  1. Jeremy Reimer. “A History of the GUI”. Στο: Ars Technica 5 (2005), σσ. 1–17. 

  2. 7GUIs: A GUI Programming Benchmark. https://eugenkiss.github.io/7guis/. Accessed: 2023-06-01. 

  3. E. S. Raymond, Z. M. Ben-Halim και Dickey T. Writing Programs with NCURSES. https://invisible-island.net/ncurses/ncurses-intro.html. Accessed: 2023-06-01.