Sunday, November 8, 2009

Using the Mozilla ActiveX Control in a Qt Application

The cross-platform application framework Qt includes WebKit, one of the three rendering engines that power todays web browsers. Creating a basic browser with Qt and WebKit takes less than five minutes.
But what about the other two engines, Internet Explorer and Gecko (used by Firefox)? Windows has a component technology called COM which allows applications to offer and consume object-oriented interfaces in a programming language independent way. One flavor of this technology is called ActiveX, which, if my understanding of this is correct, basically means "COM controls that put something on the screen". I haven't written much code that made use of Windows specific technologies like COM, so I might get some things wrong. But reading that Wikipedia article, I feel like I have a pretty good idea of how this is supposed to work. This should be sufficient to make sense of the Qt documentation on their ActiveQt Framework. All the tedious plumbing details are hopefully hidden inside ActiveQt. So let's get to it!

We'll start with embedding the existing Internet Explorer ActiveX control in a Qt application. If you don't have it already, download the Qt SDK for Windows, install it and, no, don't launch QtCreator just yet. According to the documentation, we first need to build ActiveQt. Assuming you got the latest SDK version at the time of writing and you installed it in the default location, building and installing the neccessary files is done on the command line in the following way:
C:\Documents and Settings\Your Name> cd \Qt\2009.04\bin

C:\Qt\2009.04\bin>qtenv
Setting up a MinGW/Qt only environment...
-- QTDIR set to C:\Qt\2009.04\qt
-- PATH set to C:\Qt\2009.04\qt\bin
-- Adding C:\Qt\2009.04\bin to PATH
-- Adding C:\WINDOWS\System32 to PATH
-- QMAKESPEC set to win32-g++

C:\Qt\2009.04\bin>cd ..\qt\src\activeqt

C:\Qt\2009.04\qt\src\activeqt>qmake

C:\Qt\2009.04\qt\src\activeqt>mingw32-make

---- Lots of output... -----

C:\Qt\2009.04\qt\src\activeqt>mingw32-make install

---- Some more output... -----
You should now have the files libQaxContainer.a, libQaxContainerd.a, libQaxServer.a and libQaxServerd.a in C:\Qt\2009.04\qt\lib.
Now it's time to fire up QtCreator and create a new project. Click File -> New, under Projects choose "Qt4 Gui Application" and give it a name and location. I chose "MultiBrowser" and C:\code. Accept the defaults for everything else. In the tree on the left, displaying the files of your project, double click MultiBrowser.pro and add a line containing:
CONFIG += qaxcontainer
The next step is to build the GUI. To do this open the file mainwindow.ui. This will load the GUI designer inside QtCreator. I won't give a step by step explanation of how to use the GUI designer. It's reasonably intuitive if you've ever used a similar tool. The first thing I added was a "File" menu with a "Quit" item that sends the close() signal to the MainWindow instance when clicked. Then I removed the toolbar, added two buttons for navigating back and forward and a QLineEdit for the URL. Below those, I put a QWidget. We need to replace this with a QAxWidget, which can host ActiveX controls. To do this, right click the object on the tree view in the upper left corner and choose "Promote to...". In the dialog enter the information as seen on the screenshot, click "Add" and then "Promote".

After telling QtCreator to apply a grid layout to the widgets, the result looks something like this:




Now open mainwindow.cpp and add the following line to the constructor
ui->browserWidget->setControl("{8856F961-340A-11D0-A96B-00C04FD705A2}");
Hit Ctrl-R to check whether what you have so far compiles successfully. What's left for a working browser is some event handling. If a user enters a URL in the QLineEdit and hits enter, the browser should open that URL and display pictures of cute kittens. Maybe the forward and back buttons should work too. So what we need for that are some slots that we will connect to the signals emitted by editUrl, backButton and forwardButton. In mainwindow.h add one of those slots, so that the file looks like this

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QtGui/QMainWindow>

namespace Ui
{
    class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = 0);
    ~MainWindow();

public slots:
    void navigate();

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

We don't need slots for forward and back, because those buttons can be hooked up directly to the QAxWidget instance. Our mainwindow.cpp file contains the implementation of the "navigate" slot as well as some calls to connect() in the constructor.

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QAxWidget>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    ui->browserWidget->setControl("{8856F961-340A-11D0-A96B-00C04FD705A2}");

    connect(ui->urlEdit,
            SIGNAL(returnPressed()),
            this,
            SLOT(navigate()));

    connect(ui->backButton,
            SIGNAL(clicked()),
            ui->browserWidget,
            SLOT(GoBack()));

    connect(ui->forwardButton,
            SIGNAL(clicked()),
            ui->browserWidget,
            SLOT(GoForward()));
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::navigate()
{
    ui->browserWidget->dynamicCall("Navigate(const QString&)",
                                   ui->urlEdit->text());
}

That's all we need for embedding IE in our app. Run it, type in a URL and hit enter. We got signal!

Ok, fine, but originally the plan was to embed the Gecko rendering engine in our Qt application. As the title already gives away, there is in fact an ActiveX control that lets us do that. It was written by former Netscape employee Adam Lock with the goal to be API compatible with the IE control. That means we can use this control exactly like the one we are already using. Download the installer from Adams website and run it. Once that is done, we need to slightly change our GUI to accommodate a second browser control. I chose a tab widget for this task. Open mainwindow.ui, delete the browserWidget and drag a QTabWidget in its place. The tab widget already contains two tabs. In the object tree in the upper left, select the first tab. The type of the objects that are contained in the tabs is QWidget. We can promote them to QAxWidget just like we did with the original QWidget we used above. I called the first one ieTab and the second geckoTab. The code inside mainwindow.cpp has to be adapted as well. Change every occurrence of browserWidget to ieTab and add the same code for geckoTab. Afterwards, mainwindow.cpp should look something like this

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QAxWidget>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    ui->ieTab->setControl("{8856F961-340A-11D0-A96B-00C04FD705A2}");

    connect(ui->urlEdit,
            SIGNAL(returnPressed()),
            this,
            SLOT(navigate()));

    connect(ui->backButton,
            SIGNAL(clicked()),
            ui->ieTab,
            SLOT(GoBack()));

    connect(ui->forwardButton,
            SIGNAL(clicked()),
            ui->ieTab,
            SLOT(GoForward()));

    ui->geckoTab->setControl("{1339B54C-3453-11D2-93B9-000000000000}");

    connect(ui->backButton,
            SIGNAL(clicked()),
            ui->geckoTab,
            SLOT(GoBack()));

    connect(ui->forwardButton,
            SIGNAL(clicked()),
            ui->geckoTab,
            SLOT(GoForward()));
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::navigate()
{
    ui->ieTab->dynamicCall("Navigate(const QString&)",
                           ui->urlEdit->text());
    ui->geckoTab->dynamicCall("Navigate(const QString&)",
                              ui->urlEdit->text());
} 

If you run the app, you should now have both browsers embedded and displaying whatever URL was entered in the QLineEdit widget. We can check that we have in fact two different browsers by opening a website that displays the HTTP headers that are sent by IE and Gecko.

The headers sent by the embedded Internet Explorer control.


The headers sent by the embedded Mozilla Gecko control.

There's just one little issue. Notice in the second screenshot, the user agent contains the string "Gecko/20050410". That means our Mozilla ActiveX control uses a very old version of the Gecko rendering engine (You might see a slightly different string. I installed version 1.7.7 of the control for some reason, not 1.7.12. But that one is totally out of date as well). You can get a recent version of the control as part of the XULRunner SDK from the Mozilla FTP Server. Depending on how adventurous you feel, you can either choose the latest release or the nightly build from trunk. I get the impression that the Mozilla project does not really advertise the fact that there is a recent version of the ActiveX control inside the XULRunner SDK very much. I stumbled upon its existence via a comment by Mozilla Platform Evangelist Mark Finkle on this entry in Nick Bradburys blog. Grab the SDK, extract it somewhere, uninstall the old Mozilla control and register the new one in Windows. To do this, open a command line window, navigate to the directory where you put the SDK, change to the "bin" directory and enter "regsvr32 mozctlx.dll". To get an idea about how much better the recent Gecko engine is compared to the old one, we can look at the ACID browser test.



old and busted




latest'n'greatest

Pretty sweet, innit?