この記事は、Qt Advent Calendar 2015 24日目の記事です。
野田と申します。社内システムのプログラマーや社内SEなどをしています。また、趣味でQtを使ったプログラムを書いています。ただし、完成までにはまだまだ時間というか年月(笑)がかかりそうです。
入門レベルではありますが、趣味のプログラムのためにQt4のアプリケーションプラグインの仕組みを勉強してきたので、今回は、その復習を兼ね、この記事の中で簡単なプラグインをサポートするQtアプリケーションを作ってみたいと思います。
#アプリケーションの仕様
プラグインをサポートするアプリケーションを作ろうとすると、まず最初に次の2つの処理が必要になります:
- プラグインファイル(ダイナミックリンク・ライブラリ)を読み込む処理
- 読み込んだプラグインの機能を呼び出す処理
今回は、この2つの処理を、以下の仕様で実現してみたいと思います。
- あるメニューを選択すると、ファイルを選択するダイアログが出ます。
- そのダイアログで、読み込むプラグインファイル(ダイナミックリンク・ライブラリ)を選択すると、読み込みを行い、そのプラグインの機能を呼び出すメニューが追加されます。
- 追加されたメニューを選択すると、テキストを書き込むフィールドを持つ簡単なサブウィンドウが開きます。サブウィンドウは複数開けます。
- 「Window」配下に、サブウィンドウのタイトル名のメニューが表示され、これを選択すると対応するサブウィンドウが一番前に来ます。
#開発手順
それでは、早速作ってみましょう。作り方は以下のような感じになります:
- アプリケーションのプロジェクトを作り、その中に、インターフェイスクラスを追加する。
- プラグイン(ダイナミックリンク・ライブラリ)のプロジェクトを作り、その中に、インターフェイスクラスを継承するラッパクラスを作りこむ。
- アプリケーション側に、プラグインをロードする処理を追加する。
- プラグイン側に、実処理を実行するクラスを追加する。
- アプリケーション側に、実処理を呼び出す処理を追加する。
##アプリケーションのプロジェクト作成
まずQt4のアプリケーションのプロジェクトを作ります。これは通常のアプリケーションと同様に作ります。今回はQt Creatorのウイザードで作りました。
##インターフェイスクラスの開発
次に、アプリケーションのプロジェクトの中にヘッダファイルを追加します。ここでインターフェイスクラスの定義をします。
プラグインの要になるのが、このインターフェイスクラスです。アプリケーションとプラグインの間のやりとりをvirtual関数のメンバを持つC++クラスとして定義します。そして、プラグイン内には、これを継承するラッパクラスというクラスを定義します。今回は、interface.hというヘッダファイルを作りPluginInterfaceというインターフェイスクラスを定義しました。
#ifndef INTERFACE_H
#define INTERFACE_H
#include <QtPlugin>
#include <QString>
#include <QObject>
class PluginInterface
{
public:
virtual ~PluginInterface() {}
virtual QString title() const = 0;
virtual void setQAction(QObject *argQAction) = 0;
virtual QObject* getQAction() = 0;
virtual QWidget* newWidget() = 0;
};
Q_DECLARE_INTERFACE(PluginInterface, "jp.QtAdventCalender2015.Plugin.PluginInterface/1.0")
#endif // INTERFACE_H
##プラグイン、ラッパクラスの作成、開発
次に、プラグインのプロジェクトを作ります。私の場合は、いったんQt Createrでライブラリのプロジェクトを作り、proファイルを書き換る方法がやりやすかったのでそうしました。
なお、種類は「スタティックリンクライブラリ」を選択します。「Qtプラグイン」は「アプリケーションプラグイン」ではないので、選択しません。
ライブラリのプロジェクトができました。アプリケーションプラグインとして開発するために、プロジェクトファイルを以下のように修正します:
TARGET = plugin1
TEMPLATE = lib
CONFIG += plugin
INCLUDEPATH += ../app1
SOURCES += plugin1.cpp
HEADERS += plugin1.h
この中で重要な点は2点です:
- 「CONFIG」の値を「staticlib」から「plugin」に変更。このライブラリをプラグインにするためです。
- 「INCLUDEPATH」の定義を追加。アプリケーションのプロジェクト内にある、interface.hをインクルードできるようにするためです。
次に、plugin1.h内のPlugin1をラッパクラスになるように作り替えます:
#ifndef PLUGIN1_H
#define PLUGIN1_H
#include "interface.h"
class Plugin1 : public QObject, public PluginInterface
{
Q_OBJECT
Q_INTERFACES(PluginInterface)
public:
Plugin1();
~Plugin1();
QString title() const {return QString("Text Edit");}
void setQAction(QObject *argQAction) {mQAction = argQAction;}
QObject* getQAction() {return mQAction;}
QWidget* newWidget();
private:
QObject *mQAction;
};
#endif // PLUGIN1_H
この中で重要な点は3点です:
- interface.hをインクルードすること
- QObject、PluginInterfaceを継承すること
- 「Q_OBJECT」「Q_INTERFACES(PluginInterface)」の2つのマクロを記載すること
Plugin1の実装部plugin1.cppについてもビルドが通るように枠組みだけ作っておきます。
#include "plugin1.h"
Plugin1::Plugin1()
{
}
Plugin1::~Plugin1()
{
}
QWidget* Plugin1::newWidget()
{
}
Q_EXPORT_PLUGIN2(plugin1, Plugin1)
この中で重要な点は1点です:
- 「Q_EXPORT_PLUGIN2(plugin1, Plugin1)」の第1引数にproファイルのTARGETの値、第2引数にクラス名を書くこと
なお、経験上、ラッパクラスは、なかなか一回でビルドが通らず、ビルドを通すまで何度も試行錯誤することが多いです。ですが、たとえば、
- マクロの記述が抜けていたり間違っていたりしないか見直す
- QObjectやインターフェイスクラスの継承を忘れていないか見直す
- プロジェクトの設定が間違っていないか見直す
- qmakeをかけ直す
など、いろいろがんばっていると通るようになります。
##プラグインファイル(ダイナミックリンク・ライブラリ)を読み込む処理の開発
今回は、メニューからプラグインを読み込む処理を呼び出せるようにします。まずは、アプリケーションの画面をQt Designerで次のようにデザインします。今回は、以下のように、メインウィンドウにMDIエリアを一つ貼り付けただけの簡単なデザインにしました:
また、次のように、「Load Plugin」メニューで呼び出されるアクションactionLoad_Pluginのスロットon_actionLoad_Plugin_triggered()を作成し、そこに処理を記述しました。
void MainWindow::on_actionLoad_Plugin_triggered()
{
QString pathname = QFileDialog::getOpenFileName(this, "Open Plugin File");
if (!pathname.isEmpty()) {
QPluginLoader loader(pathname);
if (PluginInterface *plugin = qobject_cast<PluginInterface*>(loader.instance())) {
QAction *action = new QAction("Call " + plugin->title(), this);
connect(action, SIGNAL(triggered()), this, SLOT(callPlugin()));
ui->menuFile->addAction(action);
plugin->setQAction(action);
plugins.append(plugin);
}
}
}
この処理の中では、QPluginLoaderという機能を使ってラッパクラスをロードします。そうすると、ラッパクラスに定義したメソッドを呼び出せるようになります。
この処理を動かすため、mainwindow.hの中で、QList、QWidget、QFileDialog、QPluginLoader、そして、インターフェイスクラスinterface.hをインクルードするようにしました。また、複数の種類のプラグインをロードできるように、ロードしたプラグインへのポインタをリストpluginsに保管できるようにしておくことにしました。mainwindow.hの内容は次のようになります:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QList>
#include <QWidget>
#include <QFileDialog>
#include <QPluginLoader>
#include "interface.h"
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void on_actionLoad_Plugin_triggered();
private:
Ui::MainWindow *ui;
QList<PluginInterface*> plugins;
};
#endif // MAINWINDOW_H
##動作確認:プラグインファイルを読み込む処理
これで、プラグインを読み込む処理ができたので、簡単に動かしてみましょう。
アプリケーションapp1とプラグインplugin1がビルドできて、app1が起動できたら、次の操作ができるはずです:
- 「Load Plugin」を選択すると、ファイルを選択するダイアログが出ます。
- そのダイアログで、プラグインの実行ファイル「libplugin1.dylib」を探して選択すると、読み込みを行い、メニュー「Call Text Edit」が追加されます。
##プラグインのプロジェクト内に、実処理をさせるクラスを追加
ここまでの開発で、プラグインのロードまでは実装できましたが、ラッパクラスのインスタンスはロードして作るので1つしか作れないという点に注意してください。さらに、ラッパクラスにはスロットが使えないなどの制約もあります。
複数のインスタンスを作りたい場合、プラグイン内に、ラッパクラスとは別のクラスを用意するとよいでしょう。今回は、次のように、プラグインのプロジェクトにQt DesignerフォームクラスFormを追加しました。
フォームのデザインは、単純にウィジェットの上にTextEditを一つ乗せただけにしました。
この記事の趣旨により、単純にTextEditを表示させるだけの実仕様にしているので、form.h、form.cppはなにもいじっていません。実際に自分のプラグインを開発される人は、ここを作り込んだり、さらに別のクラスを追加したりしていくことになるでしょう。
それはさておき、ラッパクラスPlugin1のnewWidget()にもFormを呼び出す処理を追加します。Plugin1の実装部は以下のようになりました。
#include "form.h"
#include "plugin1.h"
Plugin1::Plugin1()
{
}
Plugin1::~Plugin1()
{
}
QWidget* Plugin1::newWidget()
{
Form *widget = new Form();
return widget;
}
Q_EXPORT_PLUGIN2(plugin1, Plugin1)
##読み込んだプラグインの機能(実処理)を呼び出す処理の開発
スロットcallPlugin()を追加して、プラグインのインターフェイスクラスで定義しているnewWidget()関数を呼び出す処理を追加します。
void MainWindow::callPlugin()
{
QObject* pSender = QObject::sender();
PluginInterface *plugin;
QWidget *widget;
for (int i = 0; i < plugins.size(); ++i) {
plugin = plugins.at(i);
if (plugin->getQAction() == pSender) {
widget = plugin->newWidget();
if (widget) {
QString title = QString("Form - " + QString("%1").arg(ui->mdiArea->subWindowList().count() + 1));
QAction *action = new QAction(title, this);
ui->menuWindow->addAction(action);
QMdiSubWindow *window = ui->mdiArea->addSubWindow(widget);
window->setWindowTitle(title);
connect(action, SIGNAL(triggered()), widget, SLOT(setFocus()));
widget->show();
widget->setFocus();
}
}
}
}
また、mainwindow.hにcallPlugin()の定義とQMdiSubWindowのインクルードも追加しました。最終的に、mainwindow.hは以下のようになりました:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QList>
#include <QWidget>
#include <QFileDialog>
#include <QMdiSubWindow>
#include <QPluginLoader>
#include "interface.h"
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void on_actionLoad_Plugin_triggered();
void callPlugin();
private:
Ui::MainWindow *ui;
QList<PluginInterface*> plugins;
};
#endif // MAINWINDOW_H
##動作確認:読み込んだプラグインの機能を呼び出す処理
機能を呼び出す処理について、簡単に動かしてみましょう。
- 追加されたメニュー「Call Text Edit」を選択すると、テキストを書き込むフィールドを持つ簡単なサブウィンドウが開きます。サブウィンドウは複数個開けます。
- 「Window」配下に、サブウィンドウのタイトルが表示され、これを選択すると対応するサブウィンドウが一番前に来ます。
自分の環境では、最初に任意のサブウィンドウのフィールドにカーソルを置かないとうまく切り替わらないという不具合があります(一度カーソルを置くと切り替わるようになります)。Formクラスをちゃんと作り込めば修正できるようですが、今回の趣旨からは外れますので割愛します。
#最後に
作成したソースコードはここにBSDライセンスでアップしました。Mac OS X 10.9.5 / Qt 4.8.6 / Qt Creator 3.3.0で開発、ビルド、動作確認しています。ただし、今回の趣旨から、たとえばサブウィンドウのクローズ時処理のような重要な部分をいくつも割愛していますので、十分にご注意ください。サンプルは参考程度にして流用せずにフルスクラッチで作り直されることを強くお勧めします。
今回、時間が無くてQt5での実装はできませんでした。申し訳ありません。機会があればQt5での実装についても挑戦してみたいと思います。
#参考文献
Jasmin Blancbette, Mark Summerfield (2007) 『入門Qt4プログラミング』杵渕聡,杉田研治訳, p.455-463, 株式会社オライリー・ジャパン.