皆さんお待ちかねのリストボックスの作り方についてご説明します。
展開可能なリストボックス | 皆さんお待ちかねのリストボックスの作り方についてご説明します。 |
コンセプト | 基本的なウィンドウの作り方はもうご存知かと思います。 |
注意事項 | このチュートリアルでは、「WARHAMMER ONLINE」のヘルプウィンドウで使用されている展開可能なリストボックスを参考事例としています。 |
最初に必要なこと | 展開可能なリストボックスを作るには、まず最初にリストボックスで表示したいデータを返す関数が必要です。 |
データの再整理 | 以下は展開可能なリストボックスの基本アルゴリズムです。最初は2段階の構造を取っていますが、それらを (HelpWindow.data 内に含まれている全てではなく) リストボックス内に現在表示されているものだけを含んだ1段階の構造に変換しています。 |
エントリーをクリックする | ここではリスト上でユーザーがクリックした時にシステムがどんな反応をするかを設定します。 |
注記 | ページ最上部で使われた画像では、リスト内のエントリー毎に背景色が異なっています。このコードは上の説明には書かれていません。もし、あなたがこの機能を追加したいのであれば、以下の関数を参考にしてください。この関数は、PrepareData()の後に呼び出してください。 |
基本的なウィンドウの作り方はもうご存知かと思います。
ListBox 構造を使えば、その中に簡単にリスト機能を実装することができます。ここでは、カテゴリー、セクション毎にまとまっていて、好きなように展開・折りたたみができるリストボックスの作成方法をご説明します。
まずは特定の方法でデータを整理する必要があります。
展開可能なリストボックスを作るには、まず最初にリストボックスで表示したいデータを返す関数が必要です。
ヘルプウィンドウでは、次の方法で HelpWindowGetTopicList と呼ばれる関数を使用します。
HelpWindow.data = HelpWindowGetTopicList()
同時に、HelpWindow.data をLuaファイルの先頭でグローバル変数として初期化します。
HelpWindow.data = {}
目的は後ほど説明しますが、もう2〜3個ほど変数・テーブルが必要です。
HelpWindow.listBoxData = {} HelpWindow.rowToEntryMap = {} numTotalRows = 0 lastPressedButtonId = 0
HelpWindow.data では、次の情報をLuaに送ります。
HelpWindow.data[1-n] // リストボックスのサブセクションを含んだテーブル HelpWindow.data[1-n].id // セクションのID HelpWindow.data[1-n].name // セクション名< HelpWindow.data[1-n].expanded // 展開中のセクション - Cコードはデフォルトでは負の値を渡します HelpWindow.data[1-n].entries[1-n] // サブセクション内の個別のエントリーを含んだテーブル HelpWindow.data[1-n].entries[1-n].id // エントリーID HelpWindow.data[1-n].entries[1-n].name // エントリー名 HelpWindow.data[1-n].entries[1-n].subSection // エントリーのサブセクション番号
以下は展開可能なリストボックスの基本アルゴリズムです。最初は2段階の構造を取っていますが、それらを (HelpWindow.data 内に含まれている全てではなく) リストボックス内に現在表示されているものだけを含んだ1段階の構造に変換しています。
function HelpWindow.PrepareData() orderTable = {} HelpWindow.listBoxData = {} numTotalRows = 1 HelpWindow.rowToEntryMap = {} if( not HelpWindow.data ) then return end table.sort( HelpWindow.data, DataUtils.AlphabetizeByNames ) for sectionIndex, sectionData in ipairs( HelpWindow.data ) do if( sectionData.expanded == false ) then WindowSetShowing( "HelpWindowPlayerListRow"..numTotalRows.."MinusButton", false ) WindowSetShowing( "HelpWindowPlayerListRow"..numTotalRows.."PlusButton", true ) else WindowSetShowing( "HelpWindowPlayerListRow"..numTotalRows.."MinusButton", true ) WindowSetShowing( "HelpWindowPlayerListRow"..numTotalRows.."PlusButton", false ) end ButtonSetTextColor("HelpWindowPlayerListRow"..numTotalRows.."Name",Button.ButtonState.NORMAL, 255, 204, 102) table.sort( sectionData.entries, DataUtils.AlphabetizeByNames ) local sectionTable = {name = sectionData.name, isSection = true, id = sectionData.id} table.insert( HelpWindow.listBoxData, numTotalRows, sectionTable ) table.insert( orderTable, numTotalRows ) local indexStruct = {index = sectionIndex, isSection = true} table.insert( HelpWindow.rowToEntryMap, numTotalRows, indexStruct ) numTotalRows = numTotalRows + 1 if( sectionData.expanded == true ) then for entryIndex, entryData in ipairs( sectionData.entries ) do WindowSetShowing( "HelpWindowPlayerListRow"..numTotalRows.."MinusButton", false ) WindowSetShowing( "HelpWindowPlayerListRow"..numTotalRows.."PlusButton", false ) ButtonSetTextColor("HelpWindowPlayerListRow"..numTotalRows.."Name",Button.ButtonState.NORMAL, 255, 255, 255) local entryTable = {name = L" "..entryData.name, isSection = false, id = entryData.id} table.insert( HelpWindow.listBoxData, numTotalRows, entryTable ) table.insert(orderTable, numTotalRows) local indexStruct = {categoryIndex = sectionIndex, index = entryIndex, isSection = false} table.insert( HelpWindow.rowToEntryMap, numTotalRows, indexStruct ) numTotalRows = numTotalRows + 1 end end end ListBoxSetDisplayOrder("HelpWindowPlayerList", orderTable ) HelpWindow.ResetAllButtons() end
上から順番に解説していきましょう。どのような順番でテーブルのメンバーが表示されるのかを決めるために、リストボックスの実装に使用するorderTable を宣言します。
それから、HelpWindow.listBoxData と HelpWindow.rowToEntryMap を空にして、全体の表示される列の番号を1にします。これはインデックスとカウンターの両方として使用されており、この関数全体を通じてしばしばそうした使われ方がされています。
orderTable = {} HelpWindow.listBoxData = {} numTotalRows = 1 HelpWindow.rowToEntryMap = {}
次のステップでは、扱うことのできるデータを持っているかを調べます。もしデータが何も無いなら、何も出来ないのでこの段階で関数を終了します。
if( not HelpWindow.data ) then return end
次に、アルファベット順にカテゴリー(セクション)を並び替えます。これはそのウィンドウへの要求に応じて、変更したり取り除いたりしてください。
table.sort( HelpWindow.data, DataUtils.AlphabetizeByNames )
オペレーターのipairsを使い、各セクションを一度づつ繰り返します。ここでの非常に重要な前提条件は、テーブルが連続したインデックスの形でCから届くということです。もしこれが保証されていないなら、全ての要素がテーブルにアクセスできるようになる前に、ipairsがループを中断するでしょう。
for sectionIndex, sectionData in ipairs( HelpWindow.data ) do
次に、セクションが展開された時に、名前の隣に「マイナス」ボタンが、逆のケースでは「プラス」ボタンが表示されるようにします。
if( sectionData.expanded == false ) then WindowSetShowing( "HelpWindowPlayerListRow"..numTotalRows.."MinusButton", false ) WindowSetShowing( "HelpWindowPlayerListRow"..numTotalRows.."PlusButton", true ) else WindowSetShowing( "HelpWindowPlayerListRow"..numTotalRows.."MinusButton", true ) WindowSetShowing( "HelpWindowPlayerListRow"..numTotalRows.."PlusButton", false ) end
個々のエントリーを判別しやすくするために、カテゴリー名を黄色にします。
ButtonSetTextColor("HelpWindowPlayerListRow"..numTotalRows.."Name",Button.ButtonState.NORMAL, 255, 204, 102)
このセクション内の個々のエントリーをソートします。
table.sort( sectionData.entries, DataUtils.AlphabetizeByNames )
次に、C言語の構造体のような目的でLuaテーブルを作成します。 HelpWindow.listBoxData に、name, isSection, idを渡せるようにし、現在のテーブルの次のセルにそのテーブルをプッシュするようにします。
HelpWindow.listBoxData[1-n].name HelpWindow.listBoxData[1-n].isSection HelpWindow.listBoxData[1-n].id
このケースでの 1-n は上で定義した numTotalRows によって決定されます。
local sectionTable = {name = sectionData.name, isSection = true, id = sectionData.id} table.insert( HelpWindow.listBoxData, numTotalRows, sectionTable ) table.insert( orderTable, numTotalRows )
最後の行では、リストボックス内に表示される要素のリストに、現在の要素を追加します。
次に、rowToEntryMap にセクション番号を追加します。このrowToEntryMapは後々、参照している HelpWindow.data 内のコンポーネントを得るのに列番号を使えるようにします。
local indexStruct = {index = sectionIndex, isSection = true} table.insert( HelpWindow.rowToEntryMap, numTotalRows, indexStruct )
カウンターを+1します(表示されるリストのために、列を追加する必要があるので)。
次に、もしセクションが展開されている場合、そのエントリーをリストボックス内に表示するためにそれらの一つ繰り返します。
if( sectionData.expanded == true ) then for entryIndex, entryData in ipairs( sectionData.entries ) do
個別のエントリーを個別のエントリーを展開したり、折りたたんだりできないようにするため、マイナスとプラスボタンの両方を見えないようにします。また、エントリーの色を白に設定します。
WindowSetShowing( "HelpWindowPlayerListRow"..numTotalRows.."MinusButton", false ) WindowSetShowing( "HelpWindowPlayerListRow"..numTotalRows.."PlusButton", false ) ButtonSetTextColor("HelpWindowPlayerListRow"..numTotalRows.."Name",Button.ButtonState.NORMAL, 255, 255, 255)
次に、listBoxData と rowToEntryMap テーブルにも、同じように上記のテーブル追加を繰り返しますが、今回はエントリー毎に一度だけします。
local entryTable = {name = L" "..entryData.name, isSection = false, id = entryData.id} table.insert( HelpWindow.listBoxData, numTotalRows, entryTable ) table.insert(orderTable, numTotalRows) local indexStruct = {categoryIndex = sectionIndex, index = entryIndex, isSection = false} table.insert( HelpWindow.rowToEntryMap, numTotalRows, indexStruct )
エントリー名の前にTab(L” “)を追加している点に注目してください。こうすることで、リストボックス内でエントリー名がインデックスにぶらさがっている風に表示されます。同様に、 indexStructがカテゴリーのindexだけではなく、このエントリーのindexも含んでいる点にも注目してください。 HelpWindow.data[categoryIndex].entries[index] を使うことにより、後々 HelpWindow.data 内でそれを見つけることができます。
そして最後に、次の関数を呼び出します。
ListBoxSetDisplayOrder("HelpWindowPlayerList", orderTable ) HelpWindow.ResetAllButtons()
最初の関数は、単純にリストボックスにorderTableの順番に HelpWindow.listBoxData からエントリーを表示するよう伝えます。二番目の関数では次のコードを実行します。
function HelpWindow.ResetAllButtons() for index, data in ipairs( HelpWindow.listBoxData ) do if( lastPressedButtonId == data.id and data.isSection == false ) then ButtonSetPressedFlag("HelpWindowPlayerListRow"..index.."Name", true ) -- set newly selected entry as pressed else ButtonSetPressedFlag("HelpWindowPlayerListRow"..index.."Name", false ) end end end
ボタンが押された時に何も反応がないと更新されたのか分かりづらいので、リストボックス内の各ボタンを少し押し下げます。
ここではリスト上でユーザーがクリックした時にシステムがどんな反応をするかを設定します。
リスト内のエントリーまたはカテゴリー上を左クリックした時に反応する関数は、OnLButtonUpPlayerRow() です。この関数が実行するのは、リストがクリックされ、その情報が以下の HelpWindow.DisplayRow() に渡された時に、リスト内の列を取得することです。
function HelpWindow.DisplayRow( row ) local index = HelpWindow.rowToEntryMap[row].index if( HelpWindow.rowToEntryMap[row].isSection ) then if( HelpWindow.data[index].expanded ) then HelpWindow.data[index].expanded = false else HelpWindow.data[index].expanded = true end else HelpWindow.ResetPressedButton() local categoryIndex = HelpWindow.rowToEntryMap[row].categoryIndex lastPressedButtonId = HelpWindow.data[categoryIndex].entries[index].id ButtonSetPressedFlag("HelpWindowPlayerListRow"..row.."Name", true ) HelpWindow.DisplayHelpEntry( HelpWindow.data[categoryIndex].entries[index].id, HelpWindow.data[categoryIndex].entries[index].name ) end HelpWindow.PrepareData() end
まず最初に、関数は rowToEntrymap から categoryIndex を検索します。それから、クリックされたエントリーがセクションやエントリーなのかを調べます。もしそれがセクションで、以前に折りたたまれたものだったなら、展開されたセクションに設定します。逆に展開されたものだったなら、折りたたまれたセクションに設定します。
if( HelpWindow.rowToEntryMap[row].isSection ) then if( HelpWindow.data[index].expanded ) then HelpWindow.data[index].expanded = false else HelpWindow.data[index].expanded = true end else
プレイヤーがクリックした列がエントリーの場合には、以前に押されたボタンを押されていない状態にします。
HelpWindow.ResetPressedButton()
次に、HelpWindow.data のためのcategoryIndexを、rowToEntryMap から入手し、lastPressedButtonId には新たに押されたエントリーIDを設定します。最後に、同じボタンにボタンが押されたというフラグを設定します。
local categoryIndex = HelpWindow.rowToEntryMap[row].categoryIndex lastPressedButtonId = HelpWindow.data[categoryIndex].entries[index].id ButtonSetPressedFlag("HelpWindowPlayerListRow"..row.."Name", true )
それから、IDとNAMEを渡す DisplayHelpEntry を呼び出します。HelpWindowの場合、渡された名前と等しいメインウィンドウの名前の設定し、ユーザーがウィンドウ用の実際のエントリーテキストを見つけるためにIDを渡します。他に実行したい動作がある場合には、ここで何でも設定することができます。
そして最後に、クリックに対する変更がリストボックス内に反映されることを確認するために、HelpWindow.PrepareData() を呼び出します。
ページ最上部で使われた画像では、リスト内のエントリー毎に背景色が異なっています。このコードは上の説明には書かれていません。もし、あなたがこの機能を追加したいのであれば、以下の関数を参考にしてください。この関数は、PrepareData()の後に呼び出してください。
function HelpWindow.SetListRowTints() for row = 1, numTotalRows do local row_mod = math.mod(row, 2) color = DataUtils.GetAlternatingRowColor( row_mod ) local targetRowWindow = "HelpWindowPlayerListRow"..row WindowSetTintColor(targetRowWindow.."RowBackground", color.r, color.g, color.b ) end end