展開可能なリストボックス

皆さんお待ちかねのリストボックスの作り方についてご説明します。

Summary
展開可能なリストボックス皆さんお待ちかねのリストボックスの作り方についてご説明します。
コンセプト基本的なウィンドウの作り方はもうご存知かと思います。
注意事項このチュートリアルでは、「WARHAMMER ONLINE」のヘルプウィンドウで使用されている展開可能なリストボックスを参考事例としています。
最初に必要なこと展開可能なリストボックスを作るには、まず最初にリストボックスで表示したいデータを返す関数が必要です。
データの再整理以下は展開可能なリストボックスの基本アルゴリズムです。最初は2段階の構造を取っていますが、それらを (HelpWindow.data 内に含まれている全てではなく) リストボックス内に現在表示されているものだけを含んだ1段階の構造に変換しています。
エントリーをクリックするここではリスト上でユーザーがクリックした時にシステムがどんな反応をするかを設定します。
注記ページ最上部で使われた画像では、リスト内のエントリー毎に背景色が異なっています。このコードは上の説明には書かれていません。もし、あなたがこの機能を追加したいのであれば、以下の関数を参考にしてください。この関数は、PrepareData()の後に呼び出してください。

コンセプト

基本的なウィンドウの作り方はもうご存知かと思います。

ListBox 構造を使えば、その中に簡単にリスト機能を実装することができます。ここでは、カテゴリー、セクション毎にまとまっていて、好きなように展開・折りたたみができるリストボックスの作成方法をご説明します。

まずは特定の方法でデータを整理する必要があります。

注意事項

このチュートリアルでは、「WARHAMMER ONLINE」のヘルプウィンドウで使用されている展開可能なリストボックスを参考事例としています。

本ページのサンプルコードはSAクライアントには同梱されていませんのでご注意下さい。

最初に必要なこと

展開可能なリストボックスを作るには、まず最初にリストボックスで表示したいデータを返す関数が必要です。

ヘルプウィンドウでは、次の方法で 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
ウィンドウの一要素であるリストボックス (ListBox) は、大量のウィンドウ要素を手作業で割り当てることなく、大きなデータテーブルとして自動表示する手段を提供します。
Close