見出し画像

QML Qt6 第2章

第2章
 
前章の終わりで、SDKをインストールしたことになっています。そこで、SDKを試運転するために、Hello Worldプログラムを作成することになります。
 
そして、最初に著者自身がつまずいた、ProtoType型のプロジェクトと、CMakeというビルドシステムを使用したQt Quick Application型のプロジェクトについて解説が行われています。このドキュメントでは、特に断りがなければ、ProtoType型のプロジェクトを使っていく、というスタイルを取っています。(早く言ってほしかったですね。)
 
本格的な、製品としてのアプリケーションを作るのであれば、Qt Quick Applicationを使います。
 
そして、QMLはC++(ネイティブコード)とのバインディングが可能です。プラグインにしてインポートすることも可能であると述べています。しかし、差し当たってはQMLの側面のみを説明したいので、ProtoType型プロジェクトを扱うと言っています。
 
この一文から察するに、C++とかと組み合わせる際には、Qt Quick Applicationを使うことになります。(この説明は、もっと後で行われます)
 
ちなみに、開発キットのコンパイラについてですが、著者はDesktop Qt 6.7.1 MSVC 2019 64bitを使用しています。MinGWとかがデフォルトでチェック付いているのですが、動かないですし、何だか問題があるという話を聞きます。どこかで読んだことがあるのですが、QtはOSによって使えるコンパイラが違うようです。
 
では、HelloWorldというプロジェクトをProtoType型で作成してください。作り方は前章と同じになりますね。

import QtQuick
 
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
}
 
これがボイラープレートです。見ればわかるようなプロパティ名がついていますね。個々の説明は割愛します。この章では、このコードのバックグラウンドの処理がどうなっているのか、ということが述べられていますから、そちらの方が重要に思えます。
 
『Qt Creatorがqmlを走らせ、QML Documentを第一引数へ入れます。qmlアプリケーションはQML Documentを解析し、ユーザーインターフェースを打ち出します。』
F5キーを押してみましょう

 
これで、SDKのテストは完了です。(前章のコードもQtCreatorでやらなければできないのではないでしょうか?したがって、すでにSDKのテストはできていた気がするんですが、第1章のコードはQt Creatorを使わなくてもできるものだった、ということでしょうか???。コンソールアプリケーションから起動しろ、ということ?)
 
ちなみに、qmlprojectをダブルクリックすると、Qt Design Studioという別の開発環境が立ち上がります。
 
この章はこれだけになります。
 
これから、なぜかQMLの話が一度中断し、クラシックなQtの紹介が始まります。
QtCoreによるコンソールアプリケーションの作成や、C++のコードを使用したソースコードが展開されており、一旦わき道にそれた説明が行われます。
 
その意図はどうやらこういうことのようです。今のQt Quick Applicationができた事情を説明するためです。Qtは、顧客対応の際、GUIや外観の変更を急にお願いされることがあるようです。
 
最初にプログラムが動く最低限のコードをササっと開発しておき、顧客に届け、後は順に少しずつ機能を追加(アップデート)していくような開発手法をアジャイル開発というようで、これが主流となっている中で、QMLはこのような要請に一致したフレームワークであると言えます。
 
QMLは、表面はHTMLのようなコードで、しかし背後ではC++が動作しているというすごい仕組みです。
 
その差を見せるために、これからクラシックなコードを書いていくようです。QMLと比べて、クラシックなコードが堅苦しく、見づらい物であるのかをあらかじめ示したいのでしょう。
 
ですから、これからのコードは余り真面目に読まないようにしてください。

import QtQuick
Rectangle {
width: 240; height: 240
Rectangle {
width: 40;
height: 40
anchors.centerIn:parent
color: '#FFBB33'
}
}
 
これはQt Quickの簡単なユーザーインターフェースコードです。

 

この宣言型言語をQMLと言い、QMLは実行時にランタイムを必要とします。Qtはqmlという①ランタイムを提供します。カスタムのランタイムを書くこともできます。
このために、②QuickViewを必要とし、C++のソースとして、主要なQMLドキュメントをセットします。
 
※①ランタイム(runtime)とは、プログラムが実行される際に必要なソフトウェア環境やインフラストラクチャのことを指します。具体的には、プログラムが実行中に使用するライブラリやサービス、プログラムの実行を管理するシステムなどを含みます。
 
※②C++のソースコードを、QML内から呼び出すことができます。逆に、QMLのコンポーネントを、C++のGUIの中で使うこともできます。そのためには、QQuickViewコンポーネントを利用します。これにqmlで書いたコードを読み込ませて使います。QQuickWidgetやQQuickWindow等もその仲間で、C++で扱うQMLファイルは、QQuick~という頭文字がついたクラスを利用します。
 
#include <QGui>
#include <QtQml>
 
int main(int argc, char *argv[]) {
 
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine("main.qml");

return app.exec();
}
 
 
↑原著から抜粋したコードですが、このコードがこのタイミングで書かれている理由は正直謎です。おそらく、これがそれぞれ(クラシックなQtとモダンなQt)のランタイムだよ!っていうことを言いたいんだろうと思います。
 
次に、city modelのプログラム例を見ます。何それ?って思われるでしょう。
実は、Hello Worldプログラムを書いていただいた時に、クラシックなC++のコードがいくつか紹介されていました。しかし、私はそれを飛ばしました。QMLのみのコードに焦点を絞りたかったからです。
 
ですが、読み進めていくと、そのコードを応用してQMLのコードを書いていきますと言われましたから、その飛ばしたコードを書きたいと思います。(注意、ここからのコードは律儀に書かなくていいです。最後の最後で、あまり意味がなかったことがわかります。それでも書いてみたいという方だけ書いてください。
 
ただし、ビルドシステムはCMakeを使っています。原著はqmakeを利用しているようです。
 

 
ファイル構成はこのようになります。
 
で、ファイルは、
main.cpp
 
#include "Widget.h"
 
#include <QApplication>
 
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
CustomWidget w;
w.show();
return a.exec();
}
 
Widget.h
 
#ifndef CUSTOMWIDGET_H
#define CUSTOMWIDGET_H
 
#include <QWidget>
 
class CustomWidget : public QWidget
{
Q_OBJECT
public:
 
explicit CustomWidget(QWidget *parent = 0);
void paintEvent(QPaintEvent *event);
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
 
private:
 
QPoint m_lastPos;
};
 
#endif // CUSTOMWIDGET_H
 
 
widget.cpp
 
#include "Widget.h"
#include <QPainter>
#include <QVBoxLayout>
#include <QMouseEvent>
 
CustomWidget::CustomWidget(QWidget *parent)
: QWidget{parent}
{
 
}
 
void CustomWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
QRect r1 = rect().adjusted(10,10,-10,-10);
painter.setPen(QColor("#33B5E5"));
painter.drawRect(r1);
QRect r2(QPoint(0,0),QSize(40,40));
if(m_lastPos.isNull()) {
r2.moveCenter(r1.center());
} else {
r2.moveCenter(m_lastPos);
}
painter.fillRect(r2, QColor("#FFBB33"));
}
void CustomWidget::mousePressEvent(QMouseEvent *event)
{
m_lastPos = event->pos();
update();
}
 
void CustomWidget::mouseMoveEvent(QMouseEvent *event)
{
m_lastPos = event->pos();
update();
 
}
 
 

 
 
マウスで真ん中の四角を自由に移動させることができるようです。
 
でも、これはcity modelプログラムとは全く関係ありませんでした。
 
もう一つ載せます。
 
こちらもCustomWidgetと言う名前でした。プロジェクト名が衝突するので、前のファイルを書き換えました。
 

 

 
 
ファイル構成に変化はありません。
 
main.cpp
 
#include "Widget.h"
 
#include <QApplication>
 
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
CustomWidget w;
w.show();
return a.exec();
}
 

Window.h
 
#ifndef CUSTOMWIDGET_H
#define CUSTOMWIDGET_H
 
#include <QApplication>
#include <QWidget>
#include <QListWidgetItem>
#include <QListWidget>
#include <QLineEdit>
#include <QPushButton>
 
class CustomWidget : public QWidget
{
Q_OBJECT
 
public:
explicit CustomWidget(QWidget *parent = 0);
private slots:
void itemClicked(QListWidgetItem* item);
void updateItem();
private:
QListWidget *m_widget;
QLineEdit *m_edit;
QPushButton *m_button;
};
 
#endif // CUSTOMWIDGET_H

Widget.cpp
 
#include "Widget.h"
#include <QVBoxLayout>
 
CustomWidget::CustomWidget(QWidget *parent) :
QWidget(parent)
{
QVBoxLayout *layout = new QVBoxLayout(this);
m_widget = new QListWidget(this);
layout->addWidget(m_widget);
m_edit = new QLineEdit(this);
layout->addWidget(m_edit);
m_button = new QPushButton("Quit", this);
layout->addWidget(m_button);
setLayout(layout);
QStringList cities;
cities << "Paris" << "London" << "Munich";
foreach(const QString& city, cities) {
m_widget->addItem(city);
}
 
connect(
m_widget,SIGNAL(itemClicked(QListWidgetItem*)),
this,SLOT(itemClicked(QListWidgetItem*)));
 
connect(
m_edit, SIGNAL(editingFinished()),this,SLOT(updatItem));
connect(m_button,SIGNAL(clicked()),qApp,SLOT(quit()));
}
 
void CustomWidget::itemClicked(QListWidgetItem *item)
{
Q_ASSERT(item);
m_edit->setText(item->text());
}
 
 
void CustomWidget::updateItem()
{
QListWidgetItem* item = m_widget->currentItem();
if(item) {
item->setText(m_edit->text());
}
}
 
恐らくこれで間違いがないと思います。というのは、コードがエラーなく実行されたということです。何で自信がないのかというと、pdfファイルの横幅の関係で、途中でコードが切れているからです。そういう意味でも原著は少し問題がありますね。
 

 
このようなアプリが表示されます。
更にもう一つコードがありました。(よくわからない構成ですね・・・。)今度はcustomwidgetV2というファイルになります。
 

main.cpp
 
#include "Widget.h"
 
#include <QApplication>
 
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
CustomWidgetV2 w;
w.show();
return a.exec();
}
 
Widget.h
 
#ifndef WIDGET_H
#define WIDGET_H
 
#include <QWidget>
#include <QGraphicsView>
#include <QGraphicsScene>
 
class CustomWidgetV2 : public QWidget
{
Q_OBJECT
public:
explicit CustomWidgetV2(QWidget *parent = 0);
private:
QGraphicsView *m_view;
QGraphicsScene *m_scene;
};
#endif // WIDGET_H
 

Widget.cpp
 
#include "Widget.h"
#include <QGraphicsItem>
#include <QVBoxLayout>
 
CustomWidgetV2::CustomWidgetV2(QWidget *parent) :
QWidget(parent)
{
m_view = new QGraphicsView(this);
m_scene = new QGraphicsScene(this);
m_view->setScene(m_scene);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setSpacing(0);
layout->addWidget(m_view);
setLayout(layout);
QGraphicsItem* rect1 = m_scene->addRect(
0, 0, 40, 40);
rect1->setFlags(
QGraphicsItem::ItemIsFocusable|
QGraphicsItem::ItemIsMovable);
 
}
 

 
 
こちらも最初と同じで、先ほどと同じように中央の矩形をマウスで動かすことができるアプリでした。
 
原著は何をしたかったのかというと、『ここまでは、基本的なデータ型や、ウィジェットやグラフィックスアイテムの使い方を見てきましたね・・・。』というお話だったようですこれから、構造化されたもっと大きなデータを扱う・・・。そこで、モデルビューの仕組みを使うということのようです。
ただそのように話をつなげたかったみたいなのです。
 
とりあえず、次のコードに行きましょう。
 
今回は、CMakeLists.txtに加えるコードもあります。Sqlです。
 

CMakeLists.txt
 
cmake_minimum_required(VERSION 3.5)
...
 
find_package(
 
Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Sql
)
 
...
 
target_link_libraries(
customwidget PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt6::Sql
)
 
...

main.cpp
 
#include "Widget.h"
#include <QApplication>
 
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
CustomWidget w;
w.show();
return a.exec();
}
 

Widget.h
#ifndef CUSTOMWIDGET_H
#define CUSTOMWIDGET_H
#include <QWidget>
#include <QListView>
#include <QSqlTableModel>
#include <QLineEdit>
#include <QPushButton>
 
class CustomWidget : public QWidget
{
Q_OBJECT
 
public:
CustomWidget(QWidget *parent = nullptr);
 
private slots:
void itemClicked(const QModelIndex &index);
void updateItem();
 
private:
QListView *m_view;
QSqlTableModel *m_model;
QLineEdit *m_edit;
QPushButton *m_button;
};
 
#endif // CUSTOMWIDGET_H

Widget.cpp
#include "Widget.h"
#include <QVBoxLayout>
#include <QSqlDatabase>
#include <QSqlTableModel>
#include <QSqlQuery>
#include <QListView>
#include <QLineEdit>
#include <QPushButton>
#include <QApplication>
 
CustomWidget::CustomWidget(QWidget *parent) :
QWidget(parent)
{
QVBoxLayout *layout = new QVBoxLayout(this);
 
m_view = new QListView(this);
 
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("cities.db");
if (!db.open()) {
qFatal("unable to open database");
}
 
QSqlQuery query(db);
query.exec("CREATE TABLE IF NOT EXISTS city (name TEXT, country TEXT)");
query.exec(
"INSERT INTO city VALUES ('Munich', 'Germany')"
);
query.exec(
"INSERT INTO city VALUES ('Paris', 'France')"
);
query.exec(
"INSERT INTO city VALUES ('London', 'United Kingdom')"
);
 
m_model = new QSqlTableModel(this);
m_model->setTable("city");
m_model->setHeaderData(0, Qt::Horizontal, "City");
m_model->setHeaderData(1, Qt::Horizontal, "Country");
m_model->setEditStrategy(QSqlTableModel::OnManualSubmit); // 必要に応じて手動で変更をコミットする
 
m_view->setModel(m_model);
m_model->select(); // モデルのデータをデータベースから再読み込み
 
m_edit = new QLineEdit(this);
layout->addWidget(m_view);
layout->addWidget(m_edit);
m_button = new QPushButton("Quit", this);
layout->addWidget(m_button);
setLayout(layout);
 
connect(
m_view, &QListView::clicked,
this, &CustomWidget::itemClicked);
connect(
m_edit, &QLineEdit::editingFinished,
this, &CustomWidget::updateItem);
connect(
m_button, &QPushButton::clicked,
qApp, &QApplication::quit);
}
 
void CustomWidget::itemClicked(const QModelIndex &index)
{
Q_ASSERT(index.isValid());
m_edit->setText(
index.data(Qt::DisplayRole).toString()
);
}
 
void CustomWidget::updateItem()
{
QModelIndex item = m_view->currentIndex();
if (item.isValid()) {
m_view->model()->setData(
item, QVariant(m_edit->text())
);
m_model->submitAll(); // 変更をデータベースにコミット
}
}


 
 
今の状態だと、起動するごとに、データが追加されて行きますが・・・。間違いではないですね。
 
 

そして、ようやく、これをQtQuickで実現する、という話になります。
ですが!!!
 
import QtQuick
Rectangle {
width: 240; height: 120
ListView {
width: 180; height: 120
anchors.centerIn: parent
model: cityModel
delegate: Text { text: model.city }
}
}
 
原著は、これがQMLでモデルを使う書き方だと書いています。全体のソースコードは載せられていません。特徴的なのは、model: cityModelの部分ですね。このコードだけでは、cityModelは一体どこから来た値なのか?ということがわかりませんよね?
 

これがC++のコードです。
 
m_model = QSqlTableModel(this);
... // some magic code
QHash<int, QByteArray> roles;
roles[Qt::UserRole+1] = "city";
roles[Qt::UserRole+2] = "country";
m_model->setRoleNames(roles);//こんな関数はない!
engine.rootContext()->setContextProperty(
"cityModel", m_model);// &m_modelが正解?
 
QSqlTableModelのm_model変数を、setContextPropertyで登録している、ということがおわかりでしょうか。これで、”cityModel”という変数をQML内で利用するんだけど、それは、m_modelを指している、ということを意味しています。ここには、参照かポインタを渡すことになります。私がこの機能を使った経験から言えば、&m_modelが正しいのではないか…と思いますし、このコードには、最大のツッコミどころがあります。それは、調べた限りでは、setRoleNamesという関数が存在しないということです。QSqlTableModelとその基底クラスをぜひ調べてみてください。Qt6には、setRoleNamesという関数が存在しません。(多分Qt5にもなかったように思います。少なくともこれはQt6の本なので、Qt6に存在しないならば、無意味です。)
 
ですから、この文書は、おそらくですが、原著者の記憶だけを頼りに書かれた文書であり、単に、modelをQML内で利用する方法に焦点を当てて解説したかっただけ!ということになりそうです。本当に憶測で申し訳ありませんが、そうとしか思えないのです。だから、ここはかなりハマりました。
 
個人的に散々振り回された感じはしますけれども、以前紹介した3つのC++のコードは、載せてもほとんど意味がなかった。少なくとも、今回の話につながるような流れではなかった・・・ということになります。何だかわざわざ読ませてしまいまして申し訳ありませんでした。しかし、原著を忠実に辿って行くと、確かにそのようになっているのではないかと思います。
 
しかも、このソースコードは公開されていないようなので、どうも確認のしようがありません。

いいなと思ったら応援しよう!