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.
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
(display-release-number display)
Natomiast
(display-version-number display)jest w podręczniku, ale nie w implementacji.
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).
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
(xlib:screen-root *the-screen*)co oznacza pierwotne okno ekranu serwera (``capo di tutti capi''), obejmujące cały ekran.
(xlib:screen-black-pixel *the-screen*) (xlib:screen-white-pixel *the-screen*)O wartościach dla innych kolorów już niedługo.
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
Do rysowania niezbędne są dwie rzeczy:
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.
| Field | Default |
|---|---|
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 ...)
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.
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.
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ć
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))
Szukanie koloru po nazwie w w przypadku braku koloru o podanej nazwie
daje w CLX błąd, zamiast zwrócić nil.
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.
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.
display-force-output w odpowiednich
momentach
(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.
Błąd
Connection failure to X11.0 server unix display 0: No protocol specifiedzwią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)
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.