まぁ、ネタとしてはAdvent Calendarが埋まらなかった時のためのもので、無事に埋まったから要らない記事になったんだけど、中身の鮮度的に来年のAdvent Calendarまでは保たない(来年の今頃には本物のJsonListModelが実装されるだろう)感じだから一応公開しておくことにした。
#はじめに
なんでタイトルがこんな中途半端なのかって、カスタムエレメント作りを読んだ人の参考になるレベルで解説するだけのスキルはないので、「こんなことやってみたよ」という感想文を書くつもりで書いているからだ。(Qiitaじゃなくブログに書けと言われてしまうだろうか?w)
多分比較的最近(?)QtがJSONをサポートしてQJsonDocumentとか関連するClassが用意されてるみたいなんだけど、いろいろ探す限りQMLから使えるようにはなってないっぽく見える(既にエレメントの提案はされているみたいだけど)。
ということで、そのうちちゃんとしたエレメントが用意されるんだろうし、今のスキルでXmlListModelみたいな汎用で使えるものは作れないけど、とりあえずの練習台として「QMLでWebから取ってきたJSONデータをパースしてリストで返すエレメント」みたいなのを作ってみるかなということにした。
#参考にしたサイト
昔々に理音伊織さんがこんなブログを残しておられた。
この中で当時のNokiaのチュートリアルを参照しておられるのだがまぁ当然リンクは切れているので、最新のチュートリアルがこれだ。
あと、QAbstractListModelの英文ドキュメントを読むのがめんどくさかったから下の記事の一部を参考にさせていただいた。
もう一つ、QJsonDocumentを使ったJSONのパースについて書いている記事があったのでこっちも参考にさせてもらった。
つーワケで、この辺りを見ながらなんとかなるかなーと思ってやってみた。
#とりあえず考えてみる
XmlListModelみたいにurlを渡してそっから取得したJSONデータをParseして・・・みたいな風にできるといいんだけど、いかんせんC++の書けないへぼ野郎なのでとりあえず取得はQML側からJavaScriptでやってresponseTextをツッコんでリストを返させることにしよう。
"Writing QML Extensions with C++"によると、最低限以下のようにするらしいことが書いてある。
- QObjectを継承する
- Q_PROPERTYでプロパティを宣言する
QObjectを継承ってなってるんだけど、まぁこれを親に持つクラスならOKっぽい。今回はListModelを作るから、QAbstractListModel辺りでいいんだろう。
で、中身はこんな感じで考えた。
- クラス名:JsonListModel
- プロパティ:QString Json、QString query、QStringList roles
jsonにresponseTextを渡して、queryでどのタグのデータをリストにするのか知らせてrolesでどのプロパティのリストを返すのか指定する、みたいな感じかな。とりあえずそんなんで作ってみよう。(本当は階層があるけど、そこまで複雑なことできなかったからこれで勘弁してほしい(笑))
#とりあえず書いてみる
QtCreatorでQtQuickアプリのプロジェクトを立ち上げて、ソースのところに「新しいファイルを追加する...」からの流れで"JsonListModel"のヘッダ&ソースファイルを用意した。
という訳で書いてみた。
#ifndef JSONLISTMODEL_H
#define JSONLISTMODEL_H
#include <QAbstractListModel>
class JsonListModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(QString json READ json WRITE setJson)
Q_PROPERTY(QString query READ query WRITE setQuery)
Q_PROPERTY(QStringList roles READ roles WRITE setRoles)
public:
//
int rowCount(const QModelIndex &parent) const;
QHash<int, QByteArray> roleNames() const;
QVariant data(const QModelIndex &index, int role) const;
//
QString json() const;
void setJson(const QString &json);
QString query() const;
void setQuery(const QString &query);
QStringList roles() const;
void setRoles(const QStringList &roles);
//
explicit JsonListModel(QObject *parent = 0);
private:
QString m_json;
QString m_query;
QStringList m_roles;
QList<QHash<int, QVariant>> m_jsonList;
};
#endif // JSONLISTMODEL_H
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include "jsonlistmodel.h"
JsonListModel::JsonListModel(QObject *parent) : QAbstractListModel(parent)
{
}
//
QString JsonListModel::json() const
{
return m_json;
}
void JsonListModel::setJson(const QString &json)
{
m_jsonText = json;
QJsonDocument jsonDoc = QJsonDocument::fromJson(m_json.toUtf8());
QJsonObject jsonObj = jsonDoc.object();
QJsonArray jsonArray = jsonObj[m_query].toArray();
foreach (const QJsonValue & value, jsonArray) {
QJsonObject obj = value.toObject();
QHash<int, QVariant> jsonData;
beginInsertRows(QModelIndex(), m_jsonList.size(), m_jsonList.size());
for ( int i = 0; i < m_roles.size(); i++ ) {
jsonData.insert(i, obj[m_roles[i]].toVariant());
}
m_jsonList.append(jsonData);
endInsertRows();
}
}
QString JsonListModel::query() const
{
return m_query;
}
void JsonListModel::setQuery(const QString &query)
{
m_query = query;
}
QStringList JsonListModel::roles() const
{
return m_roles;
}
void JsonListModel::setRoles(const QStringList &roles)
{
m_roles = roles;
}
//
int JsonListModel::rowCount(const QModelIndex &) const
{
return m_jsonList.size();
}
QHash<int, QByteArray> JsonListModel::roleNames() const
{
QHash<int, QByteArray> ret;
for ( int i = 0; i < m_roles.size(); i++ ) {
ret.insert(i, m_roles[i].toUtf8());
}
return ret;
}
QVariant JsonListModel::data(const QModelIndex &index, int role) const
{
return m_jsonList[index.row()][role];
}
基本的にQAbstractListModelを継承した場合には以下の3つの関数を実装しなければならないらしい。
- rowCount() : リストの数を返す
- roleName() : role名のHashを返す
- data() : 特定のroleの特定の行のデータを返す
加えてQML側から受けとるプロパティを設定して、クラス内部で受け取って必要な処理をする関数と受け取ったプロパティを参照する関数をそれぞれ用意する。
今回は(よくわからないからっていうのもあるけど)だいぶ手を抜いて手順通りにやればデータが取れる程度のものを作ってみたけど、ちゃんと使うにはパラメータがセットされる順番にかかわらずちゃんと処理されるようにしたり、あとからデータを変更された時にリストを作り直すための処理を入れたりする必要があるんだと思う。多分。
JSONのparseについては参照サイトの4つ目を見ながらQJsonDocument、QJsonObject、QJsonArray、QJsonValue等のクラスを使って処理してみた。
最終的なリストはこういう保持形態がいいのか悪いのかよくわからないけど、まぁとりあえず意図した通りにリストにアクセスできるので今回はこれで良しという事にしよう。
#QtQcuikアプリにエレメントをレジストしてみる
比較的簡単にこう書けばいいはず。
...
#include "jsonlistmodel.h"
...
int main(int argc, char *argv[])
{
...
qmlRegistType<JsonListModel>("JsonListModel", 1, 0, "JsonListModel");
...
}
#とりあえず動かしてみる
Livedoor Weather Web Serviceから東京都の天気情報を取得して、今日、明日、明後日の天気を書き出してみた。
import QtQuick 2.7
import QtQuick.Window 2.2
import JsonListModel 1.0
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
JsonListModel {
id: jsonListModel
query: "forecasts"
roles: new Array("dateLabel", "telop")
}
ListView {
id: listView
anchors.fill: parent
model: jsonListModel
delegate: Text {
width: parent.width
height: 30
text: "%1: %2".arg(dateLabel).arg(telop)
}
}
function getWeather() {
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://weather.livedoor.com/forecast/webservice/json/v1?city=%1".arg("130010"), true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
switch (xhr.status) {
case 200:
jsonListModel.json = xhr.responseText;
break;
default:
console.debug("Something wrong to get the Weather data.");
break;
}
}
}
xhr.send();
}
Component.onCompleted: getWeather()
}
#最後に
QML/QtQuickの使い方からすれば別にカスタムエレメント作りはC++でなくてもよく、JavaScriptを使っても同じような事ができてしまう。処理速度やらなにやら特別要求がないのであれば正直JavaScript使った簡単なのかも(おいらみたいなC++書けない人にはね)。
ただ、C++でカスタムエレメントを作ってQMLから使うこと(QML側と値を受け渡しする事)自体はQt側でそういうサポートがされているおかげでさして難しいことではないという印象。さすがはQtだってことかな。
とりあえずの感想としては、上で書いた通り仕組み自体は難しくないんだけどやっぱりC++よく解らないやって話と、データの型とその扱いに関する知識が乏しかったからその辺で手間取った感じかな。
今回のコードはgithubでも公開したけど、結構酷いコードだとは思うのでもっとこう書けばいいのにとかあったら教えろくださいおねがいします。
https://github.com/helicalgear/sample-qml-extension