2015-12-23

WindowsでQtでActiveXコントロール

QtでOCXを作ります。

OCXというのはOLE Control Extensionの略で、OLEというのはObject Linking and Embeddingの略です。略語を略すなと突っ込みたくなりますが、それは置いておいて、要するにオブジェクトなのです。それが今では、ActiveXと呼ばれるようになりました。ActiveXというのは、マイクロソフトのインターネット関連技術を総称するものらしく、一種のブランドまたはバズワードのようなものですが、今ではほとんどの場合OCXと同義と考えて差し支えないでしょう。正確な定義についてはWikipediaなどでお調べください。で、それは何かというと、「オブジェクトを貼り付ける」という技術を意味します。

ありがちなケースとしては、VB(Visual Basic)で開発しているアプリで、VBだけでは実現できない機能を搭載するため、その部分だけをC++で開発してモジュール化し、VBのフォームに貼り付けるという用途が多いと思います。あと、今ではほとんど廃れてしまいましたが、ActiveXコントロールをHTMLのページ上に貼り付けることもできました。

Qt開発者ならウィジェットと言った方が通りがいいかもしれませんが、コントロールという用語も同じような意味で用いられます。簡単に言えばGUI部品です。たとえばウィンドウにボタンが貼ってあったとしましょう。この場合ボタンはコントロールです。チェックボックスやラジオボタン、スライダー、その他、ウィンドウ上に貼り付けられる部品を総称して、コントロールと言います。

ActiveXコントロールというのは、そのようなGUI部品を、共有ライブラリ(DLL)化して、別モジュールにしましょう、というものです。うまく設計すれば、複数の異なるアプリで、共通のActiveXコントロールを使い回すことができます。共有ライブラリの常として、そう思うようにいかないものですが、いちおう、モジュールを再利用できますよ、ということになっています。

ここで告白しますと、筆者は、開発経験30年くらいですが、ActiveXと、その元となっている、COM、OLEといった技術をこれまで避けてきました。複雑で面倒くさくて難しいという強い思い込みがありました。しかし、業務命令でActiveXコントロールの開発をする必要に迫られ、調査することになったのです。

とにかく作るよ

今までの常識では、ActiveXコントロールはMicrosoft Visual Studioで作るものと決まっていました。プロジェクトの新規作成で、「MFC ActiveX コントロール」を選ぶと、雛形を作ってくれます。コンパイルしたモジュールには、.ocxという拡張子が付けられるのが一般的ですが、通常はOSに登録して使用するものなので、拡張子が何であるべきかは厳密には決まっておらず、慣習として .ocx または .dll とします。

ここではもちろんQtがテーマですので、Qt Creatorを使ってActiveXコントロールを作ることになるのですが、Qtで一発でActiveXコントロールを作ってくれる雛形はありません。でも意外と簡単にできます。ことによると、Visual Studioで作るより簡単です。

まず普通のアプリを作ります

Qt Creatorを起動します。ごく普通の「Qt ウィジェットアプリケーション」を作成します。

プロジェクト名とパスは何でも構いませんが、とりあえずHogeControlとしました。

以下のようなアプリができました。いつも見慣れた光景です。

[新しいファイルを追加]で、C++ Classを選択します。

クラス名は何でも構いませんが、ここではHogeControlWidgetとしました。基底クラスはQWidgetを選択します。

新しく作ったクラスに、paintEventを実装します。

HogeControlWidget.h

#ifndef HOGECONTROLWIDGET_H
#define HOGECONTROLWIDGET_H

#include <QWidget>

class HogeControlWidget : public QWidget
{
    Q_OBJECT
public:
    explicit HogeControlWidget(QWidget *parent = 0);
    virtual void paintEvent(QPaintEvent *);
signals:
public slots:
};

#endif // HOGECONTROLWIDGET_H

HogeControlWidget.cpp

#include "HogeControlWidget.h" 
#include <QPainter>

HogeControlWidget::HogeControlWidget(QWidget *parent) : QWidget(parent)
{
}

void HogeControlWidget::paintEvent(QPaintEvent *)
{
    QPainter pr(this);
    int w = width() - 1;
    int h = height() - 1;
    pr.drawEllipse(0, 0, w, h);
}

フォームを開いてWidgetを配置します。

配置したWidgetを右クリックして、[格上げ先を指定...]を選択します。

[格上げされたクラス名]の欄に、先ほど作成したクラス名を入力し、[追加]ボタンを押します。

追加されたら、[格上げ]ボタンを押します。

[ビルド(B)]メニューの[qmake の実行]を行ってから、ビルドして実行します。

ここまではActiveXとは何の関係もない、普通のQtアプリの作り方でした。

普通のアプリをActiveXコントロールに作り変えます

GUID(UUID)を生成するツールを準備しておきます。Visual Studioのユーザーなら、C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\guidgen.exe あたりにあります。もちろんVisual Studioのメニューから[GUIDの作成]で起動しても結構です。GUID形式はレジストリ形式({xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx})を選択します。

HogeControl.def

モジュール定義ファイルです。これはテキストエディタなどで新規作成し、プロジェクトファイルと同じフォルダに置きます。

EXPORTS
DllCanUnloadNow PRIVATE
DllGetClassObject PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
DumpIDL PRIVATE

ファイルの内容は、上記の通り、そのままコピペして結構です。

HogeControl.pro

プロジェクトファイルを変更します。

(変更前)

#------------------------------------------------- 
#
# Project created by QtCreator 2015-12-11T22:07:54
#
#-------------------------------------------------

QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = HogeControl TEMPLATE = app SOURCES += main.cpp\ MainWindow.cpp \ HogeControlWidget.cpp HEADERS += MainWindow.h \ HogeControlWidget.h FORMS += MainWindow.ui

(変更後)

#------------------------------------------------- 
#
# Project created by QtCreator 2015-12-11T22:07:54
#
#-------------------------------------------------

QT       += core gui widgets axserver

TARGET = HogeControl
TEMPLATE = lib
CONFIG += dll

DEF_FILE = HogeControl.def

SOURCES += main.cpp\
        MainWindow.cpp \
    HogeControlWidget.cpp

HEADERS  += MainWindow.h \
    HogeControlWidget.h

FORMS    += MainWindow.ui

QTの定義にaxserverを追加します。DEF_FILEを追加します。あとTEMPLATEとCONFIGを変更します。

この状態でqmakeを実行すると、.idlファイルが見つからないというエラーが出るかもしれませんが、とりあえず無視します。

main.cpp

(変更前)

#include "MainWindow.h" 
#include <QApplication>
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

(変更後)

#include "MainWindow.h" 
#include <QAxFactory>

QAXFACTORY_BEGIN("{C2CA1E4C-EA0C-459B-AB7B-28B468CBC0B4}", "{95341822-BC5E-4656-BBF4-0326B9FC6917}")
    QAXCLASS(MainWindow)
QAXFACTORY_END()

ごっそり書き換えます。GUIDは各自生成したものをペーストします。クラス名もMainWindowというのはいまいちなので、変更したいところですが、後回しにします。

MainWindow.h

(変更前)

#ifndef MAINWINDOW_H  
#define MAINWINDOW_H

#include <QMainWindow>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

(変更後)

#ifndef MAINWINDOW_H  
#define MAINWINDOW_H

#include <QMainWindow>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT
    Q_CLASSINFO("ClassID", "{207B8E0C-9CB2-43C6-B396-E743B196AB59}");
    Q_CLASSINFO("InterfaceID", "{55ED0286-801D-4BFB-82E1-F287F4B7D9C8}");
    Q_CLASSINFO("EventsID", "{2E934864-071A-472B-9BA8-E81DF9BC80C8}");
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

Q_CLASSINFOというのが3行追加されました。各GUIDはそれぞれ異なるものを生成してペーストします。

ビルド

以上でコンパイルが通れば、峠は越えました。

ビルドフォルダに、HogeControl.dllというファイルができているか確認します。

Dependency WalkerにこのDLLを読ませてみます。

defファイルに定義されている5個の関数がエクスポートされていれば成功です。

登録

Qt SDKのbinフォルダから、下記3つのファイルを、ビルドフォルダにコピーします。

ビルドしてできたHogeControl.dllも含めて、4個のDLLがあります。

このフォルダでコマンドプロンプトを開き、下記コマンドを実行します。

regsvr32 HogeControl.dll

登録完了です。

解除するには、下記のように実行します。

regsvr32 /u HogeControl.dll

使ってみよう

とりあえず、Visual Studioで新規MFCアプリケーションを作成し、ダイアログボックスにコントロールを貼るための、ツールボックスにアイテムを追加する画面では下のように表示されます。

「MainWindow Class」という名前が非常にダサいので、次はこれを直しましょう。でも、HogeControlがめでたく登録されていることは確認できました。これを、Visual Studioのダイアログエディタに貼り付けてみた様子が下図です。

楕円はいいとして、ツールバーの残骸らしきものが見えます。次回はこれを整理します。

修正します

これまで作成してきたActiveXコントロールは、普通のQtウィジェットアプリケーションをベースに開発してきましたので、コントロール名が「MainWindow Class」 となっていました。さすがにそれはないだろう、ということで、 フォームを作り直します。新しいファイルの追加で、「Qt Designer フォームクラス」を選択します。

フォームテンプレートは、「Main Window」を選択します。

クラス名はお好みですが、ここではHogeControlとします。

メニューバーとステータスバーはいらないので削除します。特殊なコントロールを作成する場合で、メニューバーやステータスバーが欲しいならこの限りではありません。削除したとしても、必要になったら、後から追加すれば良いです。今回の例では、バーの類は全部削除して、代わりに、ラインエディットとボタンとラベルをを配置します。

HogeControl.h

先に作ってあるMainWindow.hから、今作ったHogeControl.hへ、Q_CLASSINFOの定義を移植します。

#ifndef HOGECONTROL_H  
#define HOGECONTROL_H

#include <QMainWindow>

namespace Ui {
class HogeControl;
}

class HogeControl : public QMainWindow
{
    Q_OBJECT
    Q_CLASSINFO("ClassID", "{207B8E0C-9CB2-43C6-B396-E743B196AB59}");
    Q_CLASSINFO("InterfaceID", "{55ED0286-801D-4BFB-82E1-F287F4B7D9C8}");
    Q_CLASSINFO("EventsID", "{2E934864-071A-472B-9BA8-E81DF9BC80C8}");
public:
    explicit HogeControl(QWidget *parent = 0);
    ~HogeControl();
private:
    Ui::HogeControl *ui;
};

#endif // HOGECONTROL_H

main.cpp

クラス名を変更します。

(変更前)
#include "MainWindow.h"
    QAXCLASS(MainWindow)

(変更後)

#include "HogeControl.h"
    QAXCLASS(HogeControl)

MainWindow.h、MainWindow.cpp、MainWindow.ui は削除します。

メソッドとイベントを実装します

外部アプリに提供するメソッドは、public slotsで実装します。コントロールからアプリに通知するためのイベントは、signalsで実装します。定義は以下のようになります。必須ではありませんが、関数名はWindowsの流儀に合わせてUpperCamelCaseな名前にします。

public slots:
    QString UpdateText(QString const &text);
signals:
    void TextChanged(QString const &text);  

上の例では、親アプリからUpdateTextメソッドを呼び出せます。文字列を引数にとり、文字列を結果として返します。コントロール側で、何かイベントが発生したとき、TextChangedで親アプリに通知できます。

メソッドが呼ばれたときの処理を実装します。

QString HogeControl::UpdateText(QString const &text)
{
    ui->label->setText(text);
    return "It's OK";
}

ボタンを押したときのイベントハンドラを実装します。

void HogeControl::on_pushButton_clicked()
{
    emit TextChanged(ui->lineEdit->text());
}

ここで一度ビルドを行い、HogeControl.idlというファイルが更新されたか確認します。ファイルの下の方に次のような行が見つかると思います。

    [id(64)] BSTR UpdateText([in] BSTR p_text);
    [id(10)] void TextChanged([in] BSTR p_text);

再登録

一度、再登録します。

regsvr32 /u HogeControl.dll 
regsvr32 HogeControl.dll 

HogeControlの基本的な実装は完了しました。次は、このActiveXコントロールを貼り付けるメインアプリを作ります。

親アプリを作るよ

Qt ウィジェットアプリケーションの新規作成を実行します。名前は何でも構いませんが、例としてMyAppとします。

MyApp.pro

プロジェクトファイルのQTの定義にaxcontainerを追加します。

#------------------------------------------------- 
#
# Project created by QtCreator 2015-12-13T12:44:53
#
#-------------------------------------------------

QT       += core gui widgets axcontainer

TARGET = MyApp
TEMPLATE = app

SOURCES += main.cpp\
        MainWindow.cpp

HEADERS  += MainWindow.h
FORMS    += MainWindow.ui

MainWindow.ui

フォームにQAxWidgetとラインエディットとボタンを配置します。

QAxWidgetを右クリックして、[コントロールを設定]を選択します。

つづいて、HogeControl Classを選択します。

OKを押すと、「コントロールがロードされました」と表示されます。

[qmake の実行]を行い、ビルドして実行します。コントロールが表示されることを確認します。ボタンのイベントハンドラはまだ実装していませんので、この時点では、ボタンを押しても何も起きません。

MyAppを閉じたら、Windowsタスクマネージャーを開きます。プロセスを探すと、MyApp.exeが残ったままになっていると思います。とりあえず強制的にプロセスを終了して、その対策のためのコードを追加します。

main.cpp

#include "MainWindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    qApp->setQuitOnLastWindowClosed(true); // 全てのウィンドウが閉じたらアプリケーションを終了する
    return a.exec();
}

QtアプリでActiveXコントロール(QAxWidget)を使用した場合、メインウィンドウを閉じただけではプロセスが終了しないように設計されています。そのため、全てのウィンドウを閉じたときにプロセスを終了することを明示的に記述する必要があります。

親アプリからコントロールのメソッドを呼び出してみましょう。ボタンをクリックしたときのハンドラを実装します。

void MainWindow::on_pushButton_clicked()
{
    ui->axWidget->dynamicCall("UpdateText(QString &)", ui->lineEdit->text());
}

次に、コントロールが発行したイベントの応答を実装します。
コントロールを右クリックして、[スロットへ移動...]を選択します。TextChanged(QString)を選択します。

Qtではコントロール側のsignalsで関数を宣言するだけで、アプリから利用できるようになります。

#include <QMessageBox>
void MainWindow::on_axWidget_TextChanged(const QString &p_text)
{
    QMessageBox::information(this, qApp->applicationName(), p_text);
}

以上で、メソッドの呼び出しとイベントの応答をの実装が完了しました。ビルドして実行してみます。

コントロール側のエディットにテキストを入力してボタンを押すと、アプリ側でメッセージボックスを表示します。アプリ側でエディットにテキストを入力してボタンを押すと、コントロールのラベルが更新されます。


WindowsでQtでActiveXコントロール

とにかく作るよ編」は Qt Advent Calendar 2015 - Qiita の11日目の記事です。

使ってみよう編」は Qt Advent Calendar 2015 - Qiita の18日目の記事です。