Разработка графического интерфейса пользователя с использованием QtAda

Вадим Годунко <vgodunko@rostel.ru>, 2007 год

Введение

Язык программирования Ada является мощным универсальным языком пр ограммирования и может использоваться для решения самых различных задач: от численных рассчётов до систем управления реального времени. Многие годы программисты были фактически лишены высокоэффективных средств разработки переносимых графических интерфейсов пользователя. Для их реализации как правило приходилось использовать платформозависимые средства, не имеющие достаточных средств автоматизации процесса разработки. Это приводило к резкому снижению эффективности использования языка в проектах, требующих наличия развитого графического интерфейса пользователя.

Ситуация начала меняться в 1998, когда компания AdaCore предложила связку GtkAda для библиотеки компонентов графического интерфейса пользователя Gtk+. Несмотря на активное использование возможностей языка Ada для заглаживания острых углов Gtk+, имеющаяся на сегодняшний день версия GtkAda наследует некоторые проблемы Gtk+.

Новые возможности языка Ada редакции 2005 года позволили в 2007 году разработать связку QtAda для библиотеки компонентов графического интерфейса пользователя и вспомогательных инструментов Qt.

Проводить сравнительный анализ Qt и Gtk+ задача не простая, но всё же хочется отметить основные отличия. Оба продукта предназначены для разработки переносимого графического интерфейса пользователя, и поддерживают три наиболее распространённые платформы: операционные системы класса UNIX, MicroSoft Windows, Mac OS X. С этой точки зрения возможности продуктов сопоставимы.

Библиотека Gtk+ является свободно распространяемым продуктом с открытым исходным кодом, а условия её лицензирования позволяют свободно содавать коммерческие приложения. Напротив, Qt является коммерческим продуктом компании Trolltech, также с открытым исходным кодом, но разрешенным для свободного использования только в проектах с открытым исходным кодом и требующим лицензирования для коммерческих проектов. Однако, условия лицензирования GtkAda запрещают её использование в коммерчески проектах без соответствующего разрешения компании AdaCore. Таким образом в данном свете продукты также выглядят совершенно эквивалентными.

Следующим сопоставимым аспектом является использование обоими библиотеками объектно-ориентированной архитектуры. Однако тут начинаются первые серьёзные различия. Gtk+ реализована полностью средствами C (однако GtkAda настолько аккуратно скрывает это, что внешне бибилотека видится как полностью реализованная на объектно-ориентированном языке). Напротив, Qt использует язык C++ и его встроенную поддержку объектно-ориентированного программирования.

Имея в своём составе фактически одинаковый набор компонентов графического интерфейса пользователя, Qt включает сбалансированный набор компонентов архитектуры модель/вид для построения списков, деревьев и таблиц, а также уникальную аналогичную архитектуру сцена/вид для построения графических векторных редакторов.

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

  • визуальный редактор графического интерфейса пользователя;
  • программа подготовки текстов для локализаци приложений;
  • встроенная справочная система, содержащая исчерпывающую информацию о всех составляющих продукта.

Установка из исходных текстов

Дистрибутив исходных текстов связки QtAda можно скачать с сайта проекта http://sourceforge.net/projects/qtada/.

Установка производится обычным путём, для программ, использующих пакет AutoTools.

1. Распаковать дистрибутив исходных текстов:

   tar xjvf qtada-0.1.0-gpl.tar.gz

2. Зайти в каталог и запустить программу конфигурирования:

   cd qtada-0.1.0-gpl
   ./configure --prefix=<путь установки>

3. Выполнить сборку библиотек и программ:

   make

4. Выполнить установку библиотек и программ:

   make install

5. Настроить переменные окружения, указывающие пути поиска исполняемых файлов, библиотек динамической загрузки и файлов проектов GNAT.

   export PATH=$PATH:<путь установки>/bin
   export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:<путь установки>/lib
   export ADA_PROJECT_PATH=$ADA_PROJECT_PATH:<путь установки>/lib/gnat

Всё готово!

Установка бинарного дистрибутива

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

Способ установки полностью определяется используемой операцинной системой и банарным дистрибутивом, поэтому здесь не описывается.

Основные концепции Qt

Как уже было сказано, Qt основана на объектно-ориентированной модели. С точки зрения программы используются два наиболее важных вида классов:

  • класс-приложение (QApplication);
  • классы-виджеты (подклассы QWidget).

Любая программа, использующая Qt должна создать один экземпляр класса-приложения. Именно этот класс обеспечивает обработку событий, ожидание ввода-вывода и другие асинхронные процессы.

Программа может создать любое количество экземпляров классов-виджетов. Используя различные подклассы-виджеты в различных комбинациях программа определяет свой внешний вид. За исключением самых простых программ, программа определяет собственные классы-виджеты, формируя их внешний вид из экземпляров готовых виджетов и доопределяя поведение всей совокупности элементов.

Экземпляры классов Qt осуществляют взаимодействие с помощью специального механизма сигналов и слотов. Каждый класс Qt объявляет список порождаемых им сигналов и список имеющихся слотов. С помощью специальной операции QObject::connect можно осуществить логическое соединение сигнала одного объекта со слотом другого. После установления логического соединения если объект вырабатывает сигнал, то осуществляется выполнение кода слотов подключенных объектов.

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

Первое приложение

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

 1   with Qt4.Applications.Constructors;
 2   with Qt4.Objects;
 3   with Qt4.Push_Buttons.Constructors;
 4   with Qt4.Strings;
 5
 6   procedure Example is
 7      App    : constant not null access Qt4.Applications.Q_Application'Class
 8        := Qt4.Applications.Constructors.Create;
 9      Quit   : constant not null access Qt4.Push_Buttons.Q_Push_Button'Class
10        := Qt4.Push_Buttons.Constructors.Create
11            (Qt4.Strings.To_Q_String ("Quit from Hello, world!"));
12
13   begin
14      Qt4.Objects.Connect (Quit,
15                           Qt4.Signal ("clicked()"),
16                           App,
17                           Qt4.Slot ("quit()"));
18
19      Quit.Show;
20      App.Exec;
21   end Example;

Строки 1..4 подключают необходимые модули связки.

Строки 7 и 8 объявляют объект-приложение и осуществляют его создание.

Строки 9..11 объявляют объект-кнопку и создают её, по пути задавая используемую надпись.

Строки 14..17 подключают слот quit() объекта-приложения к сигналу clicked() объекта-кнопки. При нажатии на кнопку она автоматически возбуждает сигнал clicked(), который теперь автоматически выполнит вызов слота quit() объекта-приложения. Таким образом, при нажатии кнопки программа завершит свою работу.

Строка 19 отображает на экране оконо с кнопкой.

Строка 20 вызывает основной цикл обработки событий, ожидания ввода/вывода и таймеров приложения. Выход из этого цикла осуществляется вызовом слота quit() объекта-приложения.

Для комиляции приведённого исходного кода удобнее всего воспользоваться файлами проектов GNAT. Ниже приводится файл проекта GNAT для нашего примера:

 1   with "qt_gui";
 2
 3   project Example is
 4
 5      for Main use ("example");
 6
 7      package Compiler is
 8         for Default_Switches ("Ada") use ("-gnat05");
 9      end Compiler;
10
11      package Binder is
12         for Default_Switches ("Ada") use ("-F");
13      end Binder;
14
15   end Example;

Строка 1 подключает связку для модуля QtGui.

Строка 5 объявляет имя главной подпрограммы.

Строка 8 включает режим поддержки Ada2005. Программы с использованием QtAda всегда должны компилироваться в режиме Ada2005.

Примечание: для компиляторов GNAT GPL и GNAT GAP режим Ada2005 включён по умолчанию, и этот ключ можно не указывать; но необходимо помнить, что GNAT Pro по умолчанию использует режим Ada95 и наличие ключа обязательно.

Строка 12 включает проверку флагов предысполнения модулей при предысполнении главной подпрограммы. Программы, использующие связку, всегда должны проверять флаги предысполнения.

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

Теперь всё готово к сборке программы:

   gnatmake -Pexample

Можно даже запустить программу:

   ./example

и получить нечто подобное:

http://www.ada-ru.org/graphics/qtada_example.png

Создание собственного класса

Использование исключительно стандартных сигналов и слотов позволяет создавать только относительно простые приложения. В некоторый момент всегда возникает ситуация, когда либо стандартных слотов становится недостаточно, либо конкретному приложению требуются существенно более сложные виджеты. Qt не делает различия в способах решения указанных задач и предлагает единый и унифицированный подход - создание пользовательских классов. Аналогично, QtAda предоставляет механизм для создания тэговых типов порождённых от стандатных тэговых типов связки.

Для каждого класса QtAda предполагает наличие двух пакетов (XXX трех для QtAda 0.3.0 и выше): пакета публичного интерфейса и дочернего к нему пакета реализации. Пользовтель не обязан использовать аналигичное соглашение, но его использование рекомендуется.

Для расширения иерархии классов виджетов Qt достаточно объявить тэговый тип порождённый от тэгового типа Qt4.Objects.Impl.Q_Object_Impl или производных от него типов, переопределив три стандартные операции (Meta_Object, Qt_Meta_Cast, Qt_Meta_Call) и объявив импортируемую константу Static_Meta_Object. Спецификация минимального пакета будет выглядеть следующим образом:

package LCD_Ranges.Impl is

   type LCD_Range_Impl is new Qt4.Widgets.Impl.Q_Widget_Impl
     and LCD_Range with private;

   overriding
   function Meta_Object (Self : not null access LCD_Range_Impl)
     return not null access constant Qt4.Meta_Objects.Q_Meta_Object;

   overriding
   function Qt_Meta_Call (Self : not null access LCD_Range_Impl;
                          Kind : in Qt4.Meta_Objects.Call;
                          Id   : in Qt4.Q_Integer;
                          Args : in Qt4.Address_Pointer)
     return Qt4.Q_Integer;

   overriding
   function Qt_Meta_Cast (Self  : not null access LCD_Range_Impl;
                          Class : in String)
     return System.Address;

   Static_Meta_Object : aliased constant Qt4.Meta_Objects.Q_Meta_Object;
   pragma Import (C, Static_Meta_Object, "__LCD_Range_Static_Meta_Object");

private

   ...    

end LCD_Ranges.Impl;

Пользователю требуется только объявть указанные элементы в спецификации пакета и указать в теле пакета, что реализации функций находятся в отдельных файлах. Кроме этого, в теле пакета обязательно необходимо объявить пустой пакет MOC и также указать, что его тело распологается в отдельном файле. Код реализации этих функций и тело пакета автоматически генерируются специальной программой amoc - компилятором метаинформации - входящей в состав QtAda.

Тело пакета будет выглядеть так:

package body LCD_Ranges.Impl is

   package MOC is
   end MOC;

   package body MOC is separate;

   overriding
   function Meta_Object (Self : not null access constant LCD_Range_Impl)
     return not null access constant Qt4.Meta_Objects.Q_Meta_Object
       is separate;

   overriding
   function Qt_Meta_Call (Self : not null access LCD_Range_Impl;
                          Kind : in Qt4.Meta_Objects.Call;
                          Id   : in Qt4.Integer;
                          Args : in Qt4.Address_Pointer)
     return Qt4.Integer
       is separate;

   overriding
   function Qt_Meta_Cast (Self  : not null access LCD_Range_Impl;
                          Class : in String)
     return System.Address
       is separate;

end LCD_Ranges.Impl;

Теперь необходимо сгенерировать дополниелные файлы. Делается это вызовом программы amoc:

   amoc -gnatW8 -gnat05 -I<путь к файлам QtAda> lcd_ranges-impl.ads

В результате работы программы amoc создаются следующие файлы:

  • lcd_ranges-impl-moc.adb - тело подпакета MOC;
  • lcd_ranges-impl-meta_object.adb - реализация функции Meta_Object;
  • lcd_ranges-impl-qt_meta_call.adb - реализация функции Qt_Meta_Call;
  • lcd_ranges-impl-qt_meta_cast.adb - реализация функции Qt_Meta_Cast.

На этом описание нового класса закончено и можно разрабатывать его реализацию.

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

   function Create return not null access LCD_Range'Class is
   begin
      return Result : not null access LCD_Range'Class
        := new LCD_Range_Impl
      do
         declare
            R : LCD_Range_Impl'Class
              renames LCD_Range_Impl'Class (Result.all);

         begin
            Qt4.Widgets.Impl.Constructors.Initialize_Director (R);
            R.Set_Dynamically_Allocated;

            ...
         end;
      end return;
   end Create;

На этом создание собственного класса закончено. Разработанный по описанным правилам класс является полноправным членом Qt. Разработав дополнительный модуль его можно даже подключить к программе дизайнера и использовать при визуальном проектировании графического интерфейса пользователя.

Добавление сигналов и слотов

После создания полноправного порождённого тэгового типа (подкласса) добавление сигналов и слотов является тривиальной задачей.

Для объявления сигнала необходимо в спецификации пакета объявить подпрограмму возбуждения сигнала и указать её имя в директиве комилятора Q_Signal:

   procedure Emit_Value_Changed (Self  : not null access LCD_Ranges_Impl;
                                 Value : in Qt4.Q_Integers);
   pragma Q_Signal (LCD_Range_Impl, Emit_Value_Changed, "valueChanged(int)");

а в тело добавить указание на использование реализации из отдельного файла:

   procedure Emit_Value_Changed (Self  : not null access LCD_Ranges_Impl;
                                 Value : in Qt4.Q_Integers)
     is separate;

Реализация подпрограммы возбуждения сигнала генерируется комилятором метаинформации amoc.

Для объявления слота необходимо в спецификации пакета объявить подпрограмму сигнала и указать её имя в директиве компилятора Q_Slot. В теле пакета пользователь определяет реализацию подпрограммы слота.

   procedure Set_Value (Self  : not null access LCD_Ranges;
                        Value : in Qt4.Q_Integer);
   pragma S_Slot (LCD_Range_Impl, Set_Value, "setValue(int)");

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