Kategorien
C Linux Open Source Programmieren Tutorials

Programmieren in C: Vollbildfenster mit Xlib

Man sollte meinen, es sei relativ einfach auf einem Linux X11 Desktop System ein Fenster ohne Verwendung von externen Bibliotheken wie SDL oder GLFW im Vollbildmodus zu öffnen. Dies trifft aber nur zur Hälfte zu. Während das Öffnen eines Fensters noch recht einfach und direkt möglich ist, gestaltet sich das Umschalten des Vollbildmodus etwas komplizierter. In diesem Beitrag möchte ich euch zeigen, wie ein Vollbildfenster unter Linux mit Xlib (X11 API) geöffnet werden kann.

Anmerkung:
Mit der moderneren Xcb (X C Bridge) funktioniert das recht ähnlich, wenn auch mit etwas weniger Umwegen, wie man hier sehen kann. Mit der Funktion „xcb_change_property“ wird das Konstruieren eines ClientMessage Events abstrahiert.

Voraussetzungen

  • Ein Linux Desktop mit Xorg und libX11
  • Ein C Compiler (gcc)
  • Ggf. eine C Entwicklungsumgebung (ich nutze KDevelop)
  • Etwas C Kenntnisse (dieser Beitrag ist kein C Tutorial)

X11 Fenster öffnen

Im Netz gibt es bereits gute deutschsprachige Anleitungen zum Öffnen eines Fensters mit Xlib (hier zum Beispiel). Daher beschränke ich diesen Teil auf meinen Beispielcode und eine kurze Zusammenfassung, was dieser tut.
Was es mit den Atomen auf sich hat, die in diesem Code verwendet werden erkläre später.

WinHandle* create_window(const WinParameter* win_param)
{
    // Verbindung zum Xorg Server herstellen
    Display* display = XOpenDisplay(NULL);
    
    // Überprüfe, ob die Verbindung erfolgreich hergestellt wurde
    if(!display) {
        log_error("create_window", "Verbindung zum Xorg Server konnte nicht hergestellt werden!");
        
        return NULL;
    }
    
    // Standardbildschirm abfragen
    int screen =  XDefaultScreen(display);
    
    // Mitgegebene Fensterparameter auslesen
    unsigned int win_width = win_param->width;
    unsigned int win_height = win_param->height;
    char* win_title = win_param->title;
    
    // Elternfenster (in dem Fall das Wurzelfenster) bestimmen
    Window parent_win =  XRootWindow(display, screen);
    
    // Rand- und Hintergrundfarbe setzen
    unsigned long border_color = XBlackPixel(display, screen);
    unsigned long background_color = border_color;
    
    // Randbreite setzen
    unsigned int border_width = 2;
    
    // X11 Fenster erstellen
    Window window = XCreateSimpleWindow(display, parent_win, 0, 0, win_width, win_height,
                                        border_width, border_color, background_color);
    
    // Fenstertitel setzen
    XStoreName(display, window, win_title);
    
    // Setzen, welche Events von der Anwendung verarbeitet
    long int event_mask = KeyPressMask | KeyReleaseMask | VisibilityChangeMask;
    XSelectInput(display, window, event_mask);
    
    // Die Anwendung soll sich um Close Events kümmern
    Atom wm_close = XInternAtom(display, "WM_DELETE_WINDOW", False);
    
    if(!wm_close) {
        log_error("create_window", "WM_DELETE_WINDOW Atom nicht gefunden!");
        
        XDestroyWindow(display, window);
        XCloseDisplay(display);
        
        return NULL;
    }
    
    XSetWMProtocols(display, window, &wm_close, 1);
    
    // Fenster anzeigen
    XMapWindow(display, window);
    
    // Warten, bis das Fenster sichtar wird
    XEvent xev;
    
    while(true) {
        XNextEvent(display, &xev);
        
        if(xev.type == VisibilityNotify) {
            break;
        }
    }
    
    // Fensterhandle konstruieren
    X11WinHandle* x11_win_handle = malloc(sizeof(X11WinHandle));
    
    // Felder des Fensterhandles setzen
    x11_win_handle->display = display;
    x11_win_handle->window = window;
    x11_win_handle->screen = screen;
    
    return x11_win_handle;
}

Zusammengefasst bewirkt der Quellcode folgende Aktionen:

  1. Verbindung zum Xorg Server herstellen
  2. Standardbildschirm abfragen
  3. Elternfenster (das Wurzelfenster) abfragen
  4. X11 Fenster erstellen
  5. Fenstertitel setzen
  6. Events angeben, für das sich die Anwendung interessiert
  7. Setzen, dass sich die Anwendung um das Close Event kümmert
  8. Fenster anzeigen
  9. Warten, bis das Fenster angezeigt wird
  10. Eine Fansterhandle Struktur konstruieren und diese als void Pointer zurückgeben

Setzen des Vollbildmodus

Um ein Fenster über den Fenstermanager in den Vollbildmodus zu schicken wird vereinfacht ausgedrückt ein ClientMessage Event von der Applikation an den Fenstermanager geschickt. Dieses Event enthält die Quelle, die auszuführende Aktion (Eingenschaft hinzufügen, löschen oder umkehren) und die Fenstereigenschaft, die hinzugefügt oder entfernt werden soll. Dabei kommen sogenannte Atome zum Einsatz. Atome sind vorzeichenlose long Zahlen und für eine gültige Zahl steht ein C-String. Man kann also vereinfacht ausdrücken, dass diese Atome praktisch Referenzen zu Strings sind.

Für das aktivieren des Vollbildmodus benötigen wir das „_NET_WM_STATE“ und das „_NET_WM_STATE_FULLSCREEN“ Atom. Diese holen wir uns mit folgenden Code:

// Benötigten Atome bestimmen
Atom wm_state = XInternAtom(x11_display, "_NET_WM_STATE", False);
Atom wm_fullscreen = XInternAtom(x11_display, "_NET_WM_STATE_FULLSCREEN", False);

// Überprüfen, ob die benötigten Atome vorhanden sind
if(wm_state == None || wm_fullscreen == None) {
    log_error("set_window_fullscreen", "Fenstermanager unterstützt kein Vollbildmodus!");
    
    return false;
}

Als nächstes muss das ClientMessage Event konstruiert werden. Dazu legen wir ein XEvent auf dem Stack an und setzen die Grunddaten (Event Typ, Display, Fenster, Nachrichten Typ und Format):

// Ein ClientMessage Event konstruieren welches das Fenster in den gewünschten Zustand bringt
XEvent xev;
xev.type = ClientMessage;
xev.xclient.display = x11_display;
xev.xclient.window = x11_window;
xev.xclient.message_type = wm_state;
xev.xclient.format = 32;

Den x11_display und das x11_window entsprechen die Variablen, die im Codebeispiel zum öffnen des Fensters im Window Handle gespeichert wurden. Der Nachrichtentyp muss nach den Vorgaben der Freedesktop Spezifikationen (siehe hier im Abschnitt über _NET_WM_STATE) auf _NET_WM_STATE gesetzt werden und das Format auf 32.
Das Format gibt in welchen Datentyp die extra Daten im Datenfeld des Events vorliegen. Bei Format 32 kann man 5 long Werte, bei Format 16 kann man 10 und bei Format 8 kann man 20 Werte in einer Nachricht unterbringen.
Als nächstes muss im ersten Datenfeld angegeben werden, ob ein Fensterattribut hinzugefügt, entfernt oder umgekehrt werden soll. In diesem Szenario macht es Sinn die Vollbildeigenschaft beim Einschalten des Vollbildmodus hinzuzufügen und beim Ausschalten diese wieder zu entfernen. Dies wird mit folgendem Code realisiert:

// Folgende Konstanten müssen definiert sein
// _NET_WM_STATE_REMOVE = 0
// _NET_WM_STATE_ADD = 1
// EVENT_SOURCE_APPLICATION = 1

if(fullscreen) {
    xev.xclient.data.l[0] = _NET_WM_STATE_ADD;
}
else {
    xev.xclient.data.l[0] = _NET_WM_STATE_REMOVE;
}

Anschließend muss gesetzt werden, welche Fenstereigenschaft hinzugefügt oder entfernt werden soll. Hier wollen wir die Vollbildeigenschaft bearbeiten.

xev.xclient.data.l[1] = wm_fullscreen;

Die letzten Felder setzen wir, wie es in der Spezifikation vorgegeben ist.

xev.xclient.data.l[1] = wm_fullscreen;
xev.xclient.data.l[2] = 0;
xev.xclient.data.l[3] = EVENT_SOURCE_APPLICATION; // Bedeutet, dass das Event von der Applikation kommt
xev.xclient.data.l[4] = 0;

Damit ist das Event fertig zum Versenden.
Um das Event zu abzusenden brauchen wir zuerst den Empfänger und die Eventmaske. Der Empfänger ist das Wurzelfenster, welches vom Fenstermanager ausgeht. Die Eventmaske setzen wir entsprechend der X11 Dokumentation auf SubstructureRedirectMask:

// Event Empfänger und Eventmaske setzen
Window x11_root_window = XRootWindow(x11_display, x11_screen);
long event_mask = SubstructureRedirectMask;

Zum Schluss muss nur noch das Event abgeschickt werden und überprüft werden, ob es beim Versenden zu einem Fehler kam:

// Event senden
int evt_success = XSendEvent(x11_display, x11_root_window, False, event_mask, &xev);

// Überprüfen, ob das Event erfolgreich gesendet wurde
if(!evt_success) {
    log_error("set_window_fullscreen", "Fehler beim Versenden des X11 Events!");

    return false;
}

return true;

Überprüfen, ob sich das Fenster im Vollbildmodus befindet

Das Abfragen des gegenwärtigen Status der Vollbildeigenschaft funktioniert recht ähnlich. Nur dass diesmal kein Event verschickt werden muss, da sich die Fenstereigenschaften mittels der Funktion „XGetWindowProperty“ direkt abfragen lassen.

Für die Rückgabe verwende ich eine Struktur bestehend aus 2 bool Werten. Der erste Wert steht für den Erfolg oder Misserfolg der Operation und der zweite steht dafür, ob sich das Fenster im Vollbildmodus befindet oder nicht. Der entsprechende Datentyp ist folgendermaßen definiert:

typedef struct BoolResultSt {
    bool success;
    bool value;
} BoolResult;

Am Anfang der Funktion werden beide Werte der Rückgabe auf false gesetzt.

// Standard Rückgabe konstruieren
BoolResult ret_val;
ret_val.success = false;
ret_val.value = false;

Für das Abfragen des Status benötigen wir das Atom von „_NET_WM_STATE“. Der folgende Codeabschnitt fragt dieses ab:

// Die benötigten Atome auslesen
Atom wm_state = XInternAtom(x11_display, "_NET_WM_STATE", False);

// Überprüfe, ob die Atome definiert sind
if(wm_state == None) {
    log_error("get_window_fullscreen", "Failed to get X11 atom!");
    
    return ret_val;
}

Im nächsten Schritt wollen wir den Wert der „_NET_WM_STATE“ Eigenschaft abfragen. Dies wird mit der Xlib Funktion „XGetWindowProperty“ erledigt:

// Daten für das Abfragen der Fenstereigenschaften deklarieren
long long_offset = 0;
long long_length = 64; // Maximale Anzahl an auszulesenden Eigenschaften
unsigned char* prop_value = NULL;
int prop_format;
unsigned long prop_items_count;
unsigned long prop_bytes_a;
Atom prop_type;

// Eigenschaften vom "_NET_WM_STATE" Schlüssel abfragen
Status get_success = XGetWindowProperty(x11_display, x11_window, wm_state, long_offset, long_length, False, AnyPropertyType, &prop_type, &prop_format, &prop_items_count, &prop_bytes_a, &prop_value);

// Überprüfe, ob das Abfragen der Eigenschaften erfolgreich war
if(get_success != Success) {
    log_error("get_window_fullscreen", "Failed to get property content for WM state!");
    
    // Aufräumen
    if(prop_value)
        XFree(prop_value);
    
    return ret_val;
}

Nach dieser Aktion befindet sich in der Variable „prop_type“ der Typ des in „prop_value“ gespeicherten Rückgabewertes (in diesem Fall steht das Atom für den Wert „ATOM“). Die Anzahl der Elemente in „prop_value“ steht jetzt in der Variable „prop_items_count“. Die „prop_type“ Variable kann aber auch den Wert 0 annahmen, falls das Fenster die „_NET_WM_STATE“ nicht hat. Dies wird im folgenden abgefangen:

// Wenn das Fenster die _NET_WM_STATE Eigenschaft nicht hat
if(prop_type == None) {
    log_error("get_window_fullscreen", "The window hasn't the WM_STATE property!");
    
    // Aufräumen
    if(prop_value)
        XFree(prop_value);
    
    return ret_val;
}

Als letztes müssen wir nur noch über alle in „prop_value“ enthaltenen Eigenschaften drübergehen und überprüfen, ob da ein Atom mit dem Wert „_NET_WM_STATE_FULLSCREEN“ dabei ist. Zuerst überprüfen wir also, ob für das aktuelle Fenster überhaupt irgendwelche Fenstereigenschaften gesetzt sind. Falls keine gesetzt sind ist klar, dass sich das Fenster nicht im Vollbildmodus befindet:

// Wenn dem Fenster keine Fensterstatuseigenschaften zugewiesen sind
if(!prop_items_count) {
    // Aufräumen
    if(prop_value)
        XFree(prop_value);
    
    // Fenster ist nicht im Vollbildmodus
    ret_val.success = true;
    return ret_val;
}

Und mit dem folgenden Code überprüfen wir alle gesetzten Eigenschaften:

// Property value zu einer Liste von Atomen umwandeln
Atom* prop_value_atom_ptr = (Atom*) prop_value;

// Gesetzte Eigenschaften für das Fenster durchgehen und schauen, ob die Vollbild
// Eigenschaft dabei ist
for(long i = 0; i < prop_items_count; i++) {
    // Aktuelles Atom aus dem Array auslesen und in CString umwandeln
    Atom current_value_atom = prop_value_atom_ptr[i];
    char* current_value_atom_str = XGetAtomName(x11_display, current_value_atom);
    
    // Debug
    log_debug("get_window_fullscreen", "current_value_atom_str: %s", current_value_atom_str);
    
    // Wenn die aktuelle Eigenschaft die ist, dass das Fenster sich im Vollbild befindet
    if(strcmp(current_value_atom_str, "_NET_WM_STATE_FULLSCREEN") == 0) {
        ret_val.value = true;
        XFree(current_value_atom_str);
        break;
    }
    
    XFree(current_value_atom_str);
}

// Aufräumen
XFree(prop_value);

ret_val.success = true;
return ret_val;

Vollständiges Codebeispiel

Ein voll funktionsfähiges Programm können Sie hier herunterladen (den Code können Sie ohne Einschränkungen entsprechend der CC0 1.0 Universal Lizenz verwenden):

Zum Kompilieren wird Meson, Ninja und ein C11 Compiler (z.B. GCC) benötigt.
Zum Kompillieren folgende Schritte ausführen:

  1. ZIP Archiv entpacken
  2. Mit der Shell in den Quellcode Ordner springen
  3. Nachfolgende Befehle eintippen
  4. meson build
  5. cd ./build
  6. ninja

Zum Starten reicht es die im build Verzeichnis erzeugte Datei „x11_fs“ doppelt anzuklicken. Daraufhin sollte sich ein kleines schwarzes Fenster öffnen. Durch drücken der F11 Taste wird zwischen Vollbild und Fenstermodus umgeschaltet und durch drücken der ESC Taste oder anklicken des Schließen Knopfes vom Fenster wird das Programm beendet.

Tl;dr

Mit SDL, GLFW, SFML und Co geht’s leichter.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.