PSD ファイルをロードする Qt 6 のモジュールを作りました

# はじめに

この記事は [Qt Advent Calendar 2024](https://qiita.com/advent-calendar/2024/qt) ６日目の記事です。

昨日は一個空いていますね。Qt が好きな人がいましたら埋めていただければと思います。

また、この記事は [Slint Advent Calendar 2024](https://qiita.com/advent-calendar/2024/slint) ６日目の記事でもあります。

昨日は @hermit4 さんの [Slint言語入門(2)](https://qiita.com/hermit4/items/3128daed1d0605e05ff9) でした。

.slint は元々 QtQuick や Qt for MCU を開発していた開発者が設計／開発をしているだけあって、QML よりもいいところがたくさんありますね。


---

今日は、PSD を読み込んで Qt で表示するモジュール（ライブラリやそれを利用したサンプル、テスト）を開発したので、それを紹介します。

# QtPsd モジュール

## モチベーション

2010頃からなにかの UI を実装することが増えてきて、デザインは PSD ファイルでもらうことが多かったという記憶があります。（当時はまだ Figma とかはなく、Sketch が使われはじめたくらいだった気がします。）

その頃から、このデザインを自動で QML に変換できないかなと思って、ことあるごとに [PSD の仕様](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) を眺めていました。

その後、[Qt Design Studio](https://www.qt.io/ja-jp/product/ui-design-tools) が [Photoshop のプラグイン](https://doc.qt.io/qtdesignstudio/psqtbridge.html) を提供し、Photoshop から QML への変換が可能になりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13867/4127a2f4-5abf-5a76-c35a-e36ff3b1c0ab.png)

が、しかし、上記のワークフローは基本的にデザイナーさんが担当するもので、プログラマーの私は「これじゃないな」と感じていました。

PSD ファイルをもらったプログラマーが、自分の思った感じの QML を出力できるといいなと思い、PSD をロードできるペイントソフト [Krita](https://krita.org/ja/) の [QML のエクスポートまわりのコード](https://invent.kde.org/graphics/krita/-/blob/master/plugins/impex/qml/qml_converter.cc?ref_type=heads) を自分用に魔改造したこともありました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13867/34630ff1-1f91-5948-09db-160dbee66813.png)

それも物足りなかったので、もう少しちゃんと PSD を解析して、もう少しちゃんと QML を生成できるようにしたいと思っていました。

先月末にパシフィコ横浜で開催された EdgeTech+ 2024 に会社として出展することにして、そのためにデモ機をいくつか準備する必要があったので、この期を逃すわけにはいくまいと開発をしたのが、以下で公開をしている Qt 向けの PSD モジュールになります。

https://github.com/signal-slot/qtpsd


## できること

- PSD ファイルを解析して、レイヤー構造とレイヤーの様々な情報を Qt で扱いやすい形で提供します
- プレビュー用のサンプルアプリがあります
  - QtQuick の形式でエクスポートができます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13867/4cfda43d-1ae9-4859-5253-cefeaf750cde.png)

上記の PSD は [Qt Design Studio のチュートリアルかウェビナー向けに公開](https://git.qt.io/public-demos/qtdesign-studio/-/blob/master/psds/cluster_tutorial_1.1.psd?ref_type=heads) されているものです。

Qt Quick にエクスポートすると以下のようになります。
この PSD の場合、オリジナルの再現度はそこまで高くはありません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13867/92983a64-11b3-192f-a60e-953592f7b051.png)


# EdgeTech+ 向けのデモを作りました

## コインパーキングの精算機の UI

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13867/66e68c27-87df-a8f4-88cf-2e2379406eb3.png)

[コインパーキングUI.psd](https://github.com/signal-slot/parkinglot/blob/main/%E3%82%B3%E3%82%A4%E3%83%B3%E3%83%91%E3%83%BC%E3%82%AD%E3%83%B3%E3%82%B0UI.psd) を [Qt Quick のコード](https://github.com/signal-slot/parkinglot/tree/main/qt/ui) に変換し、雑に [ロジックを記載した以下の qml](https://github.com/signal-slot/parkinglot/blob/main/qt/main.qml) を追加しました。

```qml:main.qml
import QtQuick
import "ui"

Window {
    id: root
    width: ui.width
    height: ui.height
    visible: true
    title: currentPage
    property int currentPage: 0
    MainWindow {
        id: ui
        welcome.visible: root.currentPage === 0
        welcome.onClicked: root.currentPage++

        header.visible: root.currentPage > 0
        cancel.onClicked: root.currentPage = 0
        footer.visible: root.currentPage > 0

        parkingSlot.visible: root.currentPage === 1
        parkingSlot.onVisibleChanged: {
            digit1.text = ''
            digit10.text = ''
        }

        accept.onClicked: root.currentPage++
        key0.onClicked: keyClicked('0')
        key1.onClicked: keyClicked('1')
        key2.onClicked: keyClicked('2')
        key3.onClicked: keyClicked('3')
        key4.onClicked: keyClicked('4')
        key5.onClicked: keyClicked('5')
        key6.onClicked: keyClicked('6')
        key7.onClicked: keyClicked('7')
        key8.onClicked: keyClicked('8')
        key9.onClicked: keyClicked('9')
        keyC.onClicked: {
            digit1.text = ''
            digit10.text = ''
        }
        function keyClicked(key) {
            if (digit1.text === '') {
                digit1.text = key
            } else if (digit10.text === '') {
                digit10.text = digit1.text
                digit1.text = key
            }
        }

        payment.visible: root.currentPage === 2
        cash.onClicked: root.currentPage++
        card.onClicked: root.currentPage++
        ic.onClicked: root.currentPage++
        pay.onClicked: root.currentPage++

        goodBye.visible: root.currentPage === 3
        goodBye.onClicked: root.currentPage = 0

        Timer {
            repeat: false
            running: ui.goodBye.visible
            interval: 3000
            onTriggered: root.currentPage = 0
        }
    }
}
```

![parkinglot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13867/40f85a02-3090-3c9c-d8b5-a53c16280d86.gif)


元の PSD を完全に再現はできていないのですが、デモとしては十分な出来です。

# 標準的な Qt 以外の対応

同じデザイン(PSD)から、色々なフレームワークで UI を作ることにもチャレンジした結果、以下のバリエーションのサンプルアプリを生成することができました。

## [QtQuick(GPU なしでも動く版)](https://github.com/signal-slot/parkinglot/tree/main/qt-nogpu)

[Toradex Verdin AM62 Solo で meta-qt6 を動かしてみた](https://qiita.com/task_jp/items/bfc8594bf6eb022f05d8) の環境で動かすと以下のようになります。

![carpark-verdin-am62.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13867/e8a668dc-abff-ed87-3344-1836ee7e6649.gif)

## Slint

https://github.com/signal-slot/parkinglot/tree/main/slint

.slint 形式でもエクスポートできるように対応しました。

任意の解像度に拡大縮小をしてエクスポートができるようにもしたので、[pico](https://github.com/signal-slot/parkinglot/tree/main/pico) や [pico2](https://github.com/signal-slot/parkinglot/tree/main/pico2) で動く Slint のアプリケーションも作成しました。

## Flutter(Experimental)

https://github.com/signal-slot/parkinglot/tree/main/flutter

Flutter の対応も進めているところです。

https://x.com/task_jp/status/1846799903143743792/video/1




## 投票所の投票端末のデモ

ちょうど準備期間に選挙があったのと、投票所で文字を書くのが面倒くさかったので、
タッチで投票できる感じのデモを作ってみました。

![votingmachine.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13867/52990000-fa5c-bd8d-c22c-a31bfda0f268.gif)

[PSD はこれ](https://github.com/signal-slot/votingmachine/blob/main/%E9%81%B8%E6%8C%99%E3%81%AE%E6%8A%95%E7%A5%A8%E7%AB%AF%E6%9C%AB.psd) で、Slint 向けにエクスポートしたものが、以下のものです。

https://github.com/signal-slot/votingmachine/tree/main/slint

先日の [Raspberry Pi 5 で slint を動かす](https://qiita.com/task_jp/items/eb9fe25112883f81cff3) で動かした UI は、実はこうやって作ったので、ソースコードが手書きに比べて構造化されていない状態でした。

# 現在のステータス

とりあえず公開はしましたが、まだまだアルファ版という感じで未対応の部分やバグがたくさんあります。

大きく分けて、以下の点をこれから改善していきたいです。

## 様々な PSD を正しく扱えるようにする

https://github.com/Agamnentzar/ag-psd/tree/master/test

のサンプルPSDをすべてちゃんとパースできるようにしたいです。

## Qt の API を改善する

とりあえず動いているけれど、理想の設計や実装からはかけ離れているところが多いので、これから改善していきたいです。

## エクスポートできるフレームワークの種類を増やす

- Qt Widgets
- その他の宣言型 UI フレームワーク

## サンプルアプリを増やす

- Qt Creator のプラグイン？
- [QImageIOPlugin](https://doc.qt.io/qt-6/qimageioplugin.html)
- [QQuickImageProvider](https://doc.qt.io/qt-6/qquickimageprovider.html)


# 謝辞

このプロジェクトの開発にあたり、フリーランスの日高隆博さんにたくさん協力をしていただきました。

この場を借りて御礼を伝えたいと思います。


# おわりに

今回は PSD ファイルをロードしてパースする Qt 向けのモジュールの紹介でした。

Qt や PSD や、アプリケーション開発のワークフローに興味がある方がいましたら、是非開発に参加して、より効率よく GUI の開発ができるような未来を作りましょう。

明日は、Slint の方は @hermit4 さんによる [Slint言語入門(3)　構文についての続き](https://qiita.com/hermit4/items/53f71549087a7d51ed15) です。
Qt の方は未定ですね。

お楽しみに！