QtScript: Exposing C++ classes (part two)

This is the second part of my QtScript walkthrough / tutorial. You can read the first part here.

As in the last tutorial, I must add the following disclaimer:

I'm writing these posts as I learn to use the QtScript module. As such, the solution I present here may not be the best / most elegant way of doing things. However, if I find a better solution, I'll certainly post back here in the future with corrections.

If there's any (QtScript-related) topic you'd like me to write about in the future, just leave a message and I'll endeavor to fulfill your request.

Background:

The last post was all about deferring some execution from a C++ class to an ECMAScript. The basic idea was that the C++ class could emit signals, zero or more of which were connected to functions in the script file. The script file set up these connections, so the the C++ code doesn't need to know which signals are being overridden, and which aren't.

This is all well and good, but you'll soon find that there's very little you can do in the script file, since the only functionality you have available is the builtin ECMASCript functions.What I'm going to look at today is how to make your own, and Qt classes available for use in the script file. In my example, I'm going to make the QMessageBox class available, but you can choose any other method you want.


Take 1:

First off, I want to be able to write "mb = new QMessageBox;" on my script, and have it work. This is actually pretty simple to achieve. The way I have chosen to do this is with a wrapper class. This wrapper derives from QMessageBox and QScriptable, and contains the magic "qscript_call" method. Here's the code:


// wrapper around QMessageBox:
class Wrapper_QMessageBox: public QMessageBox, protected QScriptable
{
Q_OBJECT
public:

Wrapper_QMessageBox(QWidget *parent =0)
: QMessageBox(parent) {}

public slots:
QScriptValue qscript_call(QWidget *parent = 0)
{
QMessageBox * const iface = new Wrapper_QMessageBox(parent);
return engine()->newQObject(iface, QScriptEngine::AutoOwnership);
}
};


This is the complete wrapper so far. Note that normally I wouldn't write all this code inline, as I firmly believe that inline functions are evil, but for demonstration code I think this makes it more readable. As you can see, we inherit from QMessageBox and from QScriptable. Note the protected inheritance! QScriptable gives us the engine() call we need later on.

The constructor is pretty straight forward - no surprises there!

Then we have a special slot called "qscript_call". This seems to be an undocumented feature in Qt, (try searching for qscript_call on google and you get very few results). Essentially, this slot gets called when our wrapper class is called as a function. I guess this is similar to the meta-method "__call__" in python. All we do in this slot is make a new instance of the wrapper class (remember that our wrapper class is a QMessageBox as well), and return it.

Our wrapper class still isn't available in the scripts however. In order to do that, we need to add this one line of code:


m_pEngine->globalObject().setProperty("QMessageBox", m_pEngine->newQObject(new Wrapper_QMessageBox, QScriptEngine::AutoOwnership));




This one line adds our wrapper class to the script global object. Once you've done this, you can no create QMessageBox instances from within a script file, like so:


mb = new QMessageBox();
mb.text = "Test!";
mb.exec();



However, there are still a few issues left to resolve:
  1. We can only use the default - empty constructor.
  2. Only properties and slots of the QMessageBox are available from the script. For example, there's no way to set the window title text<> right now. If we could use the overloaded QMessageBox constructor that took the window title as a parameter then we could just specify it at creation time, but we'll need a way to export non-slot methods as well.
Let's tackle these issues one at a time:

Adding Overloaded Constructors:

This one's pretty simple. All you need to do is add another constructor that takes the additional parameters, and passes them up to the QMessageBox class. Then, you overload the qscript_call method in a similar fashion. Here's what my class looks like now:


// wrapper around QMessageBox:
class Wrapper_QMessageBox: public QMessageBox, protected QScriptable
{
Q_OBJECT
public:

Wrapper_QMessageBox(QWidget *parent =0)
: QMessageBox(parent) {}

Wrapper_QMessageBox(Icon icon, const QString & title, const QString & text, StandardButtons buttons = NoButton, QWidget * parent = 0, Qt::WindowFlags f = Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint)
: QMessageBox(icon,title,text,buttons,parent,f) {}

public slots:
QScriptValue qscript_call(QWidget *parent = 0)
{
QMessageBox * const iface = new Wrapper_QMessageBox(parent);
return engine()->newQObject(iface, QScriptEngine::AutoOwnership);
}
QScriptValue qscript_call( Icon icon, const QString & title, const QString & text, StandardButtons buttons = NoButton, QWidget * parent = 0, Qt::WindowFlags f = Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint )
{
QMessageBox * const iface = new Wrapper_QMessageBox(icon,title,text,buttons,parent,f);
return engine()->newQObject(iface, QScriptEngine::AutoOwnership);
}
};


You can now call this new constructor straight from your script file, just as you could earlier!

Exposing Additional Methods:

Finally, we need a way to expose arbitrary methods that are not slots in our base class. This is pretty simple. We make our own method with the same name, and call into the base class method with the parameters passed to us. Consider my wrapper class, now that I have exposed the setWindowTitle method:


// wrapper around QMessageBox:
class Wrapper_QMessageBox: public QMessageBox, protected QScriptable
{
Q_OBJECT
public:

Wrapper_QMessageBox(QWidget *parent =0)
: QMessageBox(parent) {}

Wrapper_QMessageBox(Icon icon, const QString & title, const QString & text, StandardButtons buttons = NoButton, QWidget * parent = 0, Qt::WindowFlags f = Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint)
: QMessageBox(icon,title,text,buttons,parent,f) {}

public slots:
QScriptValue qscript_call(QWidget *parent = 0)
{
QMessageBox * const iface = new Wrapper_QMessageBox(parent);
return engine()->newQObject(iface, QScriptEngine::AutoOwnership);
}
QScriptValue qscript_call( Icon icon, const QString & title, const QString & text, StandardButtons buttons = NoButton, QWidget * parent = 0, Qt::WindowFlags f = Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint )
{
QMessageBox * const iface = new Wrapper_QMessageBox(icon,title,text,buttons,parent,f);
return engine()->newQObject(iface, QScriptEngine::AutoOwnership);
}
void setWindowTitle ( const QString & title )
{
QMessageBox::setWindowTitle(title);
}
};


Simple!

Conclusion:

Combined with my earlier post, this should be all you need to make a Qt application extensible through ECMAScript files. I'm doing all this work to write a small game, but the techniques covered should work for all types of projects. If / When I find anything that requires updating, I will endeavour to update these posts. Since I'm still learning QtScript myself, I'm sure these pages will be filled with innacuracies.

9 comments:

Anonymous said...

Great post! Thanks!

IcHiBoN said...

Hi, thanks for your tutorial on QtScript. One other topic I am interested with QtScript is how to dynamically change the look of a dialog with an ui file inside the script. Is this possible? Thanks!

Thomi Richards said...

Hi IcHiBoN

This should certainly be possible. If you look at the QtScript demos that come with Qt you'll see something very similar.

Once you have an interface to a Qt object in your QtScript, you can call any scriptable methods on it - you may need to create your own method to change the dialog how you want, but that's relatively simple.

If you post an example of exactly what you want to do I can provide some more detailed instructions.

Cheers,

HoRus said...

HI ! nice tutorial on QtScript.
Have you look at the demos Context2D?

I try to make like this but with other script but no image.
Just launch script in command for example.
It this possible?

Cheers,

Anonymous said...

Hi,

Interesting article but I could not get the examples work with Qt4.5.2. Keep getting "TypeError: QMessageBox is not a constructor" error. Any chance you could post the full code so I could see what I am doing wrong?

Thanks!
Cheers
Mark

asvil said...

Thanks for your post.
And what about creating a class with the ability to inherit it in the script.
Like qt script bindings QWidget. For example
MyFormWidget = function (parent) (
CoreFormWidget.call (this, parent);
this.button = new QPushButton (this);
)
MyFormWidget.prototype = new CoreFormWidget ();

I tried
QScriptValue ctor = qScriptValueFromQMetaObject (engine);
engine-> globalObject (). setProperty (T:: staticMetaObject.className (), ctor, flags);

But ctor in the script can be called only as object = new ctor (parent)
I looked at the source qt script bindings and realized that the need to create a wrapper function in c + + which will determine the caller and if the function is called as a designer will create an object, and if the function is called only in the context of another object that will fill the object properties.
Is there a shorter way?
Sorry for bad English.
Michael.

Anonymous said...

QScriptEngine engine;

engine.globalObject().setProperty("m", engine.newQObject(new Wrapper_QMessageBox));

qDebug() << engine.evaluate("m.show();").toString();

Manuel Auer said...

Hi,

before i start i have to excuse my bad english ;)
I constructed a custom class i can use in the Qt script engine. The name of the class is "QdbString". QdbString inherits only QObject! Not QString! To there it works fine. I can call methods and they will execute correctly.

var string1 = new QdbString();
var string2 = new QdbString();
string1.assign("Hello World!!!");
string2.assign(string1);

The script above evaluates with false. I found out that the error is in the 4. line. The reference of string1 will not be accepted although i implemented the following method:

void assign(const QdbString& _String);

I have descripted my problem more detailed here and with source-code:
http://qtforum.de/forum/viewtopic.php?f=1&t=15984

Maybe someone can help me.
It would be very nice!!!

Manuel

Anonymous said...

Hi,

Is "qscript_call" really a thing? Or is it just a random name?

Post a Comment