1
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Hy で GUI プログラミングをしてみる

この記事は、Lisp Advent Calendar 2019 の 16 日目の記事です。

この記事の要旨

Hy での GUI プログラミング入門として、Python の標準ライブラリに含まれている GUI ライブラリの tkinter を使用したジェネラティブアートを実装します。

こんな感じの三角関数を使った図形描画で、キー入力によってパラメータを変更できる仕様です。
2019-12-15_23h18_53.png

対象読者

  • Hy で GUI プログラミングをしてみたい人

環境構築

著者の環境は下記になります。

  • Windows 10
  • Python 3.7.2 (64bit)

仮想環境の構築とモジュールのインストール

GUI ライブラリの tkinter は Python 標準ライブラリのため、仮想環境にインストールするサードパーティー製モジュールは Hy のみです。
Hy は、12/10 時点での最新版をインストールしました( https://github.com/hylang/hy/tree/39c150d8299d9ff8f47bb78415e22a9fe976d7e5 )。

cd %USERPROFILE$\.virtualenvs
py -3 -m venv venv_hy_geometric_pattern
cd venv_hy_geometric_pattern
Scripts\activate.bat
pip install --upgrade pip
pip install git+https://github.com/hylang/hy.git
deactivate

仕様

tk.Tk() インスタンスとして作成したウインドウの中に Canvas を作成し、そこに図形を描画します。
アニメーションをさせるために、およそ 60 fps で Canvas を再描画します。
ある時刻の点の座標は、下記の式で求めます。$\theta$ を $\mathrm{d} \theta$ ずつ変化させてその時間での点の配列を作成します。

\left\{ \begin{array}{}
x(\theta , t) = x_{\mathrm{center}} + r \cos \theta + a \cos (\phi \left( t \right) + c \theta) \\

y(\theta , t) = y_{\mathrm{center}} + r \sin \theta + a \sin (\phi \left( t \right) + c \theta)
\end{array} \right.
0 \leq \theta < 2 \pi \\
0 \leq \phi < 2 \pi \\
0 \leq c < 2 \pi \\
0 < r + a < \min(x_{\mathrm{center}}, y_{\mathrm{center}}) \\

$\left( x_{\mathrm{center}}, y_{\mathrm{center}} \right)$ は Canvas の中央の座標です。
$\phi$ は時間によって $0 \leq \phi < 2 \pi$ をループするようにします。

キー入力で上記の $r, a, c$ と $\theta$ の刻み幅 $\mathrm{d} \theta$ を変更できるようにします。
キーの割り当ては下表の通りです。
また、q キーでウインドウを閉じて終了するようにも設定します。

変数 減少キー 増加キー
$r$ z a
$a$ x s
$c$ c d
$\mathrm{d} \theta$ v f

パッケージの構成

hy ディレクトリが PYTHONPATH に追加されているものとします。
__init__.hy の中身は空で、gui.hy に機能を実装していきます。
geometric_pattern.bat から geometric_pattern.gui を呼び出して実行します。

hy ─┬─ geometric_pattern ─┬─ __init__.hy
    │                     │
    │                     └─ gui.hy
    │
    └─ geometric_pattern.bat

実装

gui.hy
(import [math [sin cos radians]])
(import sys)
(import [tkinter :as tk])

; キャンバスサイズ
(setv *width* 800)
(setv *height* 600)


(defclass GeometricPattern []
  (setv *d-theta-max* 20)
  (setv *deg* 360)
  (setv *d-phi* 1)

  (defn --init-- [self &optional [center-x 0] [center-y 0]]
    "パラメータを初期化し、点のリストを取得する"
    (setv (. self center-x) center-x)
    (setv (. self center-y) center-y)

    (setv (. self r)
          (int (-> 0.8
                   (* (min (. self center-x) (. self center-y)))
                   (/ 3))))
    (setv (. self a)
          (int (-> 0.8
                   (* 2 (min (. self center-x) (. self center-y)))
                   (/ 3))))
    (setv (. self phi) 0)
    (setv (. self c) 104)

    ;; theta を変動させるときの刻み幅
    (setv (. self d-theta) 1)

    ((. self fetch-points)))

  (defn dec-r [self event]
    "rを減少する"
    (setv (. self r) (dec (. self r)))
    (if (< (. self r) 1)
        (setv (. self r) 1)))

  (defn inc-r [self event]
    "rを増加する。Canvas の短辺からはみ出さないように上限を指定している"
    (if (< (+ (. self r) (. self a))
           (min (. self center-x) (. self center-y)))
        (setv (. self r) (inc (. self r)))))

  (defn dec-a [self event]
    "aを減少する"
    (setv (. self a) (dec (. self a)))
    (if (< (. self a) 1)
        (setv (. self a) 1)))

  (defn inc-a [self event]
    "aを増加する。Canvas の短辺からはみ出さないように上限を指定している"
    (if (< (+ (. self r) (. self a))
           (min (. self center-x) (. self center-y)))
        (setv (. self a) (inc (. self a)))))

  (defn dec-d-theta [self event]
    (setv (. self d-theta) (dec (. self d-theta)))
    (if (< (. self d-theta) 1)
        (setv (. self d-theta) 1)))

  (defn inc-d-theta [self event]
    (setv (. self d-theta) (inc (. self d-theta)))
    (if (> (. self d-theta) (. GeometricPattern *d-theta-max*))
        (setv (. self d-theta) (. GeometricPattern *d-theta-max*))))

  (defn dec-c [self event]
    (setv (. self c) (dec (. self c)))
    (if (< (. self c) 0)
        (setv (. self c) (% (. self c) (. GeometricPattern *deg*)))))

  (defn inc-c [self event]
    (setv (. self c) (inc (. self c)))
    (if (>= (. self c) (. GeometricPattern *deg*))
        (setv (. self c) (% (. self c) (. GeometricPattern *deg*)))))

  (defn inc-phi [self]
    (setv (. self phi) (inc (. self phi)))
    (setv (. self phi) (% (. self phi) (. GeometricPattern *deg*))))

  (defn fetch-points [self]
    "点の座標を取得する"
    (setv (. self points)
          (lfor theta (range 0 (. GeometricPattern *deg*) (. self d-theta))
                (, (+ (+ (. self center-x) (* (. self r) (cos (radians theta))))
                      (* (. self a) (cos (radians (+ (. self phi) (* (. self c) theta))))))
                   (+ (+ (. self center-y) (* (. self r) (sin (radians theta))))
                      (* (. self a) (sin (radians (+ (. self phi) (* (. self c) theta)))))))))

    ;; パスを閉じるために最初の点をリストの最後に追加する
    ((. self points append) (. self points [0]))))


(defclass Simulator []
  (setv *ms* 16)

  (defn --init-- [self width height]
    (setv (. self width) width)
    (setv (. self height) height)

    ;; ウインドウとキャンバスの初期化
    (setv (. self window) ((. tk Tk)))
    ((. self window title) :string "Sample Trig Function on Tkinter")
    ((. self window resizable) :width False :height False)
    (setv (. self canvas)
          ((. tk Canvas) (. self window)
                         :width (. self width)
                         :height (. self height)))

    (setv (. self center-x) (/ width 2))
    (setv (. self center-y) (/ height 2))

    ;; 表示する文字の座標
    (setv (. self quit-x) 20)
    (setv (. self quit-y) 30)
    (setv (. self r-x) (- width 20))
    (setv (. self r-y) (- height 120))
    (setv (. self a-x) (- width 20))
    (setv (. self a-y) (- height 90))
    (setv (. self c-x) (- width 20))
    (setv (. self c-y) (- height 60))
    (setv (. self d-theta-x) (- width 20))
    (setv (. self d-theta-y) (- height 30))

    (setv (. self gp) (GeometricPattern (. self center-x) (. self center-y)))

    ;; 背景色(黒)でキャンバスを塗りつぶす
    ((. self canvas create-rectangle) 0
                                      0
                                      (. self width)
                                      (. self height)
                                      :fill "black")
    ((. self draw))
    ((. self canvas pack))

    ;; キーバインドの設定
    ((. self window bind) "<KeyPress-q>" (. self quit))
    ((. self window bind) "<KeyPress-z>" (. self gp dec-r))
    ((. self window bind) "<KeyPress-a>" (. self gp inc-r))
    ((. self window bind) "<KeyPress-x>" (. self gp dec-a))
    ((. self window bind) "<KeyPress-s>" (. self gp inc-a))
    ((. self window bind) "<KeyPress-c>" (. self gp dec-c))
    ((. self window bind) "<KeyPress-d>" (. self gp inc-c))
    ((. self window bind) "<KeyPress-v>" (. self gp dec-d-theta))
    ((. self window bind) "<KeyPress-f>" (. self gp inc-d-theta)))

  (defn quit [self event]
    ((. self window destroy)))

  (defn draw [self]

    ;; 図形の描画
    ((. self canvas create-line) (. self gp points) :fill "white" :tag "sample")

    ;; 文字の描画
    ((. self canvas create-text) (. self quit-x)
                                 (. self quit-y)
                                 :text "Quit: q"
                                 :tag "sample"
                                 :font (, "Arial" 12)
                                 :fill "white"
                                 :anchor (. tk W))
    ((. self canvas create-text) (. self r-x)
                                 (. self r-y)
                                 :text ((. "r: Z < {0:03d} > A" format) (. self gp r))
                                 :tag "sample"
                                 :font (, "Arial" 12)
                                 :fill "white"
                                 :anchor (. tk E))
    ((. self canvas create-text) (. self a-x)
                                 (. self a-y)
                                 :text ((. "a: X < {0:03d} > S" format) (. self gp a))
                                 :tag "sample"
                                 :font (, "Arial" 12)
                                 :fill "white"
                                 :anchor (. tk E))
    ((. self canvas create-text) (. self c-x)
                                 (. self c-y)
                                 :text ((. "c: C < {0:03d} > D" format) (. self gp c))
                                 :tag "sample"
                                 :font (, "Arial" 12)
                                 :fill "white"
                                 :anchor (. tk E))
    ((. self canvas create-text) (. self d-theta-x)
                                 (. self d-theta-y)
                                 :text ((. "d-theta: V < {0:03d} > F" format) (. self gp d-theta))
                                 :tag "sample"
                                 :font (, "Arial" 12)
                                 :fill "white"
                                 :anchor (. tk E)))

  (defn delete [self]
    "sampleタグをつけた要素を削除する"
    ((. self canvas delete) "sample"))

  (defn loop [self]
    ;; 一つ前の画面をクリア
    ((. self delete))

    ;; 新しい状態を取得して描画
    ((. self gp inc-phi))
    ((. self gp fetch-points))
    ((. self draw))

    ;; 約16ms後にloopを実行
    ((. self window after) (. Simulator *ms*) (. self loop))))


(defn main []
  (setv simulator (Simulator *width* *height*))
  ((. simulator loop))
  ((. simulator window mainloop))
  0)


(when (= --name-- "__main__")
      ((. sys exit) (main)))

上記 geometric_pattern.gui モジュールを呼び出して実行するためのバッチファイルを作成します。
仮想環境を activate し、hy コマンドと hy モジュールにパスが通った状態にします。
そして、hy コマンドを使って geometric_pattern.gui モジュールを実行します。
-m オプションを付けると、パッケージ内のモジュールをドット区切り表記で指定できます。

geometric_pattern.bat
@ECHO OFF
SETLOCAL

SET THISDIR=%~dp0
SET PYTHONPATH=%THISDIR%;%PYTHONPATH%
SET ACTIVATE=%USERPROFILE%\.virtualenvs\venv_hy_geometric_pattern\Scripts\activate.bat

CALL %ACTIVATE%
hy -m geometric_pattern.gui
SET STATUS=%ERRORLEVEL%
CALL deactivate

IF %STATUS%==0 (
  ECHO Done.
  PAUSE
  EXIT /b 0
) ELSE (
  ECHO Error.
  PAUSE
  EXIT /b 1
)

実行

それではやっていきましょう。

2019-12-16_15h26_49.png
2019-12-16_15h26_58.png
2019-12-16_15h27_25.png
2019-12-16_15h27_30.png

はい。
パスの色や太さを変えてみたり、別の数式に変えてみると楽しいかもしれません。

まとめ・補足

  • Python の標準ライブラリには tkinter という GUI ライブラリがあり、Hy からも利用可能です。
  • GUI ライブラリには、他にもサードパーティー製モジュールの Qt for Python (PySide2) などがあります。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
1
Help us understand the problem. What are the problem?