ElixirでGUI・Scenic [入門編１]

# １．はじめに

ElixirのGUIライブラリ「[Scenic](https://github.com/boydm/scenic)」を使って、簡単なウィンドウアプリを作ってみます。

:::note warn
scenicのバージョンが0.10系から0.11系になり、当記事がそのままではビルドできなくなりました。0.11系対応の記事は[こちらの投稿](https://qiita.com/myasu/items/e15ed98d08e2f2c804c3)を参照願います。

本記事では、Ver.0.10系でビルドする手順を示します。
:::

（作例）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/836db08e-4e3c-e5b6-ebe9-7c4b0fb668f4.png)

### シリーズ

|ステップ|概要|
|:--|:--|
|入門１|プロジェクトの作り方、デザイン、ボタンイベント|
|[入門２](https://qiita.com/myasu/items/540470ddf30b8776f32c)|複数プロジェクト間のイベント通信|
（続くかも･･･）

<!--
|応用１|制御HMI|Elixir.Circuitsとの連携|
|応用２||レイアウトを綺麗に|
|実践１|制御HMI|電動シリンダの制御|
-->

### 実行環境

| | |
|:--|:--|
|ハードウェア|VirtualBox for Windows|
|OS|Ubuntu Linux 20.04 LTS|
|Elixir|Ver.1.9.1|

```sh:コマンドライン
$ uname -a
Linux ubuntuvb 5.4.0-40-generic #44-Ubuntu SMP Tue Jun 23 00:01:04 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
$ elixir -v
Erlang/OTP 22 [erts-10.6.4] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1]
Elixir 1.9.1 (compiled with Erlang/OTP 22)
```

# ２．インストール

インストールから公式のチュートリアルまでを試した内容を、別記事にまとめてますので、まずはこちらにチャレンジしてみてください。

- [ElixirでGUI・Scenic チュートリアルチャレンジ](https://hotch-potch.hatenadiary.jp/entry/2020/06/20/214043)

## 最低限必要なインストール

### ①OpenGLの開発ライブラリ

```sh:コマンドライン
$ sudo apt install libglfw3 libglfw3-dev libglew-dev
```

### ②Scenic Archive

```sh:コマンドライン
$ mix archive.install hex scenic_new
```

# ３．まずはHelloWorld

テキストと図形を、ウィンドウに表示するまでを試してみます。

## (1)プロジェクトの作成から実行まで

ひな形のプロジェクトを作成して、とりあえず実行します。

```sh:コマンドライン
$ cd （ご自身のワーキングディレクトリに移動）

#scenicプロジェクトを作成
$ mix scenic.new hmihello
* creating .formatter.exs
* creating .gitignore
（･･･省略･･･）
You can also run it interactively like this:
    $ iex -S mix

#プロジェクトのディレクトリに移動
$ cd hmihello

#必要なライブラリを取得
$ mix deps.get
Resolving Hex dependencies...
Dependency resolution completed:
New:
  elixir_make 0.6.0
（･･･省略･･･）
* Getting font_metrics (Hex package)
* Getting msgpax (Hex package)

#実行
$ mix scenic.run
　（※`iex -S mix`でもOK）
```

**実行画面**

「hmihello」のウィンドウが表示されて、コンソールにはウィンドウ上で発生したイベントが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/3045262e-1a44-3552-f037-eceefce102d4.png)


## (2)コードの読み解き

まずはコードを眺めてみます。

### ウィンドウ

ウィンドウのソースは、`lib/scenes/home.ex`になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/8c10cc55-c59d-6db7-dbbe-72030a09df4a.png)


ウィンドウに表示するコントロール（で表現良いのかな？）は、init関数の`graph=Graph.build(`以下に記述します。
各コントロールをパイプ`|>`でつないで、複数のコントロールを配置するように指定しています。

```elixir:home.ex
  def init(_, opts) do
    # get the width and height of the viewport. This is to demonstrate creating
    # a transparent full-screen rectangle to catch user input
    {:ok, %ViewPort.Status{size: {width, height}}} = ViewPort.info(opts[:viewport])

    # show the version of scenic and the glfw driver
    scenic_ver = Application.spec(:scenic, :vsn) |> to_string()
    glfw_ver = Application.spec(:scenic, :vsn) |> to_string()

    graph =
      Graph.build(font: :roboto, font_size: @text_size)
      |> add_specs_to_graph([
        text_spec("scenic: v" <> scenic_ver, translate: {20, 40}),
        text_spec("glfw: v" <> glfw_ver, translate: {20, 40 + @text_size}),
        text_spec(@note, translate: {20, 120}),
        rect_spec({width, height})
      ])

    {:ok, graph, push: graph}
  end
```

|キーワード|機能|
|:--|:--|
|text_spec|テキストを配置します|
|rect_spec|四角形を描画します。ここではウィンドウサイズ一杯に無色の四角形が表示されてます|
|translate: {x, y}|描画する位置を、ウィンドウ左上基準に指定。（t: で省略表記できます）|
|font_size:|フォントの大きさ|

## (3)コードの修正

下記のようなイメージで、`Hello World`の文字、両端に丸図形、そして下線を表示します。

```
●　Hello World　●
-----------------
```

コードを修正します。

```elixir:home.ex
defmodule Hmihello.Scene.Home do
  use Scenic.Scene
  require Logger

  alias Scenic.Graph
  alias Scenic.ViewPort

  import Scenic.Primitives
  #↓ここの先頭のコメントを外してScenic.Componentsをインポート
  import Scenic.Components

（･･･途中省略･･･）

  def init(_, opts) do
    # get the width and height of the viewport. This is to demonstrate creating
    # a transparent full-screen rectangle to catch user input
    {:ok, %ViewPort.Status{size: {width, height}}} = ViewPort.info(opts[:viewport])

    # show the version of scenic and the glfw driver
    scenic_ver = Application.spec(:scenic, :vsn) |> to_string()
    glfw_ver = Application.spec(:scenic, :vsn) |> to_string()

    graph =
      Graph.build(font: :roboto, font_size: @text_size)
      |> add_specs_to_graph([
        #コメントアウト
        # text_spec("scenic: v" <> scenic_ver, translate: {20, 40}),
        # text_spec("glfw: v" <> glfw_ver, translate: {20, 40 + @text_size}),
        # text_spec(@note, translate: {20, 120}),
        # rect_spec({width, height})

        #ここから追記
        text_spec("Hello World", t: {40, 40}, font_size: 30),
        circle_spec(10, fill: :blue, stroke: {2, :white}, t: {20, 30}),
        circle_spec(10, fill: :yellow, stroke: {2, :red}, t: {190, 30}),
        line_spec({{10, 50}, {200, 50}}, stroke: {4, :cyan}, cap: :round)
      ])

    {:ok, graph, push: graph}
  end
```

|キーワード|機能||
|:--|:--|:--|
|circle_spec|丸図形を描画します||
|line_spec|直線を描画します||
|色指定|[こちら](https://hexdocs.pm/scenic/Scenic.Primitive.Style.Paint.Color.html)|

指定可能なコントロールはこちらを参考にして下さい。

- [Scenic.Primitives](https://hexdocs.pm/scenic/Scenic.Primitives.html)

### 実行

```sh:コマンドライン
$ mix scenic.run
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/7787bb2a-fe56-4de1-d086-afeba5102cf0.png)


# ４．ボタンのイベントで文字と図形を書き換える

ウィンドウ上のボタンの操作で、文字や図形を書き換えてみます。

### コードの追記①

init関数に、新たに４つのコントロールを追記します。

```elixir:home.ex
    graph =
      Graph.build(font: :roboto, font_size: @text_size)
      |> add_specs_to_graph([
        # text_spec("scenic: v" <> scenic_ver, translate: {20, 40}),
        # text_spec("glfw: v" <> glfw_ver, translate: {20, 40 + @text_size}),
        # text_spec(@note, translate: {20, 120}),
        # rect_spec({width, height})

        text_spec("Hello World", t: {40, 40}, font_size: 30),
        circle_spec(10, fill: :blue, stroke: {2, :white}, t: {20, 30}),
        circle_spec(10, fill: :yellow, stroke: {2, :red}, t: {190, 30}),
        line_spec({{10, 50}, {200, 50}}, stroke: {4, :cyan}, cap: :round),
        
        #ここから追記
        text_spec("---", t: {80, 80}, font_size: 30, id: :event_text),
        circle_spec(10, fill: :grey, stroke: {2, :white}, t: {60, 70}, id: :event_circle),
        button_spec("ON", id: :btn_on, t: {40, 90}, theme: :success),
        button_spec("OFF", id: :btn_off, t: {120, 90}, theme: :danger)
      ])

```

追記したコントロールには、全て`id:`を付加しています。
書き換えするときのターゲット指定に、idの値が使われます。


`theme:`は、ボタンの色のテーマ指定です。
**サンプル**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/64544b5c-f42e-06bd-d25e-e5c4376824e3.png)
指定できる内容は[こちら](https://hexdocs.pm/scenic/Scenic.Primitive.Style.Theme.html)になります。


### コードの追記②

ボタンイベントを受け取る関数を追加します。

既に書かれている、`def handle_input(event, _context, state) `関数のあとに、下記のソースコードを追記して下さい。

```elixir:home.ex
  @doc """
  クリックイベントの受付
  """
  def filter_event(event, _, graph) do
    graph =
      case event do
        # ONボタン
        {:click, :btn_on} ->
          # 色を変更
          Graph.modify(graph, :event_circle, &update_opts(&1, fill: :lawn_green))
          # 文字を書き換え
          |> Graph.modify(:event_text, &text(&1, "ON"))

        # OFFボタン
        {:click, :btn_off} ->
          Graph.modify(graph, :event_circle, &update_opts(&1, fill: :grey))
          |> Graph.modify(:event_text, &text(&1, "OFF"))

        # それ以外
        {_, _} ->
          # 書き換え無し
          graph
      end

    # イベント内容を表示
    Logger.info("Received event: #{inspect(event)}")

    {:cont, event, graph, push: graph}
  end
```

書き換えは`Graph.modify`関数を使います。
同時に複数の箇所を書き換える場合は、パイプでつないでまとめます。

### 実行

```sh:コマンドライン
$ mix scenic.run
```

ONボタン、OFFボタンをクリックすると、文字列とランプに見立てた図形が点灯/消灯します。


**ON・緑点灯**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/836db08e-4e3c-e5b6-ebe9-7c4b0fb668f4.png)

**OFF・消灯**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/352398c7-212f-9231-9958-4531460b8909.png)



# ５．まとめ

今回は、取っ掛かりとして、簡単なウィンドウアプリを作ってみました。

次回は、複数プロジェクト間のイベントのやりとりを試してみます。



# ６．（おまけ）イベントの読み解き

## イベントを受け取る関数

この関数は、最初から書かれています。
イベントの内容をそのまま文字で出力します。

```elixir:home.ex
  def handle_input(event, _context, state) do
    Logger.info("Received event: #{inspect(event)}")
    {:noreply, state}
  end
```

## ウィンドウのイベント例

ウィンドウの上を操作して、発生したイベントの一部を列挙します。

|イベントの対象|イベント名|発生条件|値|例|
|:--|:--|:--|:--|:--|
|マウス|:viewport_enter|ウィンドウの範囲内に入った|ウィンドウに対する{x, y}| {130.0, 464.0}|
|マウス|:viewport_exit|ウィンドウの範囲外に出た|ウィンドウに対する{x,y}|{801.0, 43.0}|
|マウス|:cursor_pos|ウィンドウの上を移動した|ウィンドウに対する{x,y}|{675.0, 24.0}|
|マウス|:cursor_button|ボタンを押した|{ボタン, :press, 0, {x, y}}|{:left, :press, 0, {x, y}}|
|マウス|:cursor_button|ボタンを離した|{ボタン, :release, 0, {x, y}}|{:right, :release, 0, {x, y}}|
|キーボード|:key|キーを押した|{:key, {文字, :press, 0}|{:key, {"A", :release, 0}|
|キーボード|:key|キーを離した|{:key, {文字, :release, 0}|{:key, {"A", :release, 0}|

##### キーの同時押しの例

キーを押した順番にイベントが発生します。

```sh:例：[Ctrl-Shift-A]を押して離したときのイベントの流れ
 Received event: {:key, {"left_control", :press, 0}}
 Received event: {:key, {"left_shift", :press, 2}}
 Received event: {:key, {"A", :press, 3}}
 Received event: {:key, {"A", :release, 3}}
 Received event: {:key, {"left_shift", :release, 3}}
 Received event: {:key, {"left_control", :release, 2}}
```

一番最後の数字は、同時押しの数っぽいですが、微妙に数が合わない・・・

# ７．参考資料

- https://hexdocs.pm/scenic/
- https://github.com/boydm/scenic/