Godot4.0でListViewを作ります

# 目的
　Godot4.0でListViewを作りたいと思います。名前＋ハイスコアなどのリストアイテムをたくさん縦に並べて、表示しきれない場合は上下にスクロールできるコントロールです。
![スクリーンショット (841).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/aa1a3a90-a71f-b7db-b0ba-76068a74705d.png)

:::note
スマホで実行すると細いスクロールバーをつかむのが難しいです。
:::
# 今回使用するControlについて
　ControlはContainerを使ってうまく配置すると、表示デバイスのピクセルサイズが変わってもある程度うまく表示してくれます。

> GODOT DOCS : Container types
>https://docs.godotengine.org/en/latest/tutorials/ui/gui_containers.html#container-types

　今回ListViewで使用するContainerを紹介します。

* **VBoxContainer**
　子要素を縦に並べます。
　リストアイテムを縦に並べるときに便利ですね。

* **HBoxContainer**
　子要素を横に並べます。
　リストアイテム内の配置に使用します。

* **Margin Container**
 子要素を縁から少し離して表示したいときに使用します。

* **PanelContainer**
　ControlやContainerは基本透明ですが、背景（単色やテクスチャなど）を設定したい場合はPanelContainerを親ノードにします。

* **ScrollContainer**
　子要素の縦横サイズが大きい場合スクロールバーを表示します。

* **label**
　文字を表示します。

* **Button**
　ボタンです
# リストアイテムの作り方の基本 ※読むだけです
　リストアイテムがLabelとかボタンだけではあまり実用的ではないので、リストアイテムをLabelとButtonを組み合わせた「シーン」として作成します。
　リストアイテムは、左側に名前とハイスコア、右側に詳細表示ボタンを置くようなデザインにします。名前をハイスコアは縦に並べるとすると、リストアイテムの基本形は下記のようになると思います。

　ルートノードはHBoxContainerです。子要素VBoxContainerとButtonを左から右に並べてくれます。
![スクリーンショット (787).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/c4b8dd32-0583-3cf6-7e7a-dc82c83baf91.png)

　HBoxContainerの子要素はVBoxContainerとButtonを追加して横に並べます。
![スクリーンショット (791).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/b79084c9-acff-520a-d3f2-bc04842d8682.png)
　VBoxContainerには子ノードとしてLabelを２つ追加して、名前とハイスコア文字列を表示します。子ノードは上から下に縦に並びます。
　![スクリーンショット (792).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/e005ca82-e526-b29e-53cb-a9975ee6ec9b.png)

　以上が基本の構成になりますが、デザイン的にいくつか問題がありそうです。
　基本の構成にいくつかContainerを追加してリストアイテムをつくりなおしましょう
　（なので保存はしなくて良いです）

# リストアイテムの作り方
　新規シーンを作成します。ユーザインタフェースボタンをクリックしてControlを追加して名称をListItemに変更しましょう。
# リストアイテムのサイズを設定する
　ListItemノードはリストアイテムのサイズを保存する役割にします。
　ListItemを選択した状態でインスペクタのControl/Layout/Transform/Sizeをx=400px、y=100pxに設定ましょう。２Ｄのエディタ上に反映されます。
　またControl/Layout/Custom Minimum Sizeをx=400px、y=100pxにしましょう。
　Custom Minimum Sizeはこのシーンをinstanciateしたときのサイズとして認識されるので、Transform/Sizeと同じ値を設定します。
![スクリーンショット (796).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/1d0ddfd5-82b1-521f-c708-719b9766ac62.png)
# リストアイテムの背景にテクスチャを設定する
　背景にテクスチャを設定するので、テクスチャ画像をダウンロードします。

　Googleで「3D TEXTURE tiles wall 001」と検索をして、下の方の「DOWNLOAD THE ALL THE MAPS」からダウンロードできます。
　ファイル名は「Tiles_Wall_001_basecolor.jpg」です。

　res://assets/texture/from3D_TEXTURE/Tiles_Wall_001_basecolor.jpgに配置しましょう。
![スクリーンショット (799).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/cadcf05f-54f9-7574-f828-109e0ddf706f.png)

　シーン内のListItemを右クリックして、子ノードを追加からPanelContainerを追加しましょう。
　親のサイズと同じにしたいので、ビューメニューの右の⊕マークをクリックして、Rect全面をクリックします。サイズが親と同じサイズになります。また親のサイズを変更した場合も追従するようです。
![スクリーンショット (797).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/3c272f55-ef3d-b2c5-f98c-fce0dafa6e38.png)
　インスペクタのConttol/Theme Overrides/Styles/Panelの＜空＞をクリックして、新規StyleBoxTetureを選択します。
![スクリーンショット (801).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/75a83c8a-e1de-5f65-05c1-73789c4612b1.png)
　追加したStyleBoxTextureをクリックして詳細設定を開きます。
　res://assets/texture/from3D_TEXTURE/Tiles_Wall_001_basecolor.jpgをtextureにドラッグアンドドロップします。
![スクリーンショット (802).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/d617eb82-a395-f696-c1ca-f5af6ac168de.png)

# リストアイテムの背景を縁取りする
　リストアイテムの縁取りを設定しましょう。
　シーン内のPanelContainerを右クリックして子ノードを追加からPanelContainerを追加します。
　（テクスチャを設定したPanelContainerの子に、縁取り用のPanelContainerを追加します）
　インスペクタのControl/Theme Overrides/Styles/Panelの＜空＞をクリックして、新規StyleBoxFlatを選択します。
![スクリーンショット (803).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/b0aee294-af3c-49aa-a1e0-9753dea722cb.png)
　追加したStyleBoxFlatをクリックして詳細設定を開きます。
　BG Colorをクリックするとカラーピッカーが表示されるのでA(アルファ）を０にして透明にします。
　BorderWidthを開いてLeft/Top/Right/Bottomをそれぞれ2pxにします。
　またCorner Radiusを開いて、各10pxにします。
　![スクリーンショット (804).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/f78a6c7b-2fb7-0533-37ba-b742ac51343c.png)
　こんな感じですが背景としてはちょっと明るいので暗くします。
![スクリーンショット (805).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/a57e7092-a50b-a540-cb4b-a209fb9fa0e1.png)
　テクスチャを設定した方（親の方）のPanelContainerを選択して、インスペクタのControl/Theme Overrides/StylesのStyleBoxTexureの詳細情報のModulateを開いて、Colorをクリックするとカラーピッカーが表示されます。
　R=G=B=70にしてテクスチャを暗くします。
![スクリーンショット (808).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/56e6e7b7-3359-55a2-f7fb-881895152674.png)

## 文字を追加する
　次に基本形で説明した、HBoxContainerを追加します。
　HBoxContainerの子はVBoxContainerとButtonでしたが、少し余白が欲しいので、HBoxContainerの子にMarginContainerを２つ追加します。一つ目のMarginContainerがリストアイテム左側に表示する文字情報用です。2つ目のMarginContainer2が右側のボタン用になります。
　リストアイテムのサイズは400×100pxとしましたが、テキスト表示幅は300pxにします。
　Controlのサイズは自動で設定されますが、最小のサイズをControl/Layout/Custom Minimum Sizeに設定することで指定可能です。
　ひとつめのMarginContainerのControl/Layout/Custom Minimum SizeのX=300pxと設定します。
　![スクリーンショット (809).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/60568ec2-81ae-2fe8-0162-d329e80a2a4e.png)

　ひとつめのMarginContainerを右クリックして子ノードとVBoxContainerを追加します。
　またVBoxContainerの子ノードとしてLabelを２つ追加します。
　それぞれLabelNameとLabelRecordと名称変更します。
　LabelNameを選択してインスペクタのLabel/textに仮で「Taro」と入力します。
　LabelRecordを選択してインスペクタのLabel/textに仮で「Score 10000」と入力します。
![スクリーンショット (810).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/d3f8e656-56ca-c2ce-a711-2984f835a87e.png)
　文字を大きくしましょう。
　LabelNameを選択してインスペクタのLabel/LabelSettingsの右の＜空＞をクリックして、「新規LabelSettings」を選択します。
　![スクリーンショット (813).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/952746d4-356e-ff24-8c26-4a62cc5265d5.png)
　追加されたLabelSettingsをクリックして詳細設定を開き、Font/Sizeを30pxに変更します。
![スクリーンショット (814).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/46837975-53af-2562-9e8a-718d6d31cdb3.png)
　LabelRecordも同様にFonts/Sizeを30pxにします。
![スクリーンショット (815).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/b6c74c56-e34d-ca66-29a3-3c2a09c1edd0.png)
　文字列の左がふちにくっついているのでマージンを設定しましょう。
　VBoxContainerの親のMarginContainerを選択して、インスペクタのControl/Theme Overrides/Constantsを開き、Margin Leftにチェックをいれて20pxと設定します。
![スクリーンショット (816).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/721955af-a626-d38f-ed0d-7fdcb082cf4a.png)

## ボタンを追加する
　MarginContainer2を右クリックして、子ノードを追加からPanelContainerを追加します。またPanelContainerを右クリックして、子ノードを追加からButtonを追加します。
　インスペクタのButton/Textに「Info」と入力します。
![スクリーンショット (821).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/40bc5fc3-ee99-737b-d6e8-c91fc8386bfc.png)
　ボタンを右端まで広げます。
　ボタンは親のサイズまで上下にサイズを広げます。ボタンの親は子のButtonの必要サイズを最小限確保しますとなっているので、ボタンの親のMarginContainer2のサイズを目いっぱい広げたいと思います。
　ボタンの親のMarginContainer2を選択してインスペクタのControl/Layout/Container Sizing/Hrizontalの「Expand」にチェックを入れます。ボタンが目いっぱい大きくなります。
![スクリーンショット (824).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/a170fd3e-0932-b7e5-9436-4f4c249c7177.png)
　ボタンもマージンを設定します。
　MarginContainer2を選択して、Control/Theme Overrides/Constantsを開き、Margin Left/Top/Right/Bottomそれぞれにチェックをいれて10pxにします。
![スクリーンショット (819).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/22f60dfe-b703-56a7-bf87-658bcb4da32d.png)

　ボタンが目立たないのでPanelContainerを編集します。
　Buttonの親のPanelContainerを選択して、インスペクタのControl/Theme Overrides/Styles/Panelの＜空＞をクリックして新規StyleBoxFlatを選択します。
![スクリーンショット (827).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/f2116a78-0a5f-09c2-79ea-b0d1f4508c43.png)
　追加されたStyleBoxFlatをクリックして詳細設定を開き、BG ColorはR=B=G=60にしました。またBorder Widthを各1pxに設定しました。
![スクリーンショット (828).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/56d83b96-3794-594f-7c75-f0ab736ce073.png)
　下記のようにボタンが縁取りされます。
![スクリーンショット (830).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/050f1557-aff4-e6aa-df35-f1977d1bdf75.png)

# リストアイテムのスクリプトを設定
　リストアイテムは名前をハイスコアの文字列を設定する必要があります。またボタンを押下した時に他のリストアイテムと区別するためにＩＤも設定したいと思います。

　ルートノードのListItemを右クリックして、スクリプトをアタッチします。
　res://list_item.gdに保存します。
![スクリーンショット (832).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/11f343fc-4380-a105-842a-e8a4566cac13.png)

　初期設定として下記のように実装します。
```GDScript
extends Control

var m_id : int

func set_info(id, name, record):
	m_id = id
	$PanelContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer/LabelName.text = name
	$PanelContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer/LabelRecord.text = record
```

　またボタンを押下したときは、signalを発行してidを通知するようにします。
　ボタンのシグナルを接続しましょう。
　Buttonを選択して、インスペクタの右のノード/シグナルを選択します。
　BaseButtonのpressedをダブルクリックしてスクリプトに接続します。
　![スクリーンショット (833).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/36022565-2d7e-f590-7f1b-8029dc822abd.png)
　ダイアログが表示されるので、受信側メソッドを確認してListItemを選択します。
![スクリーンショット (834).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/7d26ec13-444d-a101-84ad-9d99ba74822c.png)

　スクリプトに接続されました。
```GDScript
extends Control

var m_id : int

func set_info(id, name, record):
	m_id = id
	$PanelContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer/LabelName.text = name
	$PanelContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer/LabelRecord.text = record
	

func _on_button_pressed():
	pass # Replace with function body.
```
　つづいて、ボタンを押下した場合、リストアイテムシーンからは「on_list_item_button_pressed(id)」というシグナルを送信するようにします。
 
```GDScript
extends Control

signal on_list_item_button_pressed(id)

var m_id : int

func set_info(id, name, record):
	m_id = id
	$PanelContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer/LabelName.text = name
	$PanelContainer/PanelContainer/HBoxContainer/MarginContainer/VBoxContainer/LabelRecord.text = record
	

func _on_button_pressed():
	on_list_item_button_pressed.emit(m_id)
```

# ListViewのベースとなるシーンの作り方
　次にリストアイテムを並べるシーンを作成します。
　新規シーンを作成して、ユーザインタフェースボタンを押下します。
　ルートノードとしてControlが追加されるので、ListViewに名称変更しましょう。
　また表示サイズを設定します。
　インスペクタのControl/Transform/Sizeをx=400px、y=600pxにします。
![スクリーンショット (837).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/8c73cd0e-c3c8-b18b-530e-0493e2930f5a.png)
　縦にスクロールしたいのでScrollContainerを子ノードとして追加します。
　リストアイテムは縦に並べていくので、さらに子ノードとしてVBoxContainerを配置して、VBoxContainerの子としてリストアイテムを追加するようにします。表示しきれないくらいたくさん追加するとスクロールバーが表示されます。

　ScrollContainerを選択して、ビューメニューの右の⊕の右をクリックして、「Rect全面」を選択すると、ScrollContainerが親のListViewのサイズ全面を使用するようになります。
![スクリーンショット (839).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/ebab6355-bcd6-9d71-516d-9bfa800273a4.png)

# ListViewのスクリプトの実装
　ListViewwを右クリックしてスクリプトをアタッチします。
　res://list_view.gdというファイル名で保存します。
![スクリーンショット (840).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/b6cb501c-4cb2-0089-1045-0a3d2e730d73.png)

　下記のように実装しましょう。
　また保存した後に、ListViewをクリックしてインスペクタに「M Scn List Item」が表示されるので、res://list_item.tscnをドラッグアンドドロップしてください。

```GDScript
extends Control

@export var m_scn_list_item : PackedScene

var m_test_list = [
	["Taro", "Score 90000"],
	["Jiro", "Score 80000"],
	["Saburo", "Score 70000"],
	["Shiro", "Score 60000"],
	["Goro", "Score 50000"],
	["Rokuro", "Score 40000"],
	["Shichiro", "Score 30000"],
	["Hachiro", "Score 20000"],
	["Kuro", "Score 10000"],
	["Juro", "Score 00000"],
]

func _ready():
	var id=0
	for info in m_test_list:
		var ins = m_scn_list_item.instantiate()
		ins.set_info(id, info[0], info[1])
		$ScrollContainer/VBoxContainer.add_child(ins)
		id = id + 1
		# シグナルを接続する
		ins.on_list_item_button_pressed.connect(on_list_item_button_press)

func on_list_item_button_press(id):
	print(id)
```

* **ins.on_list_item_button_pressed.connect(on_list_item_button_press)**
　リストアイテム(=ins)が発行するシグナルを「on_list_item_button_press」メソッドに接続しています。

* **on_list_item_button_press**
 リストアイテムシーンを生成時にset_infoメソッドで設定したIDが通知されるので、printしています。

　実行しましょう。
![スクリーンショット (841).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/aa1a3a90-a71f-b7db-b0ba-76068a74705d.png)

# リストアイテムを大きく表示する
　ListViewを表示するウインドウの都合で大きく表示したい場合です。
　res://list_view.tscnを開いて、ルートノードのListViewを選択します。インスペクタのControl/Layout/Transform/SizeのX=600pxに拡張します。
　次にres://list_view.gdを開いて、add_childする前に1行追加します。
```GDScript
		ins.set_custom_minimum_size(Vector2(600, 200))
```
　実行すると下記のようになります。
　今回Buttonの親のMarginContainerのControl/Layout/Container SizingのHorizontalのExpandにチェックを入れたので、優先的にボタン（の親のMarginContainer）が大きくなったのだと思います。

![スクリーンショット (842).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/20cc6b92-9d3c-d0d7-1ea5-dcbe3afd4a55.png)

# 追記
　Windowsで実行するとマウスホイールで上下に動くので便利ですが、スマホで実行するととても細いスクロールバーをドラッグするしかないので使いにくいですね。

# github

https://github.com/footinglow/Godot4/tree/main/03_study/ListViewBase

以上です。