Qt Widgets から Qt Quick/QML へ移行すると「UI をもっと気軽に作れるはずだが、複雑なインタラクションは Widgets の方が楽では?」と思いがちです。実際に触ってみると、そんな先入観は簡単に崩れます。本稿では、シンプルなコードで写真を並べ、ドラッグ/ピンチ/ホイールで拡大・縮小・回転できるデスクトップアプリケーション「PhotoSurface」を題材に、QML の実装を解説します。
完成イメージ
起動すると Pictures フォルダ内の画像がランダムに重なって表示され、以下の操作が可能です。
- マウスホイール:縦スクロールで拡大縮小、横スクロールで回転
- Ctrl+ホイール:回転
- マルチタッチ:二本指でピンチ/回転、一本指でドラッグ
- 左上のフォルダアイコン:任意フォルダを開く
図1:PhotoSurface の実行画面(省略)
C++ 側で準備するデータ
QML だけで完結させることもできますが、以下の情報を C++ 側で取得してコンテキストプロパティとして公開することで、プラットフォーム差を吸収します。
サポート画像フォーマットの列挙
static QStringList supportedImagePatterns()
{
QStringList patterns;
QMimeDatabase db;
for (const QByteArray &mime : QImageReader::supportedMimeTypes()) {
for (const QString &suffix : db.mimeTypeForName(mime).suffixes())
patterns.append(QStringLiteral("*.") + suffix);
}
return patterns;
}
ピクチャフォルダのパス取得
QQmlApplicationEngine engine;
QQmlContext *ctx = engine.rootContext();
QUrl picturesUrl = QUrl::fromLocalFile(QDir::homePath());
const QStringList dirs =
QStandardPaths::standardLocations(QStandardPaths::PicturesLocation);
if (!dirs.isEmpty()) {
picturesUrl = QUrl::fromLocalFile(dirs.first());
}
ctx->setContextProperty("picturesPath", picturesUrl);
ctx->setContextProperty("imagePatterns", supportedImagePatterns());
QML 実装のポイント
1. フォルダ選択ダイアログ
FileDialog {
id: dirPicker
title: qsTr("画像フォルダを選択")
selectFolder: true
folder: picturesPath
onAccepted: listModel.folder = selectedFolder + "/"
}
2. 初期化処理
Component.onCompleted: {
listModel.nameFilters = imagePatterns;
if (picturesPath) {
listModel.folder = picturesPath + "/";
} else {
dirPicker.open();
}
}
3. フォルダアイコンとツールチップ
Image {
id: folderIcon
anchors { top: parent.top; left: parent.left; margins: 10 }
source: "qrc:/icons/folder.png"
MouseArea {
anchors.fill: parent
onClicked: dirPicker.open()
hoverEnabled: true
onEntered: tip.show(mouse.x, mouse.y)
onExited: tip.hide()
}
Shortcut {
sequence: StandardKey.Open
onActivated: dirPicker.open()
}
}
4. スクロールバーの自作
Flickable の内容サイズに応じて、シンプルな Rectangle でフェードイン/アウトするスクロールバーを配置します。
Rectangle {
id: vScroll
anchors { right: parent.right; margins: 2 }
width: 6; radius: 3
color: "#33ffffff"
height: flick.height * (flick.height / flick.contentHeight)
y: flick.contentY * (flick.height / flick.contentHeight)
NumberAnimation on opacity { to: 0; duration: 400 }
onYChanged: { opacity = 1.0; fadeTimer.restart() }
}
5. 写真グリッド(Flickable + Repeater)
Flickable {
id: flick
anchors.fill: parent
contentWidth: width * 2
contentHeight: height * 2
Repeater {
model: FolderListModel {
id: listModel
showDirs: false
nameFilters: imagePatterns
}
delegate: PhotoFrame {
imageSource: listModel.folder + fileName
}
}
}
6. 一枚の写真を表す PhotoFrame.qml
Rectangle {
id: frame
property url imageSource
property real baseScale: 0.2
color: "transparent"
border { width: 2; color: "black" }
Image {
id: img
anchors.centerIn: parent
source: imageSource
fillMode: Image.PreserveAspectFit
antialiasing: true
}
Component.onCompleted: {
scale = baseScale
x = Math.random() * flick.width - width / 2
y = Math.random() * flick.height - height / 2
rotation = Math.random() * 20 - 10
}
PinchArea {
anchors.fill: parent
pinch.target: frame
pinch.minimumScale: 0.1
pinch.maximumScale: 10
pinch.minimumRotation: -360
pinch.maximumRotation: 360
onPinchStarted: frame.bringToFront()
onSmartZoom: frame.smartZoom(pinch.scale)
MouseArea {
drag.target: frame
onPressed: frame.bringToFront()
onWheel: {
if (wheel.modifiers & Qt.ControlModifier)
frame.rotation += wheel.angleDelta.y / 120 * 5;
else
frame.scale += frame.scale * wheel.angleDelta.y / 1200;
}
}
}
function bringToFront() {
frame.z = ++root.topZ;
frame.border.color = "red";
}
function smartZoom(factor) {
if (factor > 0) {
frame.rotation = 0;
frame.scale = Math.min(root.width, root.height) /
Math.max(img.sourceSize.width, img.sourceSize.height) * 0.9;
frame.x = flick.contentX + (flick.width - frame.width) / 2;
frame.y = flick.contentY + (flick.height - frame.height) / 2;
} else {
// 元の位置・サイズに戻す
}
}
}
まとめ
以上のように、QML の宣言的記述と PinchArea、MouseArea、Flickable などのビルトインタイプを組み合わせることで、ドラッグ/ピンチ/ホイールといった複雑なジェスチャも数十行程度で実装できます。Qt Quick を使えば「UI を変更するたびに C++ をコンパイルし直す」という負荷もなく、プロトタイピングや本番開発の両方で大きなメリットを得られるでしょう。