Rob Tougher : robt@robtougher.com

Вступ

Xlib -- це бібліотека, яка дозволяє вам малювати графіку на екрані будь якаого Х-сервера, локального чи віддаленого, використовуючи мову С/С++. Усе, що вам для цього необхідно -- це підключити до програми бібліотеку <X11/Xlib.h>, злінкувати її використавши опцію -lX11, і тепер Ви готові використовувати будь яку функцію бібліотеки.

Для прикладу, створимо і відобразимо вікно на локальній машині. Для цього використайте наступний код:

#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

І вуаля, на протязі десяти секунд на вашому дисплеї буде відображатися вікно:

Метою цієї статті є познайомити вас із декількома простими класами, які можна використовувати при розробці Xlib-додатків.

Чому не використовуються візуальні елементи (widget)?

Ви можете задатися питанням: "А чом би не використовувати бібліотеки візуальних елементів (віджетів), скажімо QT або GTK?". Розумне питання. Я використовую QT, і вважаю її дуже зручною для розробки додатків на C++ для платформи Linux.

Причина, по якій я пишу ці рядки, полягає у намірі дати вам глибше розуміння X Window System, а для цього потрібно заглянути під покрив бібліотек QT і GTK. Я вже декілька разів приходив до висновку, що уміння писати Xlib додатки є досить корисним.

Ссподіваюся, що ця стаття допоможе вам використовувати наявні класи у ваших додатках.

Основи

Тепер давайте подивимося на приклади джерельного коду. В цьому розділі ми розглянемо основні особливості бібліотеки Xlib.

Відкриття дисплею

Перший створений мною клас -- це клас дисплею, котрий відповідає за відриття і закриття дисплею. Зверніть увагу на те, що у першому прикладі ми не закриваємо дисплей належним чином використовуючи функцію XCloseDisplay(). З цим класом він буде закритий перед завершенням програми. Тепер наш приклад виглядає наступним чином:

#include <unistd.h>

#include "xlib++/display.hpp"
using namespace xlib;

main()
{
  try
    {
      // Open a display.
      display d("");

      // Create the window
      Window w = XCreateWindow((Display*)d,
                   DefaultRootWindow((Display*)d),
                   0, 0, 200, 100, 0, CopyFromParent,
                   CopyFromParent, CopyFromParent, 0, 0);

      // Show the window
      XMapWindow(d, w);
      XFlush(d);

      // Sleep long enough to see the window.
      sleep(10);
    }
  catch ( open_display_exception& e )
    {
      std::cout << "Exception: " << e.what() << "\n";
    }
  return 0;
}

Справді, нічого захоплюючого. Просто відкривається, і закривається дисплей. Однак, Ви напевно звернули увагу на те, що екземпляр класу display у даній реалізації приводиться до типу Display*, таким чином, створюючи екземпляр цього класу, Ви насправді отримуєте вказівник на Xlib Display.

Зверніть увагу на блок try/catch. Всі класи у цій статті для сповіщення про помилки, породжують виключення.

Створення вікна

Далі, я хотів би спростити процес створення вікна, для цього я додав клас window]. Цей клас створює і малює вікно у конструкторі, і руйнує його у деструкції. Тепер наш приклад виглядає наступним чином (зверніть увагу на клас event_dispatcher, який ми розглянемо трішки нижче):

#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
    {
      // Open a display.
      display d("");

      event_dispatcher events ( d );
      main_window w ( events ); // top-level
      events.run();
    }
  catch ( exception_with_text& e )
    {
      std::cout << "Exception: " << e.what() << "\n";
    }
  return 0;
}

Зверніть увагу на те, що наш клас main_window породжений від класу xlib::window. Коли створюється об'єкт main_window, викликається базовий конструктор, який створює вікно Xlib.

Обробка подій

Ви напевно звернули увагу на клас event_dispatcher в останньому прикладі. Цей клас отримує події з черги подій додатку і передає їх необхідному вікну.

Цей клас визначається наступним чином:

      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. Всі класи вікон у цій статті є спадкоємцями саме цього класу, і після реєстрації себе викликом методу register_window, можуть отримувати повідомлення від диспетчера. З оголошення класу window_base виходить, що всі класи, які породжуються від нього, зможуть отримувати події, реалізувавши наступні методи:

      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 наступний код:

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, і передав його в наше вікно через виклик зумовленого методуand on_left_button_down method.

Малювання

Тепер давайте спробуємо малювати у нашому вікні. Система X Window визначає концепцію "графічного вмісту", котрий ви малюємо, тож я створив клас під назвою graphics_context. Ось його визначення:

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 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, ви, використовуючи для цього відповідні методи, отримуєте можливість малювати на поверхні вікна. Давайте спробуємо. Додайте до нашого прикладу наступний код:

#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() викликається щоразу, як вікно виводиться на екран. Усередині цього методу я розмістив код, котрий малює лінію і що виводить деякий текст на поверхні вікна (у клієнтській його області). Коли ви скомпілюєте і запустите цей приклад, то ви повинні побачити приблизно наступне:

Клас graphics_context широко використовується у даній статті.

Ви могли зауважити у наведеному вище коді два допоміжні класи: point і line. Це маленькі класи, які я створив для спрощення побудови фігур. Зараз вони не такі необхідні, однак, пізніше, коли потрібно буде виконувати комплексні операції типу трансформації фігур, вони виявляться корисними. Наприклад, значно простіше написати "line.move_x(5)", ніж "line_x += 5; line_y += 5;". І простіше, і менша вірогідність допустити помилку.

Створення кнопки

Вимоги до кнопки

Достатня кількість простого матеріалу дозволяє нам йти далі, до створення графічного елементу, який може використовуватися багато разів. Зараз ми сконцентруємося на створенні командної кнопки, котру ми можемо використовувати у нашому додатку. Вимоги до цієї кнопки є наступними:

  • повинна мати своє вікно для прийому подій;
  • повинна мати два стани — "натиснута", і "не натиснута";
  • повинна малювати стан "натиснута", від моменту натискання на ній кнопкою миші, до її відпускання;
  • повинна малювати стан "не натиснута";
  • властивість типу text із методами get та set;
  • повинна передавати клієнту подію "on_click()".

Завдання видається досить простим, однак, його реалізація буде не такою вже й тривіальною. Про це у наступних розділах.

Створення власного вікна

Перш за все, нам доведеться створити окреме вікно для цієї командної кнопки. Конструктор звертається до методу show, який у свою чергу передає управління методу create, який відповідає за створення вікна:

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, після чого починає отримувати події, і остаточно встановлює його фон.

Зауважте, що ми передаємо id батьківського вікна у виклик XCreateSimpleWindow (). Цим ми говоримо Xlib, що хочемо, щоб командна кнопка була дитячим вікном батька.

Реалізація станів "натиснута" і "не натиснута"

Оскільки командна кнопка реєструвалася з event_dispatcher, з'являється можливість при необхідності перемалювання отримувати події on_expose().

Ось код, що використовуватиметься для стану "не натиснута":

// bottom
gc.draw_line ( line ( point(0,
            rect.height()-1),
            point(rect.width()-1,
            rect.height()-1) ) );
// right
gc.draw_line ( line ( point ( rect.width()-1, 0),
            point ( rect.width()-1,
            rect.height()-1 ) ) );

gc.set_foreground ( white );

// top
gc.draw_line ( line ( point ( 0,0 ),
            point ( rect.width()-2, 0 ) ) );
// left
gc.draw_line ( line ( point ( 0,0 ),
            point ( 0, rect.height()-2 ) ) );

gc.set_foreground ( gray );

// bottom
gc.draw_line ( line ( point ( 1, rect.height()-2 ),
            point(rect.width()-2,rect.height()-2) ) );
// right
gc.draw_line ( line ( point ( rect.width()-2, 1 ), 
            point(rect.width()-2,rect.height()-2) ) );

Після компіляції і запуску додатку наша кнопка виглядатиме приблизно так:

Відповідно, наступний код реалізує стан "натистута":

// bottom
gc.draw_line ( line ( point(1,rect.height()-1),
point(rect.width()-1,rect.height()-1) ) );
// right
gc.draw_line ( line ( point ( rect.width()-1, 1 ),
            point ( rect.width()-1, rect.height()-1 ) ) );

gc.set_foreground ( black );

// top
gc.draw_line ( line ( point ( 0,0 ),
            point ( rect.width()-1, 0 ) ) );
// left
gc.draw_line ( line ( point ( 0,0 ),
            point ( 0, rect.height()-1 ) ) );

gc.set_foreground ( gray );

// top
gc.draw_line ( line ( point ( 1, 1 ),
            point(rect.width()-2,1) ) );
// left
gc.draw_line ( line ( point ( 1, 1 ),
            point( 1, rect.height()-2 ) ) );

Готова кнопка виглядатиме десь так:

Різні аспекти при малюванні стану

Здавалося б, усе досить просто: коли над кнопкою натискається кнопка миші — малюється "натиснута" кнопка, а коли кнопка миші відпускається — малюється "не натиснута". Проте це не зовсім вірно. Якщо над зображенням кнопки натискається, а потім утримується в натиснутому стані, ліва клавіша миші, а після цього показник миші переміщається за межі кнопки, то, не дивлячись на те, що клавіша миші залишається натиснутою, кнопка повинна відобразити стан "не натиснута".

Для обробки такої ситуації клас command_button має два поля — m_is_down та m_is_mouse_over. Спочатку, натиснення клавіші миші над кнопкою (ливіться on_left_button_down()) переводить її у стан "натиснута" і перемальовує її, потім, якщо курсор миші виводиться за межі кнопки (дивіться on_mouse_exit()), то поле m_is_mouse_over' встановлюється у стан false, і кнопка знову перемальовувався, але вже як "не натиснута". Якщо тепер курсор миші знову перемістити на кнопку, то поле m_is_mouse_over перейде у стан true і кнопка перемальовує як "натиснута". Коли клавіша миші відпускається, то кнопка переводиться у стан "не натиснута" і перемальовувався.

Присвоєння значення властивості "text"

Це досить просте завдання. Ми просто хочемо використати цю кнопку для отримання, присвоєння та відображення нового заголовку. Ось код:

command_button.hpp

std::string get_name() { return m_name; }
void set_name ( std::string s ) { m_name = s; refresh(); }

Виклик методу refresh() відбувається таким чином, що кнопка перемальовується з новим заголовком.

Ґенерація події "on_click()"

Тепер потрібно використати цю кнопку для того, щоб знати, коли вона натиснута. Для того, щоб зробити це, ми ґенеруємо подію "on_click()". Далі — визначення класу command_button_base.

command_button_base.hpp

namespace xlib
{
    class command_button_base : public window_base
    {
        public:
        virtual void on_click () = 0;
    };
};

По суті цей код стверджує наступне: "кнопка підтримує всі події, які підтримує клас вікна, плюс ще одне — on_click()". В результаті, породивши дочірній клас, програміст отримує можливість реалізувати метод on_click() для виконання необхідних дій.

Висновок

Я щиро сподіваюся, що Вам сподобалася ця стаття. Ми розглянули багато властивостей бібліотеки Xlib і "загорнули" їх у класи C++, щоб зробити розробку програм на основі Xlib простішою. Якщо у вас є якісь питання, коментарі або пропозиції по даній статті або по роботі з Xlib в цілому, можете написати мені (robt [песик] robtougher.com).