blog

Photo by Shahadat Rahman on Unsplash

Adding Scripting to a Qt Application – Part 2

by

QT logoIn Part I of this set of articles we introduced the basics of adding scripting capabilities to your Qt application. In this article we extend that to allow the user to build and utilize his own QtDesigner-created widgets through script.

This article focus on adding more advanced capabilities of allowing the user to load widgets for UI files created in QtDesigner.

The example code used for this article can be found on github.

Creating Widgets in Script

Now that we have the ability to execute scripts, let’s expose a slot to the script to load ui files and create a dialog.

QScriptValue MainWindow::loadUi(QString uiPath)
{
   if (uiPath.isEmpty())
   {
      uiPath = QFileDialog::getOpenFileName(this,
       "Select file", ".", "UI Files (*.ui);;All files (*.*);;");
   }
   if (uiPath.isEmpty())
   {
      return QScriptValue(&fScriptEngine, 0);
   }
   QUiLoader loader;
   QFile file(uiPath);
   if (!file.open(QFile::ReadOnly))
   {
      QMessageBox::warning(this, "Invalid UI file",
      QString(
       "The UI file specified, [%1], could not be loaded as a designer file")
       .arg(uiPath));
      return QScriptValue(&fScriptEngine, 0);
   }
   QWidget* widget = loader.load(&file);
   file.close();
   return fScriptEngine.newQObject(widget);
}

In this function, if no path is provided it will prompt the user to select one, use QUiLoader to load the file, and return the created widget to the script.

Using the QtDesigner, we have created a simple ui file that defines a dialog box with a table and a push button:

Software screen capture

So, now if we run the application, enter the following script, and then select our example ui file, we should see the dialog defined by the ui file:

var widget = app.loadUi(“”);
widget.exec();

To extend that even further, we can create a simple function to do something in the script in response to the button click:

function handleButton()
{
   pushButton.text = "toggled";
}
var widget = app.loadUi(“”);
var pushButton = widget.findChild("pushButton");
pushButton.clicked.connect(handleButton);
widget.exec();

This simply changes the text of the button when the user clicks the one button on the widget.

Exposing Public Members

While this is cool (and I’m sure you can see the possibilities), remember from Part I that the script only has access to properties and slots. Take a quick look at the QTableWidget documentation and it becomes clear that we can’t do much of anything without access to the public functions; Without any of the public members, there isn’t even any means to add items to the table.

So, how do we get around this?

The bad news is that there is no magic bullet to just flip a switch and have all of the public methods exposed. The only way to expose them is to subclass QUiLoader and override the virtual function createWidget. This allows us to intercept the instantiation of any widget class from QUiLoader and replace it with an instance of our own (which should be derived from the class being requested.)

There are two steps to building a custom UiLoader. First create the CustomUiLoader class, derived from QUiLoader, and override the createWidget method. Here is a simple CustomUiLoader class that only replaces the QTableWidget class.

class CustomUiLoader : public QUiLoader
{
   Q_OBJECT
public:
   CustomUiLoader(QObject *parent = 0) : QUiLoader(parent) { }
   virtual QWidget* createWidget(const QString &className,
   QWidget *parent = 0, const QString &name = QString())
   {
      QWidget* widget = NULL;
      if (className == "QTableWidget")
      {
         // replace any QTableWidget instances with instance of our
         // own CustomTableWidget
         widget = new CustomTableWidget(parent);
      }
      if (NULL != widget)
      {
         widget->setObjectName(name);
      }
      else
      {
         // let base class handle any widgets for which we don't have a
         // custom class
         widget = QUiLoader::createWidget(className, parent, name);
      }
      return widget;
   }
};

Of course, this implies that we will require a QTableWidget-derived class called CustomTableWidget. This example class only adds a single method for setting data on an item. One important note is that the new method setItemData is exposed using the Q_INVOKABLE macro. We could also have shared it as a public slot on that class to achieve the same result.

class CustomTableWidget : public QTableWidget
{
   Q_OBJECT
public:
   CustomTableWidget(QWidget *parent = 0) : QTableWidget(parent) { }   Q_INVOKABLE void setItemData(int row, int column, QVariant data,
    int userRole = Qt::DisplayRole)
   {
      QTableWidgetItem* item = this->item(row, column);
      if (NULL == item)
      {
          item = new QTableWidgetItem(data.toString());
          this->setItem(row, column, item);
      }
      item->setData(userRole, data);
   }
};

Once we replace the QUiLoader in MainWindow::loadUi with our CustomUiLoader, scripts can load QtDesigner-created UI files and any tables in that UI file will be exposed to the script as our new CustomTableWidget instead of the base QTableWidget. The following script makes use of the new functionality.

function handleButton()
{
   var table = widget.findChild("tableWidget");
   var count = table.rowCount;
   table.rowCount = count + 1;
   table.setItemData(count, 0, "Variable " + count);
   table.setItemData(count, 1, count);
}
var widget = app.loadUi("");
var pushButton = widget.findChild("pushButton");
pushButton.clicked.connect(handleButton);
widget.exec();

This script simply adds an item to the table whenever the user clicks the button. Note that this script uses existing QTableWidget properties (rowCount) as well as the new setItemData method.

Software screen capture

Conclusion

While there is still a lot lacking, it is a start in the right direction. The next step for a production application with scripting capabilities would be:

  1. Create custom classes for any QWidget-based classes you want your UiLoader to support
  2. Expose public methods on those classes, and
  3. Return instances of those classes in CustomUiLoader::loadWidget.

This article shows the technical capabilities; the rest is “busy work”.

+ more