1. Введение Xlib -- это библиотека, позволяющая программам на языке C рисовать на экране любого X-сервера -- локального или удаленного. Все, что для этого требуется -- вставить в исходный файл программы строку "include <X11/Xlib.h>", добавить в Makefile ключ компоновщика -lX11 , и вот вы уже готовы к вызову любой функции из библиотеки Xlib.
Для примера рассмотрим как нарисовать окно на экране локального компьютера. Это можно сделать следующим образом:
Listing 1: http://gazette.linux.ru.net/lg78/articles/misc/tougher/example1.cpp.txt
#include <X11/Xlib.h>#include <unistd.h>main(){ // Открыть дисплей Display *d = XOpenDisplay(0); if ( d ) { // Создать окно Window w = XCreateWindow(d, DefaultRootWindow(d), 0, 0, 200, 100, 0, CopyFromParent, CopyFromParent, CopyFromParent, 0, 0); // Нарисовать окно на экране XMapWindow(d, w); XFlush(d); // Выполнить задержку, достаточную // по времени, чтобы мы смогли увидеть окно sleep(10); } return 0;} Скомпилируйте программу командой:
prompt$ g++ test.cpp -L/usr/X11R6/lib -lX11 Запустите:
prompt$ ./a.out И, вуаля, в течение 10 секунд можете любоваться окном на экране:
Рисунок. Если очень хочется -- можнопосмотреть на http://gazette.linux.ru.net/lg78/articles/misc/tougher/screen_small.png
Цель данной статьи - познакомить вас с некоторыми простыми классами, которые можно использовать при разработке Xlib-приложений. Мы создадим приложение с одним окном и кнопкой в этом окне. Кнопку мы напишем сами, используя только библиотеку Xlib.
2. Почему не используются визуальные элементы (виджеты)? Вы можете задаться вопросом: "А почему бы не использовать библиотеки визуальных элементов (виджетов), скажем QT (http://www.trolltech.com/) или GTK (http://www.gtk.org/) ?". Законный вопрос. Я использую QT, и нахожу ее очень удобной для разработки приложений на C++ для платформы Linux.
Причина, по которой я пишу эти строки, заключается в намерении дать вам более глубокое понимание X Window System, а для этого нужно заглянуть под покров библиотек QT и GTK. Несколько раз уже я приходил к выводу, что умение писать Xlib приложения действительно полезно .
Я надеюсь, что эта статья поможет вам использовать имеющиеся классы в ваших приложениях.
3. Основы В этом разделе мы пройдемся по основным особенностям библиотеки Xlib. Давайте сейчас рассмотрим исходный код примера.
3.1 Открытие дисплея Первый класс, который создается в программе -- это класс display (http://gazette.linux.ru.net/lg78/articles/misc/tougher/display.hpp.txt) , основная задача которого -- открытие и закрытие дисплея. Заметьте, что в примере examle1.cpp, дисплей не закрывается явно с помощью вызова XCloseDisplay(). Дисплей будет закрыт самим классом display перед завершением программы. Немножко усложним наш пример, и он уже выглядит вот так:
Listing 2: http://gazette.linux.ru.net/lg78/articles/misc/tougher/example2.cpp.txt
#include <unistd.h>#include "xlib++/display.hpp"using namespace xlib; main(){ try { // Открыть дисплей display d(""); // Создать окно Window w = XCreateWindow((Display*)d , DefaultRootWindow((Display*)d ), 0, 0, 200, 100, 0, CopyFromParent, CopyFromParent, CopyFromParent, 0, 0); // Нарисовать окно на экране XMapWindow(d, w); XFlush(d); // Выполнить задержку, чтобы успеть увидеть окно sleep(10); } catch ( open_display_exception& e ) { std::cout << "Exception: " << e.what() << "\n"; } return 0;} Собственно, ничего особенного. Все то же самое -- открывается и закрывается дисплей. Однако, вы наверняка заметили, что экземпляр класса display в данной реализации (http://gazette.linux.ru.net/lg78/articles/misc/tougher/display.hpp.txt) приводится к типу Display*, таким образом, создавая экземпляр этого класса , вы в действительности получаете указатель на Xlib Display.
Конечно же вы заметили и блок try/catch. Все классы в данной статье для извещения об ошибках порождают исключения.
3.2 Создание окна Далее, я хотел бы упростить процесс создания окна, для этого я добавлю класс window (http://gazette.linux.ru.net/lg78/articles/misc/tougher/window.hpp.txt) . Этот класс создает и отрисовывает окно в конструкторе, а "разрушает" окно в деструкторе. Теперь пример выглядит так (обратите внимание на класс event_dispatcher, который мы рассмотрим несколько ниже):
Listing 3 : http://gazette.linux.ru.net/lg78/articles/misc/tougher/example3.cpp.txt
#include "xlib++/display.hpp"#include "xlib++/window.hpp" using namespace xlib;class main_window : public window{ public: main_window ( event_dispatcher& e ) : window ( e ) {}; ~main_window(){};}; main(){ try { // Открыть дисплей display d(""); event_dispatcher events ( d ); main_window w ( events ); // верхний уровень events.run(); } catch ( exception_with_text& e ) { std::cout << "Exception: " << e.what() << "\n"; } return 0;} Обратите внимание на то, что наш класс main_window порожден от класса xlib::window . Когда создается объект main_window, вызывается базовый конструктор, который создает окно Xlib.
3.3 Обработка событий Вы наверняка обратили внимание на класс event_dispatcher (http://gazette.linux.ru.net/lg78/articles/misc/tougher/event_dispatcher.hpp.txt ) в последнем примере. Этот класс получает события из очереди событий приложения и передает их требуемому окну.
Определен этот класс следующим образом:
Listing 4 : http://gazette.linux.ru.net/lg78/articlesmisc/tougher/event_dispatcher.hpp.txt
class event_dispatcher { // constructor, destructor, and others... [snip...] register_window ( window_base *p ); unregister_window ( window_base *p ); run(); stop(); handle_event ( event ); } Класс event_dispatcher передает события классу окна через интерфейс класса window_base (http://gazette.linux.ru.net/lg78/articles/misc/tougher/window_base.hpp.txt) . Все классы окон, в этой статье, являются наследниками именно этого класса и, после регистрации себя вызовом метода register_window , могут получать сообщения от диспетчера. Из объявления класса window_base (http://gazette.linux.ru.net/lg78/articles/misc/tougher/window_base.hpp.txt) следует, что все классы, порождаемые от него, смогут получать события, реализовав следующие методы:
Listing 5 : window_base.hpp (http://gazette.linux.ru.net/lg78/articles/misc/tougher/window_base.hpp.txt)
virtual void on_expose() = 0; virtual void on_show() = 0; virtual void on_hide() = 0; virtual void on_left_button_down ( int x, int y ) = 0; virtual void on_right_button_down ( int x, int y ) = 0; virtual void on_left_button_up ( int x, int y ) = 0; virtual void on_right_button_up ( int x, int y ) = 0; virtual void on_mouse_enter ( int x, int y ) = 0; virtual void on_mouse_exit ( int x, int y ) = 0; virtual void on_mouse_move ( int x, int y ) = 0; virtual void on_got_focus() = 0; virtual void on_lost_focus() = 0; virtual void on_key_press ( character c ) = 0; virtual void on_key_release ( character c ) = 0; virtual void on_create() = 0; virtual void on_destroy() = 0; Давайте проверим, а так ли это в действительности? Попробуем обработать событие ButtonPress в нашем окне. Добавим в определение нашего класса main_window следующий код:
Listing 6 : example4.cpp (http://gazette.linux.ru.net/lg78/articles/misc/tougher/example4.cpp.txt)
class main_window : public window{ public: main_window ( event_dispatcher& e ) : window ( e ) {}; ~main_window(){}; void on_left_button_down ( int x, int y ) { std::cout << "on_left_button_down()\n"; } }; Скомпилируйте и запустите приложение, а потом щелкните в окне мышкой. Код сработал! Класс event_dispatcher получил событие ButtonPress и передал его в наше окно через вызов предопределенного метода on_left_button_down .
3.4 Рисование Теперь попробуем рисовать в нашем окне. Система X Window определяет концепцию "графического контекста" ("graphics context"), поэтому я, естественно, создаю класс graphics_context . Вот определение класса:
Listing 7 : graphics_context.hpp (http://gazette.linux.ru.net/lg78/articles/misc/tougher/graphics_context.hpp.txt)
class graphics_context { public: graphics_context ( display& d, int window_id ); ~graphics_context(); void draw_line ( line l ); void draw_rectangle ( rectangle rect ); void draw_text ( point origin, std::string text ); void fill_rectangle ( rectangle rect ); void set_foreground ( color& c ); void set_background ( color& c ); rectangle get_text_rect ( std::string text ); std::vector<int> get_character_widths ( std::string text ); int get_text_height (); long id(); private: display& m_display; int m_window_id; GC m_gc; }; Передав этому классу id окна и объект display, вы, используя для этого соответствующие методы, получаете возможность рисовать на поверхности окна. Давайте попробуем. Добавьте в наш пример следующий код:
Listing 8 : example5.cpp (http://gazette.linux.ru.net/lg78/articles/misc/tougher/example5.cpp.txt)
#include "xlib++/display.hpp"#include "xlib++/window.hpp"#include "xlib++/graphics_context.hpp" using namespace xlib;class main_window : public window{ public: main_window ( event_dispatcher& e ) : window ( e ) {}; ~main_window(){}; void on_expose () { graphics_context gc ( get_display(),id() ); gc.draw_line ( line ( point(0,0), point(50,50) ) ); gc.draw_text ( point(0, 70), "I'm drawing!!" ); } }; Метод on_expose() вызывается всякий раз, когда окно выводится на экран. Внутри этого метода я разместил код, на поверхности окна (в клиентской его области) рисующий линию и выводящий некоторый текст. Когда вы скомпилируете и запустите этот пример, то вы должны увидеть примерно следующее:
Рисунок на http://gazette.linux.ru.net/lg78/articles/misc/tougher/draw_line.png
Класс graphics_context широко используется в данной статье.
Вы могли заметить в выше приведенном коде два вспомогательных класса: point и line (http://gazette.linux.ru.net/lg78/articles/misc/tougher/shapes.hpp.txt) . Эти маленькие классы, которые я создал для упрощения построения фигур. Сейчас они не столь необходимы, но позднее, когда потребуется выполнять комплексные операции типа трансформации фигур, они окажутся полезными. Например, куда как проще написать "line.move_x(5)", чем "line_x += 5; line_y += 5;". И проще, и ниже вероятность допустить ошибку.
4. Создание кнопки 4.1 Требования к кнопке Давайте приступим к созданию визуального элемента, который потом может быть использован в других наших программах. Требования к кнопке можно выразить так:
должна иметь свое окно для приема событий должна иметь два состояния -- "нажатая кнопка" и "отпущенная кнопка" должна отображаться как "нажатая" при нажатии на нее кнопкой мыши (т.е. когда указатель мыши неподвижно расположен над кнопкой) должна отображаться как "ненажатая" если кнопка мыши отпущена, либо когда указатель мыши находится вне пределов кнопки должна иметь свойство text и методы get и set для управления им должна передавать клиенту событие "on_click()" Выглядит довольно просто, но реализация всего этого не столь тривиальная задача.
4.2 Создание собственного окна Для начала создается отдельное окно кнопки. Конструктор вызывает метод show , который в свою очередь передает управление методу create , ответственному за создание окна:
Listing 9 : command_button.hpp (http://gazette.linux.ru.net/lg78/articles/misc/tougher/command_button.hpp.txt)
virtual void create() { if ( m_window ) return; m_window = XCreateSimpleWindow ( m_display, m_parent.id(), m_rect.origin().x(), m_rect.origin().y(), m_rect.width(), m_rect.height(), 0, WhitePixel((void*)m_display,0), WhitePixel((void*)m_display,0)); if ( m_window == 0 ) { throw create_button_exception( "could not create the command button" ); } m_parent.get_event_dispatcher().register_window ( this ); set_background ( m_background ); } Очень похоже на конструктор класса window , не так ли? Первым делом создается окно с помощью вызова Xlib API XCreateSimpleWindow(), затем окно регистрируется в event_dispatcher, включаясь тем самым в цикл обработки событий, и наконец -- устанавливается фон.
Примечательно, что в XCreateSimpleWindow() передается id родительского окна, тем самым сообщая Xlib, что кнопка является дочерним окном указанного родителя.
4.3 Реализация состояний "нажатая" и "отпущенная" Поскольку кнопка регистрирует свое окно в event_dispatcher, появляется возможность при необходимости перерисовки получать события on_expose() . Для отображения обоих состояний кнопки используется класс graphics_context .
Ниже показан ход отображения "отпущенной" кнопки
Listing 10 : command_button.hpp (http://gazette.linux.ru.net/lg78/articles/misc/tougher/command_button.hpp.txt)
// нижняя грань gc.draw_line ( line ( point(0, rect.height()-1), point(rect.width()-1, rect.height()-1) ) ); // правая грань gc.draw_line ( line ( point ( rect.width()-1, 0 ), point ( rect.width()-1, rect.height()-1 ) ) ); gc.set_foreground ( white ); // верхняя грань gc.draw_line ( line ( point ( 0,0 ), point ( rect.width()-2, 0 ) ) ); // левая грань gc.draw_line ( line ( point ( 0,0 ), point ( 0, rect.height()-2 ) ) ); gc.set_foreground ( gray ); // серая полутень нижней грани gc.draw_line ( line ( point ( 1, rect.height()-2 ), point(rect.width()-2,rect.height()-2) ) ); // серая полутень правой грани gc.draw_line ( line ( point ( rect.width()-2, 1 ), point(rect.width()-2,rect.height()-2) ) ); После компиляции и запуска приложения, кнопка будет выглядеть примерно так:
Рисунок на http://gazette.linux.ru.net/lg78/articles/misc/tougher/command_button.png
Следующий фрагмент кода рисует "нажатую" кнопку:
Listing 11 : command_button.hpp (http://gazette.linux.ru.net/lg78/articles/misc/tougher/command_button.hpp.txt)
gc.set_foreground ( white ); // нижняя грань gc.draw_line ( line ( point(1,rect.height()-1), point(rect.width()-1,rect.height()-1) ) ); // правая грань gc.draw_line ( line ( point ( rect.width()-1, 1 ), point ( rect.width()-1, rect.height()-1 ) ) ); gc.set_foreground ( black ); // верхняя грань gc.draw_line ( line ( point ( 0,0 ), point ( rect.width()-1, 0 ) ) ); // левая грань gc.draw_line ( line ( point ( 0,0 ), point ( 0, rect.height()-1 ) ) ); gc.set_foreground ( gray ); // серая полутень верхней грани gc.draw_line ( line ( point ( 1, 1 ), point(rect.width()-2,1) ) ); // серая полутень левой грани gc.draw_line ( line ( point ( 1, 1 ), point( 1, rect.height()-2 ) ) ); Нажатая кнопка выглядит так:
Рисунок на http://gazette.linux.ru.net/lg78/articles/misc/tougher/command_button_pressed.png
4.4 Дополнительные аспекты при отображении состояния кнопки Казалось бы, все довольно просто: когда над кнопкой нажимается клавиша мыши -- рисуется "нажатая" кнопка, а когда клавиша мыши отпускается -- рисуется "отпущенная". Однако это не совсем верно. Если над изображением кнопки нажимается, а затем удерживается в нажатом состоянии, левая клавиша мыши, а после этого указатель мыши перемещается за пределы кнопки, то, не смотря на то, что клавиша мыши остается нажатой, кнопка должна отобразить состояние "отпущенная".
Для обработки такой ситуации класс command_button (http://gazette.linux.ru.net/lg78/articles/misc/tougher/command_button.hpp.txt) имеет два поля -- m_is_down и m_is_mouse_over . Сначала, нажатие клавиши мыши над кнопкой (смотри on_left_button_down() ) переводит ее в состояние "нажатая" и перерисовывает ее, затем, если курсор мыши выводится за пределы кнопки (смотри on_mouse_exit() ), то поле m_is_mouse_over устанавливается в состояние false и кнопка опять перерисовывается, но уже как "отпущенная". Если теперь курсор мыши опять переместить на кнопку, то поле m_is_mouse_over перейдет в состояние true и кнопка будет перерисована как "нажатая". Когда клавиша мыши отпускается, то кнопка переводится в состояние "отпущенная" и перерисовывается.
4.5 Свойство "text" Реализация свойства "text" -- довольно простая задача. Для управления этим свойством в распоряжение программиста предоставляется два метода: первый -- получить текст надписи на кнопке, второй -- изменить его:
Listing 12 : command_button.hpp (http://gazette.linux.ru.net/lg78/articles/misc/tougher/command_button.hpp.txt)
std::string get_name() { return m_name; } void set_name ( std::string s ) { m_name = s; refresh(); } Вызов метода refresh() служит для отображения кнопки с обновленной надписью.
4.6 Генерация события "on_click()" Теперь необходимо снабдить нашу кнопку возможностью порождать событие "on_click()" в тот момент, когда по ней производится щелчок мышью. Ниже приведено определение класса command_button_base :
Listing 13 : command_button_base.hpp (http://gazette.linux.ru.net/lg78/articles/misc/tougher/command_button_base.hpp.txt)
namespace xlib{ class command_button_base : public window_base { public: virtual void on_click () = 0; };}; По существу этот код утверждает: "кнопка поддерживает все события, которые поддерживает класс окна, плюс еще одно -- on_click()". В результате, породив дочерний класс, программист получает возможность реализовать метод on_click() для выполнения необходимых действий.
5. Заключение Я надеюсь, что вам понравилась эта статья. Мы рассмотрели некоторые из свойств библиотеки Xlib и "завернули" их в классы C++, чтобы сделать разработку программ на основе Xlib проще. Если у вас есть какие либо вопросы, комментарии или предложения по данной статье или по работе с Xlib в целом, можете написать мне. .
a. Ссылки b. Файлы Rob Tougher Роб -- пишущий на C++ программист из Нью-Йорка. В свободное от работы время его можно найти прогуливающимся по пляжу со своей девушкой Николь (Nicole) и их собакой Холли (Halley).