ckotinko (ckotinko) wrote,
ckotinko
ckotinko

Categories:

Gtk howto. Part 1

    Хотя GtkBuilder и представляет собой удобное решение для ленивых девелоперов GUI-морд, а также экономит туеву хучу байтов в сравнении с реализацией морды ручками, нормальной документации на него нету. Поэтому(а также потому, что скотинку попросили накатать мануал о том, как этой штукой пользоваться), ниже будет на пальцах объяснено, как и с чем есть этот GtkBuilder. Также сей потс будет служить примером того, как надо писать примеры использования софта.

    Для тех, кто вообще не в теме, GtkBuilder создан для того, чтобы пользователь мог нарисовать морду своего приложения и ряд других объектов в Glade 3, сохранить всё это в XML, и на ходу подключить к своей программе. Это позволяет, к примеру, использовать в своей программе несколько разных интерфейсов, к примеру первый представляет данные в виде дерева, а второй - в виде набора иконок.

   Пусть у нас есть окно с именем "foo" а где-то внутри него кнопка с именем "bar". Имя окна и кнопки - это не текст, который отображается на экране, а уникальный идентификатор, который задаётся в момент создания интерфейса в Glade. Пусть мы также хотим иметь возможность использовать в зависимости от ситуации два разных шаблона окна, причем хотим, чтобы оба они были доступны "на лету". Пусть мы пишем на C++, и с каждым вышеописанным окном будет связан класс foo:

class foo {

    И вот первый подводный камень для тех, кто пользовался ранее Glade 2: GtkBuilder создаёт ровно одну копию объекта, описанного в XML. Если мы хотим создать новый объект, надо создать GtkBuilder заново, залить в него XMLник, и только тогда после этого получить указатель на вновь созданный объект. В GtkBuilder всё может быть поименовано, в том числе и описания окон, поэтому для использования окна нам надо знать, где находится xml-файл, и как в нём названо наше окно

   public:
      foo(const char * path_to_xml_file,
          const char * window_template_name);

    Если же мы хотим лишь присосаться к уже созданному окну, то опишем конструктор так:

   public:
      foo(GtkBuilder * builder,
          const char * window_template_name);

    Как все приличные люди, мы сделаем деструктор класса виртуальным, сиречь адресуемым через указатель, находящийся в ассоциированной с классом таблице переопределяемых методов:

      virtual ~foo();

    В нашем примере мы будем обрабатывать 2 события: попытку закрыть окно и нажатие на кнопку. Поскольку предполагается, что эти функции будут вызываться из библиотеки Gtk, их можно сделать видимыми лишь внутри нашего класса(private), но делать это не обязательно. Следующая функция будет получать управление, когда пользователь закрывает окон, и должна вернуть false, если не согласна с этим. Так реализуются, кстати, диалоги, спрашивающие вас "хотите ли вы сохранить свою работу перед тем, как закрыть окно?".

      bool trying_to_close_window();

    А это будет обработчик нажатия на кнопку, который помещен сюда как дополнительный пример:

      void button_clicked();

    Копировать класс, который должен работать с одним единственным окном - плохая идея, поэтому надо сделать класс некопируемым, унаследовав его от другого некопируемого класса, или написав вот это:

   private:
      foo(const foo&);
      foo& operator =(const foo&);

    Реализации двух вышеуказанных функций можно не писать, их задача как раз состоит в том, чтобы не быть вызванными никогда.

    возможно, мы захотим уничтожить класс до уничтожениия окна, поэтому надо запомнить, какие сигналы ведут к нашему классу, и при уничтожении класса или окна немедленно отключать их. В нашем примере сигнала всего два:

   private:
      unsigned long m_sig_window_delete;
      unsigned long m_sig_button_click;

    Однако для корректной работы надо ловить еще один сигнал - собственно уничтожение оконного объекта. Поскольку мы не знаем, в какой последовательности будут уничтожаться элементы окна, мы подключим по одному сигналу к каждому элементу, с которым мы работаем:

      unsigned long m_sig_window_destroy;
      unsigned long m_sig_button_destroy;

    Поскольку мы пишем на С++, нам потребуются функции-переходники: написанная на С библиотека Gtk не знает, как вызывать методы класса, но статические(сиречь стоящие отдельно от класса, и принадлежащие ему лишь формально) члены она вызвать может. Посмотрев в документацию к Gtk, мы увидим, что нам надо реализовать обработчики delete-event для окна:

      static gboolean c_window_delete_event(
                         GtkWidget *widget,
                         GdkEvent *event,
                         gpointer klassptr);

    ...и clicked для кнопки:

      static gboolean c_button_button_click(
                         GtkButton *button,
                         gpointer klassptr);

    ...и тот самый сигнал уничтожения окна destroy-event:

      static gboolean c_window_destroy_event(
                         GtkWidget *widget,
                         GdkEvent *event,
                         gpointer klassptr);
      static gboolean c_button_destroy_event(
                         GtkWidget *widget,
                         GdkEvent *event,
                         gpointer klassptr);

    Вот, в общем-то, и всё содержимое нашего класса. Осталось только закрыть скобку, и написать, что именно она закрывает:

}; //class foo

    Можно, конечно, не писать, но написание однострочного комментария очень облегчает впоследствии поиск потерянных фигурных скобок. Объявление нашего класса готово, и можно приступать к реализации его методов. Начнем с создания отдельного окна прямо из XML:

foo::foo(const char * path_to_xml_file,
         const char * window_template_name)
{

    Если мы хотим создать новый объект, надо создать GtkBuilder заново, залить в него XMLник, и только тогда после этого получить указатель на вновь созданный объект. Созданием объектов по требованию разработчики Gtk как-то не озаботились.

   GError * err=NULL; //мы ведь хотим в случае ошибки
                      //получить человеко-читаемую строку
                      //с описанием ошибки?
   GtkBuilder * builder=gtk_builder_new();

    Еще один важный момент: как вы знаете, в нашей стране программы покупать не принято, поэтому, если вы пишете коммерческое ПО, вам следует озаботиться поддержкой различных языков. То же самое относится к тем авторам свободного или freeware ПО, которые не хотят быть упомянутыми матерными словами на всех языках мира. Если же вы пишете бесплатную утилиту для себя любимого, и еще пары друзей, или поддержка многоязычности вам нужна по иным причинам, то можете не писать следующую строчку:

   gtk_builder_set_translation_domain(builder, PACKAGE_NAME);

    PACKAGE_NAME - это строка, уникально идентифицирующая вашу программу в системе, и позволяющая библиотеке gettext автоматически подобрать файл переводов. Как работает gettext в связке с Gtk, вы можете прочитать здесь. Если вы собираете программу при помощи autotools, и умеете ими пользоваться, то писать "#define PACKAGE_NAME ..." не надо. Вместо этого следует первым делом включить в ваши .с и .сpp файлы директиву:

#include "path/to/my/config.h"

    где "path/to/my/config.h" - это пусть к файлу config.h, сгенерированному скриптом ./configure. Этот файл должен содержать определение PACKAGE_NAME, заданное в configure.in вашей программы.

   if(!gtk_builder_add_from_file(
         builder,
         path_to_xml_file,
         &err))
   {
      // при обработке xml-файла
      // возникли ошибки

      // освободим выделенные ресурсы
      g_object_unref(builder);
      // кинем какое-нибудь исключение
      throw something_evil(err);
   }

    Если же вы хотите лишь присосаться к уже имеющемуся окну, и вам не потребуется создавать его копии, а GtkBuilder ваш конструктор класса получает в виде параметра, вышенаписанный код вам не нужен, и можно сразу переходить к следующему шагу:

   GObject * window=gtk_builder_get_object(
         builder,
         window_template_name);
   GObject * button=gtk_builder_get_object(
         builder,
         "mybutton");

    Не плохо будет проверить, что нам на самом деле вернул GtkBuilder. Понятно, что там должны быть окно и кнопка, но вдруг это не окно...

   if(!window || !GTK_IS_WINDOW(window) ||
      !button || !GTK_IS_BUTTON(button) ||

    ...или если найденная кнопка не является элементом найденного окна?

      window!=G_OBJECT(gtk_widget_get_toplevel(GTK_WIDGET(button))))    {
      // xml-файл в порядке
      // но в нем или нет нужного нам объекта

      // или он есть, но он не окно
      g_object_unref(builder);
      // кинем другое исключение
      throw another_exception();
   }

    Теперь надо подключить обработчики сигналов от окна:

   m_sig_window_delete=g_signal_connect(
         window,
         "delete-event",
         G_CALLBACK(c_window_delete_event),
         this); // указатель на наш класс
                // попадёт в аргумент klass обработчика

   m_sig_window_destroy=g_signal_connect(
         window,
         "destroy-event",
         G_CALLBACK(c_window_destroy_event),
         this);

   m_sig_button_click=g_signal_connect(
         window,
         "destroy-event",
         G_CALLBACK(c_window_button_click),
         this);

   m_sig_button_destroy=g_signal_connect(
         window,
         "destroy-event",
         G_CALLBACK(c_button_destroy_event),
         this);

    Да, кстати, будет очень благоразумно добавить ссылку на окно, особенно если мы создали новое окно из файла: удалив GtkBuilder на выходе из конструктора, мы немедленно удалим и только что созданное окно, т.к. единственную ссылку на него держит тот самый GtkBuilder. То же относится и к кнопке.

   g_object_ref(window);
   g_object_ref(button);
} //foo::foo()

    Теперь примемся за сигналы. Начнем с delete-event для окна и clicked для кнопки в нём. Наша задача состоит лишь в том, чтобы перебросить их внутрь нашего класса:

gboolean foo::c_button_button_click(
                         GtkButton *button,
                         gpointer klassptr)
{
   ((foo*)klassptr)->button_clicked();
}

gboolean foo::c_window_delete_event(
                         GtkWidget *widget,
                         GdkEvent *event,
                         gpointer klassptr)
{
   bool result=((foo*)klassptr)->trying_to_close_window();
   // преобразуем результат к gboolean
   return result ? TRUE:FALSE;
}

    Никогда не приводите разные "булевые" типы друг к другу напрямую, и самое главное: никогда не проверяйте переменную булевого типа имеющую не встроенный тип bool на равенство TRUE. Сравнение if(x==TRUE) без ругани прожевывается компилятором gcc и становится в дальнейшем идеальным стеллс-багогенератором в вашей программе. Писать надо if(x!=FALSE) потому, что FALSE всегда равно нулю, а значение TRUE может сильно меняться от одной библиотеки к другой.

    Теперь перейдём к сигналу уничтожения окна. Этот сигнал означает, что Gtk хочет уничтожить наш оконный объект. Например, так угодно пользователю, системе, или звезды на небе сложились, и требует, чтоб мы отпустили ссылку на окно, которую мы предусмотрительно подхватили в конструкторе нашего класса. Мы, конечно, можем заупрямиться, и не отпустить ссылку, но действие это довольно бессмысленное, так как наше окно уже находится в процессе уничтожения. Поэтому мы сперва отсоединим сигналы, потом отпустим ссылку на окно, и прикажем удалить наш класс.

gboolean foo::c_window_destroy_event(
                         GtkWidget *widget, // это и есть указатель на наше окно
                         GdkEvent *event,
                         gpointer klassptr)
{
   //отсоединяем идущие от окна(и только от него сигналы)
   g_signal_handler_disconnect(G_OBJECT(widget), m_sig_window_delete);
   g_signal_handler_disconnect(G_OBJECT(widget), m_sig_window_destroy);
   //и обнуляем номера сигналов
   m_sig_window_delete=0;
   m_sig_window_destroy=0;
   //отпускаем окно(и только его)
   g_object_unref(G_OBJECT(widget));
   //проверим, все ли сигналы отсоединены
   if(!m_sig_window_destroy && !m_sig_button_destroy)
   //и если так - самоуничтожаемся!
      delete ((foo*)klassptr);
}

    Аналогично выглядит и обработчик delete-event для кнопки. Вы можете съоптимизировать проверку, все ли сигналы отключены, заведя в классе переменную(например m_signals_connected, присвоить ей в конструкторе число подключенных сигналов, и вычитать в обработчиках delete-event количество отключенных сигналов, а по достижении нуля удалять сам класс. Но для примера с окном и кнопкой можно проверять и в лоб.

    Как вы понимаете, наш класс можно создавать только оператором new, но что вы хотели от класса, который является обёрткой к динамически создаваемому окну? Смешивать оконный интерфейс – это не только дурной тон, и но и в будущем источник головной боли при переделке программы. Поэтому наш класс – это лишь обертка, которая знает, где лежат данные программы, и как с ними работать, но не более того.

   Поскольку размер поста в ЖЖ ограничен 32 килобайтами, на сегодня всё.

Алсо, скотинки вовсе не сдохли и скоро вернутся в ЖЖ чтобы писать новые потсы

Tags: gtk, программирование
Subscribe

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 14 comments