10.17 メッセージ履歴のカスタマイズ 〜アクションボタン〜(その2)

今回はまず addActionButton メソッドから見ていくね。
確か、メッセージ履歴アクションボタンを追加するメソッド、だったよね?
§10.16 参照。
そうだよ。
ただ、スクリプトを見る前に、メッセージ履歴アクションボタンをどうやって管理してるかを簡単に説明しとくね。
わざわざ説明するってことは、変わった管理をしてるってこと?
んー、そんなに特殊な管理をしてるわけじゃないんだけど、管理の仕方が解ってた方がスクリプトも理解しやすいかなって思って。
そーなんだ。
まず、メッセージ履歴アクションボタンは必要に応じて作るようにしてるから、最初の状態ではメッセージ履歴アクションボタンは1つも無いの。
それは前回も言ってたね。
ん。で、メッセージ履歴アクションボタンを表示する必要ができて初めてメッセージ履歴アクションボタンのオブジェクトが作られるんだ。

<初期状態→メッセージ履歴アクションボタンが1つ作られた状態>

最初に作られるメッセージ履歴アクションボタンのオブジェクトは配列の最初の要素になるから、 actionButtons[0] になるわけね。
で、このボタンの visibletrue にすることで、メッセージ履歴画面に表示するのと同時に、 「このボタンは使用中」ってことが判るようにするんだ。
使わないのにメッセージ履歴アクションボタンを作る必要なんてないんだから、使用中なのは当たり前なんじゃないの?
使用中なのが判るようにする意味は後でわかると思うよ。
そーなの?
うん。
で、この状態でさらにもう1つメッセージ履歴アクションボタンを表示する場合は…

<メッセージ履歴アクションボタンが1つ作られた状態→メッセージ履歴アクションボタンが2つ作られた状態>

こんなふうに、新しくメッセージ履歴アクションボタンを作るわけね。
ちなみにこのボタンは actionButtons[1] だよ。
あと、これも visibletrue にして「使用中」の状態にするんだ。
…だから使用中なのは当然でしょ?
まぁまぁ。
で、今度は最初に作ったメッセージ履歴アクションボタンが要らなくなったとするね。
要らなくなることってあるの?
例えば、履歴メッセージをスクロールしたことで、メッセージ履歴アクションボタンが画面外に出て表示されなくなったりすると要らなくなるね。
あ、なるほどね。
メッセージ履歴アクションボタンが要らなくなると、そのボタンのオブジェクトを削除(無効化)するんじゃなくて、 そのボタンを見えなくするんだ。
つまり visiblefalse にするってことだね。

<メッセージ履歴アクションボタンが2つ作られた状態→最初のメッセージ履歴アクションボタンが不要になった状態>

見えなくする(visiblefalse にする)ことで、 そのメッセージ履歴アクションボタンは「未使用」ってことが判るようにするの。
そっか。それでわざわざボタンが使用中ってことが判るようにするって言ってたんだね。
そういうこと。
でも何でメッセージ履歴アクションボタンが要らなくなったのに削除(無効化)しないの?
メッセージ履歴を読む時はメッセージを頻繁にスクロールさせるから、 メッセージ履歴アクションボタンが画面外に出て見えなくなっても、すぐまた別のメッセージ履歴アクションボタンが表示されたりするよね。
うん、まーそうかもね。
つまり、オブジェクトを無効化してもすぐまた新しいオブジェクトを作らなくちゃいけなくなるから、 頻繁にオブジェクトを作ったり無効化したりを繰り返すより、要らなくなったら非表示にして、 必要になったら表示するだけにした方が効率的かなって思ったの。
なるほどねぇ…
で、また新しくメッセージ履歴アクションボタンを表示する時には…

<最初のメッセージ履歴アクションボタンが不要になった状態→新たに別のメッセージ履歴アクションボタンが必要になった状態>

「未使用」の状態になってた actionButtons[0] のメッセージ履歴アクションボタンを表示して(visibletrue にして)、また「使用中」の状態にするわけ。
こんな感じにメッセージ履歴アクションボタンを管理していくんだけど、管理のやり方はわかったかな?
うん、大体はね。
それじゃ今度はスクリプトの方を見てくね。

addActionButton メソッド>

function addActionButton(x, y, action)
{
    // 現在使われていないボタンを探します
    var i;
    for(i=0;i<actionButtons.count;i++)
    {
        if(!actionButtons[i].visible)
            break;
    }
    var actionButton;
    if(i < actionButtons.count)
    {
        // 見つかった場合はそのボタンを使います
        actionButton = actionButtons[i];
        actions[i] = action;
    }
    else
    {
        // 見つからなかった場合は新しくボタンを作ります
        actionButtons.add(actionButton = new LButtonLayer(window, this));
        actionButton.loadImages(actionButtonStorage);
        actions.add(action);
    }
    // ボタンの位置を設定します
    // ※フォントの高さに合わせてボタンの位置を調整しています
    if(verticalView)
        actionButton.setPos(x + (fontHeight - actionButton.width) \ 2, y);
    else
        actionButton.setPos(x, y + (fontHeight - actionButton.height) \ 2);
    // ボタンを表示状態にします
    actionButton.visible = true;
}

これってさっき言ってたやり方でメッセージ履歴アクションボタンを管理するスクリプトなんだよね?
そだよ。
とりあえず最初から見てこっか。
うん。
じゃまずはこの for ブロックは何やってるかわかる?
えーっと…まず “for(i=0;i<actionButtons.count;i++)” だから、 メッセージ履歴アクションボタンのオブジェクトを1つずつ見ていってるみたいだね。
ん。
それから、for ブロックの中で “!actionButtons[i].visible” が成り立つかどうかをチェックしてるね。
これって actionButtons[i]visiblefalse だったら成り立つんだよね。
だから、actionButtons[i] が表示されてなかったら break が実行されて、 for ブロックから外に出るのかな。
そう。つまり、メッセージ履歴アクションボタンのオブジェクトを1つずつ見ていって、未使用のボタンがあったらそこで break するってことだね。
じゃその次の if ブロックは?
if(i < actionButtons.count)” だから、 iactionButtons.count(メッセージ履歴アクションボタンの数だよね)より小さかったら if ブロックの中身を実行して、iactionButtons.count 以上だったら else ブロックの中身を実行するんだよね。
そうそう。
これって visiblefalse になってるメッセージ履歴アクションボタンがあったら if ブロックの中身が実行されて、無かったら else ブロックの中身が実行されるっぽいけど…?
そうだよ。
visiblefalse になってるメッセージ履歴アクションボタンがあったら、 その時点で break するから、ivisiblefalse になってるメッセージ履歴アクションボタンの番号になってるよね。
だね。
visiblefalse になってるメッセージ履歴アクションボタンがなかったら、 途中で break することはないから、for ループが終わるのは “i<actionButtons.count” っていう条件が成り立たなくなった時ってことになるよね。
そっか。だから visiblefalse になってるメッセージ履歴アクションボタンがなかったら、 その後の if の条件も成り立たなくなって else ブロックの中身が実行されるんだね。
そういうこと。
じゃまずは未使用のメッセージ履歴アクションボタンがあった時に実行される if ブロックの中身を見てみよっか。
actionButton っていう変数に actionButtons[i] を代入してて、 actions[i] っていう配列の要素に action を代入してるね。
actionButton は表示するメッセージ履歴アクションボタンのオブジェクトになるわけね。
で、actions は履歴アクションを記録しとく配列になってて、 actions[i]actionButtons[i] のメッセージ履歴アクションボタンをクリックした時に実行される履歴アクションだよ。
actions ってメンバ変数だっけ?
そだよ。じゃ次は未使用のメッセージ履歴アクションボタンが無かった時に実行される else ブロックね。
え〜っと…最初に LButtonLayer っていうクラスのオブジェクトを作ってて、 それを actionButton に代入してて…
さらにそれを actionButtons 配列に追加してるのかな?
※配列(Array クラス)の add メソッドについては §1.14 参照。
ん。ちなみに LButtonLayer クラスはボタンのクラスの一種で、ButtonLayer クラスを継承して作られてるんだ。
ButtonLayer クラスについては §5.3 参照。また、LButtonLayer クラスは HistoryLayer.tjs 内で定義されています。
それって普通のボタンと何か違うの?
クリックすると、親レイヤの onButtonClick メソッドが呼び出されるようになってるの。
つまり、ボタンをクリックするとメッセージ履歴レイヤの onButtonClick メソッドが呼び出されるわけね。
onButtonClick メソッドって、メッセージ履歴画面を閉じるボタンをクリックした時に呼び出されるメソッドだったよね?
onButtonClick メソッドについては §10.13 参照。
うん。メッセージ履歴アクションボタンが押された時の処理も onButtonClick メソッドでやるんだ。
あ、そーなんだ。
えっと、それから actionButtonloadImages メソッドを呼び出してるね。
これってボタンに画像を読み込んでるんだよね?
LButtonLayer クラスでは loadImages メソッドがオーバーライドされていないので、ButtonLayer クラスの loadImages メソッドと同一です。
うん。ボタン画像関係は button タグで作るボタンと同じだよ。
そーなんだ。
えっと、あとは最後に actionactions 配列に追加してるんだね。
まとめると、未使用のメッセージ履歴アクションボタンが無かった場合は、新しくボタンオブジェクトを作って、 それを actionButtons の配列の最後に追加して、 ボタンに画像を読み込んで、履歴アクションの配列の最後にこのボタン用の履歴アクションを追加してるってことだね。
読み込む画像は actionButtonStorage になってるけど、 これって Override.tjs で設定してたやつだよね?
§10.02 参照。
ん、そうだよ。
それじゃ次いくね。
ちなみにここからのスクリプトは未使用のメッセージ履歴アクションボタンがあっても無くても実行されるよ。
メッセージ履歴アクションボタンオブジェクトの setPos メソッドを呼び出してるってことは、 ボタンの位置を設定してるってことだよね?
うん。addActionButton メソッドの第1、第2引数はそれぞれメッセージ履歴アクションボタンの x, y 座標だからね。
…でも縦書きの時は setPos メソッドの第1引数が x になってないし、 横書きの時は第2引数が y になってないよ?
あぁ、これはボタンの位置を調整してるからだよ。
ボタンの位置を調整するって?
ボタンの幅(高さ)と履歴メッセージの幅(高さ)が必ずしも同じになるとは限らないでしょ。
だから、こんなふうにボタンの中心の位置と履歴メッセージの中心の位置が揃うように調整してるんだ。

<メッセージ履歴アクションボタンの位置調整>

縦書き表示の時はボタンの x 座標を調整してて、 横書き表示の時はボタンの y 座標を調整してるから、 縦書きの時に setPos メソッドの第1引数が x になってなくて、 横書きの時に setPos メソッドの第2引数が y になってないの。
なるほどねぇ。
じゃスクリプトに戻って…
後は、最後にメッセージ履歴アクションボタンの visibletrue にしたらメッセージ履歴アクションボタンの追加は完了だね。
これでメッセージ履歴アクションボタンが表示されて、「使用中」ってことになるんだよね。
そ。じゃ addActionButtons メソッドはこれくらいにして、 次はメッセージ履歴アクションボタンが押された時に呼び出される onButtonClick メソッドを見てくね。

onButtonClick メソッド>

function onButtonClick(button)
{
    if(button == closeButton)
    {
        // 閉じるボタンが押された時は
        // メッセージ履歴画面を閉じます(非表示にします)
        hide();
    }
    else
    {
        // メッセージ履歴アクションボタンが押された時は
        // そのアクションを実行します
        Scripts.eval(actions[actionButtons.find(button)]);
    }
}

そんなに複雑にはなってないから、特に問題ないんじゃないかな?
これ、メッセージ履歴画面を閉じるボタンが押された時に呼び出されるメソッドだよね。
§10.13 参照。
うん。
さっきもちょっと言ったけど、前に onButtonClick メソッドが出てきた時は、ボタンがメッセージ履歴画面を閉じるボタンしかなかったから、 メッセージ履歴画面を閉じるだけのスクリプトだったんだけど、 メッセージ履歴アクションボタンもボタンの一種だから、これも押された時に onButtonClick メソッドが呼び出されるようにしてるの。
じゃあ引数の button は、押されたメッセージ履歴アクションボタンになってるんだね。
このメソッドはメッセージ履歴アクションボタンとメッセージ履歴画面を閉じるボタンで共有するから、 button はメッセージ履歴アクションボタンかもしれないし、 メッセージ履歴画面を閉じるボタンかもしれないよ。
あ、そっか。
えっと…あ、だから最初の if のとこでどっちのボタンか見分けてるんだ。
そ。buttoncloseButton ならメッセージ履歴画面を閉じるボタンで、 それ以外だったらメッセージ履歴アクションボタンってわかるよね。
だね。
buttoncloseButton なら、前と同じように hide メソッドを呼び出してメッセージ履歴画面を閉じてるんだね。
ん、そう。
buttoncloseButton じゃなかったら、 メッセージ履歴アクションボタンが押されたってことで…その時は“Scripts.eval(actions[actionButtons.find(button)]);”が実行されるんだね。
まず、actionButtons.find(button) の戻り値がどうなるかはわかる?
find メソッドって、引数に指定されてる値と同じ値の要素を配列から探すメソッドだったよね…?
ってことは、戻り値は actionButtons 配列の中の button と同じ値の要素の番号かな?
find メソッドについては §1.14 参照。
ん、そうだね。
じゃ actions[actionButtons.find(button)] は?
さっき「actions[i]actionButtons[i] のメッセージ履歴アクションボタンをクリックした時に実行される履歴アクション」って言ってたよね。
だから、押されたメッセージ履歴アクションボタンに設定されてるメッセージ履歴アクションってことかな。
そうそう。
じゃあ Scripts.eval(actions[actionButtons.find(button)]) は?
えっと…Scripts.eval は、eval タグと同じ機能のメソッドだったはずだから…
引数を TJS 式として評価するってことになるのかな?
Scripts.eval メソッドについては §7.4 参照。
そ。actions[actionButtons.find(button)] には、メッセージ履歴アクションとして実行するスクリプト(TJS式)が文字列で入ってるから、 Scripts.eval メソッドを呼び出すことで、実際にアクションが実行されるわけだね。
そーいえば、前回メッセージ履歴アクションが TJS 式を表す文字列だから厄介みたいなことを言ってたよね?
あれって結局どういうことなの?
メッセージ履歴アクションが TJS 式を表す文字列になってること自体は別に何も問題ないんだけど、 場合によっては注意して使わないと、思ったように動作しなくなっちゃうこともあるんだ。
そーなの?
特に、マクロの中で hact タグを使う場合はね。
例えば、こんなふうにマクロを作るとうまく動かなくなるね。

<正しく動作しない例>

[macro name=myhact]
[hact exp="kag.se[0].play(%['storage' => mp.storage])"]
[endmacro]

[myhact storage="voice.wav"]こんにちは。[endhact]

…この myhact マクロって、メッセージ履歴アクションで再生するファイルを storage 属性で指定できるマクロ、なのかな?
「そういうつもりで作ったマクロ」だね。
え、どういうコト?
これだと、メッセージ履歴アクションが実行される時、つまり Scripts.eval メソッドが実行される時に渡される引数は、 "kag.se[0].play(%['storage' => mp.storage])" になるよね。
そーだね。
…特に問題なさそうに見えるけど?
mp”ってなんだっけ?
マクロに指定されてる属性が記録されてる辞書配列じゃなかったっけ?
§4.8 参照。
正確に言うと、“現在実行中のマクロ”に指定されてる属性が記録されてる辞書配列、だね。
ん? それってどういうこと?
つまり、この“mp.storage”は、myhact マクロを実行してる時は確かに属性が記録されてる辞書配列になってるんだけど、 myhact マクロの実行が終わった後(この例の場合だと「こんにちは。」っていうメッセージが表示される時だね)には、 もう myhact マクロに指定されてる属性が記録されてる辞書配列じゃなくなっちゃってるってことだよ。
え、そーなの?
そーなの。
メッセージ履歴アクションボタンをクリックした時には、普通は何かのマクロを実行してる最中じゃないから、 mp.storage 自体が無くなっちゃってるんだ。
※マクロ実行中以外は mpnull(何のオブジェクトも示していない)になっていますので、メッセージ履歴アクションを実行しようとすると「null オブジェクトにアクセスしようとしました」という例外が発生します。
そっか…だから、"kag.se[0].play(%['storage' => mp.storage])" を実行しても、ちゃんと再生されないんだ?
そういうこと。
じゃあこの場合はどうすればいいの?
hact タグの exp 属性に指定する文字列の“mp.storage”の部分が“voice.wav”に置き換えられるようにすればOKだよ。
こんな感じにね。

<正しく動作する例>

[macro name=myhact]
[hact exp="&'kag.se[0].play(%[\'storage\' => \'' + mp.storage + '\'])'"]
[endmacro]

[myhact storage="voice.wav"]こんにちは。[endhact]

ちょっとややこしくなるけど、エンティティを使って、exp 属性の“&”以降の部分を予め TJS 式として評価しとけば大丈夫。
それって 'kag.se[0].play(%[\'storage\' => \'' + mp.storage + '\'])' の部分ってこと?
うん。文字列の中の '\' って書いてるからちょっとわかりにくくなってるけど、 exp 属性に指定する文字列は "kag.se[0].play(%['storage' => '" + mp.storage + "'])" になるから、 mp.storage"voice.wav" だと、exp 属性の値は "kag.se[0].play(%['storage' => 'voice.wav'])" になるの。
確かにわかりにくいね…
この方法の他にも、TJS 式の文字列を生成するメソッドを使ったやり方が KAG System リファレンスに載ってるから、参考にしてみてね。
※KAG System リファレンスの「TJSをもっと使うために」のページの「hactタグの応用」の項目に掲載されています。
はーい。
あと残ってるのは dispUninit メソッドとデストラクタ関係ぐらいかな。
dispUninit メソッドって、メッセージ履歴画面を閉じる時に呼び出されるメソッドだったよね?
dispUninit メソッドについては §10.15 参照。
ん。メッセージ履歴画面を閉じる時には、メッセージ履歴アクションボタンを削除するようにしてるから、 こんなふうに deleteActionButtons メソッドを呼び出す部分を追加してるんだ。

dispUninit メソッド>

function dispUninit()
{
    deleteActionButtons(); // メッセージ履歴アクションボタンを削除します
    deleteMessageLayer(); // メッセージ履歴表示用レイヤを削除します
    super.dispUninit(); // スーパークラスの dispUninit メソッドを呼び出します
}

ちなみに deleteActionButtons メソッドはこれね。

deleteActionButtons メソッド>

function deleteActionButtons()
{
    // すべてのメッセージ履歴アクションボタンを無効化します
    for(var i=0;i<actionButtons.count;i++)
        invalidate actionButtons[i];
    actionButtons.clear();
    actions.clear();
}

最初の for のところでボタンのオブジェクトを無効化して…
あとは actionButtonsactions の配列の中身を削除してるんだね。
clear メソッドについては §1.14 参照。
そ。で、デストラクタはこれね。

<デストラクタ>

function finalize()
{
    deleteActionButtons(); // メッセージ履歴アクションボタンを削除(無効化)します
    deleteMessageLayer(); // メッセージ履歴表示用レイヤを削除(無効化)します
    invalidate scrollBar if scrollBar !== void// スクロールバーを無効化します
    super.finalize(); // スーパークラスのデストラクタを呼び出します(closeButton はここで無効化されます)
}

deleteActionButtons メソッドが追加されてるだけみたいだね。
ん。それだけだよ。
さて、かなり長くなっちゃったけど、メッセージ履歴画面のカスタマイズは大体こんなところだね。
メッセージ履歴画面をカスタマイズするのも結構大変だねぇ。
まぁね。
でも色々カスタマイズしてみるとメッセージ履歴画面の仕組みがわかったりして、それなりに面白いんじゃないかな。
まーそれはあるかもね。
それじゃ、メッセージ履歴画面のカスタマイズはこれでおしまい。
次は何するの?
今まではわりと基本的なことをやってきたと思うから、次はちょっと趣向を変えてみようと思うんだ。
趣向を変えるって?
次はね、インターネットを使って色々データをダウンロードして使える機能をつけてみるつもりだよ。
えっ、そんなことできるの?
んー、元々吉里吉里にはインターネットに接続する機能が用意されてないから、普通はできないんだけどね。
だよねぇ。そんな機能があるって聞いたことないし…
でも、「普通はできない」ってことは、何か特別なことをすればできるようになるってコト?
特別ってほどじゃないとは思うけど、まぁそんな感じかな。
詳しくは次回からやっていくことにするね。
うん、わかった。
それじゃ、次の章もがんばってついてきてね!


前へ | TOP | 次へ