Help us understand the problem. What is going on with this article?

Qt4でプラグインをサポートするアプリケーションを作ってみる

More than 3 years have passed since last update.

この記事は、Qt Advent Calendar 2015 24日目の記事です。

野田と申します。社内システムのプログラマーや社内SEなどをしています。また、趣味でQtを使ったプログラムを書いています。ただし、完成までにはまだまだ時間というか年月(笑)がかかりそうです。

入門レベルではありますが、趣味のプログラムのためにQt4のアプリケーションプラグインの仕組みを勉強してきたので、今回は、その復習を兼ね、この記事の中で簡単なプラグインをサポートするQtアプリケーションを作ってみたいと思います。

アプリケーションの仕様

プラグインをサポートするアプリケーションを作ろうとすると、まず最初に次の2つの処理が必要になります:

  • プラグインファイル(ダイナミックリンク・ライブラリ)を読み込む処理
  • 読み込んだプラグインの機能を呼び出す処理

今回は、この2つの処理を、以下の仕様で実現してみたいと思います。

  1. あるメニューを選択すると、ファイルを選択するダイアログが出ます。
  2. そのダイアログで、読み込むプラグインファイル(ダイナミックリンク・ライブラリ)を選択すると、読み込みを行い、そのプラグインの機能を呼び出すメニューが追加されます。
  3. 追加されたメニューを選択すると、テキストを書き込むフィールドを持つ簡単なサブウィンドウが開きます。サブウィンドウは複数開けます。
  4. 「Window」配下に、サブウィンドウのタイトル名のメニューが表示され、これを選択すると対応するサブウィンドウが一番前に来ます。

開発手順

それでは、早速作ってみましょう。作り方は以下のような感じになります:

  • アプリケーションのプロジェクトを作り、その中に、インターフェイスクラスを追加する。
  • プラグイン(ダイナミックリンク・ライブラリ)のプロジェクトを作り、その中に、インターフェイスクラスを継承するラッパクラスを作りこむ。
  • アプリケーション側に、プラグインをロードする処理を追加する。
  • プラグイン側に、実処理を実行するクラスを追加する。
  • アプリケーション側に、実処理を呼び出す処理を追加する。

アプリケーションのプロジェクト作成

まずQt4のアプリケーションのプロジェクトを作ります。これは通常のアプリケーションと同様に作ります。今回はQt Creatorのウイザードで作りました。

fig1.png

fig2.png

インターフェイスクラスの開発

次に、アプリケーションのプロジェクトの中にヘッダファイルを追加します。ここでインターフェイスクラスの定義をします。

fig3.png

プラグインの要になるのが、このインターフェイスクラスです。アプリケーションとプラグインの間のやりとりをvirtual関数のメンバを持つC++クラスとして定義します。そして、プラグイン内には、これを継承するラッパクラスというクラスを定義します。今回は、interface.hというヘッダファイルを作りPluginInterfaceというインターフェイスクラスを定義しました。

interface.h
#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ファイルを書き換る方法がやりやすかったのでそうしました。

fig4.png

なお、種類は「スタティックリンクライブラリ」を選択します。「Qtプラグイン」は「アプリケーションプラグイン」ではないので、選択しません。

fig5.png

fig6.png

ライブラリのプロジェクトができました。アプリケーションプラグインとして開発するために、プロジェクトファイルを以下のように修正します:

plugin1.pro
TARGET = plugin1
TEMPLATE = lib
CONFIG += plugin
INCLUDEPATH += ../app1

SOURCES += plugin1.cpp

HEADERS += plugin1.h

この中で重要な点は2点です:

  • 「CONFIG」の値を「staticlib」から「plugin」に変更。このライブラリをプラグインにするためです。
  • 「INCLUDEPATH」の定義を追加。アプリケーションのプロジェクト内にある、interface.hをインクルードできるようにするためです。

次に、plugin1.h内のPlugin1をラッパクラスになるように作り替えます:

plugin1.h
#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についてもビルドが通るように枠組みだけ作っておきます。

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エリアを一つ貼り付けただけの簡単なデザインにしました:

fig19.png

また、次のように、「Load Plugin」メニューで呼び出されるアクションactionLoad_Pluginのスロットon_actionLoad_Plugin_triggered()を作成し、そこに処理を記述しました。

fig7.png

fig8.png

mainwindow.cpp
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の内容は次のようになります:

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が起動できたら、次の操作ができるはずです:
1. 「Load Plugin」を選択すると、ファイルを選択するダイアログが出ます。
2. そのダイアログで、プラグインの実行ファイル「libplugin1.dylib」を探して選択すると、読み込みを行い、メニュー「Call Text Edit」が追加されます。

fig9.png
fig10.png
fig11.png

プラグインのプロジェクト内に、実処理をさせるクラスを追加

ここまでの開発で、プラグインのロードまでは実装できましたが、ラッパクラスのインスタンスはロードして作るので1つしか作れないという点に注意してください。さらに、ラッパクラスにはスロットが使えないなどの制約もあります。

複数のインスタンスを作りたい場合、プラグイン内に、ラッパクラスとは別のクラスを用意するとよいでしょう。今回は、次のように、プラグインのプロジェクトにQt DesignerフォームクラスFormを追加しました。

fig12.png
fig13.png

フォームのデザインは、単純にウィジェットの上にTextEditを一つ乗せただけにしました。

fig14.png

この記事の趣旨により、単純にTextEditを表示させるだけの実仕様にしているので、form.h、form.cppはなにもいじっていません。実際に自分のプラグインを開発される人は、ここを作り込んだり、さらに別のクラスを追加したりしていくことになるでしょう。

それはさておき、ラッパクラスPlugin1のnewWidget()にもFormを呼び出す処理を追加します。Plugin1の実装部は以下のようになりました。

plugin1.cpp
#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()関数を呼び出す処理を追加します。

mainwindow.cpp
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は以下のようになりました:

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

動作確認:読み込んだプラグインの機能を呼び出す処理

機能を呼び出す処理について、簡単に動かしてみましょう。

  1. 追加されたメニュー「Call Text Edit」を選択すると、テキストを書き込むフィールドを持つ簡単なサブウィンドウが開きます。サブウィンドウは複数個開けます。
  2. 「Window」配下に、サブウィンドウのタイトルが表示され、これを選択すると対応するサブウィンドウが一番前に来ます。

fig15.png

fig16.png

fig17.png

fig18.png

自分の環境では、最初に任意のサブウィンドウのフィールドにカーソルを置かないとうまく切り替わらないという不具合があります(一度カーソルを置くと切り替わるようになります)。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, 株式会社オライリー・ジャパン.

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away