The Qt Binding

Starting with 0.22 comes with a large set of Qt classes available through the Ruby binding mechanism. This allows integration of Qt user interfaces with Ruby scripts and to use the Qt API, for example the network or SQL classes. To use the Qt bindings, KLayout must be compiled with Qt binding, i.e.

build.sh -with-qtbinding ...

The API provided covers the functionality of a certain Qt version. Currently this is Qt 4.6 and Qt 5.5. The API covers the following Qt 4 and Qt 5 modules:

This article covers the use of the Qt API and special topics related to that. It is recommended to read the article about the Ruby binding (The Ruby Language Binding) for a deeper understanding of the mapping of the Qt API to Ruby.

There is some overlap with the "qtruby" project. This project also makes the Qt API available for Ruby. The approach of "qtruby" is similar. Yet there are some differences, in particular the event feature of KLayout, which allows a convient binding of code blocks to signals.

A First Sample

This is a first sample of how to use the Qt API. To run that sample, open the Macro Development IDE from the "Macros" menu. Add a new macro with the "+" button in the left top tool bar. Choose "General KLayout Macro" from the "General" group as the template. Paste the code above into the macro and run the macro with F5.

module MyMacro

  include RBA

  dialog = QDialog::new(Application.instance.main_window)
  dialog.windowTitle = "My Dialog"
  
  layout = QHBoxLayout::new(dialog)
  dialog.setLayout(layout)
  
  button = QPushButton.new(dialog)
  layout.addWidget(button)
  button.text = "Click Me"
  button.clicked do 
    QMessageBox::information(dialog, "Message", "I was clicked!")
  end
  
  dialog.exec

end

The sample creates a QDialog with a layout and a button in it. When the button is clicked, a message box appears. This code demonstrates some features of the Ruby binding of the Qt library. For example, the button's text is set with an attribute assignment ("button.text = ...") rather than a method call ("button.setText(...)").

A noteworthy feature is the event binding which allows associating code blocks with signals. In pure Qt, the "clicked" signal of the button would have to be connected to a slot. This is not possible without creating a receiver object. With events, that receiver is created internally and a code block can be attached to the signal directly:

  button.clicked do 
    QMessageBox::information(dialog, "Message", "I was clicked!")
  end

That binding includes the ability to receive signal arguments through block arguments.

Binding Details

Given the rules stated in the general Ruby binding documentation (The Ruby Language Binding) the ruby versions of most methods can be derived readily. There are some exceptions however that we will cover here.

First, the C++ to Ruby binding lacks some features that are required for some methods. Those methods cannot be bound. Specifically that concerns:

Some template types are made available to Ruby. In particular that is valid for some QPair specializations. For example "QPair<double, double>" is available as "QDoublePair".

The naming of some methods has been aligned to Ruby:

In those cases, the original declaration is still available also.

The inheritance hierarchy of classes is mapped to Ruby in most cases. Sometimes that is not possible. For example, if the base class is a template (i.e. QPolygon, where the base class is a QVector<QPoint>). In that case, methods are provided that implement the features from the base class in the derived class.

Operators are bound to Ruby operators where that makes sense. For example, for QPoint the operators are available as expected ("==", "+" etc.).

"destroy" and "create" have been renamed to "qt_destroy" and "qt_create" to avoid name clashes with identical methods inherited from the RBA binding. "destroy" and "create" are standard methods which KLayout's Ruby binding defines for every object exposed to Ruby.

When a Qt object is created in Ruby space with a parent, its ownership is passed to the parent. It is safe however to keep a reference to that object, because KLayout's Ruby binding employs a special Ruby class internally (a proxy) which keeps track of the lifetime of the Qt object. If the parent is destroyed, the Qt object is destroyed as well. The internal Qt object will be notified and the reference to the Qt object will be invalidated. As a consequence, the Ruby proxy will refuse to execute methods on that object.

Destroying a Qt object can be necessary for example to free resources. To perform the equivalent of the C++ delete operator, use the "destroy" method that comes with every class exposed to Ruby. After an object is destroyed, the Ruby part of the binding still persists until all references to that object are removed. However, it is no longer possible to call methods on these objects.

The Qt binding significantly benefits from the dynamic binding of C++ objects. If a C++ pointer is returned, this pointer often is a pointer to a base class. Behind the pointer often is an object of a derived class. C++ allows calling of base class methods on that pointer, but not methods of the derived class - the identity of the object is reduced to the base class.

For example, if a method returns a QWidget pointer for a QPushButton, it is not possible to directly set the buttons text, because the method required for that is not part of the QWidget interface. In C++ one would dynamic_cast the pointer to QWidget and set the text then.

The Ruby binding automatically upcasts the pointer to the actual object, so the value returned has the real object's identity. In that case, delivering a QWidget would render a Ruby object that has a QPushButton identity and it's possible to set the text immediately. If the object was not a QPushButton, an error would be issued when an attempt is made to call a QPushButton method.

Enums

Enum types are available as classes to give them a specific context. Since Ruby does not allow declaration of classes within classes, enums declared inside a class must be declared as separate classes outside that class. The relationship is indicated by the Enum's class name. For example, QMessageBox::Icon (C++) is available as the Ruby class QMessageBox_Icon. The enum values are defined as constants within that class and the enclosing class. For example QMessageBox::Critical which is a value for QMessageBox::Icon is available as QMessageBox_Icon::Critical and QMessageBox::Critical in Ruby.

Starting with version 0.24, the QFlags template is supported as a separate class. The name of the class indicates the relationship to the enum class. For example, QFlags<QMessageBox::Icon> is available as QMessageBox_QFlags_Icon. Enum classes are derived from their respective flags class, so they can serve to initialize arguments expecting flags. It's hardly required to operate with the flags classes directly, since they are created automatically when joining enum's with the "or" (|) operator:

  QMessageBox::Ok                         # A QMessageBox_StandardButton object
  QMessageBox::Ok | QMessageBox::Cancel   # A QMessageBox_QFlags_StandardButton object

With these definitions, the following is allowed:

  QMessageBox::information(parent, title, text, QMessageBox::Ok | QMessageBox::Cancel)
  QMessageBox::information(parent, title, text, QMessageBox::Ok)

Using the designer

It is possible to load a dialog from a UI designer (.ui) file. Have a look at the following sample:

module MyMacro
 
  include RBA

  ui_file = QFile::new(QFileInfo::new($0).dir.filePath("MyDialog.ui"))
  ui_file.open(QIODevice::ReadOnly)
  dialog = QFormBuilder::new.load(ui_file, Application::instance.main_window)
  ui_file.close
  
  def dialog.setup
    button.clicked do
      slider.value = (slider.value + 1) % 100
    end
  end

  dialog.setup
  dialog.exec

end

This sample tries to locate a designer file called "MyDialog.ui" relative to the macro's path (in $0). It uses the QFormBuilder class to load and create the dialog. In that sample, "MyDialog" defines a dialog with two widgets: a QPushButton ("button") and a QSlider ("slider"). Because of the dynamic binding in Ruby, "dialog" will already have the correct class and we don't have to cast the pointer delivered by QFormBuilder::load before we can call "exec".

This sample exploits a nice Ruby feature: in Ruby it is possible to dynamically add methods to an instance. This allows extending the QDialog object we got from QFormLoader by custom code. In our case we add a "setup" method. This method installs the custom logic of the dialog. It makes use of a convenience feature implemented in QObject's Ruby binding: all named child objects of an object are available through accessor methods. Therefore we can access "button" and "slider" by their name to install an event handler that updates the slider value each time the button is clicked.

After calling setup on the dialog we have initialized it and we can show it with "exec".

Behind The Scenes

The mechanism behind the Ruby binding a based on the RBA/GSI framework of KLayout. In order to be able to derive from existing classes, that framework needs to add a kind of interfacing class atop of existing classes. Thus, the framework exposes every Qt class in two ways:

The difference between the native and interface classes is important if you test the type of a object. The difference between both cases is the scope. The native classes will always match. The interface classes will only match if the object was created by Ruby code:

b = dialog.button
# this will not render true, if the button was created by QFormBuilder for example
b.is_a?(QPushButton)
# this is correct:
b.is_a?(QPushButton_Native)

To avoid confusion, the native classes do not appear in the documentation. They would just add another level of inheritance without providing additional methods.