Programowanie z użyciem CLX --- uproszczony przewodnik

Zbyszek Jurkiewicz
zbyszek@mimuw.edu.pl

Poniżej opisujemy krótko najważniejsze funkcje interfejsu CLX.

Wszystkie funkcje i makra CLX zdefiniowane są w pakiecie xlib, dlatego należy ich nazwy poprzedzać przez xlib:... lub wywołać na początku

  (use-package :xlib)

W definicjach funkcji podano tylko najczęściej używane parametry. Nawiasy kwadratowe oznaczają opcjonalność argumentów.

Nawiązywanie i zamykanie połączenia

Pracę z CLX należy rozpocząć od uzyskania dostępu do ekranu, co wymaga nawiązania połączenia z serwerem X stacji graficznej. Służy do tego funkcja

  (open-display komputer [:display wyświetlacz])

W przypadku udanego połączenia wartością jest obiekt type display, który należy zachować (np. na zmiennej globalnej *the-display*), bo będzie potrzebny w kolejnych wywołaniach. Wartość nil oznacza nieudane połączenie.

Parametr komputer podaje nazwę komputera, na którym pracuje serwer X. Nazwa serwera jest napisem zależnym od implementacji, zwykle akceptowane są nazwy internetowe. Dla komputera lokalnego można podać "localhost", zwykle jednak (zależnie od implementacji) wystarcza "" lub nil.

Parametr wyświetlacz podaje numer wyświetlacza na podanym serwerze X. Musi być liczbą całkowitą. Ponieważ wartość domyślna wynosi 0, prawie zawsze parametr ten można pomijać. Ma znaczenie tylko w sytuacjach, gdy mamy kilka wyświetlaczy na tym samym komputerze.

  > (defparameter *the-display* nil
  >   "Bieżące połączenie z serwerem X")
  NIL
  > (setq *the-display* (xlib:open-display "localhost"))
  #<XLIB:DISPLAY :0 (The XFree86 Project, Inc R3360)>

Większość operacji graficznych odnosi się do określonego ekranu na serwerze, zwykle jest to ekran domyślny otrzymywany funkcją

  (display-default-screen display)

Zwraca ona domyślny ekran serwera X (ale nie wszędzie działa). Zgodnie z dokumentacją jest ona równoważna

  (first (display-roots display))
  > (defparameter *the-screen* nil
  >   "Wybrany ekran na serwerze")
  NIL
  > (setq *the-screen* (xlib:display-default-screen *the-display*))
  #<XLIB:SCREEN :0.0 800x600x16 TRUE-COLOR>

Otrzymany ekran posiada szereg atrybutów, które warto poznać, aby korzystać z nich w zaawansowanych aplikacjach. Należą do nich wysokość, szerokość i głębokość (liczba bitów na kolor) ekranu.

  > (xlib:screen-height *the-screen*)
  600
  > (xlib:screen-width *the-screen*)
  800
  > (let ((depth (xlib:screen-root-depth *the-screen*)))
  >   (format t (if (= depth 1)
  >                 "Color plane depth = ~A (monochrome).~%"
  >                 "Color plane depth = ~A.~%")
  >           depth))
  Color plane depth = 16.
  NIL

Połączenie z serwerem X zamykamy używając funkcji

  (close-display display)
gdzie display jest obiektem zwróconym przez open-display.

Funkcja ta zamyka podane połączenie z serwerem X, zwalniając wszystkie pobrane zasoby: okna, fonty, pixmapy, colormapy, kursory i konteksty graficzne, nie wolno się więc od tego momentu do nich odwoływać. Ponadto funkcja ta podobno usuwa wszystkie niezrealizowane żądania wyjścia już umieszczone w kolejce w buforze, lecz jeszcze nie wysłane. W praktyce nie zawsze jest to prawdą, więc lepiej samemu oczyścić bufor.

  > (xlib:close-display *display*)
  NIL

Dodatkowe funkcje nie opisane w podręczniku CLX

  (display-release-number display)

Natomiast

   (display-version-number display)
jest w podręczniku, ale nie w implementacji.

Ekrany

Z ekranem związanych jest szereg istotnych informacji. Funkcje

  (screen-height ekran)
  (screen-width ekran)
podają jego rozmiary, natomiast funkcje
  (screen-black-pixel ekran)
  (screen-white-pixel ekran)
zwracają domyślne wartości kolorów (jako obiekty typu color).

Funkcja

  (screen-root ekran)
zwraca podstawowe okno ekranu (tzw. okno tła), natomiast funkcja
  (screen-default-colormap ekran)
domyślną paletę kolorów ekranu (obiekt typu colormap).

Okna

Następnym krokiem po nawiązaniu połaczenia jest tworzenie okien i ich wyświetlanie na ekranie. Okna oraz pixmapy to obiekty typu drawable. Część funkcji jest wspólna, np.

  (drawable-height drawable)
  (drawable-width drawable)
  (drawable-x drawable)
  (drawable-y drawable)
  (drawable-display drawable)
Okna tworzymy używając
 
  (create-window :parent okno :x integer :y integer   
                 :width integer :height integer
                 [:border-width integer=0
                 :background kolor-tła :border kolor-ramki
                 :colormap colormap
                 :override-redirect {:on | :off}
                 :class {:input-output | :input-only}
                 :save-under {:on | :off}
                 :event-mask mask-list :cursor kursor])
gdzie

Funkcja zwraca nowy obiekt typu window. Owszem, należy go zachować, inaczej nigdy nie wyświetlimy okna na ekranie.

Utworzenie okna nie powoduje bowiem wyświetlenia go. Wyświetlenia okna należy zażądać osobną funkcją

  (map-window okno)

Spowoduje to przygotowanie okna do wyświetlania. Jeśli okno nadrzędne jest widoczne na ekranie, to okno zostanie wyświetlone, w przeciwnym razie nastąpi to dopiero po wyświetleniu okna nadrzędnego.

W tym momencie warto powiedzieć, że żądania do serwera są buforowane i wysyłane paczkami. Natychmiastowe wysłanie żądań z bufora można wymusić używając funkcji

  (display-force-output wyświetlacz)

W naszym przypadku lepiej jednak skorzystać z funkcji

  (display-finish-output wyświetlacz)
która powoduje wysłanie wszystkich żądań znajdujących się w buforze do serwera. Następnie oczekuje na ich przetworzenie.

Funkcji tej warto używać przed likwidacją okna, zamknięciem połączenia i w innych podobnych sytuacjach, żeby nie dopuścić do pozostawienie w kolejce zleceń dla nie istniejących obiektów.

Ponieważ jednak wyświetlenie okna zajmuje pewien czas, nawet użycie tej funkcji nie gwarantuje, że podane zaraz po jej wywołaniu polecenia rysowania nie zostaną zgubione (bo okno nie będzie jeszcze gotowe na ich przyjęcie). Można sobie z tym radzić doraźnie, używając funkcji sleep z Common Lispu.

  (defparameter *moje-okno* nil)

  ...

  (setq *moje-okno*
        (xlib:create-window :parent (xlib:screen-root *the-screen*)
                            :x 50 :y 50 :width 300 :height 200
                            :background (xlib:screen-white-pixel *the-screen*)
                            :border (xlib:screen-black-pixel *the-screen*)))
  (xlib:map-window *moje-okno*)
  (xlib:display-finish-output *the-display*)
  (sleep 3)

Lepszą metodą jest jednak uruchomienie pętli obsługi przychodzących zdarzeń, o czym powiemy wkrótce.

Do schowania okna służy funkcja

  (unmap-window window)
a jego całkowitej likwidacji dokonuje funkcja
  (destroy-window window)
Niszczy ona okno oraz wszystkie jego podokna (okna podrzędne), zwalniając zasoby serwera. Jeśli okno było wyświetlone, to znika z ekranu.

Funkcja

  (map-subwindows window)
powoduje przygotowanie do wyświetlenia wszystkich podokien podanego okna, zaś konstrukcja
  (with-state (okno) wyrażenie ...)
łączy kilka komunikatów generowanych wyrażeniami tak, aby zostały wspólnie wysłane.

%% (query-tree okno) ==> children, parent

Rysowanie

Do rysowania niezbędne są dwie rzeczy:

Konteksty graficzne

W systemie X-Windows operacje graficzne wymagają struktur nazwanych kontekstami graficznymi (graphics-contexts, GC). CLX representuje konteksty graficzne strukturami Common Lispu. Poniżej lista pól wraz z wartościami domyślnymi.

FieldDefault
arc-mode :pie-slice
background "white"
cap-style :butt
clip-mask :none
clip-ordering unsorted
clip-x 0
clip-y 0
dash-offset 0
dashes 4
exposures off
fill-rule even-odd
fill-style solid
font undefined
foreground "black"
function 2
join-style :miter
line-style :solid
line-width 0
paint
plane-mask maska jedynek
stipple undefined
subwindow-mode:clip-by-children
tile undefined
ts-x 0
ts-y 0

Więcej informacji o kontekstach graficznych w dokumentacji CLX.

Kontekst graficzny tworzymy funkcją

  (create-gcontext :drawable drawable
                   [:background kolor-tła
                    :foreground kolor-rysowania
                    :font czcionka])

Funkcja ta tworzy i zwraca nowy kontekst graficzny, pobierając z podanego okna lub piksmapy głębokość (liczbę bitów na kolor piksela) oraz informacje o ekranie i jego oknie pierwotnym.

Pozornie dla każdego okna powinno się tworzyć nowy kontekst graficzny; w praktyce nie jest to jednak konieczne (choć możliwe), gdyż zwykle większość używanych okien ma tę samą głębokość i ekran. Konteksty (poza pierwszym) tworzy się więc dla przyśpieszenia rysowania, aby mieć szybki dostęp do podanej kombinacji atrybutów rysowania.

  > (defparameter *moj-kontekst* nil)
  *MOJ-KONTEKST*
  > (setq *moj-kontekst*
  >       (xlib:create-gcontext
  >         :drawable *moje-okno*
  >         :background (xlib:screen-white-pixel *the-screen*)
  >         :foreground (xlib:screen-black-pixel *the-screen*)))
  #<XLIB:GCONTEXT localhost:0 62914562>

Dla atrybutów kontekstu zdefiniowane są funkcje dostępu, np.

  (gcontext-background kontekst)
  (gcontext-foreground kontekst)
  (gcontext-font kontekst)

Atrybuty można definiować lokalnie używając konstrukcji

  (with-gcontext (kontekst :foreground color
                                :background color)
    wyrażenie ...)

Operacje rysowania

Biblioteka CLX dostarcza funkcje rysowania podstawowych obiektów graficznych: punktów, linii, prostokątów, łuków i okręgów.

Okno lub jego fragment można oczyścić funkcją

  (clear-area okno :x integer :y integer
                        :width integer :height integer)

Do rysowania linii służy funkcja

  (draw-line okno kontekst x1 y1 x2 y2)
Kreśli ona linię pomiędzy podanymi punktami używając bieżącego koloru rysowania.

Prostokąty rysujemy funkcją

  (draw-rectangle okno kontekst x y szerokość
                  wysokość [czy-wypełniać])
Rysuje ona prostokąt (w miejscu podanym współrzędnymi lewego górnego rogu) o podanej wysokości i szerokości. Jeśli podano argument czy-wypełniać i jest on różny od domyślnej wartości nil, to wnętrze prostokąta będzie wypełnione kolorem rysowania.

W CLX nie ma osobnej funkcji rysowania okręgów. Okrąg uzyskujemy rysując łuk pełny. Do rysowania łuków (wycinków okręgu lub dowolnej elipsy służy jedna funkcja

  (draw-arc okno kontekst x y szerokość wysokość
            kąt początkowy kąt końcowy [czy-wypełniać])

Parametry x i y wyznaczają lewy górny prostokąta o podanej szerokości i wysokości, zawierającego elipsę, której fragmentem jest łuk (czyli jej środek jest w punkcie o współrzędnych x+szerokość/2 oraz y+wysokość/2).

Zakres łuku wyznaczony jest podanymi kątami, mierzonymi w radianach (protokół X używa jako jednostki 1/64 stopnia, co uwielbiają programiści w C korzystający z Xlib). Tak więc okrąg o środku w punkcie (90,90) i promieniu 40 można narysować następująco

  > (xlib:draw-arc *moje-okno* *moj-kontekst*
  >                (- 90 40) (- 90 40) (* 2 40) (* 2 40)
  >                0 (* 2.0 pi))
  NIL 

Wypełnianie zadawane jest dodatkowym parameterm opcjonalnym, tak jak dla prostokątów.

Do wyświetlania napisów służą funkcje

  (draw-glyphs okno kontekst x y napis)
  (draw-image-glyphs okno kontekst x y napis)
??? W Clispie nie działa funkcja draw-glyph.

Fonty

Aby móc wyświetlać napisy należy określić czcionkę, jaka ma być użyta. Czcionki tworzymy używając funkcji

  (open-font display nazwa-czcionki)
Zwraca ona czcionkę o podanej nazwie. Drugi argument to napis będacy nazwą fontu, np. "fixed" lub "fg-16".

Zdefiniowano szereg funkcji do pobierania cech czcionki

  (font-ascent czcionka)
  (font-descent czcionka)
  (max-char-ascent czcionka)
  (max-char-descent czcionka)

Funkcja wielowartościowa

  (text-extents {czcionka | kontekst} napis)
zwraca kompletny opis odwzorowania napisu w wybranej czcionce. Pierwsza wartość to szerokość napisu w punktach.

Obsługa zdarzeń

Praktycznie każdy program dla X Windows musi zawierać obsługę zdarzeń. Zdarzenia to nie tylko naciśnięcie klawisza klawiatury czy przycisku myszy, lecz również pojawienie się okna lub jego fragmentu na ekranie (np. podczas pierwszego wyświetlania lub po odsłonięciu przez inne okno).

Dlatego typowy program dla X Windows ma następującą postać

  1. nawiązanie połączenia z serwerem
  2. utworzenie okien, kontekstów graficznych itp.
  3. wyświetlenie okien
  4. oczekiwanie na zdarzenia i ich obsługa
  5. zwolnienie zasobów i zamknięcie połączenia

Każde okno otrzymuje informacje tylko o tych zdarzeniach, którymi wyraziło zainteresowanie. Jednym z atrybutów okna jest {maska zdarzeń. Maski tworzymy funkcją

  (make-event-mask nazwa-zdarzenia ...)
gdzie zdarzenia reprezentowane są słowami kluczowymi, np. :key-press, :button-press, :exposure, :button-release, :enter-window, :leave-window.

Po utworzeniu maski należy jej użyć do ustawienia odpowiedniego atrybutu okna (można to także zrobić od razu podczas tworzenia okna)

  (setf (xlib:window-event-mask *moje-okno*)
        (xlib:make-event-mask :exposure :key-press
                              :button-press :map-notify))

Podstawową funkcją obsługi zdarzeń jest process-event. Prościej jednak użyć konstrukcji

  (event-case (display :discard-p boolean
                           :force-output boolean)
    (wzorzec-zdarzenia (atrybut zdarzenia ...)
     wyrażenie ...)
    ...)
gdzie wzorzec zdarzenia to nazwa zdarzenia, lista nazw zdarzeń, symbol otherwise lub symbol t. Listy zaczynające się od wzorców zdarzeń nazywać będziemy klauzulami.

Atrybuty zdarzenia to symbole, zależą one od konkretnego zdarzenia i są traktowane są jak zmienne lokalne, do wartości których można się odwoływać w wyrażeniach. Zawsze dostępny jest atrybut window.

Konstrukcja ta pobiera z kolejki zdarzeń pierwsze zdarzenie pasujące do wzorca którejś z klauzul i po kolei oblicza jej wyrażenia. Jeśli wartością ostatniego obliczonego wyrażenia jest nil, pobierane jest kolejne zdarzenie aż do momentu otrzymania wyniku różnego od nil.

Typowe zdarzenia i ich argumenty to:

:exposure(window count)
:button-press(window x y)
:button-release(window x y)
:enter-notify(window)
:leave-notify(window kind)
otherwise()

Dla zdarzenia :exposure najczęściej warto odświeżać dopiero, gdy count = 0. W zdarzeniu :leave-notify parametr kind to np. :ancestor.

Poniższa pętla rysuje w oknie prostokąt i czeka na nacisnięcie myszy w oknie, odrysowując prostokąt po każdym odsłonięciu się okna

  (xlib:event-case (*the-display*)
    ;; Return nil to quit, t to keep going.
    (:exposure (window)
      ;; Send trace printout to Lisp console.
      (format t "Window is exposed~%")
      (xlib:draw-rectangle window *moj-kontekst* 30 30 100 100)
      nil)
    (:map-notify (window)
      (format t "Window is mapped~%")
      (xlib:draw-rectangle window *mój-kontekst* 30 30 100 100)
      nil)
    (:button-press ()
      (format t "A Mouse button was pressed~%")
      t))
Przy okazji obejrzeliśmy sposób śledzenia obsługi zdarzeń na konsoli Lispu.

Funkcja

  (query-pointer ekran)
zwraca bieżące współrzędne kursora myszy jako dwie wartości. Przydaje się w obsłudze zdarzeń

Podręcznikowe funkcje keycode-keysym i keycode-character w rzeczywistości nazywają się keycode->keysym i keycode->character (prawdopodobnie błąd składu dokumentacji). Ta druga działa na Allegro CL i na CMU CL, a na Clispie wylatuje z błędem.

Prawidłowy sposób wołania keycode->keysym:

  (keycode->keysym display kod-klawisza
                   (default-keysym-index display
                                         kod-klawisza
                                         stan-klawiatury))

Wskazówki praktyczne

Szukanie koloru po nazwie w w przypadku braku koloru o podanej nazwie daje w CLX błąd, zamiast zwrócić nil.

Managing the Server Connection

Podczas kończenia programu należy zamknąć połączenie z serwerem, inaczej narażamy się na kłopoty. Powód? Większość serwerów X ma ograniczenie na liczbę jednocześnie otwartych połączeń z klientami. Po jej przekroczeniu serwer po prostu odmawia otwierania nowych połączeń i zostajemy z error handlerem przyglądającm się komunikatowi ``Unable to connect''. Zadbaj, żeby program zawsze kończył się wywołaniem close-display, nawet przy nieoczekiwanym zakończeniu. Najprostszy sposób to umieścić pętlę obsługi zdarzeń wewnątrz unwind-protect:

  ...
  (unwind-protect 
    (catch :event-loop
      (loop
        (process-next-event display)))  
    (close-display display))
  ...

Ta technika pozwala zawsze bezpiecznie przerwać błędny program i zacząć od nowa.

Lepiej też unikać wiązanie globalnych zmiennych specjalnych (dynamicznych) z obiektami display objects representującymi otwarte połąćzenia z serwerem. Łatwo o nich zapomnieć lub odśmiecić otwrty display.

Debugowanie

Uruchamianie programu CLX (a także jakigokolwiek programu użuwającego X Window System) wymaga uświadomienia sobie prostej prawdy: komunikacja client-server jest buforowana. Wywołanie w CLX funkcji draw-line niekoniecznie spowoduje pojawienie się linii na ekranie. Zamiast tego CLX umieszcza odpowiednie żądanie w buforze wyjścia dla serwera i kontynuuje działanie. Żądanie zostanie wysłane do serwera i wykonane później, podczas opróżniania bufora (por. zapis do pliku dyskowego w Uniksie).

Zwykle nie treba się tym przejmować, ponieważ CLX automatycznie opróżnia bufor w ``właściwym'' momencie
\footnote{Gdy bufor jest pełen lub (domyślnie) przed próbą pobrania następnego zdarzenia wejściowego.}.
Jednak podczas lokalizacji błędu możemy chcieć wykonywać żądania natychmiast. Można to osiągnąć dwojako.

  1. Wywoływać ręcznie display-force-output w odpowiednich momentach
  2. Wywołać
      (setf (display-after-function display)
            #'display-force-output)
    
    Spowoduje to przejście CLX w synchroniczny tryb pracy, gdy każde żądanie jest realizowane ,,natychmiast'', tzn. bez buforowania.

Konsekwencją buforowania jest też obsługa błędów. Jeśli żadanie jest błędne, serwer nie pokaże od razu błędu. Zamiast tego wyśle do programu (klienta) error reply. Jest to rodzaj ``input event'' i zanim zostanie odczytany, obsłużone zostaną poprzednie komunikaty w buforze. Efekt: kiedy error handler otrzymuje sterowanie, program jest już w innym stanie niż w momencie wystąpienia błędu. Użycie display-after-function, co jest wykonaniem krokowym, trochę pomaga. Niemniej należy dobrze przeanalizować komunikat o błędzie, porównując go nawet z opisami błędów dla danego żądania w X Protocol Specification, co może pomóc zlokalizować błąd.

Inna pożyteczna technika uruchamiania to użyć w programie (lokalnie związanych) zmiennych dla obiektów display i ważniejszych window. Ułatwi to inspekcję tych obiektów, gdy wykonamy break w dogodnym miejscu (na przykład gdy program czeka na kolejne zdarzenie -- idle) i obejrzymy stan atrybutów lub wyślemy żądania do serwera z żądaniami dodatkowych szczegółów.

Śledzenie CLX pod Linuksem

Błąd

  Connection failure to X11.0 server unix display 0: No protocol specified
związany jest prawdopodobnie z problemami podczas autoryzacji X11. Należy sprawdzić wartość zmiennej środowiska $DISPLAY, oraz przyjrzeć się wynikom uruchomienia w świeżej sesji CMUCL następującego kodu:
  (require :clx)
  (trace xlib:open-display)
  (trace xlib::get-best-authorization)
  (machine-instance)
  (ext:open-clx-display)

Dodatkowe funkcje interfejsu CLX

W pliku debug/util.lisp dystrybucji CLX są m.in. funkcje

  (display-root display)  ==> window)
  (display-black display) ==> color)
  (display-white display) ==> color)
  (describe-window window)
  (describe-gc gc)
  (degree degrees)
  (radian radians)

Tamże wiele innych pomocniczych plików.