Vz resources : MACRO FAQ
2008-05-22T21:39:56+09:00

VZ Macro FAQ - メニュー

メニューに設定したアイテムの文字列の先頭番地は変数pm(選択したメニューのメッセージポインタ)に格納されます。したがってpmが指す番地を先頭とする文字列は,出力することが可能です。

1 ^^ ""                 ;入力窓やコマンドラインでも使用可能
        !01 (r<0)?.
        &?("%s",pm)
*p
01      "選択アイテムを出力",20,4
        "りんご" "みかん" "かき" "もも"

もしもメニューに選択用ダイレクトキーをセットしている場合は,出力したい位置までのバイト数を足します。例えば,"A りんご" というアイテムで「りんご」だけを出力したい場合は,「&?("%s",pm+2)」のように指定します。

メニューで正常にアイテムが選択された場合,メニューの返り値は選択されたアイテムの番号になります。アイテム番号は0から数え始めますので注意してください。

1 ^^ "" ?.
        !01 (r<0)?.
        &m("%d番のアイテムが選択されました.",r)
*p
01      "選択アイテム番号を表示",20,4
        "りんご" "みかん" "かき" "もも"

メニューの構造は次のようになっています。便宜的にメニューの先頭をメニュー番号の位置とし,メニュー先頭を0とした相対番地で示します。

メニュー先頭からの相対番地値を使用する要素
0メニュー番号
1-2メニューのサイズ(下位・上位)
3横幅
4アイテム数
5パラメータ幅
6選択アイテム番号(初期値0)

通常,メニューのカーソルは初回起動時は一番上にあり,二回目以降は前回に選択したアイテム上にあります。マクロを起動して表示されたメニューのカーソルを常に特定の位置に固定するには,メニューに記録された情報を強制的に書き換える必要があります。

次の例は,メニューのカーソルを常に二番目のアイテムに置きます。

1 ^^ "" ?.
        &> r.6=1,       ;起動時にメニューのカーソル位置を設定
        !01 (r<0)?.
        &m("%sが好きです.",pm)
0:      (2+$)   ;--- Top of Menu ---
*p
01      "みかんが好き",20,4
        "りんご" "みかん" "かき" "もも"

通常のメニューは,次のアクションのために複数のアイテムから選択させるという使い方をしますが,ヘルプメニューのような場合は選択の必要がなく,むしろメニューカーソルがあると見づらい,格好悪いということになります。このようなときはメニューを選択型でなく参照型とすることができます。

メニューの書式は次のようになっています。

番号 "[タイトル]",幅,アイテム数 [,パラメータ幅 [,-1] [;コメント] ]
"アイテム1" "アイテム2" …

大括弧[と]に囲まれた部分は省略可能な項目です。したがってメニューの先頭行に必要なのは,番号 "",幅,アイテム数だけです。以下の項目のうち「-1」とあるのが参照型メニューとするための特別な値です。これを記述する場合は,ひとつ前のパラメータ幅の項目が省略できないので,カンマだけは余分に入れます。カンマを忘れると,参照型とならずにカーソルが表示され,パラメータ幅が最大になって枠線を消してしまいますので,すぐに気付くでしょう。

1 ^^ "" ?.
        !01
*p
1      "",20,4,,-1
        "りんご" "みかん" "かき" "もも"

前項でメニューの書式を簡単に書きましたが,アイテムについてはかなり省略したので,改めてアイテムの書式を説明します。

ひとつのアイテムの詳細な書式は次のようになります。

"<キー>アイテム[<TAB><ON><SPC><OFF>]"[,#nn|&nn|!nn|オプション]

具体的にどのようなところで使われているかと言えば,モードメニューです。例えば次のような記述です。

* P メニュー

2       "",30,14,6
        "1 システム情報の表示",Ds
        "2 画面の横幅(2〜250)",WE
        "3 ページング単位$(9)全画面 半画面",Eh

オプションの値が変更できるのは,このような形式で設定し,かつパラメータの表示幅を指定している場合(この例ではパラメータの表示幅は6文字が指定されています)です。それでは順に見ていきましょう。

パラメータにON/OFFを表示

1の例です。「"1 システム情報の表示",Ds」のように,表示すべき値が「ON/OFF」で判別できる(あるいは「ON/OFF」でないと判別できない)スイッチの指定に使われます。[CR][Enter])で選択すると「ON/OFF」を切り替え,変更後もメニュー表示を継続します。ダイレクトキーで選択した場合は,変更後にメニューから抜けます。

パラメータに値をを表示

2の例です。「"2 画面の横幅(2〜250)",WE」のように,値そのものを表示しなければならないオプションの指定に使われます。選択すると入力窓に切り替わって,入力後はメニューに戻ります。(ダイレクトキーで選択した場合は,即座にメニューから抜けます。)

パラメータに設定した文字列を表示

3の例です。「"3 ページング単位$(9)全画面 半画面",Eh」のように,「ON/OFF」よりも判別しやすい対の文字列をスペースで区切って設定することもできます。アイテムの文字列を工夫すれば「ON/OFF」でも十分な場合もあります。この例では「"3 半画面ページング",Eh」とすることもできるでしょう。

メニューから別のメニューを呼び出すことができます。メニューのアイテムに続けてカンマとメニューの呼出命令を記述します。

        "N 次のメニュー",!02

サブメニューからオートセーブの設定を呼び出す具体例が製品版にありますので,参考にしてください。

3       "",26,13  #45 #52 #54 #66 #67 &26 &27 #68 &15 #79 #83
        "S オートセーブの設定",!06
        "$4EMS free/total: $ef/$em"

6       "【オートセーブ】",23,3,4
        "T ディレイタイム",Qt
        "C 入力文字数",Qc
        "W 確認待ち時間",Qw

さて,メニュー同士をリンクさせていけば「リングメニュー」?になるであろうことは,特に説明を要するものではないと思います。ここではもう一歩踏み込んで,前後のメニューを簡単に渡り歩く手法を紹介します。

メニューマクロで最も一般的であるのが外部マクロ選択メニューだと思われます。その代表が見米快之榊原知両氏によるEXMENU.DEFで,このマクロでは左右のカーソルキーで前後のメニューを渡り歩くことができるようになっています。

まず,左右の矢印キーを押すことで特定の動作をさせるには,メニュー表示中に特殊キーが押されたときの振る舞いを理解する必要があります。別の項目として用意してあるので,まず「メニューで押された特殊キーに応じた動作をさせるには」を参照してください。

それでは本題です。

左右カーソルキーに応じてメニュー番号の値を増減させ,1〜30の範囲でモジュール内に実在するメニューを探し,あればそのメニューを,なければ前後の一番近い番号のメニューを再度探して表示するという動作になります。

これを実現するコードをEXMENU.DEFから抜き出して構成すると,次のマクロのようになります。

= menutest =
* m
1 ^^ ""
        t1,                     ;メニュー番号初期値
 :m     &Sp(pu,"!%02dr",t) &e(pu)
        (r>=0)?{ n=r, >a }
        (r+1)??.
        n=r>>8 & $7F,
        (r&$FF+256)>!{
                (#s)&04         ;[<--]前メニュー
                (#d)&05         ;[-->]次メニュー
                (#h)(t=t>1)     ;[BS]ルートメニュー
        }
        (t)?>m .
 :a     ;;以下は選択したアイテムに応じた動作をさせる

; - - - ローカルマクロ - - -;
;       tはメニュー番号($E1〜$FEの30枚が通常メニューとして使用できる)
;       ($FFはモジュールメニューの専用番号だから使用しない)
04:     ;前メニュー
        (t--)??t30, &10 ??>^

05:     ;次メニュー
        (t++>30)?t1, &10 ??>^

10:     ;メニュー検索
;       モジュール内のメニューをリンクポインタをたどって探す
;       難しいのでブラックボックスをごっくんということで
;-->
;t=メニュー番号
;<--
;r=0:メニュー無し
;r=その他:メニューの先頭アドレス
        r=mg..2..0-3,           ;実行中マクロの先頭
:A      r+=r++..0+2,
        (r.0)>!{(t+$E0)>B (0)>Z ($FF)>Z } >A
:B      (r+r.4*2+9).
:Z      (0).

* p
1 "壱",12,3
        "アムロ" "セイラ" "シャア"
2 "弐",12,3
        "マチルダ" "ガルマ" "カイ"
3 "参",12,3
        "ララァ" "キシリア" "ギレン"
*

メニューは通常マクロファイルの内部に呼出番号もアイテム固定されていますが,これは静的なメニューにあたります。

動的なメニューは生成場所により,次の三種類に分類されます。

  1. 静的メニューのアイテムを書き換える
  2. モジュール内部のバッファにメニュー構造を構築する
  3. モジュール外部のバッファにメニュー構造を構築する

1の半動的とも言えるメニューは,単一のメニューで構造さえ理解していれば比較的容易に生成できます。歴史的にも,比較的早い段階に自然発生的に開発された技術です。古典とも言えるNOMAD氏のcalender.defが代表的な使用例です。メニューのヘッダ部分にはほとんど手を付けません。

2の内部バッファ方式,3の外部バッファ方式とも,メニューのヘッダ部分から構築しますのでアイテム数や表示幅なども自由に設定可能です。

内部バッファ方式の代表例はsaki...氏のstkHELP.defTOM氏のvztags.defなどで,バッファを確保しているため環境による制約はありませんが,バッファの分だけマクロが大きくなります。

外部バッファ方式の代表例は,Yushi氏の高速カレンダーEmmett氏のstkmenu.defなどで,その多くはラインバッファ上にメニューを構築しています。モジュール内部にメニュー用のバッファを確保しなくてもよいのでマクロのサイズが小さくできますが,環境によってラインバッファの大きさが異なることを念頭に置く必要があります。

動的メニューの構築方法はいずれも同じです。メニューの構造は決まっているため当然のことなのかもしれません。ですが,呼出方法はそれぞれのマクロで色々と工夫されていてさまざまです。詳しいことは,個々のマクロを参照してください。

原型マクロ

次のマクロは,マクロの魔術師と呼ばれたYushi氏が動的メニューの原型を発表したときのものです。(nifty:FGALPK/mes/13/2685, 1996/01/27 01:16)

=T12=
* M

1""
        ?.
        &09
        p.0=15,                 ; メニューの横幅
        p.1=i=2,                ; アイテム数
        p+=2,
        (i*2+4){ p.0-, p++, }
        &Sc(p,"==MENU==") p=r+1,
        &Sc(p,"menu1") p=r+1,
        &Sc(p,"menu2") p=r+1,
        !01
 
9:      p$-4, p+=5+p..0,
97:     "$[900]"
*

文字数は固定という制限はありますが,メニューの構造を理解することで,タイトルの文字列を書き換えること比較的簡単にできます。まず,メニューの構造を理解するために「メニューカーソルを常に特定の位置に固定するには」を参照してください。

さて,メニューの構造体のなかでカーソル位置を制御する値の位置を求めるのは簡単ですが,タイトルの始まる位置を求めることは少しばかり計算が必要になります。メニューの先頭を0番地としたとき,アイテム数は4番地にあります。このアイテム数に応じてタイトルの番地が変動するので,まずアイテム数を取得してからタイトル位置を計算することになります。

まず,書き換えたいメニューの直前にアンカーを埋め込みます。

そしてタイトルにダミーの文字列で書式を定めて充填しておきます。ダミーですから,同じバイト数の空白でも構わないのですが,書式を確認する意味でも所期の出力を模した文字列で埋めておくのがよいでしょう。

00:     (3+$).
* popup menu
1       "0000/00/00(・) 00:00:00",27,10
        "A" "B" "C" "D" "E" "F" "G" "H" "I" "J"

この状態でバッファがどのようになっているか,0番マクロの先頭以降64バイトを16バイトずつダンプしてみます。

                                0  1  2  3  4  5  メニュー先頭からのバイト数
 80 07 00 28 33 2B 24 29-2E 00 E1 46 00 1B 0A 00  ...(3+$)...F....
 ↑   ↑  ( 3 + $ ) . ↑ ↑   ↑  ↑ ↑ ↑
 │   │                    │ │   │  │ │ └─ パラメータ幅(B)
 │   └─ マクロサイズ(W)  │ │   │  │ └─ メニューアイテム数(B)
 └─ ローカルマクロ番号(B) │ │   │  └─ メニュー幅(B)
      (0+128==$80)        │ │   └─ メニューサイズ(W)
 ローカルマクロ終端のヌル ─┘ └─ メニュー番号(1+224==$E1)

0番マクロを実行すると,戻り値rにメニューの先頭アドレスが格納されます。そのアドレスから4バイト先にあるのがメニューのアイテム数が記録されている位置です。r.4とポインタを使用するとその数値が取得できます。

  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21  メニュー先頭からのバイト数
 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
 ↑   ↑ │1  2  3  4  5  6  7  8  9 10 11 12 13
 │   │ │←メニューアイテム1個につき2バイトのパラメタ用エリア(i*2)
 │   └─ システムが使用するリンク先アドレス(W)
 └─ 選択アイテム(メニューカーソル)位置(B)

 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37  メニュー先頭からのバイト数
 00 00 00 00 00 00 00 30-30 30 30 2F 30 30 2F 30  .......0000/00/0
 14 15 16 17 18 19 20 0 0 0 0 / 0 0 / 0
 パラメタ用エリア →│↑
                    │└─ タイトル文字列先頭

メニューの先頭から9バイト先に,メニューアイテム1個につき2バイトのパラメタ用エリアがあります。この領域のサイズがアイテム数に応じたものになりますので,取得したアイテム数を2倍して9を足すと,タイトル文字列先頭位置が確定します。

 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53  メニュー先頭からのバイト数
 30 28 81 45 29 20 30 30-3A 30 30 3A 30 30 00 41  0(・) 00:00:00.A
 0 (   ・  )   0 0 : 0 0 : 0 0 ↑ A
                                           │ ↑
                タイトル文字列終端のヌル ─┘ │
         メニューアイテム文字列1個目の先頭 ─┘

次のマクロは,表示されるメニューのタイトルを,当日の日付・起動時の時刻に変更するという例です。

= test = by H_ear
#if 0
メニューのタイトルを書き換えるテスト
#endif
* mad for vz!
1 ^\ "" ?.
        ww="日月火水木金土",    ;曜日のテーブル
        &02                     ;起動時の日時を取得
        &00 i=r.4,              ;メニューアイテム数を取得
        r+=i*2+9,               ;タイトルの先頭アドレスをrに保存
        &Sp(r,"%4d/%02d/%02d(%2s) %02d:%02d:%02d",y,m,d,ww+w*2,j,f,b)
        !01                     ;メニューを呼び出す

        ;; ==== 略 ====

2:      ;; ==== 日時の取得 ====
        ax=$2A00, &i($21)
        y=cx, m=dx>>8, d=dx&$ff, w=ax&$ff,
        ax=$2C00, &i($21)
        j=cx>>8,f=cx&$ff,b=dx>>8)

00:     (3+$).
* popup menu
1       "0000/00/00(・) 00:00:00",27,10
        "A" "B" "C" "D" "E" "F" "G" "H" "I" "J"
*

出力バイト数と出力先の(ダミー文字列の)タイトル長は,「きっちり」合わせる必要があります。書式つき文字列で数値の桁数をかならず指定し,変数の値は指定桁を溢れないようにしましょう。

タイトル長よりも出力バイト数が少ないと,最初のアイテムの前に余分なアイテムが挿入され,逆にタイトル長よりも出力バイト数が多いと,溢れた桁数に応じてアイテムが潰されてしまいます。その理由は,文字列の出力により終端のヌルが埋め込まれるため,そのヌルの出力される位置が変わるとヌルで区切られる文字列の個数が変動してしまうからです。

メニュー表示中にコマンドキーが押されると,メニューはキャンセルされますが,どのコマンドキーが押されたかは返り値を参照することで知ることができます。しかし,条件分岐に利用するには,得られた値を加工しなければなりません。その加工の仕方を次に示します。

まずメニューの返り値(2バイト)の構造ですが,最上位ビット(第15ビット)は特殊キーが押されたことを示すフラグになっています。最上位ビットが立つと,符号付き整数では負の数になりますので,メニューで特殊キーが押された場合の返り値はすべて負の数になるということになります。そして下位の7ビット(第0ビット〜第6ビット)にコマンド番号が格納されます。もしもコマンドに割り当てられておらず,マクロに割り当てられたキーである場合は,下位から8ビット目(第7ビット)がマクロキーのフラグとして立ち,下位7ビットはマクロ番号をビット反転させた値になります。

┌─┐
│15│特殊キーフラグ(特殊キーが押された時は立つ=32768(8000h)加算)
├─┼─┬─┬─┬─┬─┬─┐
│14│13│12│11│10│ 9│ 8│カーソル位置(0〜127の範囲)
├─┼─┴─┴─┴─┴─┴─┘
│ 7│マクロキーフラグ(マクロキーが押された時は立つ=128(80h)加算)
├─┼─┬─┬─┬─┬─┬─┐
│ 6│ 5│ 4│ 3│ 2│ 1│ 0│コマンド番号,または
└─┴─┴─┴─┴─┴─┴─┘ビット反転したマクロ番号

(r>=0)?{n=r,>C}       ;r>=0 なら通常の入力
(r+1)??.              ;r=-1 なら[ESC]
n=r>>8 & $7F,         ;n=メニューカーソル位置
(r&$FF+256)>!{        ;コマンド番号で分岐
        (#v)>01
        (#21)(c1,>C)
}

これは裏技の類ですが,リードオンリーメニューを動的に構築したときに,「使える」技です。

通常,メニューで上下カーソルキーを押すと,メニューカーソル上下に移動します。これはリードオンリーメニューであっても同じ動作となり,キー入力に伴う返り値を取得することはできません。次のサンプルは,メニューで押されたキーの返り値を表示するというものです。

= m_result =
*
1 ^\ "メニューの特殊キー返り値"
        ?. !01 &> (r!=-1)?>^ &d #] .
0:      a=r&$ff,
#if tVZ
        &m("Result: %d %u $%x %b / &$FF: $%02x(%d)['%c'] {&%02d}",r,r,r,r,a,a,a,$ff-a)
#else
        &m("Result: %d %u $%x / &$FF: $%02x(%d)['%c'] {&%02d}",r,r,r,a,a,a,$ff-a)
#endif
        (r)
* p
1       "",34,1,,-1
        "...Hit any key ↑=FF05h ↓=FF06h"
*

このように通常は上下カーソルキーの戻り値は得られませんから,苦し紛れにメニュー内部に表示してごまかしています。

しかし,メニューの中で擬似カーソルを動かしたり,上位・下位のイメージでサブメニューに遷移したりするような場合は,上下カーソルキーも使って直感に近い自然なインターフェイスにしたいと誰もが思うでしょう。これを実現するのが,魔術師Yushi氏の開発したコマンドキーテーブルのスワップ技です。

コマンドキーテーブルというのは,コマンドキーの割り当てをコマンド番号順に保存している領域で,vp+102(pq..99)を先頭アドレスとする200バイトの領域に1コマンドあたり2バイトずつで記録されています。コマンド番号#01から#99の198バイト+2バイト(FFFFh)で,#00で定義されているプリフィクスキーは含まれませんので注意してください。

スワップするためには必ず相手が必要です。また入れ替えるのはメニューを表示しているときに限られ,メニューから出たら元に戻さなければなりません。相手は通常使っていないコマンドで,かつ一対の組コマンドであるものが適しています。また,メニューで戻り値を取得するのが目的なので,機能がかち合わないように特殊キーを割り当てていないものがよいでしょう。製品版サンプルのVZ[IBM[J]].DEFでは #33/#34(2行ロールアップ・ダウン)にコマンドキーを割り当てていませんから,このコマンドと交換してみることにしましょう。

まず,擬似的に有効にしたいコマンドキーが割り当てられている#05(#e)/#06(#x)はそれぞれvp+110/vp+112にあり,それぞれ2バイトです。交換したいキーは上記の #33/#34 で vp+166/vp+168にあり,こちらもそれぞれ2バイトです。これらを律義に2バイトずつ交換してもよいのですが,連続した4バイト同士ですのでロング演算関数の&Le()(ロング整数の代入)を使えば簡潔に記述することができます。

ふたつの函の中身を片手で交換するには一時置きする場所が必要です。これは一時置きなので空いているバッファを使えばよく,自前バッファを用意しなくてもpu,pr,paなどのバッファで十分です。手順としては,キーをスワップし,メニューを表示し,キーを戻す,となります。

1 ^\ ""
        ?.
        &Le(pu,vp+166) &Le(vp+166,vp+110) &Le(vp+110,pu)
 :m     !01 (r+1)??>z
        (r&$FF+$100)>!{
                (#33) (m"↑")
                (#34) (m"↓")
        } &m("%sキーが押されました.",m)
        >m
 :z     &Le(vp+110,vp+166) &Le(vp+166,pu)
        &m("") &d .
*p
1 "",6,1 "TEST"
*

他のキーによる組合せのアドレスを表にしておきます。

アドレスコマンド番号機能初期キー(IBM-PC)初期キー(NEC-98)
vp+110#05(#e)up^E [UP]^E [UP]
vp+112#06(#x)down^X [DOWN]^X [DOWN]
vp+114#07(#s)left^S [<--]^S [<--]
vp+116#08(#d)right^D [-->]^D [-->]
vp+118#09(#a)left word^A \[<--]^A \[<--]
vp+120#10(#f)right word^F \[-->]^F \[-->]
vp+122#11(#<)beginning-of-line^QS ^[<--]^QS ^[<--]
vp+124#12(#>)end-of-line^QD ^[-->]^QD ^[-->]
vp+162#31(#w)Roll-Up^W \[PGUP]^W \[RLDN]
vp+164#32(#z)Roll-Down^Z \[PGDN]^Z \[RLUP]
vp+166#33Up*2--
vp+168#34Down*2--
vp+170#35(#r)Page-Up^R [PGUP]^R \[UP]
vp+172#36(#c)Page-Down^C [PGDN]^C \[DOWN]
vp+174#37Smooth-Up^QW \[UP]^QW [RLDN]
vp+176#38Smooth-Down^QZ \[DOWN]^QZ [RLUP]
vp+178#39Window-Top^QE^QE ^[UP]
vp+180#40Window-Bottom^QX^QX ^[DOWN]
vp+182#41(#^)beginning-of-file^QR^QR ^[RLDN]
vp+184#42(#_)end-of-file^QC^QC ^[RLUP]

VZ Macro FAQ - menu
The VZ resources, valid XHTML 1.0 Strict. ©2005-2008 by mezala.