0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

QtAdvent Calendar 2024

Day 4

Pyside6でアナログ時計を作ってみる

Last updated at Posted at 2024-12-03

はじめに

 Qtアドベントカレンダーも4日目に突入。今の所毎日記事が入っているので嬉しいですね。
 昨日は、@task_jpさんによる「Toradex Verdin AM62 Solo で meta-qt6 を動かしてみた」でした。ET+2024は僕も展示をお手伝いさせていただきました。meta-qt6は別件でも利用していますが、yoctoにしては、はまったり、困ったりすることが少なく、ビルド時間以外はとても重宝します。まぁ、下回りの実装が半端なママ、自分で頑張れって雰囲気になっていますが、そこらへんは組込みあるあるですしね。yoctoでQt6に挑まれるかたは見てみると良いかも。

本日のお題について

 先日、Slintアドベントカレンダーの方でSlint-Pythonを使ったアナログ時計の実装を紹介しました。そこで、Pyside6とSlintで比較できるようにPyside6+QMLでも同じアナログ時計を実装したいと思います。Slintの記事と重複するけど、Slintには興味が無いやという人のためにこちらの記事だけでも読めるように記載しておきます。

 QtではQML用のdemoとして世界時計があります。ここからアナログ時計部分だけを抜き出してSlint記事と比較できるようにしておきます。

clocks.png

検証環境

検証環境はKUbuntu 24.04ですが、Ubuntu24.04でも同じ手順で試せるかと思います。

/etc/os-release
PRETTY_NAME="Ubuntu 24.04.1 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04.1 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo

環境構築

Slint-Python用モジュールはpipで提供されています。ですのでpipでインストールできます。今回はvenvを使って仮想環境にインストールしています。

sudo apt install python3-venv
python3 -m venv work/.venv
source work/.venv/bin/activate
pip install pyside6

アナログ時計の仕様

実装してみよう

QML側UI定義

~/work/qml/ui/clock.qml
import QtQuick
import QtQuick.Controls

ApplicationWindow {
    id : clock
    visible: true
    width: 320
    height: 240
    color: "#646464"
    property int hours
    property int minutes
    property int seconds

    function timeChanged() {
        var date = new Date;
        hours = date.getHours()
        minutes = date.getMinutes()
        seconds = date.getUTCSeconds();
    }

    Timer {
        interval: 100; running: true; repeat: true;
        onTriggered: clock.timeChanged()
    }

    Image {
        id: base
        anchors.centerIn: parent
        source : "https://raw.githubusercontent.com/qt/qtdoc/refs/heads/dev/examples/demos/clocks/images/clock-night.png"
        Image {
            id: hour
            x: base.width/2 - width/2
            y: base.height/2 - height + 18
            source: "https://raw.githubusercontent.com/qt/qtdoc/refs/heads/dev/examples/demos/clocks/images/hour.png"
            transform: Rotation {
                id: hourRotation
                origin.x: hour.width/2
                origin.y: hour.height - 18
                angle: (clock.hours * 30) + (clock.minutes * 0.5)
                Behavior on angle {
                    SpringAnimation { spring: 2; damping: 0.2; modulus: 360 }
                }
            }
        }
        Image {
            id: minute
            x: parent.width/2 - width/2
            y: parent.height/2 - height + 18
            source: "https://raw.githubusercontent.com/qt/qtdoc/refs/heads/dev/examples/demos/clocks/images/minute.png"
            transform: Rotation {
                id: minuteRotation
                origin.x: minute.width/2
                origin.y: minute.height - 18
                angle: clock.minutes * 6
                Behavior on angle {
                    SpringAnimation { spring: 2; damping: 0.2; modulus: 360 }
                }
            }
        }
        Image {
            id: second
            x: parent.width/2 - width/2
            y: parent.height/2 - height + 18
            source: "https://raw.githubusercontent.com/qt/qtdoc/refs/heads/dev/examples/demos/clocks/images/second.png"
            transform: Rotation {
                id: secondRotation
                origin.x: second.width/2
                origin.y: second.height - 18
                angle: clock.seconds * 6
                Behavior on angle {
                    SpringAnimation { spring: 2; damping: 0.2; modulus: 360 }
                }
            }
        }
        Image {
            anchors.centerIn: base; source: "https://raw.githubusercontent.com/qt/qtdoc/refs/heads/dev/examples/demos/clocks/images/center.png"
        }
    }
}

Python側ロジック

~/work/qml/clock.py
from PySide6.QtWidgets import QApplication
from PySide6.QtQml import QQmlApplicationEngine
import sys

if __name__ == "__main__":
    app = QApplication(sys.argv)
    engine = QQmlApplicationEngine()
    engine.load("ui/clock.qml")

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec())

起動

cd ~/work/qml
python3 clock.py

clock.png

ざっくり解説

QMLコードについて

 QMLは左上(0,0)、右下(width,height)とする座標系になっています。宣言型言語で親の中に子を入れることができます。今回は画像を5つ使うだけのシンプルなものですので、利用しているのは外枠のApplicationWindowとImage、更新のためのTimerエレメントと時刻更新関数だけとなります。

 Qtデモそのままではなく、少し親子関係や配置について調整します。

 文字盤baseを親としてその中に、短針、長針、秒針、キャップを兄弟として配置しています。Z軸を指定することもできますが、通常は定義順に重ねられるので省略しています。
 配置としては、初期状態は00:00:00で、回転はなしとなります。X方向では画像の中心となるように、Y方向は針の端が中心に来るように調整するのですが、画像サイズからそのままだと針が文字板を超えてフレーム枠に乗ってしまいます。

 Qt Demoでは針が盤面の枠を超えて配置されていますが、長針と秒針が目盛りの上に乗るように、短針はその内側で目盛りを隠さないように配置するほうがアナログ時計としては一般的と思うので、針画像のY座標はそれぞれ18pxほどずらして配置しています。

アナログ時計は時刻を360度の円で表します。小学校の授業を思い出してみましょう。

  • 時針は12時間で360度、1時間で30度動きます。さらに60分で30度移動なので1分で0.5度回転することになります
  • 分針は60分で360度回転するので、1分で6度回転することになります
  • 秒針は60秒で360度回転するので、1秒で6度回転することになります

 ただし、短針の動作をそのまま実装すると、11時59分時点で短針が先に12時を指して見えてしまい格好がつかないので、あえて0.4を掛けるようにして、11:59に長針と短針が重なるように微調整しています。

 時刻更新は100ms周期として、Timerを使ってQML側で実装しています。QMLではEcmaScript6相当の機能が利用できるため時刻取得などもQML側で実装しています。もちろんPython側にロジックを移すことも可能ですが、Slintと違う部分の強調ということもあって今回はQML側での実装にしました。

 また100ms周期とはいえ、時刻の1秒で一気に6度移動するのは見た目上とんで見えるため回転にアニメーションをつけています。デモアプリの実装に合わせてSpringAnimationを実装しています。

Pythonコードについて

 Qtでは、QQuickViewを使う方法と、QQmlApplicationEngineを使う方法がありますが、今回は後者で実装しています。アナログ時計程度だとロジックはすべてQML側で実装できるので、Python側は定番の読み出しコードだけになっています。
 さっくりQMLをお試しするには、コンパイラやその他諸々を用意しなくてもお試しできるので、Pyside6は非常に便利ですね。もちろんWidgetsの機能も利用できるうえ、Qtが公式にサポートしているので、Pythonを使いつつ本格的なUIを必要とする場合、重要な選択肢になってきているかと思います。

まとめ

 今回は、Slintアドベントカレンダーとの並行企画のため、Pyside6で簡単なアナログ時計を例としてQMLでの実装をご紹介しました。あまり身がない上にSlint側の記事との比較で間違い探しみたいになってしまいましたが、ま、まぁ、記事を書く人が減っている現状ですから、きっと怒られは発生しないはず…
 次回の記事は、きちんとQt6周りについて何か書きたいと思いますのでご容赦ください。

 明日は、@CharNoe さんの「Android対応したときの話」とのことです。楽しみに待ちましょう。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?