ブログトップ 記事一覧 ログイン 無料ブログ開設

凹みTips RSSフィード Twitter

May 05, 2013

QtQuick で簡単なファイルブラウザを作る

| 22:27 |  QtQuick で簡単なファイルブラウザを作るを含むブックマーク  QtQuick で簡単なファイルブラウザを作るのブックマークコメント

はじめに

ファイルブラウザ欲しいな、と思ったところ FolderListModel という要素を見つけました。

これをリストビューで表示してちょっといじって移動できる簡単なファイルブラウザを作成したというお話です。

環境

基礎

QML ではディレクトリインポートすることで、そのディレクトリに含まれる QML のファイル名を要素として使えるようになります。例えば以下の様な Hoge.qml を作成します。

import QtQuick 2.0

Rectangle {
	property alias textColor: text.color
	Text {
		id: text
		anchors.centerIn: parent
		text: 'HOGE'
	}
}

これを同一ディレクトリ内の Fuga.qml から以下のように使います。

import QtQuick 2.0
import './'

Hoge {
	width: 360
	height: 360
	color: '#000'
	textColor: '#fff'
}

Fuga.qml を実行すると以下のようになります。

f:id:hecomi:20130505220627p:image

ルート要素のプロパティはそのまま書き換えることができ、子要素に関しても alias を設定してあげれば書き換えられるようになります。初期値を与えたい時は以下のようにすると良いと思います。

import QtQuick 2.0

Rectangle {
	property color textColor: '#fff'
	Text {
		id: text
		anchors.centerIn: parent
		color: textColor
		text: 'HOGE'
	}
}

これを利用して import できる FileListView.qml を作ってみました。

ソース

import QtQuick 2.0
import Qt.labs.folderlistmodel 1.0

ListView {
    id: list
    model: folderModel
    delegate: fileDelegate
    highlight: highlight
    clip: true

    property color color: "#333333"
    property color highlightColor: "#44ffaa"
    property variant filters: []
    property string folder: "/Users"
    property int itemHeight: 24
    property string selectedItem: ""

    FolderListModel {
        id: folderModel
        nameFilters: list.filters
        folder: list.folder
        showDirsFirst: true
        showDotAndDotDot: true
    }

    Component {
        id: fileDelegate
        Item {
            width: parent.width
            height: list.itemHeight
            Text {
                id: item
                anchors.verticalCenter: parent.verticalCenter
                text: fileName
                color: list.color
            }
            MouseArea {
                anchors.fill: parent
                onClicked: {
                    list.currentIndex = index;
                }
                onDoubleClicked: {
                    list.currentIndex = index;
                    var selectedFolder = currentItem.children[0].text;
                    if (list.folder === '/') {
                        list.folder += selectedFolder;
                    }
                    else if (selectedFolder === '..') {
                        list.folder = list.folder.replace(/\/[^\/]*$/, '');
                    }
                    else {
                        list.folder += '/' + selectedFolder;
                    }
                }
            }
        }
    }

    Component {
        id: highlight
        Rectangle {
            id: highlightBar
            width: list.width
            height: list.itemHeight
            color: highlightColor
            y: list.currentItem.y
            Behavior on y {
                PropertyAnimation {
                    duration: 100
                }
            }
        }
    }
}

FolderListModel を ListView のモデルにして、後は選択した要素をハイライトするデリゲータを書いています。これを利用するコードは以下のようになります。

import QtQuick 2.0
import "./"

Rectangle {
    width: 360
    height: 360
    color: '#000'
    Text {
        id: currentFolder
        width: parent.width
        height: 30
        verticalAlignment: Text.AlignVCenter
        text: fileList.folder
        color: '#fff'
        anchors.left: parent.left
        anchors.top: parent.top
    }
    FileListView {
        id: fileList
        width: parent.width
        color: '#aaa'
        highlightColor: '#555'
        filters: ['*.jpg', '*.JPG']
        anchors.top: currentFolder.bottom;
        anchors.bottom: parent.bottom
    }
}

実行すると以下のようになります。

f:id:hecomi:20130505222433p:image

f:id:hecomi:20130505222432p:image

おわりに

もっと混みいったことをしようとすると FileListModel の機能は物足りないので、C++ 側と連携する必要がありそうです。

May 03, 2013

QtQuick での C++ × QML バインディングについてまとめてみた

| 00:23 |  QtQuick での C++ × QML バインディングについてまとめてみたを含むブックマーク  QtQuick での C++ × QML バインディングについてまとめてみたのブックマークコメント

はじめに

でもチラッと書きましたが、もう少し詳細に QtQuick での C++ バインディングについて調べてみたのでまとめてみました。

Qt 始めたてなので幾つか間違い含んでいるかもしれません、見つけたら教えて下さい。

参考

基本的には「Using QML Bindings in C++ Applications」の内容を要約して自分の解釈を付け加えた形になっています。

(メモ)QtQuick 1 と QtQuick 2 の差異について

Qt 5 が昨年末に登場し、その目玉の一つに QtQuick 2 が挙げられます。QtQuick 2 では JS エンジンを従来の JavaScriptCore に代わり V8 を採用するなど大きな変化が起こりました。このようにいくつかの変更が加わったため、ネットに落ちている幾つかの情報も古いものになっています。例えば QDeclarative* 系のクラスが Qml* 系のクラスへと変化したりしたようです。ただ、C++ バインディングに限った話をすれば、機能を利用するユーザ側で大きな変更は必要無いようです。

QML と C++ バインディングへのアプローチ

いくつかの方法があります。

  • QML 側で定義した要素を C++ から読み込んで使う
  • C++ 側で定義したクラスを QML から扱う
  • QML 側で定義した関数C++ から呼ぶ
  • C++ 側で QML で使える新しい型を定義する

これらを紹介します。

QML 側で定義した要素を C++ から読み込んで使う

QtQuick 2 アプリケーションプロジェクトを Qt Creator で作成すると以下の様な QML が自動生成されます。

import QtQuick 2.0

Rectangle {
    width: 360
    height: 360
    Text {
        text: qsTr("Hello World")
        anchors.centerIn: parent
    }
    MouseArea {
        anchors.fill: parent
        onClicked: {
            Qt.quit();
        }
    }
}

これを C++ からアクセスして Hello World文字列を書き換えてみたいと思います。以下のように、デフォルトの main.cpp からコメントのある 3 行を付け加えます。

#include <QtGui/QGuiApplication>
#include <QQuickItem>
#include "qtquick2applicationviewer.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QtQuick2ApplicationViewer viewer;
    viewer.setMainQmlFile(QStringLiteral("qml/CppBindTest/main.qml"));
    viewer.showExpanded();

    // ルート要素 (Rectangle)
    QObject* root = viewer.rootObject();
    // Text 要素へアクセス
    QObject* title = root->children().at(0);
    // プロパティの書き換え
    title->setProperty("text", "Hello, C++ world!");

    return app.exec();
}

他にも要素へのアクセス方法は色々あり、QML 側で objectName を定義しておけば、QObject::findChild でアクセスできます。詳細については Qt Creator で QObject にカーソルを合わせた状態で F1 キーを押下するとドキュメントが表示されるので、そこを参照してみて下さい。

f:id:hecomi:20130503210544p:image

C++ 側で定義したクラスを QML から扱う

先ほど Hello C++ World! とした場所に C++ から与えた現在時刻を表示させてみます。

まずは動かしてみる

Qt Creator の「ファイル/プロジェクトの新規作成」から 「C++ > C++ ヘッダー」を選び、「applicationdata.h」を作成して下さい。そしてヘッダファイル内を以下のようにします。

#ifndef APPLICATIONDATA_H
#define APPLICATIONDATA_H

#include <QObject>
#include <QDateTime>

class ApplicationData : public QObject
{
    Q_OBJECT
public:
    Q_INVOKABLE QDateTime getCurrentDateTime() const {
        return QDateTime::currentDateTime();
    }
};

#endif // APPLICATIONDATA_H

で「main.cpp」側を以下のようにします。

#include <QtGui/QGuiApplication>
#include "qtquick2applicationviewer.h"
#include "applicationdata.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QtQuick2ApplicationViewer viewer;
    viewer.setMainQmlFile(QStringLiteral("qml/CppBindTest/main.qml"));
    viewer.showExpanded();

    // QML 側へクラスをセット
    ApplicationData data;
    viewer.rootContext()->setContextProperty("applicationData", &data);

    return app.exec();
}

そして QML を以下のように書き換えます。

import QtQuick 2.0

Rectangle {
    width: 360
    height: 360
    Text {
        text: applicationData.getCurrentDateTime()
        anchors.centerIn: parent
    }
    MouseArea {
        anchors.fill: parent
        onClicked: {
            Qt.quit();
        }
    }
}

これで実行すると現在時刻が表示されるようになります。JS から C++ で定義した getCurrentDateTime にアクセスできているのがとても不思議ですね...。

ちなみに警告が出る場合は、.pro に

QT += declarative

を追記するとなくなります。

何をやっているのか

Qt にはメタオブジェクトコンパイラ(moc)と呼ばれるツールが付属しています。Qtプログラムを書いていると、public、private、protected に加えて、signalsslots といったアクセス指定子が出てきたりします。他にも上で見たような Q_INVOKABLE のようなキーワードがいくつが出てきます。これらは C++ のコード的には無害(public や空白等)になるように定義されており、qmake した時点で、こういったキーワードをアノーテーションとして解釈して追加の C++ コードを生成する Makefile を吐き出し、make を実行すると追加の C++ ファイルを生成します。例えば applicationdata.h では次のような追加のファイルが書きだされます。

/****************************************************************************
** Meta object code from reading C++ file 'applicationdata.h'
**
** Created by: The Qt Meta Object Compiler version 67 (Qt 5.0.1)
**
** WARNING! All changes made in this file will be lost!
*****************************************************************************/

#include "../CppBindTest/applicationdata.h"
#include <QtCore/qbytearray.h>
#include <QtCore/qmetatype.h>
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'applicationdata.h' doesn't include <QObject>."
#elif Q_MOC_OUTPUT_REVISION != 67
#error "This file was generated using the moc from 5.0.1. It"
#error "cannot be used with the include files from this version of Qt."
#error "(The moc has changed too much.)"
#endif

QT_BEGIN_MOC_NAMESPACE
struct qt_meta_stringdata_ApplicationData_t {
    QByteArrayData data[3];
    char stringdata[37];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
    Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
    offsetof(qt_meta_stringdata_ApplicationData_t, stringdata) + ofs \
        - idx * sizeof(QByteArrayData) \
    )
static const qt_meta_stringdata_ApplicationData_t qt_meta_stringdata_ApplicationData = {
    {
QT_MOC_LITERAL(0, 0, 15),
QT_MOC_LITERAL(1, 16, 18),
QT_MOC_LITERAL(2, 35, 0)
    },
    "ApplicationData\0getCurrentDateTime\0\0"
};
#undef QT_MOC_LITERAL

static const uint qt_meta_data_ApplicationData[] = {

 // content:
       7,       // revision
       0,       // classname
       0,    0, // classinfo
       1,   14, // methods
       0,    0, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       0,       // signalCount

 // methods: name, argc, parameters, tag, flags
       1,    0,   19,    2, 0x02,

 // methods: parameters
    QMetaType::QDateTime,

       0        // eod
};

void ApplicationData::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        ApplicationData *_t = static_cast<ApplicationData *>(_o);
        switch (_id) {
        case 0: { QDateTime _r = _t->getCurrentDateTime();
            if (_a[0]) *reinterpret_cast< QDateTime*>(_a[0]) = _r; }  break;
        default: ;
        }
    }
}

const QMetaObject ApplicationData::staticMetaObject = {
    { &QObject::staticMetaObject, qt_meta_stringdata_ApplicationData.data,
      qt_meta_data_ApplicationData,  qt_static_metacall, 0, 0}
};


const QMetaObject *ApplicationData::metaObject() const
{
    return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}

void *ApplicationData::qt_metacast(const char *_clname)
{
    if (!_clname) return 0;
    if (!strcmp(_clname, qt_meta_stringdata_ApplicationData.stringdata))
        return static_cast<void*>(const_cast< ApplicationData*>(this));
    return QObject::qt_metacast(_clname);
}

int ApplicationData::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QObject::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id < 1)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 1;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
        if (_id < 1)
            *reinterpret_cast<int*>(_a[0]) = -1;
        _id -= 1;
    }
    return _id;
}
QT_END_MOC_NAMESPACE

この moc によって QtC++ の文法では簡単に記述するのが難しいシグナル/スロットなどの幾つかの機能を実現しています。

まずクラスを見てみると Q_OBJECT という記述があります。このマクロは moc で利用する幾つかのメタ情報へと展開されます。

次に JS からもアクセス出来ている getCurrentDateTime() メソッドを見てみると、Q_INVOKABLE なるキーワードが付加されています。これは

#define Q_INVOKABLE

と定義されているので実質何もしていないように見えますが、このアノーテーションにより、上で述べたように moc によって追加のコードが生成され JS からもアクセスできるようになります。moc によって書きだされたファイルを見てみると ApplicationData::qt_static_metacall 内でこの getCurrentDateTime を呼び出していることが分かります。ちなみにこの qt_static_metacall は Q_OBJECT で展開されるメンバの一つです。

#define Q_OBJECT \
public: \
    Q_OBJECT_CHECK \
    static const QMetaObject staticMetaObject; \
    virtual const QMetaObject *metaObject() const; \
    virtual void *qt_metacast(const char *); \
    QT_TR_FUNCTIONS \
    virtual int qt_metacall(QMetaObject::Call, int, void **); \
private: \
    Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
    struct QPrivateSignal {};

次にプロパティおよびそのアクセサと変更を検知するイベントハンドラを追加してみます。

#include <QObject>

class Hoge : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString hoge READ hoge WRITE setHoge NOTIFY hogeChanged)

public:
    Hoge() : hoge_("hogehoge") {}

    QString hoge() const {
        return hoge_;
    }

    void setHoge(const QString& str) {
        hoge_ = str;
        emit hogeChanged();
    }

signals:
    void hogeChanged();

private:
    QString hoge_;
};

これで文字列をメンバ変数に持ち、そのアクセサおよび変更があったら呼ばれるイベントハンドラを持つクラスが出来ました。アクセサやイベントハンドラは次のようにマクロを使って定義します。

Q_PROPERTY(型 JSでのプロパティ名 READ C++でのgetter WRITE C++でのsetter NOTIFY C++でのイベントハンドラ)

でこれに基づくプライベート変数や getter/setter を定義します。イベントハンドラに関しては JS 側の関数が呼ばれるように moc で定義が作成されるため、宣言のみに留めておきます。

次にこのクラスを利用した QML を見てみます。

import QtQuick 2.0

Rectangle {
    width: 360
    height: 360
    Text {
        id: message
        text: Hoge.hoge
        anchors.centerIn: parent
    }
    MouseArea {
        anchors.fill: parent
        onClicked: {
            Hoge.hoge = "fugafuga";
        }
    }
    function onHogeChanged() {
        message.text = Hoge.hoge;
    }
}

クリックすると Hoge.hoge を "hogehoge" から "fugafuga" に書き換え、この結果、onHogeChanged が呼ばれ、テキストが書き換わるという簡単な内容になっています。

以上のような形で C++ のクラスを QML 側へセットすることができます。

QML 側で定義した関数C++ から呼ぶ

JS 側の関数を呼ぶには前回も書きましたが、以下のようにします。

QMetaObject::invokeMethod(QObject, "QML 側の関数名", Q_ARG(型名, 引数に与える変数), ...);

実際にコードで見てみましょう。まず以下の様な QML を書きます。

import QtQuick 2.0

Rectangle {
    width: 360
    height: 360
    function hoge() {
        console.log("hogehoge!");
    }
    function fuga(arr, obj) {
        arr.forEach(function(elem) {
            console.log("Array item:", elem);
        });
        for (var key in obj) {
            console.log("Object item:", key, obj[key]);
        }
    }
}

この hoge() と fuga() を C++ 側から呼んでみます。

#include <QtGui/QGuiApplication>
#include <QQuickItem>
#include "qtquick2applicationviewer.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QtQuick2ApplicationViewer viewer;
    viewer.setMainQmlFile(QStringLiteral("qml/CppBindTest/main.qml"));
    viewer.showExpanded();

    QQuickItem* root_obj = viewer.rootObject();

    // 引数なしの hoge を呼ぶ
    QMetaObject::invokeMethod(root_obj, "hoge");

    // Array と Object を引数にとる fuga を呼ぶ
    QVariantList list;
    list << 10 << QColor("green") << "bottles";

    QVariantMap map;
    map.insert("language", "QML");
    map.insert("released", QDate(2010, 9, 21));

    QMetaObject::invokeMethod(root_obj, "fuga",
            Q_ARG(QVariant, QVariant::fromValue(list)),
            Q_ARG(QVariant, QVariant::fromValue(map)));

    return app.exec();
}
結果
hogehoge!
Array item: 10
Array item: #008000
Array item: bottles
Object item: language QML
Object item: released Tue Sep 21 2010 00:00:00 GMT+0900 (JST)

C++ 側で QML で使える新しい型を定義する

先ほど定義したクラス HogeHoge という型で QML で使えるようにしてみます。

#include <QtGui/QGuiApplication>
#include "qtquick2applicationviewer.h"
#include "applicationdata.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    
    // ライブラリ名、メジャーバージョン、マイナーバージョン、型名
    qmlRegisterType<Hoge>("HogeLibrary", 1, 0, "Hoge");

    QtQuick2ApplicationViewer viewer;
    viewer.setMainQmlFile(QStringLiteral("qml/CppBindTest/main.qml"));
    viewer.showExpanded();

    return app.exec();
}

qmlRegisterType を1行書くだけです。これで次のように利用することができます。

import QtQuick 2.0
import HogeLibrary 1.0

Rectangle {
    width: 360
    height: 360
    Hoge {
        id: hoge
        hoge: "hogehoge"
        onHogeChanged: {
            console.log("hoge was changed!")
        }
    }
    Text {
        id: message
        text: hoge.hoge
        anchors.centerIn: parent
    }
    MouseArea {
        anchors.fill: parent
        onClicked: {
            hoge.hoge = "fugafuga";
        }
    }
}

クリックすると MouseArea の onClicked が呼ばれ、hoge.hoge のテキスト "hogehoge" が "fugafuga" へと変わり、onHogeChanged が呼ばれると共に、Text の text がプロパティバインディングによって "hogehoge" から "fugafuga" に変わります。

おわりに

Qt の仕組み良く出来ていて面白いですね。特に C++JSバインディングv8 を生で使うよりもずっと楽な感じがします。また、moc も中でどういった処理をしているのか、まだよく理解できていないので調べてみたいです。

May 02, 2013

QtQuick 2 でフルスクリーン表示 (Mac OS X)

| 18:23 |  QtQuick 2 でフルスクリーン表示 (Mac OS X)を含むブックマーク  QtQuick 2 でフルスクリーン表示 (Mac OS X)のブックマークコメント

はじめに

Mac で QtQuick アプリを作成していたのですが、QtQuick 1 では QWidget::setFullScreen() を呼べば簡単にフルスクリーン化出来たのに対し、QtQuick 2 に切り替えて同じ事をしてもフルスクリーン化出来ず困りました。色々と試行錯誤して、一部駄目なところもありますが何とか出来たので、内容をメモします。

問題

同じように困っている人もチラホラ見かけるのですが 0 replies ...。ここにも書いてあるように、showMaximized() や、showMinimized() はうまく動作するのですが showFullScreen() だけ駄目なようです。

上記エントリにも書いてあるように QtQuick 2 からは QML を表示するビューが QWidget 派生クラスから QWindow 派生クラス(QQuickView)になったことに関係しているのかな?

QtQuick 1

こんな感じで出来てました*1

QmlApplicationViewer viewer;
viewer.setMainQmlFile(QLatin1String("qml/Hoge/main.qml"));
viewer.showFullScreen();

QtQuick 2

試行錯誤した結果、以下のようになりました*2

QtQuick2ApplicationViewer viewer;
viewer.setMainQmlFile(QStringLiteral("qml/Piyo/main.qml"));
viewer.setFlags(Qt::WindowFullscreenButtonHint);
viewer.showFullScreen();

showFullScreen() するまえに Qt::WindowFullscreenButtonHint を setFlags で伝えておきます。すると実行後、フルスクリーンで表示されます!ただ、フルスクリーン ON/OFF を切り替えるアイコンは表示されず、一度フルスクリーンを解除してしまうと再度フルスクリーン化は出来ません。

おわりに

どなたか詳しい方、教えていただけると助かります (-人-)

*1:QmlApplicationViewer は Qt Creator で Qt Quick 1 プロジェクトを生成すると生成され、プラットフォーム間の違いを吸収してくれる処理が書いてある QDeclarativeView 派生クラス(元を辿ると QWidget 派生クラス)です

*2:QtQuick2ApplicationViewer は Qt Creator で Qt Quick 2 プロジェクトを生成すると生成され、プラットフォーム間の違いを吸収してくれる処理が書いてある QQuickView 派生クラス(元を辿ると QWindow 派生クラス)です