Skip to content

Using custom C++ classes with QtRuby

Friday, 16 February 2007  |  richard dale

I've recently been having a discussion with Eric Landuyt on the Korundum site help forum about wrapping custom C++ classes in QtRuby. I told Eric that you just needed to create a QObject derived class with the slots and properties you wanted to expose, give it a name via a QObject::setObjectName() call, and create it with qApp as the parent. Then wrap the class in a Ruby extension using an extconf.rb script to generate the makefile to build it. Once your new extension is loaded, you can find the instance of the C++ class by using Qt::Object.findChild() with the object name you gave it.

Here is a simple example class with a property and a slot. The header is defined like this:

#include <QtCore/qobject.h>
#include <QtCore/qcoreapplication.h>
#include <QtCore/qstring.h>

class TestObject : public QObject { Q_OBJECT Q_PROPERTY(int value READ value WRITE setValue) public: TestObject(QObject *parent = 0);

void setValue(int value);
int value();

public slots: int foobar(bool yn, QString text);

private: int m_value; };

And then the implementation with a C Init_testqobject() function as an entry point. It creates an instance of a C++ TestObject with QCoreApplication::instance() as parent, and an objectName of "QtRuby TestObject":

#include "testobject.h"

TestObject::TestObject(QObject *parent) : QObject(parent), m_value(0) { }

int TestObject::value() { return m_value; }

void TestObject::setValue(int value) { m_value = value; }

int TestObject::foobar(bool yn, QString text) { qDebug("in foobar yn: %s text: %s\n", yn ? "true" : "false", (const char *) text.toLatin1()); return 123; }

void init() { TestObject * test = new TestObject(QCoreApplication::instance()); test->setObjectName("QtRuby TestObject"); }

extern "C" {

void Init_testqobject() { init(); }

};

The extconf.rb to build the 'testqobject' extension creates the makefile, and execs the moc utility. It looks like this:

require 'mkmf'
$CPPFLAGS += " -I/opt/kde4/include/QtCore -I/opt/kde4/include "
$LOCAL_LIBS += '-L/opt/kde4/lib -lQtCore -lstdc++'
create_makefile("testqobject")
exec "/opt/kde4/bin/moc #{$CPPFLAGS} testobject.h -o moc_testobject.cpp"

In your ruby code you can get hold of the instance of you TestObject like this:

irb -rQt
irb(main):001:0> app = Qt::Application.new(ARGV)
=> #<Qt::Application:0xb6ab4f08 objectName="irb">
irb(main):002:0> require 'testqobject'
=> true
irb(main):003:0> test = app.findChild(Qt::Object, "QtRuby TestObject")
=> #<TestObject:0xb6aaf134 objectName="QtRuby TestObject">
irb(main):004:0> test.value = 456
=> 456
irb(main):005:0> test.value
=> 456

Note that you need to do the require statement for 'testqobject' after you've created the Qt::Application so it has a non null 'qApp' as a parent. Retrieve the instance with 'app.findChild()' and it returns a Ruby instance of 'TestObject' even though the QtRuby runtime knew nothing about that class in advance. Then you can set and get properties from ruby directly, or you can invoke slots by connecting them to a QtRuby signal and emitting it.

That does work fine, but Eric asked why he couldn't just invoke the slots directly. He said:

I created a custom C++ Qt class, named TestObject, derivating from QObject, with a custom mouseClick() Qt slot. Obtaining the C++ instance by using 'test = findChild(TestObject, "test")' works properly. However, if I try to invoke a Qt slot directly, such as test.MouseClick(), it fails with a NoMethodError. ...

As you suggested it, direct support in the runtime would be really perfect! :) In fact, I really like the idea to use C++ classes throught Qt (as it avoid to use something like SWIG or a C Ruby extension only to generate a wrapper). Basically, I derivate all C++ classes from QObject, replace 'public:' by 'public slots:' and I get all my C++ classes accessible from the Ruby side for free, with introspection as an unexpected benefit! :) If required, the only missing thing to write at the C++ side would be a small object factory to directly instanciate C++ classes from the Ruby side.

So I went ahead and did what he said and checked the code into the svn today, and it will be in the next release of QtRuby. You can invoke you custom slot directly like this:

irb(main):006:0> test.foobar(false, "hi there")
in foobar yn: false text: hi there
=> 123

This is actually really useful for KDE programming because it means you can now invoke slots as methods directly on KPart instances, as well as setting properties on them with really simple ruby code.

If you want to do something similar with the current QtRuby release, Eric posted this sample code to achieve the same effect:

class ObjectWrapper < Qt::Object
  def initialize(parent)
    super(parent)
    slots = parent.metaObject.slotNames
    self.class.signals *slots.collect { |slot| slot = '_' + slot }
    slots.each do |slot|
      connect(self, SIGNAL('_' + slot), parent, SLOT(slot))
      method = slot.split('(')[0]
      instance_eval <<-EOS
        def #{method}(*args)
          emit _#{method}(*args)
        end
      EOS
    end
  end
end

app = Qt::Application::instance test = ObjectWrapper.new(app.findChild(Qt::Object, 'TestObject'))

Another really good thing with QtRuby happened today; thanks to the efforts of Guillaume Laurent and Thomas Moenicke you can now build both qtruby, and the smoke library it uses, entirely with cmake. This is a huge improvement over autoconf/automake which often barely worked on Linux let alone Windows or Mac OS X where it was utterly useless. So I'm now hoping that QtRuby will be relatively easy to build on any platform, and the next step will be packaging it as a gem built by cmake, and that will allow a lot more people to use it.