Программирование на языке Ruby.

12.4. QtRuby.

Qt — это библиотека и комплект средств разработки, созданные и распространяемые компанией Trolltech. Основной упор в Qt сделан на кросс-платформенности, единый программный интерфейс предоставляется для операционных систем Windows, Mac, и UNIX. Разработчику нужно написать код только один раз, он будет оттранслирован на всех трех платформах без модификации.

Qt распространяется на условиях одной из двух лицензий: GPL или коммерческая лицензия для разработки продуктов без раскрытия исходных текстов. Такой же политики двойного лицензирования придерживаются и другие компании, например MySQL. Она позволяет использовать библиотеку в проектах с открытыми исходными текстами, в которых предлагаемые средства находят полезное применение. Но при этом Trolltech может получать доход от продажи коммерческих лицензий клиентам, которых не устраивают ограничения GPL.

12.4.1. Обзор.

Привязки QtRuby — результат работы многих людей, прежде всего Ричарда Дейла (Richard Dale). Эшли Уинтерс (Ashley Winters), Жермен Гаран (Germain Garand) и Давид Форе (David Faure) написали большую часть инструмента генерации кода привязки (он называется SMOKE). Другие отправляли отчеты о найденных ошибках и вносили исправления.

Расширение QtRuby содержит не только обширный набор относящихся к графическим интерфейсам классов, но и целый комплект дополнительных средств, часто необходимых программистам (например, библиотеки для работы с XML и SQL).

В последние несколько лет привязки QtRuby основывались на версии Qt 3.x. В конце 2005 года вышла версия 4. Сейчас есть варианты QtRuby и для Qt3, и для Qt4, но это разные пакеты. Поскольку Qt3 никогда не поставлялась в исходных текстах для Windows, то в этой книге мы рассматриваем только привязки к Qt4. Однако приведенные в этом разделе примеры будут работать и для Qt3. Весь код был проверен на платформах Windows, Linux и Mac с версией QtRuby для Qt4.

Ключевой аспект Qt, а значит и QtRuby, — концепция сигналов и слотов. Сигналы представляют собой асинхронные события, возникающие, когда в приложении происходит какое-то событие (например, щелчок кнопкой мыши или ввод текста в поле). Слот — это просто метод, вызываемый в ответ на возникновение сигнала. Для связывания сигналов со слотами мы будем использовать метод connect.

Чтобы иметь возможность пользоваться сигналами и слотами, а также многими другими возможностями QtRuby, все наши классы будут наследовать классу Qt::Object. Более того, классы, используемые в графических интерфейсах, будут наследовать классу Qt::Widget, который, в свою очередь, является производным от Qt::Object.

12.4.2. Простое оконное приложение.

Приложение QtRuby должно в самом начале загрузить библиотеку Qt. QtRuby раскрывает свою функциональность посредством модуля Qt (следовательно, имена всех классов начинаются с префикса Qt::). Имена всех классов в исходной библиотеке Qt начинаются с буквы Q, но при переходе к QtRuby эта буква опускается. Так, например, класс, основанный на QWidget, в QtRuby будет называться Qt::Widget.

Require 'Qt'.

App = Qt::Application.new(ARGV).

Str = Time.now.strftime("Today is %B %d, %Y").

Label = Qt::Label.new(str).

Label.show.

App.exec.

Рассмотрим этот код подробнее. Вызов Qt::Application.new запускает приложение Qt; он инициализирует оконную систему и выполняет подготовительные действия для создания виджетов.

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

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

12.4.3. Кнопки.

Создание кнопки в QtRuby сводится к созданию экземпляра класса Qt::PushButton (см. листинг 12.14 и рис. 12.7). Обычно при нажатии кнопки нужно выполнить некоторое действие. Для этого применяется механизм событий и слотов QtRuby.

Листинг 12.14. Кнопки в QtRuby.

Require 'Qt'.

Class MyWidget < Qt::Widget.

 slots 'buttonClickedSlot()'.

 def initialize(parent = nil).

  super(parent).

  setWindowTitle("QtRuby example");

  @lineedit = Qt::LineEdit.new(self).

  @button = Qt::PushButton.new("All Caps!",self).

  connect(@button, SIGNAL('clicked()'),

   self, SLOT('buttonClickedSlot()')).

  box = Qt::HBoxLayout.new.

  box.addWidget(Qt::Label.new("Text:")).

  box.addWidget(@lineedit).

  box.addWidget(@button).

  setLayout(box).

 end.

 def buttonClickedSlot.

  @lineedit.setText(@lineedit.text.upcase).

 end.

End.

App = Qt::Application.new(ARGV).

Widget = MyWidget.new.

Widget.show.

App.exec.

Программирование на языке Ruby.

Рис.12.7. Кнопки в Qt.

В этом примере мы создали собственный класс виджета с именем MyWidget, он наследует классу Qt::Widget, являющемуся предком любого нестандартного виджета.

Перед инициализацией мы подготовили список слотов, которые будут определены в нашем классе. Слоты — это обычные методы класса, но необходимо указать их имена, чтобы во время выполнения QtRuby знала, что мы собираемся использовать их именно в качестве слотов. Метод класса slots принимает список строк:

Slots = 'slot1()', 'slot2()'.

Инициализатор класса принимает аргумент parent, он есть почти у всех виджетов в Qt и определяет, какой виджет будет владельцем вновь создаваемого. Значение nil означает, что это «виджет верхнего уровня», у которого нет владельца. Концепция «владения», наверное, имеет более понятный смысл в C++; родители владеют своими детьми, то есть при уничтожении или удалении родителя удаляются и все его потомки.

Наш класс создает объект Qt::LineEdit для ввода текста и кнопку Qt::PushButton с надписью All Caps!. В качестве родителя каждому виджету передается self. Это означает, что создаваемый экземпляр MyWidget «усыновляет» эти виджеты.

Далее мы обращаемся к ключевой части библиотеки Qt — механизму соединения сигналов со слотами. В классе Qt::Pushbutton определен сигнал clicked, который испускается при нажатии кнопки. Этот сигнал можно соединить со слотом, в данном случае с методом buttonClickedSlot. Имя слота может быть любым, суффикс Slot мы употребили просто для наглядности.

В самом конце мы создаем экземпляр класса Qt::HBoxLayout. При добавлении виджетов в этот контейнер он автоматически изменяет их размеры, так что нам больше не о чем беспокоиться.

12.4.4. Текстовые поля.

Как видно из листинга 12.14, в QtRuby есть класс Qt::LineEdit для ввода одной строки текста. Для ввода нескольких строк предназначен класс Qt::TextEdit.

В листинге 12.15 демонстрируется многострочное текстовое поле. Под ним расположена метка, в которой отображается текущая длина текста (рис. 12.8).

Листинг 12.15. Простой редактор в Qt.

Require 'Qt'.

Class MyTextWindow < Qt::Widget.

 slots 'theTextChanged()'.

 def initialize(parent = nil).

  super(parent).

  @textedit = Qt::TextEdit.new(self).

  @textedit.setWordWrapMode(Qt::TextOption::WordWrap).

  @textedit.setFont( Qt::Font.new("Times", 24) ).

  @status = Qt::Label.new(self).

  box = Qt::VBoxLayout.new.

  box.addWidget(@textedit).

  box.addWidget(@status).

  setLayout(box).

  @textedit.insertPlainText("This really is an editor").

  connect(@textedit, SIGNAL('textChanged()'),

   self, SLOT('theTextChanged()')).

 end.

 def theTextChanged.

  text = "Length: " + @textedit.toPlainText.length.to_s.

  @status.setText(text).

 end.

End.

App = Qt:Application.new(ARGV).

Widget = MyTextWindow.new.

Widget.setWindowTitle("QtRuby Text Editor").

Widget.show.

App.exec.

Программирование на языке Ruby.

Рис. 12.8. Простой редактор в Qt.

Виджет конструируется примерно так же, как в предыдущем примере. Но теперь мы создаем объект Qt::TextEdit, а также метку Qt::Label для показа текущего состояния.

Стоит отметить, что для объекта @textedit мы указали шрифт Times высотой 24 пункта. У каждого класса, наследующего Qt::Widget (в том числе и у Qt::TextEdit) есть свойство font, которое можно опросить или установить.

Затем мы создаем менеджер вертикального размещения ( Qt::QBoxLayout), который будет контейнером для всех своих потомков, добавляем в него виджет @textedit и связываем сигнал textChanged с определенным нами слотом theTextChanged.

В методе theTextChanged мы запрашиваем у редактора текст и получаем его длину, а затем записываем возвращенное значение в метку @status.

Отметим, что весь механизм сигналов и слотов работает асинхронно. После того как приложение входит в цикл обработки событий ( арр.ехес), оно уже не получает управления явно. Вот почему сигналы и слоты так важны. Мы определяем события, которые нас интересуют (сигналы), и действия, которые нужно выполнить при возникновении таких событий (слоты).

12.4.5. Прочие виджеты.

В библиотеке Qt есть еще много встроенных виджетов, например переключатели, флажки и т.п. В листинге 12.16 продемонстрированы некоторые из них, а на рис. 12.9 показано, как выглядит окно приложения.

Листинг 12.16. Прочие виджеты в Qt.

Require 'Qt'.

Class MyWindow < Qt::Widget.

 slots 'somethingClicked(QAbstractButton *)'.

 def initialize(parent = nil).

  super(parent).

  groupbox = Qt::GroupBox.new("Some Radio Button",self).

  radio1 = Qt::RadioButton.new("Radio Button 1", groupbox).

  radio2 = Qt::RadioButton.new("Radio Button 2", groupbox).

  check1 = Qt::CheckBox.new("Check Box 1", groupbox).

  vbox = Qt::QBoxLayout.new.

  vbox.addWidget(radio1).

  vbox.addWidget(radio2).

  vbox.addWidget(check1).

  groupbox.setLayout(vbox).

  bg = Qt::ButtonGroup.new(self).

  bg.addButton(radio1).

  bg.addButton(radio2).

  bg.addButton(check1).

  connect(bg, SIGNAL('buttonClicked(QAbscractButton *)'),

   self, SLOT('somethingClicked(QAbstractButton *)') ).

  @label = Qt::Label.new(self).

  vbox = Qt::VBoxLayout.new.

  vbox.addWidget(groupbox).

  vbox.addWidget(@label).

  setLayout(vbox).

 end.

 def somethingClicked(who).

  @label.setText("You clicked on a " + who.className).

 end.

End.

App = Qt::Application.new(ARGV).

Widget = MyWindow.new.

Widget.show.

App.exec.

Программирование на языке Ruby.

Рис. 12.9. Простое приложение Tk.

В этом классе мы сначала создаем объект Qt::GroupBox — контейнер с рамкой и необязательным заголовком, в который можно помещать другие виджеты. Далее создаются два переключателя Qt::RadioButtons и флажок Qt::CheckBox, а в качестве их родителя указывается ранее созданный контейнер.

Затем создается менеджер размещения Qt::VBoxLayout, в который помещаются переключатели и флажок, после чего этот менеджер связывается с групповым контейнером и начинает управлять его размещением на экране.

Следующий важный шаг — создание объекта Qt::ButtonGroup, в который помещаются флажок и переключатели. Qt::ButtonGroup предназначен для логической группировки кнопок, флажков и переключателей. На их визуальное расположение он никак не влияет, зато обеспечивает, к примеру, взаимное исключение (гарантирует, что только один из группы виджетов может быть отмечен). В данном случае этот объект будет источником сигнала buttonClicked, который испускается при нажатии любой кнопки в группе.

Этот сигнал отличается от виденных ранее тем, что ему сопутствует аргумент, а именно объект, по которому щелкнули мышкой. Обратите внимание на то, как синтаксис — QAbstractButton* — напоминает о C++-ных корнях Qt. В некоторых случаях употребления принятой в C++ нотации для обозначения типов параметров не избежать (хотя в будущих версиях это, возможно, и исправят).

В результате такого вызова метода connect при щелчке по любому виджету, принадлежащему группе, этот виджет будет передан слоту somethingClicked. Наконец, мы создаем метку Qt::Label, контейнер Qt::QBoxLayout и увязываем все вместе.

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

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

Листинг 12.17. Нестандартный виджет TimerClock.

Require 'Qt'.

Class TimerClock < Qt::Widget.

 def initialize(parent = nil).

  super(parent).

  @timer = Qt::Timer.new(self).

  connect(@timer, SIGNAL('timeout()'), self, SLOT('update()')).

  @timer.start(25).

  setWindowTitle('Stop Watch').

  resize(200, 200).

 end.

 def paintEvent(e).

  fastHand = Qt::Polygon.new([Qt::Point.new(7, 8),

   Qt::Point.new(-7, 8),

   Qt::Point.new(0, -80)]).

  secondHand = Qt::Polygon.new([Qt::Point.new(7, 8),

   Qt::Point.new(-7, 8),

   Qt::Point.new(0, -65)]).

  secondColor = Qt::Color.new(100, 0, 100).

  fastColor = Qt::Color.new(0, 150, 150, 150).

  side = [width, height].min.

  time = Qt::Time.currentTime.

  painter = Qt::Painter.new(self).

  painter.renderHint = Qt::Painter::Antialiasing.

  painter.translate(width() / 2, height() / 2).

  painter.scale(side / 200.0, side / 200.0).

  painter.pen = Qt::NoPen.

  painter.brush = Qt::Brush.new(secondColor).

  painter.save.

  painter.rotate(6.0 * time.second).

  painter.drawConvexPolygon(secondHand).

  painter.restore.

  painter.pen = secondColor.

   (0...12).each do |i|

   painter.drawLine(88, 0, 96, 0).

   painter.rotate(30.0).

  end.

  painter.pen = Qt::NoPen.

  painter.brush = Qt::Brush.new(fastColor).

  painter.save.

  painter.rotate(36.0 * (time.msec / 100.0)).

  painter.drawConvexPolygon(fastHand).

  painter.restore.

  painter.pen = fastColor.

   (0...60).each do |j|

   if (j % 5) != 0.

    painter.drawLine(92, 0, 96, 0).

   end.

   painter.rotate(6.0).

  end.

  painter.end.

 end.

End.

App = Qt::Application.new(ARGV).

Wid = TimerClock.new.

Wid.show.

App.exec.

Созданный в этом примере виджет называется TimerClock. В инициализаторе мы создаем объект Qt::Timer, который конфигурируется для периодического испускания сигнала. Его сигнал timeout мы соединяем со слотом update нашего виджета. Это встроенный слот, он заставляет виджет перерисовать себя.

Таймер запускается методом start. Переданный ему аргумент говорит, что таймер должен срабатывать (и испускать сигнал timeout) каждые 25 миллисекунд. Следовательно, слот update будет вызываться каждые 25 миллисекунд.

Далее определяется метод paintEvent. Мы переопределяем одноименный метод класса Qt::Widget. Когда виджет собирается перерисовать себя (то есть при срабатывании таймера), он вызывает этот метод. Переопределяя его, мы решаем, как виджет должен отображаться на экране. Код этого метода вызывает различные графические примитивы рисования.

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

Задаются значения нескольких свойств. Устанавливаются цвета Qt::Color обеих стрелок. Аргументами инициализатора Qt::Color являются значения в формате RGB с необязательной альфа-прозрачностью.

Часы должны быть квадратными, поэтому в переменную side (длина стороны) записывается минимум из ширины и высота виджета. Кроме того, мы запоминаем текущее время, обращаясь к методу Qt::Time.currentTime.

Далее создается объект Qt::Painter, и с его помощью мы начинаем рисовать. Задается режим сглаживания (antialiasing), чтобы на стрелках часов не было «лесенки». Начало координат помещается в центр области рисования ( painter.translate (width/2, height/2)). Для объекта Painter устанавливается масштаб в предположении, что сторона квадрата составляет 200 единиц. Если размер окна изменится, то масштабирование будет произведено автоматически.

Затем выполняется последовательность операций рисования. Различные геометрические преобразования (например, поворот), сопровождаются парой вызовов painter.save и painter.restore. Метод save сохраняет текущие свойства объекта Painter в стеке, чтобы их можно было позднее восстановить.

Программа рисует обе стрелки, предварительно повернув их на нужный угол в соответствии с текущим временем. Кроме того, мы наносим риски вдоль границы циферблата.

И напоследок мы сообщаем объекту Painter, что рисование закончилось (вызывая метод painter.end). Довершают картину четыре строчки, в которых создаются объект приложения Qt::Application и наш виджет, а затем запускается цикл обработки событий. На рис. 12.10 показан конечный результат.

Программирование на языке Ruby.

Рис. 12.10. Виджет TimerClock.

12.4.6. Дополнительные замечания.

Поскольку библиотека Qt написана на C++, неизбежны некоторые идиомы, отражающие ограничения этого языка. Иногда перевод на Ruby не выглядит на 100% естественным, поскольку в Ruby аналогичные вещи делаются несколько иначе. Поэтому в ряде случаев вводится избыточность, позволяющая выражать свои намерения «по-рубистски».

Например, имена методов, в которых сохранена «верблюжьяНотация», свойственная C++, можно записывать и с подчерками (_). Так, следующие два вызова эквивалентны:

Qt::Widget::minimumSizeHint.

Qt::Widget::minimum_size_hint.

Все методы установки свойств в Qt начинаются со слова set, например, Qt::Widget::setMinimumSize. В Ruby можно это слово опускать и пользоваться присваиванием, например:

Widget.setMinimumSize(50).

Widget.minimumSize = 50  # To же самое.

Widget.minimum_size = 50 # To же самое.

Аналогично в Qt имена методов, возвращающих булевское значение, часто начинаются с is или has, например, Qt::Widget::isVisible. QtRuby позволяет именовать их в духе Ruby:

А.isVisible.

A.visible? # То же самое.