graphics.hatenablog.com

技術系テクニカルアーティストのあれこれ

GUIとレイアウトの基本

こないだ会社で軽く説明したかんじ、意外と需要がありそうだったのでまとめておく。
ツールプログラミングの第一歩。理論編。

基本のクラス構造

詳細設計はフレームワークによって様々だし、実際にはもっと複雑だけど、ざっくりまとめるとだいたいこんなかんじ。

f:id:hal1932:20190630145524p:plain
基本の継承関係

EventDispatcher

名前の通り「イベントを Dispatch(送り出す)人」で、これがないと始まらない。

UI 系はユーザーからの入力を如何に受け取るかがとても大事で、「ユーザー入力」は「Event」というかたちに抽象化されて、モジュール間を行き来することになる。

たとえば「ウィンドウ内に配置されているボタンがクリックされた」ときは、だいたいこんなかんじの挙動になる。*1

  1. ウィンドウが Clicked イベントを発行する。
  2. クリックされた位置がボタンの描画範囲内であれば、ボタンが Clicked イベントを発行する。

とりあえずここでは、「ユーザー入力が Event というかたちに抽象化される」「モジュール間で Event の受け渡しが起きる」ということだけ理解できれば問題なし。

Widget

f:id:hal1932:20190630155455p:plain
Widgetの例
「ボタン」や「チェックボックス」「テキストラベル」といった実際の UI 部品のことを Widget と呼ぶ。*2

実際にユーザー入力を受け取ったり、ユーザーに何らかの情報を提示するために使われる。必然的に、実際に画面上に描画されてユーザーが視覚的に認知できる要素であることが多い。*3

Layout

f:id:hal1932:20190630150458p:plain
Layout内にButtonを横並び配置した例
Widget をいいかんじに配置するための仕組みを Layout と呼ぶ。*4

Widget の具体的な座標値を決め打ちで手動配置することもあれば、Widget の大きさなどから配置場所を計算して自動配置*5を行うこともある。特に後者の自動配置システムのことをレイアウトシステムと呼ぶ。利用するフレームワークによって様々なレイアウトシステムがあるが、基本とも呼べる配置系がいくつかあるので、それについては後述する。

また、Layout は再帰的に配置することもできる。*6
妙に複雑な配置の場合は、どういった再帰(ツリー)構造になっているかを考えてみると良い。

f:id:hal1932:20190630163359p:plain
横向き Layout(緑)の中に縦 Layout(赤)を配置した例

ItemsWidget

f:id:hal1932:20190630145557p:plain
ItemsWidget/Widgetの包含関係
f:id:hal1932:20190630151314p:plain
複数のTextLabel Widgetを内包するListWidget

Widget の中には「子 Widget を持つことができる Widget」というものがあり、ここではそれを便宜的に ItemsWidget と呼ぶ。*7

大抵の ItemsWidget にはレイアウトシステムと同様の仕組みが実装されていて、子 Widget の配置は自動的に決まることが多い。

自動レイアウトの基本形

以下にあるレイアウトは大抵のフレームワークで提供されているので、暗記しておくと良い。*8

Stack 系

子供を縦横一直線に並べるためのレイアウト。*9
並べる方向によって Row 系と Column 系に分かれる。

Row (Horizontal)

f:id:hal1932:20190630150458p:plain
Stack/Horizontal

横向きに並べるためのレイアウト。RowLayout, HorizontalStack とも呼ばれる。

Column (Vertical)

f:id:hal1932:20190630150543p:plain
Stack/Vertical

縦向きに並べるためのレイアウト。ColumnLayout, VerticalStack とも呼ばれる。

Grid 系

子供を格子状に並べるためのレイアウト。縦横に配置される子供の数をどう決めるかによって Fixed 系と Flow 系に分かれる。

Fixed


f:id:hal1932:20190630170413p:plain
f:id:hal1932:20190630170441p:plain
Grid/Fixed

縦横に並べる個数をあらかじめ決めておく格子状レイアウト。
レイアウト自身がリサイズされても、縦横に並ぶ個数は変わらない。

ちなみに、Stack 系を組み合わせることでも似たようなことが実現できる。行や列ごとに「並べる子供の数」や「子供の大きさ」を変えたいときは、こちらを使うほうが制御しやすいことが多い。

f:id:hal1932:20190630165500p:plain
VerticalStack(緑)の中に複数の HorizontalStack(黃+青)を配置した例

Flow


f:id:hal1932:20190630150943p:plain
f:id:hal1932:20190630151009p:plain
Grid/Flow

縦横のサイズに応じて並べる個数を動的に変える格子状レイアウト。*10
レイアウト自身がリサイズされると並ぶ個数も変わるので、いくつ並べればいいか分からないときに便利。

ScrollWidget

f:id:hal1932:20190630164438p:plain
Scroll + VerticalStack

Layout ではないけど覚えておきたいものとして、ScrollWidget がある。*11
Widget や Layout をこれの子供にすると、子供の描画範囲が自分の描画範囲よりも広いときにスクロールできるようになる。*12

レイアウトシステムを使うときの必然として、「レイアウト後の配置」が実行するまで確定しない。つまり、各 GUI 要素があらかじめ用意していた範囲よりも大きく描画されてしまう*13ことがある。そういった場合が想定されるときは、あらかじめ Scroll を仕込んでおくと良い。*14

親子間での Event Routing

最後に、少しだけ小難しい話題として Event Routing *15を取り上げておく。


f:id:hal1932:20190630152222p:plain f:id:hal1932:20190630152827p:plain
Window - Group - ColumnStack - Button

この図にあるように、GUI というのは階層構造として組まれることになる。
まず OS がユーザー入力を受け取り、それを各アプリのメインウィンドウに通知する。通知を受け取ったメインウィンドウ*16は、それを自分の子供に伝える。そして、子供を持たない UI 要素*17まで通知が届いたら、今度はそれを親に返す。このとき、イベントが子供に伝わる処理を tunneing/captureing/previewing、親に戻す処理を bubbling と呼ぶ。

基本的には、bubbling だけを意識しておけば良い。*18

例えば「子供の UI 要素が無効化されている」場合、レイアウトシステムがそれを検知して、処理負荷軽減のためにイベントの tunneling を中断したりする。
また、何らかの理由で「それぞれ異なる階層にある UI 部品同士を連携させたい」場合や、「イベントの通知を偽装したい」場合などに、tunneing や bubbling の片方または両方をフックしてやることがある。

ただ、Event Routing のフック処理なんてものは例外的な処理だと考えるほうが無難だと思う。特殊処理であることを明確に意識して実装するなら構わないが、基本的にはまず、プログラム設計のほうを見直すことをおすすめしたい。

*1:実際には、まず最初にマウス処理を請け負うドライバソフトウェアが Clicked を発行して、OS がそれを受け取る。そのクリック位置がウィンドウの描画範囲内だったときに、OS のウィンドウ管理システムが、ウィンドウにイベント処理を移管する。

*2:フレームワークによっては Control と呼ばれることもある。

*3:「状況に応じて表示/非表示を切り替えられる Widget」というのも存在するが、なんにせよ「表示可能」というのがひとつの大切な要素ではある。

*4:フレームワークによっては Panel や Block と呼ばれることもある。

*5:ある程度の規模の GUI だと総 Widget 数が 4 桁以上になることも決して珍しくはないので、実際のところ自動配置がないとやってられない場合が多い。

*6:というか、ひとつの Layout だけで完結するケースはあまり多くない。大抵の GUI再帰的に配置されている。

*7:一般的な名前があるのかはよく分からない。他にも ItemsControl や ItemView といった呼び名がある。

*8:というか、世の中にある自動レイアウトの大半は、これらの亜種や、これらを組み合わせたものだったりする。

*9:フレームワークによっては Box とも呼ばれる。

*10:フレームワークによっては Wrap とも呼ばれる。

*11:フレームワークによっては ScrollView とも呼ばれる。

*12:WidgetではなくLayoutにみえるかもしれないが、これ自体はScrollBarという「ユーザー入力の受け取り手」の存在を前提としていることもあり、基本的にはWidgetに分類される。

*13:Widget がたくさん追加された場合など

*14:実際、List 系の Widget にはあらかじめ Scroll が仕込まれていることも多い。

*15:フレームワークによっては Event Propagation とも呼ばれる。

*16:ルートノード

*17:リーフノード

*18:そもそも、フレームワークによっては bubbling 用のインターフェイスしか実装されていない。