MocaScript Guide
Peggy Built-in Edition

2008/10/30, Version 4.61

株式会社エスコア
〒674-0082 兵庫県明石市魚住町中尾337
TEL: 078-946-5609
FAX: 078-946-5552
anchor@anchorsystems.jp
http://www.anchorsystems.jp/

    - 目次 -

    Hello, world

            スクリプトファイルの作成
            スクリプトの実行
            ドラッグ&ドロップ実行
            ピンポイント・ドロップ実行

    操作編

            コマンド宣言
            コマンドの識別
            コマンドの分類
            スクリプトファイルを開く
            コーディング
            コンパイル
            デバッグ
            スクリプトの実行
                ボタンの割り当て
                ショートカットの割り当て

    プログラミング編

            文字列挿入スクリプト
            挿入文字の組み立て
            汎用改行コード
            複数のinsertText()
            複数の挿入.vs.文字列の連結
            Undoをまとめる
            入力ボックス

        ViewとPoint
            ファイル行と表示行
            Pointオブジェクト
            表示行/カラムからファイル行/文字インデックスへの変換
            Pointのショートカット
            カレントポイント、アンカーポイント、そしてEOF
            二種類の編集メソッド

        編集ウインドウのあるとき.vs.ないとき

        スコープ
            編集ウインドウがないとき
            変数の作られる場所
            クラスの組み込み

        他のウインドウ
            他の編集ウインドウ
            アウトプットウインドウ

        カスタムダイアログボックス
            ダイアログボックステンプレート
            ダイアログボックステンプレートの作成
            コントロールの種類
            カスタムダイアログボックスの表示
            簡単に表示するならcustomBox()
            結果の取り出し
            [OK]以外のボタンでダイアログボックスを閉じる
            [OK]以外のボタンで値を返す
            擬似コントロール
            GROUPBOXの配置機能
            SPACER擬似コントロール
            プロパティ値の継承
            10秒後に自動的に閉じるダイアログボックス
            テキストが入力されたら[OK]ボタンを有効にする
            値の確認
            ラベルを付ける
            スピンを付ける
            カラーボタン
            ツリーリスト
            ダイアログシート

        プロジェクト
            プロジェクトファイルを開く
            保存する・閉じる
            アイテムとクラス
            グループ中のアイテム
            アイテム種別の判定
            選択されているアイテム
            アイテムの挿入
            いろいろなアイテムの挿入
            アイテムの削除
            ファイルアイテムの特定
            ファイルを開く・閉じる
            グループに対応するフォルダ

        Search オブジェクト

        Event オブジェクト

    逆引き編

            スクリプトの実行を止めるには?
            関数の機能を調べるには?
            正確な関数名が思い出せないときは?
            関数へ渡す引数の意味を忘れてしまったら?

            文字列を探すには? (1)
            文字列を探すには? (1)
            文字列を探すには? (2)
            範囲を指定して文字列を検索
            文字列をすべて探す
            <h1>、<h2>...の一覧を作成するには?
            選択範囲の中を捜す

            指定行のテキストを取得する
            カーソル行のテキストを取得する
            カーソル位置の文字を取得する
            選択範囲のテキストを取得する

            指定行にテキストを挿入するには?
            テキストの最後の文字を挿入するには?
            選択範囲の前後に文字列を挿入するには?

            文字列を置換するには?
            文字列をすべて置換するには?
            選択範囲の&などをすべてエンティティ(&amp;)に変換するには?
            編集ウインドウを更新するには?
            ファイルのパスを取得するには?
            ファイルの行数を調べるには?
            ある部分の文字を置き換えるには?
            スクリプトで行った編集操作を一発でUndoするには?
            カーソルが見えるようにスクロールするには?
            ファイルの漢字コードを知るには?
            タブ間隔を知るには?
            編集禁止かどうかを知るには?

            プロジェクトに登録されているすべての*.cファイルを取得するには?






Hello, world

定石どおりPeggyにビルトインされたMocaScriptも、"Hello, world"から始めましょう。

スクリプトファイルの作成

スクリプトはファイルに記述し、Peggyをインストールしたフォルダの<share\script>に入れます。 このとき拡張子は、必ず<.ms>としてください。 Peggyの起動時には、このフォルダにある<*.ms>ファイルをスクリプトとして認識します。


   Anchor
     │
     ├─ Peggy
     │     └─ peggy.exe 他...
     └─ share
            ├─ ac_J2SE.kwd         色づけ定義、DLLなど
            ├─ etagsdef.txt        その他の設定ファイル
            ├─ template            テンプレートのフォルダ
            :     └─ ...          テンプレートファイル
            :
            └─ script             スクリプトフォルダ
                   ├─ sample.ms    スクリプトファイル
                   ├─ hello.ms     今から作るサンプルスクリプト
                   ├─ iterator.msl スクリプトのライブラリファイル
                   ├─ string.msl             〃
                   ├─ CSIDL.msl              〃
                   ├─ CustDlg.msl            〃
                   ├─ ConvDig.msl            〃
                   └─ input.msl              〃

このフォルダに、<hello.ms>という名前のファイルを作成し、 その中にスクリプトとして実行したいMocaScriptの文を記述します。


    // アクティブな編集ウインドウに Hello, world と挿入する。
    insertText("Hello, world\n");

スクリプトファイルの追加や削除を行った場合は、 メニューの[ツール]-[スクリプト]-[リロード]を実行してください。 <share\script>フォルダ内を調べ直して、 プロジェクトウインドウのスクリプトページを更新します。 これによって、先ほど作成した、<hello.ms>ファイルも表示されます。

スクリプトの実行

helloを開くとペンのアイコンの横に[実行]と表示されています。 これを「スクリプトコマンド」と呼ぶことにします。 スクリプトコマンドを実行するには、このペンのアイコンをダブルクリックしてください。 編集ウインドウのカーソル位置に「Hello, world」という文字列が挿入されます。

【メモ】
ここでエラーが発生するかもしれません。 それは、文字列を挿入すべき編集ウインドウが開かれていないためです。 insertText()は、アクティブな編集ウインドウへ文字列を挿入する関数なので、 ウインドウが見つからないとエラーとなります。 この場合、[ファイル]-[新規]などで新しい編集ウインドウを開き、 再度[実行]をダブルクリックしてください。


ドラッグ&ドロップ実行

スクリプトは、Hello, worldサンプルのような単純なものから複雑なテキスト処理まで、 さまざまな用途で使われます。 そしてそれらを使う状況もいろいろです。 状況に合わせて便利に実行できるよう、 ダブルクリック以外にも色々な方法でスクリプトを走らせることができます。

その1つは、プロジェクトウインドウの[スクリプト]ページからスクリプトコマンドをドラッグし、 編集ウインドウ上へドロップする方法です。 ドロップした編集ウインドウのカーソル位置でスクリプトコマンドが実行されます。 これをドラッグ&ドロップ実行と呼びます。 ドラッグ&ドロップ実行では、アクティブでない編集ウインドウへドロップして実行させることもできます。

ピンポイント・ドロップ実行

スクリプトコマンドをドラッグ中にCtrlキーを押すと、 編集ウインドウにドロップポイントを示すマーカーが表示されます。 そのままドロップすると、 カーソルをドロップポイントに設定してからスクリプトの実行が始まります。 Hello, worldの例では、 ドラッグ&ドロップで好みの挿入場所を決めてから実行できることになります。 文字列を挿入するスクリプトには都合のよい実行方法と言えるでしょう。

他にも、ツールバーのボタンやショートカットから実行する方法がありますが、 詳しい説明は後回しにして先へ進みましょう。

【ヒント】
長くかかるスクリプトの実行を途中で止めるには、Escキーを押してください。






操作編

コマンド宣言

1つ1つの機能を別々のスクリプトファイルにしていたのでは、 ファイルだらけになってしまって大変です。 そこで、1つのスクリプトファイルに複数のスクリプトコマンドを登録する方法を用意しました。 スクリプトファイル中にどのようなスクリプトコマンドがあるのかを、 スクリプトファイルの先頭で#command文により宣言するのです。


    // ファイル中にあるスクリプトコマンドの宣言
    #command Hello    "Hello, worldを挿入"
    #command Goodbye  "Goodbye, worldを挿入"

#command文は、 Peggyに対して「このファイル中には、...のスクリプトコマンドがありますよ」と宣言しているだけで、 実際に実行したときは何もしません。 HelloGoodbyeをスクリプトコマンド名(または単にコマンド名)、 "Hello, worldを挿入""Goodbye, worldを挿入"をコマンドラベル(または単にラベル)と呼びます。 コマンド名は、MocaScriptの変数名などと同じ識別子です。 ラベルは、Peggyのメニューなどに表示される任意の文字列です。 あまり長くならない程度で判り易いラベルを付けておきましょう。 ラベルは省略することもできます。

Peggyのメニューから[ツール]-[スクリプト]-[リロード]を選択すると、 各スクリプトファイル中の#command宣言文を調べて、 スクリプトページを更新しているのです。 実際、<hello.ms>に上記のコマンド宣言を追加してからリロードすると、 スクリプトページに2つのラベルが表示されるはずです。

コマンドの識別

どのコマンドを選んで実行しても、 スクリプトは常にファイルの先頭から実行されます。 何かの目印により途中をスキップしてくれるような魔法は存在しません。 そこで必要になるのは、ファイル中のどのコマンドが選ばれて実行されたのかを知る方法です。

Peggy/MocaScriptでは、最も単純な方法を採用しました。 それは、「commandという名前の変数にコマンド名が文字列として代入された状態で実行が開始される」というものです。 実行が開始されたら、 if文やswitch文でcommand変数を調べ、 適切な処理に分岐するのです。 まとめると、以下のようになります。


    // ファイル中にあるスクリプトコマンドの宣言
    #command Hello    "Hello, worldを挿入"
    #command Goodbye  "Goodbye, worldを挿入"

    // コマンドの処理
    if (command == "Hello")
        insertText("Hello, world\n");
    else if (command == "Goodbye")
        insertText("Goodbye, world\n");
    else
        error("不明コマンド");

最後のerror(...)は、 コマンド宣言だけを追加して処理部分を忘れていたとか、 コマンド名をミスタイプしたなど万が一場合に備えての安全策です。 ビープ音を鳴らしてメッセージをステータスバーに表示し、 スクリプトの実行を終了します。

コマンド数が増えると、switch文を使った方が読みやすくなります。


    // ファイル中にあるスクリプトコマンドの宣言
    ...

    // コマンドで分岐 (MocaScriptでは、case 文に文字列を指定できる)
    switch (command) {
    case "Hello":
        insertText("Hello, world\n");
        break;

    case "Goodbye":
        insertText("Goodbye, world\n");
        break;

    default:
        error("不明コマンド");
    }

【メモ】
command変数以外にもう1つARGVという名前の変数が予め定義されています。 ARGVはコマンドライン版MocaScriptと同じように、ARGV[0]にスクリプトファイル自身のパス、 ARGV[1]にコマンド名(command変数と同じ文字列)を要素として持つ配列です。


コマンドの分類

さらにコマンドが増えると、グループに分けて表示したくなります。 それには、#command宣言のコマンド名をドット(.)で区切って続けます。


    // ファイル中にあるスクリプトコマンドを階層分けして宣言
    #command Insert          "挿入(&I)"
    #command Insert.Hello    "&Hello, world"
    #command Insert.Goodbye  "&Goodbye, world"

    #command Delete          "削除(&D)"
    #command Delete.Word     "単語(&W)"
    #command Delete.Line     "行(&L)"

このように宣言することで、プロジェクトウインドウの[スクリプト]ページ、 [ツール]-[スクリプト]メニューなどでコマンドが階層表示されます。 上手に分類すりことで、多くのコマンドを効率よく探すことができるようになるでしょう。 階層の深さは8段まで可能です。

コマンドラベルの中の"(&D)"とか、"(&W)"のような記述は、 このラベルがメニューに表示されたときのキーシンボルを指定するものです。 スクリプトコマンドをメニューから実行するときに、キーボードからの操作が楽になります。

この例のInsertDeleteは、 その下のスクリプトコマンドをまとめる「コマンドグループ」です。 必ずしもコマンドグループを宣言する必要はありませんが、 #command文で宣言することにより判りやすいラベルを付けることができます。

スクリプト実行開始時のcommand変数の値は、 "Insert.Hello"のようになります。


    // ファイル中にあるスクリプトコマンドの宣言
    ...

    // 階層付きのコマンドで分岐
    switch (command) {
    case "Insert.Hello":
        insertText("Hello, world\n");
        break;

    case "Insert.Goodbye":
        insertText("Goodbye, world\n");
        break;

    case "Delete.Word":
        deleteWord();
        break;

    case "Delete.Line":
        deleteLine();
        break;

    default:
        error("不明コマンド");
    }

【メモ】
コマンドの追加や削除を行ったときは、[ツール]-[スクリプト]-[リロード]を実行し、 プロジェクトウインドウの[スクリプト]ページを更新してください。
【メモ】
プロジェクトウインドウの[スクリプト]ページをマウスの右ボタンでクリックし、 [コマンド名を表示]を選択すると、 ラベルとコマンド名の両方を確認できます。


スクリプトファイルを開く

スクリプトファイルを開くには、 プロジェクトウインドウの[スクリプト]ページの編集したいスクリプトファイル、またはコマンドを右マウスボタンでクリックし、 ポップアップメニューから[開く]を選択してください。 スクリプトコマンドから[開く]を選択した場合は、 そのコマンドの#command宣言を自動的に選択します。

【メモ】
スクリプトファイルのアイコンをドラッグ&ドロップすることでもオープンできます。


コーディング

MocaScriptのプログラムを記述するときは、 Peggyの言語モードをMocaScriptにしておきましょう。 それには、[オプション設定]ダイアログボックスの[拡張子]ページの[拡張子マップ]で、 拡張子"ms""msl"をMocaScriptに対応付けます。 言語モードMocaScriptが見当たらないときは、 Anchor SystemsのホームページからCRECの色づけ定義ファイル<ac_moca.kwd>をダウンロードし、 <share>フォルダに入れてください。

【メモ】
言語モードをMocaScriptにすることで、 スクリプトファイルが色分け表示され、 キーワードチップやキーワード補完機能も働くので効率よくプログラミングできるようになります。


コンパイル

MocaScriptは、プログラムをバイトコード形式にコンパイルしてから実行します。 コンパイルはスクリプトを実行するとき自動的に行われるため、意識する必要はありません。 またコンパイルも非常に短時間で終わるため、初回の実行でもあまり気になることはないでしょう。 コンパイルに成功すると、プログラムが修正されない限り同じバイトコードを繰り返し使います。

実行せずコンパイルだけしたい場合は、 プロジェクトウインドウの[スクリプト]ページのポップアップメニューから[コンパイル]を選択してください。

文法エラーなどがあるとコンパイルを中断し、 アウトプットウインドウの[スクリプト]ページにエラーメッセージを出力します。 このエラーメッセージはタグジャンプ可能な形式なので、 F4キーを押すことでエラー原因となった行へジャンプできます。

【メモ】
Peggyはコンパイルしたバイトコードとともに、その時のスクリプトファイルのタイムスタンプを持っています。 スクリプトコマンドを実行する場合、このタイムスタンプを調べて変化がなければ、 バイトコードをそのまま使い、スクリプトファイルが更新されていれば再コンパイルします。
【注意】
自動再コンパイルのためのタイムスタンプの比較は、 スクリプトファイルに対してのみです。 そのファイルからインクルードしているファイルはチェックされません。 そのため、*.mslをインクルートしているファイルを編集しても自動的には再コンパイルされません。 これらのファイルを修正した場合は、 プロジェクトウインドウの[スクリプト]ページのポップアップメニューから[コンパイル]を実行してください。


デバッグ

Peggyに搭載されたMocaScriptには、デバッグのための簡単なトレース実行機能を用意しました。 トレース実行を行うには、プロジェクトウインドウの[スクリプト]ページの実行したいコマンドを右マウスボタンでクリックし、 ポップアップメニューから[トレース実行]を選択してください。

どの命令をどの順番で実行したかの情報がアウトプットウインドウの[スクリプト]ページへ出力されます。 これもタグジャンプ可能な形式なので、F4キーを連続して押すことにより、 順番にたどることができます。

しかしトレース実行機能は実行した命令の足跡をたどるだけで、 変数の内容を見たり変更することはできません。 残念ながら現バージョンでは、途中で止めたり変数の内容を確認する機能は備わっていません。

変数の中身を確認したいときは、 writeln()関数などを使って変数値をアウトプットウインドウへ出力してください。 これら関数の出力も[スクリプト]ページへ送られるので、 トレース結果に挟まれる形で実行した行とその時の値を確認することができます。

スクリプトの実行

最初に少し触れましたが、ここではスクリプトの実行方法をすべて紹介します。

場所 実行方法 最大数
[スクリプト]ページ
  • コマンドをダブルクリックする。
  • コマンドを選択してEnterキーを押す。
  • コマンドをドラッグして編集ウインドウ上へドロップする。
  • コマンドをドラッグして編集ウインドウ上へCtrキーを押しながらドロップする。
  • ポップアップメニューから[実行]を選択する。
  • ポップアップメニューから[トレース実行]を選択する。
無制限
編集ウインドウ
  • ポップアップメニューから[スクリプト]-....を選択する。
  • MocaScriptの実行文を選択して、ポップアップメニューから[スクリプト]-[選択範囲を実行]を選択する。
8階層
500コマンド
メニュー
  • [ツール]-[スクリプト]の下にあるスクリプトコマンドを選択する。
8階層
500コマンド
ツールバーボタン
  • ツールバーに配置したボタンをクリックして実行する。
72個
ショートカット
  • ショートカットキーによりスクリプトコマンドを実行する。
72個
コールバック
  • ファイルを開く、保存する、閉じるなど25種類イベントが発生したときに、 予め登録しておいたコールバック関数が自動的に実行される。
    【参照】Eventオブジェクト
25種類
Peggyの起動
  • <share\script>フォルダに<startup.ms>という名前のスクリプトファイルがあると、 Peggyの起動時に自動実行される。

メニューと編集ウインドウのポップアップメニューは同じものです。 プロジェクトウインドウの[スクリプト]ページに表示されているコマンド階層がメニュー形式で選択できるようになっています。

[選択範囲を実行]コマンドは、 編集中のウインドウの選択範囲をMocaScriptのプログラムと見なして実行する機能です。 短いプログラムを試してみたい場合に便利です。

ツールバーのボタンとショートカットは、 以下のようにしてPeggyをカスタマイズする必要があります。

ボタンの割り当て
スクリプトを実行させるためのボタンは、72個用意してあります。 メニューから[ツール]-[カスタマイズ]で表示される[カスタマイズ]ダイアログボックスの[コマンド]ページにある[スクリプト]を選択してください。 [ボタン]のところに72個のスクリプト実行用のボタンが表示されます。 このボタンをドラッグして、好みのツールバー上へドロップしてください。 これでスクリプトのボタンがツールバー上へ配置できました。

次に同じダイアログボックスの[スクリプト]ページで、スクリプトコマンドとボタンの結び付けを行います。 [スクリプト]ページにはボタンの一覧があります。 スクリプトコマンドを割り当てたいボタンの[コマンド]の欄をクリックしてください。 現在利用可能なスクリプトコマンドのリストが現れます。 リストから割り当てたいコマンドを選択してEnterキーを押すと、 コマンドの割り当てが完了します。

【注意】
ボタンとスクリプトコマンドの結びつきは、 スクリプトのファイル名とコマンド名に頼っています。 具体的には、「ボタン1に割り当てたコマンドは、"<ファイル名>:コマンド名"である」という形式で記憶しています。 Hello, worldの場合であれば、"Hello.ms:Insert.Hello"となります。 そのためスクリプトファイルを編集してコマンド名が変わった、 またはスクリプトファイルの名前を変えたような場合、 ボタンとスクリプトコマンドのリンクが切れてしまいます。 ボタンに割り当てたコマンドの名前を変更した場合は、 再度ボタンの割り当てを行ってください。
【メモ】
ボタンの割り当てを保存したファイルは、テキストファイルです。 保存したファイルを開くと、ボタンとコマンドが上記の形式でマップされている様子を見ることができます。

ショートカットの割り当て
スクリプトコマンドはショートカットからも実行できますが、 ショートカットを直接スクリプトコマンドに割り当てるのではありません。 スクリプトコマンドは、スクリプトファイルを編集したり、ファイルの追加/削除で知らない間に変わってしまうためです。 そこで、スクリプトのボタンに対してショートカットを割り当てることにしました。


    (1) [カスタマイズ]ダイアログボックス   (2) 同じページの[ショートカット変更]
        [スクリプト]ページで割り当てる。       ボタンをクリックしてキーを割り当てる。

   ┏━━━━━━━━━━━━━━┓      ┏━━━━━━┓      ┏━━━━━━━━┓
   ┃    スクリプトコマンド      ┃<---->┃   ボタン   ┃<---->┃ ショートカット ┃
   ┃  "Hello.ms:Insert.Hello"   ┃      ┃   (72個)   ┃      ┃                ┃
   ┃                            ┃      ┃            ┃      ┃                ┃
   ┗━━━━━━━━━━━━━━┛      ┗━━━━━━┛      ┗━━━━━━━━┛

スクリプト用のツールバーボタンは、全部で72個あります。 まず、メニューから[ツール]-[カスタマイズ]を選択して[カスタマイズ]ダイアログボックスを開き、 [スクリプト]ページをクリックしてください。 そこに72個のスクリプトボタンの一覧が表示されています。

次に、リストからボタンを選んでスクリプトコマンドを割り当てます。 最後に[ショートカット変更]ボタンをクリックして、ショートカットキーを割り当ててください。 以後、ショートカットキーを使ってスクリプトを実行できるようになります。 ショートカットは、[ツール]-[ショートカット]ダイアログボックスを使っても変更できます。

さらに、ボタンのイメージを変更することも可能です。 ボタンイメージを変更するには、[ボタン変更]ボタンをクリックし、好みのイメージを選択してください。





プログラミング編

ここからは、Peggyに組み込まれたMocaScriptのプログラムを作成する上でポイントやコツを紹介します。 MocaScript言語そのものの入門は、MocaScriptガイド、 文法の詳しい説明は、MocaScriptリファレンスを参照してください。

文字列挿入スクリプト

スクリプトでエディタをカスタマイズしたい場合の半分以上は、 何らかの定型か、半定型の文字列を組み立てて挿入する処理の追加でしょう。 この種のスクリプトは、幾つかの関数を覚えるだけで簡単に作ることができます。

その中心になるのは、Hello, worldのサンプルでも使ったinsertText()メソッドです。 insertText()は、キーボードからも文字列をタイプするのと同じ方法でカーソル位置に文字列を挿入し、 カーソルを挿入した文字列の直後に移動します。 以下は、日付と時間をカーソル位置に挿入する例です。


    // コマンド宣言
    #command Insert        "挿入(&I)"
    #command Insert.Hello  "&Hello world"
    #command Insert.Date   "日付(&D)"

    // コマンドで分岐
    switch (command) {
    case "Insert.Hello":
        insertText("Hello, world\n");
        break;

    case "Insert.Date":
        // 現在の日付と時間を挿入する
        date = new Date();
        text = date.toLocaleString();
        insertText(text);
        break;

    default:
        error("不明コマンド");
    }


挿入文字の組み立て

メニューの[編集]-[挿入]-[日付と時間]コマンドは、挿入する書式が決まっていましたが、 toLocaleString()を他のDateクラスのメソッドに変えることで、 どのような形式でも可能になります。 例えばif文を使って、時間によって適切な挨拶を挿入するコマンドを作ることができます。


    // 時間によって適当な挨拶を挿入する
    date = new Date();
    text = "只今 " + date.toLocaleString() + " です.\r\n";
    text += "みなさん";

    hour = date.getHours();
    if (5 <= hour && hour < 11)
        text += "おはようございます.";
    else if (11 <= hour && hour < 17)
        text += "こんにちは.";
    else if (17 <= hour && hour < 21)
        text += "こんばんわ.";
    else if (21 <= hour)
        text += "夜分すみません.";
    else // (0 <= hour && hour < 5)
        text += "こんな時間にどうしたんですか?";

    text += "\r\n";
    insertText(text);
【メモ】
コマンド宣言とswitchcase文は省略してあります。

文字列はプラス記号(+)で連結することができます。 すでに変数へ入っている文字列には、+=演算子で文字列を追加できます。 このようにして文字列を組み立て、最後にinsertText()メソッドで挿入しています。

汎用改行コード

文字列の中の\r\nは、改行コードCR-LFです。 Windowsのテキストファイルだけを扱っている限り、改行コードは常にCR-LF(\r\n)と決め付けても問題ありません。

しかし他のオペレーティングシステム上のファイルを編集する機会があるならば、 もっと汎用的に使えるスクリプトにしたくなるでしょう。 それには、View.getNewline()を使ってファイルの改行コードを取得し、 その改行コードを文字列へ追加するようにします。


    // 時間によって適当な挨拶を挿入する(汎用改行コード版)
    date = new Date();
    text = "只今 " + date.toLocaleString() + " です." + getNewline();
    text += "みなさん";

    ... 省略 ...

    text += getNewline();
    insertText(text);

文字列が長くなる場合、何度もgetNewline()を呼び出すのは少々面倒です。 以下のようにすると、簡単に改行コードをファイルに一致させることができます。


    // 時間によって適当な挨拶を挿入する(汎用改行コード改良版)
    date = new Date();
    text = "只今 " + date.toLocaleString() + " です.\n";
    text += "みなさん";

    ... 省略 ...

    text += "\n";
    insertText(text.replace(/\n/g, getNewline()));

文字列を組み立てている途中では、LF(\n)だけを改行コードとして使っておき、 最後にString.replace()メソッドにより、 一気にファイルの改行コードに変換したものを挿入します。

複数のinsertText()

ここまでのサンプルでは、文字列を組み立ててから最後にinsertText()を呼び出していましたが、 insertText()そのものを繰り返し呼び出してもかまいません。 ある位置に文字列を挿入した後カーソル移動し、別の場所にまた挿入するような処理では、 その回数だけinsertText()を呼び出すことになります。

次のサンプルでは、一行おきに空白行を挿入するため、insertText()を繰り返し呼び出しています。 lineDown()は、カーソルを1行したに移動させるViewクラスのメソッドです。


    // 1行ごとに空白行を10行挿入する
    for (i = 0; i < 10; i++)
    {
        insertText(getNewline());
        lineDown();
    }


複数の挿入.vs.文字列の連結

同じ所に連続して文字列を挿入する場合、その都度insertText()で挿入するのと、 文字列を連結しておいてから最後に挿入する方法とでは何が違うのでしょうか?

挿入結果はどちらも同じになりますが、処理スピードやUndo(取り消し)の方法に違いがあります。 結論から言うと、文字列を連結しておいてから挿入したほうがお得です。

insertText()は、渡された文字列をViewオブジェクトに挿入し、 Viewオブジェクトの内部の状態を更新します。 また、挿入操作をUndoコマンドで取り消せるようにUndo情報を作成します。 これは結構複雑で、Undoのためのオーバーヘッドが増えてしまいます。

これに対して文字列の連結は、 メモリ上で文字列を移動させるだけなので簡単です。 Undo情報も1回分しか作成されないので、メモリ消費も抑えることができます。

Undoをまとめる

上記のサンプルを実行した後で間違いに気づいた場合、メニューの[編集]-[元に戻す]コマンドで取り消すことができます。 しかしinsertText()を10回呼び出すとUndo情報も10個作成されているので、 元へ戻すのに[編集]-[元に戻す]を10回実行してやらなければなりません。

しかしこれでは面倒なので、ViewクラスのメソッドenterUndoGroup()leaveUndoGroup()を使って一連の編集操作を1つのUndoにまとめることにします。


    // 1行ごとに空白行を10行挿入する(Undo改善版)
    enterUndoGroup();
    for (i = 0; i < 10; i++)
    {
        insertText(getNewline());
        lineDown();
    }
    leaveUndoGroup();

enterUndoGroup()を実行してからleaveUndoGroup()までの処理が1つの塊としてUndoの対象になります。 カーソル移動しては挿入を繰り返すようなスクリプトでも、Undo一発で元へ戻ります。 スクリプトの実行が終了すると自動的にleaveUndoGroup()が実行されるので、 leaveUndoGroup()は省略してもかまいません。

入力ボックス

挿入系のスクリプトでよく見かけるもう一つのパターンは、 「ダイアログボックスを表示して文字列を入力してもらい、 それを使って挿入文字列を組み立てる」というものです。 それには、inputBox()関数を使います。

inputBox()関数は、最大25の入力欄を作ることができます。 ユーザが文字列をタイプし[OK]ボタンをクリックすると、 各入力欄にタイプされた文字列を要素とした配列を返します。 [キャンセル]が押されると、nullを返します。


    // 入力ダイアログボックスを表示する。
    fields = inputBox("住所録",
                      "名前、電話番号、住所を入力してください.",
                      "名前(&N):",
                      "電話番号(&T):",
                      "住所(&A):");

    // キャンセルが押されたら(戻り値がnullなら)終了。
    if (!fields)
        exit();

    // 入力された文字列を取り出す。
    var name = fields[0];
    var tel  = fields[1];
    var addr = fields[2];

    // name、tel、addr を適当に加工してテキストに挿入する。
    ... 省略 ...

【ヒント】
inputBox()の他にも、 メッセージを表示するmessageBox()、 リスト表示するlistBox()、 カラーのRGB値を入力するcolorBox()、 ファイルを選択するfileBox()、 自由にコントロールを配置できるcustomBox()があります。 カスタムダイアログボックスに関する詳しい説明は、こちらを参照してください。

挿入スクリプトは簡単ですが、 これだけでかなりの機能が作成できます。 Viewクラスのメソッドだけでなく、MocaScriptに備わっている組み込みクラスはすべて利用できるので、 Peggy外部へアクセスして、その結果を挿入するようなことも可能です。

次は、Peggyに組み込まれたMocaScriptの仕組みの基本部分に付いて説明します。 基本部分が理解できれば、より高度なスクリプトを作成することができようになります。

ViewとPoint

Peggyに組み込まれたMocaScriptには、 コマンドライン版にはない独自のクラスViewPointが追加されています。 Viewのオブジェクトは、Peggyの編集ウインドウを表します。 Viewクラスには多くのメソッドが用意してあり、 それらを使って文字の挿入や削除といったテキストの加工が自在に行えます。

テキストを加工するには、 テキストのどの部分を対象にするかを指定する方法が必要です。 Pointは、テキストの編集対象となる「場所」を表現するためのオブジェクトです。 Viewクラスのメソッドは、Pointオブジェクトを受け取り、 そこに対して文字を挿入・削除します。


          編集ウインドウ --- Viewオブジェクトは編集ウインドウを表す
         ┏━━━━━━━━━━━━━━━━┓
         ┣━━━━━━━━━━━━━━━━┫
         ┃                                ┃
         ┃Let's try MocaScript.           ┃
         ┃MocaScript を始めましょう!!     ┃
         ┃            ↑                  ┃
         ┃            └─────────╂─ Point オブジェクト
         ┃                                ┃   編集対象となるテキスト中の位置を表す
         ┃                                ┃
         ┗━━━━━━━━━━━━━━━━┛

他にも幾つかのクラスが追加されています。 Peggy専用に追加されたクラスを下表にまとめました。 Project以下の9クラスは、プロジェクトを操作するためのクラスです。

クラス名用途
View編集ウインドウを表す
Pointテキストの編集対象となる場所を表す
Outputアウトプットウインドウの1ページを表す
Dialogダイアログボックスを表す
Search検索文字列や条件のプロパティを保持するオブジェクト
Projectプロジェクト全体を表す
ProjectItemプロジェクトのアイテム一般を表す親クラス
FileItemプロジェクトのファイルアイテムを表すクラス
GroupItemプロジェクトのグループアイテムを表すクラス
ToolItemプロジェクトのツールアイテムを代表するクラス
FindItemプロジェクトの検索アイテムを表すクラス
GrepItemプロジェクトのファイルから検索アイテムを表すクラス
DiffItemプロジェクトの比較アイテムを表すクラス
KeyboardMacroItemプロジェクトのキーボードマクロアイテムを表すクラス


ファイル行と表示行

Pointオブジェクトの詳細に入る前に、編集位置の指定方法が2種類あることを理解しておく必要があります。 「ファイル行」と「表示行」です。 「エディタ的行番号」、「ワープロ的行番号」と表現しているエディタもあるようです。 編集位置を指定するにはそれに横方向の情報を加え、 「ファイル行・文字インデックス」と「表示行・カラム」とします。

指定方法 方向 範囲
ファイル行・文字インデックス 改行コードまでを1行とカウントするファイル行番号 1 〜 ファイル行の行数
行頭の文字をインデックス0、次の文字を1、...とする文字インデックス 0 〜 行の文字数
表示行・カラム 画面上に表示されている1行を1行とカウントする表示行番号 1 〜 表示行の行数
画面の左端をカラム1、次をカラム2、...とするカラム番号 1 〜 レイアウト幅

【メモ】
表示行は表示の折り返し幅が変化すると、表示行番号もそれにともなって増減するので厄介です。 しかし、Peggy内部ではファイル行と表示行の両方を管理していて、相互に変換することができます。 編集を行う関数の多くは、デフォルトでファイル行を基準として動作しますが、 表示行基準で指定することも可能です。


Pointオブジェクト

Pointオブジェクトは、 上記のファイル行、文字インデックス、表示行、カラムなどをプロパティとして持つことで、 編集しているテキストの任意の場所を表現しています。 Pointオブジェクトのプロパティを下表にまとめました。

指定方法 プロパティ名 デフォルト値
ファイル行・文字インデックス line integer fileLine の別名 ※注参照
fileLine integer ファイル行番号 (1 〜 ) ※注参照
index integer ファイル行中の文字インデックス (0 〜 ) 0
表示行・カラム viewLine integer 表示行番号 (1 〜 ) ※注参照
column integer カラム (1 〜 ) 1
threshold integer 次の文字と見なすカラム違いのしきい値 (1 〜 ) 0
【※注】
fileLinelineviewLineは、最低どれか1つ指定する必要があります。 どれもないオブジェクトは、Pointオブジェクトとして正しくありません。

両方の指定方法を混在させることも可能ですが、 その場合はファイル行・文字インデックスが優先します。 line、もしくはfileLineがある場合はファイル行・文字インデックス基準、 viewLineしかない場合だけ表示行・カラム基準とみなされます。 以下は、Pointオブジェクトを作成してカーソルを移動させる例です。


    // ファイル行12の文字インデックス34を表すPointオブジェクトを作成しカーソルを移動する。
    var p1 = EP(12, 34);
    gotoPoint(p1);

    ...

    // 表示行100の10カラムを表すPointオブジェクトを作成しカーソルを移動する。
    var p2 = VP(100, 10);
    gotoPoint(p2);

EP(fileLine, index)VP(viewLine, column)は、簡単にPointオブジェクトを作成するためのグローバル関数です。 gotoPoint()は、Pointオブジェクトにより指定された場所へカーソルを移動するViewクラスのメソッドです。

EP()関数を使って作成したPointオブジェクトには、 ファイル行・文字インデックスのプロパティだけが含まれます。 同様にVP()関数を使って作成したPointオブジェクトは、 表示行・カラムのプロパティだけを持っています。 gotoPoint()メソッドは、 どちらの形式のPointオブジェクトかを調べ、カーソルを移動させます。

【メモ】
文字インデックスプロパティindexに行末を越える非常に大きな値を指定した場合、 行の最後の文字と改行コードの間が指定されたとして処理します。


表示行/カラムからファイル行/文字インデックスへの変換

カラムは半角単位です。 そのため、あるカラムを指定したけれどもそこには文字がない、 またはあっても全角文字やタブ文字の途中だったということがあります。

表示行と文字のないカラムからファイル行/文字インデックスへの変換は、 その表示行で指定されたカラムに最も近い文字のインデックスとなります。

指定カラムが文字の途中を指していた場合は、その文字の前と判定するか後ろと判定するかの切り分けが必要になります。 それを指定するのが、Pointオブジェクトのthresholdプロパティ値です。

thresholdプロパティがない、または0以下の値であれば、常に文字の前として変換します。 thresholdが正のとき、 文字の先頭から指定カラムまでの差がthreshold以上であれば文字の終りと判定します。 差がthreshold未満であれば、文字の先頭に変換します。

【メモ】
Pointオブジェクトのthresholdは、矩形選択の範囲に対して処理を行うような場合の微調整として機能します。 矩形選択をしているとき画面上に反転表示されている領域は、threshold = 1としてカラムから文字インデックスへ変換したものと一致します。


Pointのショートカット

Pointオブジェクトによる位置の指定は非常に柔軟で汎用ですが、 かなりコーディング量が増えて面倒です。 そこでもっと簡単に指定する方法を用意しました。

Pointオブジェクトの代わりに数値を渡すと、 数値が表すファイル行の先頭と解釈するのです。 例えば数値の 10 は、EP(10, 0) と同じ場所を指しているのです。 ファイル行の途中を表したいときだけPointオブジェクトを作成すればよくなります。 このショートカットを使うと、XX行目へのジャンプなどはとても簡単に記述できます。


    // 15ファイル行目へジャンプ
    gotoPoint(15);


カレントポイント、アンカーポイント、そしてEOF

Peggyは編集ウインドウ毎に3つのポイントを常に管理しています。 カレントポイントとアンカーポイント、EOFです。 カレントポイントは、カーソルのある位置を表しています。 アンカーポイントは、選択を開始した位置を保持しています。 選択範囲のないときは、2つのポイントは同じになります。 EOF(エンド・オブ・ファイル)は、その名の通りファイルの終りを表しています。

カレントポイント/アンカーポイント/EOFは、 ViewオブジェクトのプロパティCPAPEOFとしていつでも参照できます。 これらの値はPointオブジェクトです。 ファイル行・文字インデックスを表すlinefileLineindexと、 表示行・カラムを表すviewLinecolumnのすべてが常に最新の状態に更新されているので、 いつでもカーソル値、選択開始位置を知ることができます。

Viewオブジェクトのプロパティ名
CP Point カレントポイント(カーソル位置)を表すPointオブジェクト
CP.line ........ カーソルのあるファイル行番号
CP.index ....... カーソルのある文字のインデックス
CP.viewLine .... カーソルのある表示行番号
CP.column ...... カーソルの桁位置
AP Point アンカーポイント(選択開始位置)を表すPointオブジェクト
AP.line ........ 選択開始位置のファイル行番号
AP.index ....... 選択開始位置の文字のインデックス
AP.viewLine .... 選択開始位置の表示行番号
AP.column ...... 選択開始位置の桁
EOF Point ファイルの終りを表すPointオブジェクト
EOF.line ........ 最後のファイル行番号
EOF.index ....... 最後の文字のインデックス
EOF.viewLine .... 最後の表示行番号
EOF.column ...... 最後の桁

【メモ】
CPAPプロパティは読み出し専用なので、 例えばCP.lineに行番号を書き込んでもカーソル位置は移動しません。 カーソルを移動させるには、 gotoPoint()などのViewクラスのメソッドを使います。

以下のサンプルは、カーソル位置のファイル行番号と文字インデックスをステータスバーに表示します。


    // カーソル位置をステータスバーに表示する
    setStatusText("カーソルは ", CP.line, "行、", CP.index + 1, "文字目にあります.");


二種類の編集メソッド

Viewクラスのメソッドは、大きく以下の2つのグループに分けることができます。

前者の代表はinsertText()です。 カーソル位置に対して操作を行うメソッドは、Pointオブジェクトで場所を指定する必要がありません。 そして挿入/削除などが終わると、ちょうどキーボードから操作したときと同じようにカーソル位置を更新します。 キーボードからの操作順を思い浮かべながらプログラミングできる利点があります。 このグループに属する関数を「カーソル追従型」と呼ぶことにします。

一方後者の代表はinsertTextAt()メソッドです。 文字列を挿入する位置を指定するPointオブジェクト受け取り、 そこへ文字列を挿入します。 カーソルとは無関係な場所へ挿入し、挿入後もカーソルは移動しません。 ファイルのいろいろな部分へ変更を加えるような処理に適しています。 このグループに属する関数を「カーソル独立型」と呼ぶことにします。

また、これら2グループのメソッドを混ぜて使うことも可能です。 カーソルの近くは前者の関数、遠く離れた場所やカーソルを動かさずに何かしたような場合は後者の関数...と使い分けます。 中には、Pointオブジェクトが引数として渡されている場合はそこから、 渡されていないとデフォルトとしてカーソル位置を処理対象とみなす「両刀使い型」のメソッドもあります。

以下に代表のメソッドをまとめました。 メソッド名の最後がAtBetweenで終わっているものはカーソル独立型です。 それぞれの関数の詳細は、MocaScript Peggy ビルトイン関数リファレンスを参照してください。

分類ポイント移動テキスト取得挿入削除検索/置換
カーソル追従型 charLeft()
charRight()
lineUp()
lineDown()
...他、多数
getText()
getWord()
insertText() deleteText() findForward()
findBackward()
カーソル独立型 movePoint() getTextAt()
getTextBetween()
insertTextAt() deleteTextAt()
deleteTextBwteen()
replaceTextAt()
replaceTextBetween()
search()
replace()
両刀使い型   getLine()
getLineLength()
getLineByteLength()
   

【ヒント】
charLeft()charRight()をはじめ、 Peggyのコマンドに対応した多くのメソッドがカーソル追従型です。
【ヒント】
カーソル独立型のメソッドを使って編集を行った場合でも、 カーソルのある部分を削除してしまったようなときはカーソルが(しかたなく)移動します。

次のサンプルは、insertTextAt()を応用して、 常にファイルの末尾に文字列を追加する関数appendText()を定義しています。


    insertText("カーソル位置に挿入");
    appendText("ファイルの最後に追加");

    function appendText(text)
    {
        // ファイルの終りにテキストを追加する関数
        insertTextAt(EOF, text);
    }


編集ウインドウのあるとき.vs.ないとき

スクリプトは編集ウインドウが開いているときでも、いないときでも実行できます。 しかし編集ウインドウが開いていない状態では、 当然ながらViewクラスのプロパティやメソッドは利用できません。 例えばカーソルを1文字左へ移動するViewクラスのメソッドを呼び出すと、 「関数ではありません: charLeft」のようなエラーとなります。

これを事前に調べ、もっと適切なエラーメッセージを出すようにするには、以下のようにしてください。


    // 編集ウインドウがあることの確認
    if (!view)
        error("編集ウインドウがありません.");

    // コマンド処理
    switch (command) {
    ... 省略 ...

viewは、編集ウインドウがあるときだけそのウインドウを表すViewオブジェクトを参照しています。 編集ウインドウがないときは、undefinedとなります。 これには次の「スコープ」が深く関係しています。

スコープ

編集ウインドウのありなしによってviewがViewオブジェクトを参照したりundefinedになるのは、 スコープの仕組みで説明できます。 下のサンプルプログラムと図は、編集ウインドウがある状態でスクリプトが起動されたとき、 スクリプトで使っている変数や関数がどこにあるものなのかの対応を示しています。 コメント中の番号が下の表の「変数/関数の種類」に対応しています。


    if (!view /* 8. */)
        error /* 13. */ ("ウインドウがありません.");

    function popupSmile /* 4. */ (msg /* 1. */)
    {
        var text /* 3. */ = msg + " :-)";
        ok /* 7. */ = messageBox /* 13. */ (text, MB_OK /* 14. */);
        return ok;
    }

    var date /* 5. */ = new Date /* 12. */ ();
    message /* 6. */ = date.toString() + " です";
    popupSmile(message);

スコープ名称
(存続期間・ライフタイム)
スコープチェーン
(上から下に向かって検索)
変数/関数の種類
ローカルスコープ
(関数の実行中)
msg
arguments
text

  1. 関数へ渡された引数
  2. 引数を参照するArgumentsオブジェクト
  3. 関数の中でvarにより宣言した変数
スクリプトスコープ
(スクリプト実行終了まで)
popupSmile()
date
message
ok

  1. スクリプトで定義している関数
  2. 関数の外でvarにより宣言した変数
  3. 関数の外でvar宣言無しで使った変数
  4. 関数の中でvar宣言無しで使った変数
Viewスコープ
||
Viewオブジェクト
編集ウインドウがあるときだけ存在
(編集ウインドウが閉じられるまで)
view
CPAPEOF
charLeft()charRight()、...

  1. Viewオブジェクト自身を参照するviewプロパティ
  2. カーソルの位置などを示すプロパティ(Pointオブジェクト)
  3. 多数のViewクラスのメソッド
グローバルスコープ
||
グローバルオブジェクト
(Peggyを終了するまで)
global
String()Array()Date()、...
exit()messageBox()error()...
NaNMB_OK、...
output[]
project

  1. グローバルオブジェクト自身を参照するglobalプロパティ
  2. コンストラクタ関数
  3. グローバル関数
  4. グローバル定数(プロパティ)
  5. アウトプットウインドウの配列
  6. 開いているProjectオブジェクトを参照するprojectプロパティ
 

【ヒント】
Viewスコープは編集ウインドウ毎に存在します。 スクリプト実行コマンドを選択したときにアクティブだった編集ウインドウのViewオブジェクトがViewスコープとして選択されます。 それに対して、グローバルスコープは1だけです。

【注意】
Eventオブジェクトに登録されているイベントハンドラ呼び出されて実行中は、 上図のスクリプトスコープ、Viewスコープがスキップされた(見えない)スコープチェーンとなります。 イベントハンドラは、スクリプト実行中にファイル保存などによる操作の結果呼び出されることもあれば、 ユーザがファイルを保存したときに呼び出されることもありますが、 イベントハンドラからは常に同じ(スクリプト、Viewスコープがない)スコープチェーンに見えるようになっています。

ある関数を実行中に変数や関数が使われると、 上図の「ローカルスコープ」から下に向かって順番に変数を探します。 同じく関数の外を実行中に関数や変数が使われると、 「スクリプトスコープ」から検索が始まり、最初に見つかったものの値を取り出し、または代入します。

編集ウインドウがないとき

編集ウインドウがないときは、上図の「Viewスコープ」が存在しません。 そのために、charLeft()を呼ぶと「関数が見つからない」というエラーになったのです。 Viewオブジェクト自身を参照するviewプロパティもViewスコープの中にあるので、 有効なviewがあるかどうかをif文でテストすることにより、 編集ウインドウがないことを調べていたのです。


    if (!view)
        error("ウインドウがありません.");

同様に、CPAPEOFなど編集ウインドウのポイントをあらわすプロパティも、 Viewスコープが存在するときだけ利用することができます。

変数の作られる場所

関数の中でvarにより宣言した変数は、 ローカルスコープの中に作られ、関数からリターンすると消滅します。 同様に関数の外でvarにより宣言した変数は、 スクリプトスコープの中に作られ、スクリプトの実行終了とともに消滅します。 varなしでいきなり使った変数も、 暗黙のルールとしてスクリプトスコープに作成されます。

では、Viewスコープやグローバルスコープ中にデータを記憶させるのは、どうすればよいのでしょう? それには、Viewオブジェクト自身を参照するviewプロパティと、 グローバルオブジェクト自身を参照するglobalプロパティを使います。


    // スクリプトスコープのデータ
    shortLifeData = "スクリプトの終了で消えてしまうデータ";

    // Viewオブジェクトにデータを保存する。
    view.viewLifeData = "編集ウインドウがクローズされるまで生き残るデータ";

    // グローバルスコープにデータを保存する。
    global.longLifeData = "Peggy終了まで生き残るデータ";

    // グローバルオブジェクのプロパティはグローバル変数です。
    writeln(longLifeData);

グローバルオブジェクトのデータプロパティは、すなわちグローバル変数です。 上記サンプルのようにしてグローバルスコープに保存したデータは、 以後グローバル変数としてアクセスできます。

このテクニックを使うと、 inputBox()で入力された値を次回のデフォルト値として保存しておくことができます。


    // 前回の入力をデフォルト値として使う入力ダイアログボックス
    var params = inputBox("賢いデフォルト",
                          "入力したものが次回のデフォルトになるよ",
                          ["何でもどうぞ:", defaultValue ]);
    if (params)
        global.defaultValue = params[0];

【メモ】
inputBox()に入力欄にデフォルト値を設定する方法は、 inputBox()関数を参照してください。

さらに、スクリプト中で定義している関数をグローバル関数へ昇格させ、 他のスクリプトから共有することも可能です。


    // アウトプットウインドウのすべてのページをクリアする。
    function clearAllOutputPages()
    {
        for (var i = 1; i <= 7; i++)
            output[i].clear();
    }

    // グローバル関数として登録する。
    global.clearAllOutputPages = clearAllOutputPages;

    // スクリプトはここで終了するけれど、clearAllOutputPages()関数は
    // グローバルスコープに残っているので、他から利用できる。

これをPeggyの起動時に自動実行される<startup.ms>に入れておけば、 最初からの組み込み関数と同じように、すべてのスクリプトから利用することができるようになります。

クラスの組み込み

次は同じテクニックにより、MocaScriptのサンプルとして付いているInputクラスをPeggyに組み込む例です。


    // 起動時の自動実行ファイル startup.ms でInputクラスを取り込む
    #include "input.msl"
    global.Input = Input;

【メモ】
Inputは、ワイルドカードを含んだパスで指定された複数のファイルから順番に1行づつ読み出すためのクラスです。 ライブラリとして<input.msl>ファイルに収められています。

これだけでInputクラスがグローバルオブジェクトに組み込まれ、 他のスクリプトからあたかも組み込みクラスのように使うことがでまきす。


    // Inputクラスを使ってCRECファイル(*.kwd)から言語の名前を取り出す。
    var input = new Input("C:\\Program Files\\Anchor\\share\\*.kwd");
    var text;
    while (text = input.next())
    {
        if (text.search(/^Id:/i) >= 0)
            write(input.file, ":", input.line, ": ", text);
    }

【メモ】
#include vs グローバルオブジェクトへの登録
Inputクラスの例のように、Peggyの起動時にグローバルオブジェクトへ組み込んでしまう方法と、 必要なスクリプトの先頭で毎回 #include "input.msl" とする方法には、 以下のような違いがあります。

  • 多くのスクリプトの先頭で #include "input.msl" とした場合、 Inputクラスは毎回コンパイルされ、Peggy内部にInputクラスのバイトコードが重複して存在することになる。 大きなクラスになると、メモリ消費が気になる。
  • 逆に、"startup.ms"で読み込む場合、"startup.ms"に何らかの問題があって失敗すると、 Inputクラスが読み込まれていることを期待している他のスクリプトまで動かなくなる。
以上の点を考慮して、どちらの方がより適しているかを判断してください。


他のウインドウ

スコープによって、アクティブな編集ウインドウを簡単に操作できることができます。 しかしその他のウインドウはどうでしょう?

他の編集ウインドウ

他のファイルの編集ウインドウを取得するには、isFileOpened()関数を使います。


    // readme.txtを開いていれば、そのファイルのViewオブジェクトを返す。
    var readme = isFileOpened("C:\\Program Files\\Anchor\\Peggy\\readme.txt");
    if (readme)
    {
        // readme.txt の2行目にカーソル移動する。
        readme.gotoPoint(2);

        // readme.txt の2行目を取得しメッセージボックスで表示する。
        var linetext = readme.getLine();
        messageBox(linetext);
    }

目的とする編集ウインドウのViewオブジェクトが取得できれば、 それに対してすべてのViewクラスのメソッドが使えます。 スコープに組み込まれて自動的にアクセスできるアクティブな編集ウインドウとの違いは、 操作の対象となる編集ウインドウを aView . charLeft() という形式で指定しなければならない点だけです。

メソッドだけでなくプロパティも、aView . CP.line のようにしてアクセスできます。

【メモ】
新規ファイルを作成するnewFile()関数、既存のファイルを開くopenFile()関数もViewオブジェクトを返します。これらも同じように、Viewクラスのメソッドを使って操作可能です。


アウトプットウインドウ

アウトプットウインドウの各ページは、特殊な編集ウインドウです。 これらは、Viewクラスの機能を継承したOutputクラスのオブジェクトとして扱うことができます。 Outputクラスのオブジェクトは、Viewクラスと比較して以下の点が異なっています。

機能編集ウインドウアウトプットウインドウ
編集禁止 禁止/可能を切り替えられる 常に編集禁止
追加書き込み insertText(EOF, string) 編集はできないが、追加メソッドにより最後に文字列を追加することができる。
write()writeln()printf()
クリア deleteAll() 専用のクリアメソッドclear()により、各アウトプットウインドウページに適したクリアを行う。
【例】ファイル比較ページをクリアすると、比較中の編集ウインドウの背景色もクリアする
言語モード 言語モードにより色分け表示 なし
上書き保存、名前を付けて保存、読み直し saveFile()
saveFileAs()
reopenFile()
できない (特定のファイルと関連づけできない)
閉じる closeFile()
closeWindow()
できない

【メモ】
saveFileTo()メソッドはアウトプットウインドウに対しても使うことができます。

アウトプットウインドウへアクセスするには、グローバルスコープにある配列output[]を使います。 この配列の要素1〜7から、アウトプットウインドウの各ページのOutputオブジェクトが参照できます。 また要素0は、現在アクティブなアウトプットウインドウのページが参照できます。

output配列参照するページ
output[0]Output現在選択されているページ
output[1]Output実行結果
output[2]Outputファイルから検索
output[3]Outputフォルダ比較
output[4]Outputファイル比較
output[5]Outputバージョン管理
output[6]OutputTAGS
output[7]Outputスクリプト
output.select(n)Output nにより指定したページを選択し、
そのページのOutputオブジェクトを返す。
lengthinteger8

以下は、アウトプットウインドウのスクリプトページをクリアする例です。


    // アウトプットウインドウのスクリプトページをクリアします。
    var scriptPage = output[7];
    scriptPage.clear();






カスタムダイアログボックス

Dialogオブジェクトを使うと、色々なコントロールを組み合わせた独自のダイアログボックスを作成し、表示することができます。下図はカスタムダイアログボックスの一例です。 カスタムダイアログボックスのデザインは、「ダイアログボックステンプレート」で定義します。



ダイアログボックステンプレート

ダイアログボックステンプレートとは、 customBox()関数やDialog.open()メソッドで表示するカスタムダイアログボックスのデザインを決めるMocaScriptのArrayオブジェクト(配列)です。

配列の個々の要素は、 ダイアログボックス上に配置される部品の種類や位置を指定するオブジェクトです。 部品のことを「コントロール」、 コントロールの種類や位置を指定するMocaScriptのオブジェクトを「Controlオブジェクト(メモ参照)」と呼ぶことにします。 つまりダイアログボックステンプレートの実体は、 「Controlオブジェクトの配列」と言えます。 以下のように、配列リテラルオブジェクトリテラルを使って記述できます。


    // ダイアログボックステンプレートは「Controlオブジェクトの配列」
    var template =
    [
        { type: "EDIT" },
        { type: "EDIT" },
    ];
    customBox("簡単なダイアログボックス", template);

カスタムダイアログボックス全体の大きさは、 テンプレートのコントロールがすべて収まるよう自動的に調整されます。

【メモ】
説明の都合上「Controlオブジェクト」と呼んでいますが、 Controlというクラスは(今のところ)存在しません。 コントロールの作成に必要なプロパティを持っていれば、 どんなオブジェクトでもControlオブジェクトとすることができます。 Controlオブジェクトで意味のあるプロパティの一覧は、 Dialogプロパティを参照してください。


ダイアログボックステンプレートの作成

ダイアログボックステンプレートを作成するには、2通りの方法があります。
  1. ArrayオブジェクトやControlオブジェクトを直接手入力する。
  2. リソースエディタなどのでビジュアルに作成したリソースファイルから変換する。

1.の方法は、通常MocaScriptのプログラミングと同じように、とにかく記述します。 コントロールの位置と大きさは、 コントロール毎に明示的に指定する方法と、 HFLOWVFLOWなどの擬似コントロールに任せて自動配置する方法があります。

2.は、予めリソースエディタなどを使ってビジュアルに作成したダイアログボックスを元に、 ツールを使ってMocaScript形式へ変換します。 変換ツールもMocaScriptで記述されていて、<ConvDlg.ms>ファイルに収められています。 このファイルを<share\script>にコピーすれば使えるようになります。 以下の手順により、リソースファイル内のダイアログを変換できます。

  1. リソースファイル(テキスト形式のもの)をPeggyで開く。
  2. 変換したいダイアログのBEGINEND(1回1つのみ)を選択する。
  3. <ConvDlg.ms>のスクリプトConvertDialogを実行する。

変換結果はアウトプットウインドウの「スクリプト」ページに出力されます。 同時にクリップボードにもコピーされているので、 変換したダイアログボックステンプレートを利用したいMocaScriptファイルを開き、貼り付けることができます。 貼り付け後は、必要に応じて修正してください。

【メモ】
ダイアログボックステンプレートは、Controlオブジェクトの配列です。 ここに示した方法に限らず、最後が同じ形式になっていれば、どのような方法で作成してもかまいません。 そのときの状況に合わせて、プログラムでテンプレートを生成することも可能です。
【メモ】
カスタムダイアログボックス全体の大きさは、 テンプレートのコントロールがすべて収まるよう自動的に調整されます。


コントロールの種類

カスタムダイアログボックス上に配置できるコントロールには、以下のものがあります。 コントロールの種類は、Controlオブジェクトのtypeプロパティに文字列として設定します。 typeプロパティの大文字/小文字は区別しません。 その他のControlオブジェクトのプロパティに関しては、 Controlオブジェクトを参照してください。

コントロールの種類
(typeプロパティ)
コントロールの機能
"STATIC" ダイアログボックス上置く文字列。編集はできない。
"EDIT" テキストの入力欄。スタイルを指定することにより、数値入力欄、複数行入力欄としても使える。
"LIST" リストボックス。複数のアイテムをリスト形式で表示し、1つ、または複数を選択してもらう。
"CHECKLIST" チェック付きリストボックス。リストボックスに表示した複数のアイテムをチェックして選択できる。
"COMBO" リストとテキスト入力欄が合体したもの。テキストを入力することもできるし、リストから選択することも可能。
"CHECK" チェックボックス。
"BUTTON" プッシュボタン。
"RADIO" ラジオボタン。一つを選択すると以前の選択が自動的にクリアされる。
"COLOR" カラーボタン。ダイアログボックス上に色を表示し、選択/変更する。
"TREELIST" ツリーリスト。ツリーとリストを組み合わせた高度な表示ができるコントロール。
"GROUPBOX" コントロールのグループを線で囲んで視覚的に示す。
"VFLOW" コントロールを縦方向に自動配置するための擬似コントロール。
"HFLOW" コントロールを横方向に自動配置するための擬似コントロール。
"SPACER" 自動配置で隙間をあけるための擬似コントロール。

コントロールにはスタイルを設定するとで、見え方や動作を細かく指定することができます。 スタイルは、コントロールの種類にかかわらず共通のものと、 コントロール独自のものがあります。 詳しく、コントロールのスタイルを参照してください。

【メモ】
Controlオブジェクトのtypeプロパティは必須ですが、 他のプロパティは省略することができます。


カスタムダイアログボックスの表示

カスタムダイアログボックスは、タイトルとダイアログボックステンプレートを指定してDialogオブジェクトを作成し、 open()メソッドで表示できます。


    // 簡単なダイアログボックスを表示する。
    var template =
    [
        { type: "EDIT" },
        { type: "BUTTON", label: "OK", id: IDOK }
    ];

    var dialog = new Dialog("シンプルな例", template);
    if (dialog.open() == IDOK)
    {
        // [OK]ボタンが押された
        ...
    }



簡単に表示するならcustomBox()

単純なカスタムダイアログボックスであれば、 customBox() グローバル関数を使うことで、 より簡単に表示できます。 customBox()は、 自動的にダイアログボックステンプレートへ[OK]、[キャンセル]ボタンを追加します。 そのため、ダイアログボックステンプレート中にこれらのボタンを定義する必要はありません。


    // 簡単なダイアログボックスを表示する。
    var template =
    [
        { type: "EDIT" },
        { type: "EDIT" },
    ];

    var results = customBox("シンプルな例", template);
    if (results)
    {
        // [OK]ボタンが押された
        ...
    }

【メモ】
ダイアログボックスにより高度な動作をさせたいときは、 Dialog.open()メソッドを使います。
【メモ】
customBox()関数は内部でDialogオブジェクトを生成しています。 おおよそ以下のコードと同等です。
    function customBox(title, template)
    {
        var dialog = new Dialog(title, template);
        dialog.addOkCancel = Dialog.ADD_OKCANCELLINE;
        return dialog.open() == IDOK ? dialog.results : null;
    }
【ヒント】
Dialog.open()メソッドでダイアログボックスを表示するときでも、 DialogオブジェクトのaddOkCancelプロパティを設定するとで、 [OK]、[キャンセル]、[ヘルプ]ボタンを追加することができます。 addOkCancelプロパティに設定できる値は、 Dialog.ADD_CANCELDialog.ADD_OKCANCELDialog.ADD_HELPDialog.ADD_LINEDialog.ADD_OKCANCELDialog.ADD_OKCANCELLINEのいずれか、 またはビットOR演算子(|)で複数設定することも可能です。
    var template =
    [
        { type: "EDIT" },
        { type: "EDIT" },
    ];
    var dialog = new Dialog(title, template);
    dialog.addOkCancel = Dialog.ADD_OKCANCELLINE;
    dialog.open();


結果の取り出し

ダイアログボックスを[OK]ボタン、またはEnterキーにより閉じたときは、 各コントロールの値を集めたArrayオブジェクトが作成され、 Dialogオブジェクトのresultsプロパティに設定されます。 この配列にアクセスすることで、ユーザからの入力を取り出せます。

customBox()関数でダイアログボックスを表示したときは、 customBox()関数の戻り値が結果を収めたArrayオブジェクト、 つまりdialog.resultsになっています。

すべてのコントロールの値がresultsに収められるわけではありません。 例えば、文字列を表示するのが目的のSTATICコントロールの値は結果に含まれません。 どの種類のコントロールの値が収められるかは、 Controlオブジェクト一覧表の最後の行を参照してください。

さらにControlオブジェクトがnameプロパティを持っている場合、 resultsからその名前をキーとして値を取り出すこともできます。 配列のインデックスを使って値を取り出す方法は、 コントロールの追加削除によって位置が変わってしまいますが、 名前であれば順番を気にするとこなくアクセス可能です。 名前を付けておくことで、変更に強いプログラムになります。


    // 結果の取り出し
    var template =
    [
        { type: "EDIT", name: "edit1" },
        { type: "EDIT", name: "edit2" },
        { type: "BUTTON", label: "OK", id: IDOK }
    ];

    var dialog = new Dialog("インデックスと名前", template);
    if (dialog.open() == IDOK)
    {
        // 配列の要素から値を取り出す。
        var text1 = dialog.results[0];
        var text2 = dialog.results[1];

        // 名前付きのコントロールなら、その名前でも取り出せる
        var text3 = dialog.results.edit1;
        var text4 = dialog.results.edit2;

        ...
    }

【ヒント】
[OK]ボタン、Enterキーでresultsプロパティに保存される値は、 Dialog.saveValues()メソッドにより取り出される値と同じです。 これ以外の情報も保存したい場合は、 onOK()コールバックメソッドを定義し、その中で保存します。


[OK]以外のボタンでダイアログボックスを閉じる

ダイアログボックスを閉じるボタンを用途に応じて複数用意しておきたいことがあります。 それには、onClickコールバック関数付きのボタンと、close()メソッドを使います。 close()メソッドの引数として渡した整数値がopen()メソッドの戻り値となります。


    // ダイアログボックスを閉じる複数のボタン - その1
    var template =
    [
        { type: "BUTTON", label: "ボタン&1", onClick: function(dialog) { dialog.close(101); }},
        { type: "BUTTON", label: "ボタン&2", onClick: function(dialog) { dialog.close(102); }},
        { type: "BUTTON", label: "ボタン&3", onClick: function(dialog) { dialog.close(103); }}
    ];

    var dialog = new Dialog("ボタンで選択", template);
    switch (dialog.open()) {
    case 101:
        messageBox("ボタン1が押された.");
        break;

    case 102:
        messageBox("ボタン2が押された.");
        break;

    case 103:
        messageBox("ボタン3が押された.");
        break;
    }

【ヒント】
customBox()関数は、[OK]ボタンがクリックされたときだけ値を返します。 複数のボタンでダイアログボックスを閉じる場合は、 Dialog.open()メソッドを使う必要があります。

よく似た関数リテラルをいくつも定義したくない場合は、 Controlオブジェクトのプロパティとして値を決めておき、 その値を返すコールバック関数を作成して各ボタンから共有するとよいでしょう。


    // ダイアログボックスを閉じる複数のボタン - その2
    function callback(dialog, control)
    {
        dialog.close(control.buttonNumber);
    }

    var template =
    [
        { type: "BUTTON", label: "ボタン&1", buttonNumber: 101, onClick: callback },
        { type: "BUTTON", label: "ボタン&2", buttonNumber: 102, onClick: callback },
        { type: "BUTTON", label: "ボタン&3", buttonNumber: 103, onClick: callback },
    ];

    var dialog = new Dialog("ボタンで選択", template);
    switch (dialog.open()) {
    case 101:
        messageBox("ボタン1が押された.");
        break;

    case 102:
        messageBox("ボタン2が押された.");
        break;

    case 103:
        messageBox("ボタン3が押された.");
        break;
    }

【ヒント】
Controlオブジェクトもオブジェクトですから、 任意のプロパティを持つことも可能です。 この例では、buttonNumberを追加しています。
【ヒント】
Controlオブジェクトのコールバック関数には、 引数としてDialogとControlオブジェクトが渡されます。 また、予約語thisを使ってもControlオブジェクトを参照できます。


[OK]以外のボタンで値を返す

[OK]ボタンをクリックしてダイアログボックスを閉じると、 各コントロールの値がresultsプロパティに自動保存されますが、 他のボタンから閉じた場合には保存されません。

[OK]以外のボタンでも値を保存するには、 そのボタンのコールバック関数の中からsaveValues()メソッドを使います。


    // 値を保存して閉じるボタン
    function callback(dialog, control)
    {
        // 値を取り出してDialogオブジェクトのresultsプロパティに保存する
        dialog.results = dialog.saveValues();
        dialog.close(this.buttonNumber);
    }

    var template =
    [
        { type: "EDIT", name: "edit1" },
        { type: "BUTTON", label: "ボタン&1", buttonNumber: 101, onClick: callback },
        { type: "BUTTON", label: "ボタン&2", buttonNumber: 102, onClick: callback },
        { type: "BUTTON", label: "ボタン&3", buttonNumber: 103, onClick: callback },
    ];

    var dialog = new Dialog("ボタンで選択", template);
    switch (dialog.open()) {
    case 101:
        messageBox("ボタン1が押された.\n入力テキスト: " + dialog.results.edit1);
        break;

    case 102:
        messageBox("ボタン2が押された.\n入力テキスト: " + dialog.results.edit1);
        break;

    case 103:
        messageBox("ボタン3が押された.\n入力テキスト: " + dialog.results.edit1);
        break;
    }

【ヒント】
結果を保存するプロパティはresultsと決まっているわけではありません。 ボタンの用途によって保存するプロパティを変えるといったテクニックも可能です。 また、一部のコントロールの値だけでよい場合、 getValue()メソッドを使い必要な値だけを保存する方法も有効です。


擬似コントロール

擬似コントロールは、位置や大きさの指定が省略されているコントロールを縦方向、 または横方向に順番に配置するために用意しました。 そのため、ダイアログボックス上に見える部分はありません。

擬似コントロールには、VFLOW、HFLOW、SPACERの3種類があります。

擬似コントロールも他のコントロールと同じようにControlオブジェクトとして定義します。 擬似コントロールを定義するときに意味を持つプロパティは、 Controlオブジェクトを参照してください。

コントロールはオブジェクトですが、 擬似コントロールは配列として定義した方が便利です。 配列の要素0、1、2、...に、自動配置したいControlオブジェクトをセットするためです。 また、要素以外に少なくともControlオブジェクトの必須プロパティであるtypeをセットしなければなりません。


    // 擬似コントロール
    var hflow =
    [
        { type: "EDIT" },
        { type: "EDIT" },
        { type: "EDIT" },
        { type: "EDIT" }
    ];
    hflow.type = "HFLOW";
    customBox("横に配置", [ hflow ]);

HFLOW、VFLOW内で位置を示すxyプロパティが省略されているコントロールは、 適当な場所へ自動配置されます。 ただし、Controlオブジェクトで明示的にx, y位置を指定してあるものは、その指定位置に配置されます。

VFLOW、HFLOW擬似コントロール内のコントロールの位置(x, y)は、 すべて擬似コントロールの左上隅からの相対となります。 例えば{ type: "...", x: 0, y: 0, ... }は、 擬似コントロールの左上隅を意味します。

【ヒント】
MocaScript(JavaScriptも)では、オブジェクトと配列は僅かな違いがあるでけで、実はほとんど同じものです。 配列は、整数値をキーとする連想配列として実装されています。 従って上記サンプルは、キーが整数であるオブジェクトリテラルを使って以下のように書くこともできます。

    // オブジェクトリテラルで表した擬似コントロール
    var hflow =
    {
        type:   "HFLOW",
        length: 4,
        0:      { type: "EDIT" },
        1:      { type: "EDIT" },
        2:      { type: "EDIT" },
        3:      { type: "EDIT" }
    };
    customBox("横に配置", [ hflow ]);

typeプロパティをまとめることができる反面、 lengthプロパティと各コントロールのインデックスを記述しなければならず、 やはり面倒です。 そこで以下のように可変個の引数を受け取るラッパ関数HorzFlow()を用意します。

    function HorzFlow()
    {
        var flow = new Array();
        flow.type = "HFLOW";

        for (var i = 0; i < arguments.length; i++)
            flow.push(arguments[i]);
        return flow;
    }

    var hflow = HorzFlow(
        { type: "EDIT" },
        { type: "EDIT" },
        { type: "EDIT" },
        { type: "EDIT" });

    customBox("横に配置", [ hflow ]);

ライブラリファイル<CustDlg.msl>には、 HorzFlow()を含むいくつかのカスタムダイアログボックス用ユーティリティ関数収められています。 必要に応じてインクルードしてください。 また、いちいちインクルードしなくてもこれら関数を使えるようにしたい場合は、 <CustDlg.msl>ファイルの先頭付近にあるコメントの手順に従って設定してください。

VFLOW、HFLOW擬似コントロールは入れ子にできます。 例えば2段組でコントロールを配置した場合、擬似コントロールを以下のように入れ子することで可能になります。


     //  HFLOW →
     // ┏━━━━━━━━━━━━━━━━━━┓
     // ┃┌───────┐┌───────┐┃
     // ┃│VFLOW         ││VFLOW         │┃
     // ┃│↓            ││↓            │┃
     // ┃│              ││              │┃
     // ┃│              ││              │┃
     // ┃│              ││              │┃
     // ┃│              ││              │┃
     // ┃└───────┘└───────┘┃
     // ┗━━━━━━━━━━━━━━━━━━┛

    #include "CustDlg.msl"

    var template =
    [
        HorzFlow(
            VertFlow( { type: "EDIT" }, { type: "EDIT" }, { type: "EDIT" }, { type: "EDIT" } ),
            VertFlow( { type: "EDIT" }, { type: "EDIT" }, { type: "EDIT" }, { type: "EDIT" } )
        )
    ];
    customBox("2段組配置", template);



GROUPBOXの配置機能

GROUPBOXコントロールにも擬似コントロールVFLOWと同じ自動配置機能が備わっています。 つまり、GROUPBOXに入れ子になっているコントロールで位置指定のないものは、 縦方向に自動配置されます。

VFLOWとの違いは、範囲を示す枠を描画することと、 グループ枠内側の上端から10、下左右それぞれから5ダイアログ単位のマージンがとられることです。 GROUPBOXは、実際のコントロールなので、 有効/無効の切り替え、 表示/非表示の切り替えもできます。


    // GROUPBOXの自動配置機能
    var template =
    [
        { type: "GROUPBOX", label: "グループ1", length: 3,
            0: { type: "EDIT" },
            1: { type: "EDIT" },
            2: { type: "EDIT" }},
        { type: "GROUPBOX", label: "グループ2", length: 3,
            0: { type: "EDIT" },
            1: { type: "EDIT" },
            2: { type: "EDIT" }}
    ];
    customBox("GROUPBOX", template);

【ヒント】
ライブラリファイル<CustDlg.msl>の中のGroupBox()関数を使って生成することもできます。

    // GroupBox() 関数を使ったグループ化
    #include "CustDlg.msl"

    var template =
    [
        GroupBox("グループ1",
            { type: "EDIT" },
            { type: "EDIT" },
            { type: "EDIT" }),
        GroupBox("グループ2",
            { type: "EDIT" },
            { type: "EDIT" },
            { type: "EDIT" })
    ];
    customBox("GroupBox() 関数", template);


SPACER擬似コントロール

SPACER擬似コントロールは、SPACERコントロールで指定した部分に指定幅の空白を挿入します。 VFLOW擬似コントロールの中で使うとheightプロパティで指定した上下方向、 HFLOW擬似コントロールの中で使うとwidthで指定した水平方向に空白を確保します。 heightwidthを省略すると、5ダイアログ単位とみなします。


    // SPACERで隙間を空ける。
    var template =
    [
        { type: "EDIT" },
        { type: "SPACER", height: 10 },  // 10ダイアログ単位の隙間を作る
        { type: "EDIT" },
        { type: "SPACER", height: 0 },   // 隙間を無くす
        { type: "EDIT" }
    ];
    customBox("SPACER", template);

【ヒント】
ライブラリファイル<CustDlg.msl>の中のSpacer()関数を使って生成することもできます。

    // GroupBox() 関数を使ったグループ化
    #include "CustDlg.msl"

    var template =
    [
        { type: "EDIT" },
        Spacer(10),
        { type: "EDIT" },
        Spacer(0),
        { type: "EDIT" }
    ];
    customBox("Spacer() 関数", template);


プロパティ値の継承

xyプロパティを省略して自動配置されるControlオブジェクトは、 VFLOW(上から下へ自動配置)の中では直前のコントロールのx位置、 HFLOW(左から右へ自動配置)の中では直前のコントロールのy位置を継承します。 つまり、VFLOW中の最初のコントロールのxプロパティで水平位置を設定すると、 それに続くコントロールはxプロパティを省略できるということです。

EDIT、LIST、COMBOコントロールは、 それ以前で最も近いEDIT/LIST/COMBOコントロールのwidthプロパティで指定された幅も継承します。

さらに、STATIC、EDIT、COMBO、LIST、RADIOコントロールのlabelプロパティによってラベルを付ける場合、 ラベルの幅(labelWidth)やスタイル(labelStyle)プロパティも継承されます。

これらの継承機能により、いちいち位置や幅を指定しなくてもコントロールを綺麗に並べることができます。


    // プロパティ値の継承
    var template =
    [
        { type: "EDIT", label: "名前(&N):", labelWidth: 40, x: 45, width: 100 },
        { type: "EDIT", label: "住所(&A):" },
        { type: "EDIT", label: "電話番号(&P):"},
        { type: "COMBO", label: "通勤時間(&T):", style: CBS_DROPDOWNLIST, options: ["1時間未満", "1〜2時間", "2時間以上"] }
    ];
    customBox("プロパティ値の継承", template);

【注意】
入れ子になったHFLOW/VFLOW擬似コントロール、GROUPBOXコントロールの先頭で初期化されます。 これらの内部にある先頭のコントロールでは、必要なプロパティを設定するようにしてください。


10秒後に自動的に閉じるダイアログボックス

タイマーを使うことで、一定時間後に自動的に閉じるダイアログボックスを作ることができます。 Dialogオブジェクトのtimerプロパティにタイマー間隔をミリ秒単位で設定すると、 ダイアログボックス表示中、 その時間間隔でプロパティonTimerに登録されているコールバックメソッドが呼び出されます。 コールバックメソッドの中からDialogオブジェクトは、予約語thisで参照できます。


    var template =
    [
        { type: "EDIT", name: "edit1" },
        { type: "BUTTON", label: "OK", id: IDOK }
    ];

    var dialog = new Dialog("自動クローズ", template);

    dialog.count = 10;
    dialog.timer = 1000;  // 単位はミリ秒
    dialog.onTimer = function()
    {
        // 1秒ごとに呼ばれ、0までダウンカウントしたら自動クローズする。
        this.setTitle("あと " + --this.count + "秒");
        if (this.count == 0)
            this.close(IDCANCEL);
    };
    dialog.open();

ダイアログボックス表示とともにタイマーを起動するのではなく、 必要に応じてタイマーを起動、停止させたいときは、 Dialog.setTimer() メソッドを使います。


    // スタート/ストップ付きストップウォッチ
    var template =
    [
        { type: "BUTTON", label: "スタート", onClick: function (dialog, control) { dialog.setTimer(100); }},
        { type: "BUTTON", label: "ストップ", onClick: function (dialog, control) { dialog.setTimer(0); }},
        { type: "BUTTON", label: "OK", id: IDOK }
    ];

    var dialog = new Dialog("ストップウォッチ", template);
    dialog.counter = 0;
    dialog.onTimer = function () { this.setTitle(sprintf("経過時間: %g 秒", ++this.counter / 10.0)); };
    dialog.open();


テキストが入力されたら[OK]ボタンを有効にする

[OK]ボタンを最初は無効にしておき、 テキストが入力されたときはじめて有効にするには、 ControlオブジェクトにonChangeコールバック関数を設定します。 [OK]ボタンのControlオブジェクトのstyleプロパティにより、 [OK]ボタンの初期状態を「無効」から始めることができます。


    function callback(dialog, control)
    {
        var text = dialog.getValue(control);
        dialog.enableControl(IDOK, text != "");
    }

    // テキストが入力されたら[OK]ボタンを有効化する。
    var template =
    [
        { type: "EDIT", name: "edit1", onChange: callback },
        { type: "BUTTON", label: "OK", id: IDOK, style: WS_DISABLED }
    ];

    var dialog = new Dialog("テキスト入力", template);
    dialog.open();

何かの文字を入力すると、onChangeプロパティに設定されているコールバック関数callbackが呼び出されます。Controlオブジェクトのコールバック関数には、DialogオブジェクトとControlオブジェクトが引数として渡されます。

【ヒント】
getValue()enableControl()など、 コントロールを操作対象とするDialogオブジェクトのメソッドは、 最初の引数でオブジェクトを指定します。 オブジェクトそのもの、nameプロパティによるオブジェクトの名前、またはID番号のいずれでも指定可能です。


値の確認

ControlオブジェクトのonValidateプロパティにコールバック関数をセットしておくと、 [OK]ボタンがクリックされたときやDialog.saveValues()メソッドが呼び出されたとき、 コントロールの値が正しい範囲にあるかどうかを検証するために呼び出されます。

onValidateコールバック関数には、 Dialogオブジェクトとコントロール、それに検証する値の3つが引数として渡されます。 また、Controlオブジェクトは予約語thisでも参照できます。

値が正しい場合はtrue、 範囲外であればfalseを返します。 このときmessageBox()関数などにより何がどう正しくないのかを表示すれば、 間違っている個所を探し易くなります。

onValidate()コールバック関数を持っているコントロールを順番に呼び出します。 onValidateコールバック関数がfalseを返すと、 [OK]ボタンの処理を中止します。 それ以降のコントロール値の検証も行われません。


    function checkNotEmpty(dialog, control, value)
    {
        if (value != "")
            return true;

        // どこが悪いのかを教えてあげると親切
        messageBox("何も入力されていません.");
        return false;
    }

    // 空でないテキストを入力する
    var template =
    [
        { type: "EDIT", name: "edit1", onValidate: checkNotEmpty },
    ];

    var results = customBox("テキスト入力", template);



ラベルを付ける

EDITコントロールなどの入力欄には、それが何のためのものかを示す「ラベル」を付けることができます。 「ラベル」を付けるには、ControlオブジェクトにlabellabelWidthlabelStyleなどのラベルプロパティを追加します。 ラベルは、"EDIT""STATIC""LIST""COMBO""RADIO"に付加することができます。


    // ラベル付きのコントロール
    var template =
    [
        { type: "EDIT", name: "edit1", x: 50, label: "電話番号(&T): ", labelWidth: 45 },
        { type: "EDIT", name: "edit2", x: 50, label: "名前(&N):", labelWidth: 45 },
    ];
    var results = customBox("ラベル付きEDIT", template);

ラベルは、xプロパティで指定したコントロールのx位置より左に配置されるので注意してください。 labelWidthによりラベルの横幅を指定すると、 ラベルの左端はx - labelWidthとなり、 xからEDITなどのコントロールが配置されます。 そのため上記サンプルでは、ラベル幅45に対してコントロールのx位置を50と指定しています。

ラベルは"STATIC"を使って個別に作ることもできます。 個別にラベルを作るのと、EDITコントロールの付属コントロールとして作る方法には、以下のような違いがあります。

付属コントロール独立した"STATIC"
Dialog.enableControl()メソッド、 Dialog.showControl()メソッドで付属ラベルもまとめて有効化/無効化、表示/非表示が行える。 "STATIC"で作ったラベルと"EDIT""LIST"などを個別に処理(有効化/無効化など)しなければならない。
ダイアログリソースからこの形式には自動変換できない この形式には自動変換できる


スピンを付ける

EDITコントロールに数値を上限させる"SPIN"コントロールは、 ラベルと同じようにEDITの付属コントロールとして作成することができます。 EDITコントロールにスピンを付けるには、上下させる範囲を配列としてspinプロパティに指定するだけです。


    // スピン付き"EDIT"コントロール
    var template =
    [
        { type: "EDIT", name: "edit1", x: 50, label: "年齢(&A): ", labelWidth: 45, value: 30, spin: [0, 125] },
        { type: "EDIT", name: "edit2", x: 50, label: "身長(&T):", labelWidth: 45, value: 175, spin: [50, 200] },
        { type: "EDIT", name: "edit3", x: 50, label: "体重(&W):", labelWidth: 45, value: 60, spin: [3, 150] },
    ];
    var results = customBox("スピン付きEDIT", template);



カラーボタン

カラーボタンは、RBG値に対応した色を表示し、新しい色を選択するためのコントロールです。 colorBox()関数でも色を扱うことができますが、 いちいち別のダイアログボックスを表示させるのは面倒です。 カラーボタンコントロールを使うことにより、スマートに色の確認と変更が行えます。


    // カラーボタンによる色の選択
    var template =
    [
        { type: "COLOR" }
    ];
    var results = customBox("カラーボタン", template);
    if (results)
        messageBox(sprintf("RGB = %06X", results[0]));

カラーボタンの結果はRGB値を収めた0xBBGGRR形式の整数です。 HTMLのカラー値#RRGGBBとは赤(RR)と青(BB)の位置が逆転しているのため注意してください。

初期カラーはvalueプロパティで指定することができます。 ボタンの大きさや位置も、他のコントロールと同じように指定することができます。


    // 初期カラーを指定してカラーボタンによる色の選択
    var template =
    [
        { type: "COLOR", value: 0x00ffff, width: 105, height: 25 }
    ];
    customBox("カラーボタン", template);

カラーボタンをクリックしたときに表示されるパレットは変更できます。 標準のパレット以外に2種類の拡張パレットが用意されていて、 styleプロパティにより切り替えられます。 それ以外にも、用途にあったカスタムパレットを作成することも可能です。 カスタムパレットは、RGB値(整数)の配列で指定します。


    // パレットのカスタマイズ。
    var palette = new Array(256);
    for (var i = 0; i <= 255; i++)
        palette[i] = (255 - i) << 16;

    var template =
    [
        { type: "COLOR", label: "標準 20色:", x: 60, labelWidth: 55 },
        { type: "COLOR", label: "HTML 140色:", x: 60, style: CPS_HTMLCOLORS },
        { type: "COLOR", label: "HTML 216色:", x: 60, style: CPS_SAFECOLORS },
        { type: "COLOR", label: "カスタム青 256色:", x: 60, options: palette }
    ];
    customBox("パレットのカスタマイズ", template);

カスタムパレットの縦横サイズを明示的に指定するには、 パレット配列にプロパティrowscolsを設定してください。 また、余ったセルの色は、パレット配列のfillプロパティで変更することができます。


    // パレットのカスタマイズ。
    var palette = new Array(128);
    for (var i = 0; i <= 255; i += 2)
        palette[i] = ((255 - i) << 16) | (i << 8);

    palette.rows = 13;        // 縦13
    palette.cols = 10;        // 横10
    palette.fill = 0xffffff;  // 残ったセルは白

    var template =
    [
        { type: "COLOR", label: "グラデーション:", x: 60, labelWidth: 55, options: palette }
    ];
    customBox("パレットのカスタマイズ", template);



ツリーリストコントロール

ツリーリストコントロールは、ツリーとしてもリストとしても、 またそれらを合わせたツリーリストとしても使える強力なコントロールです。 しかし、それだけに使い方も少々複雑になっています。
単純なグリッド表示
まずは簡単なグリッド表示の例からスタートして、次第に複雑なツリーリストの応用方法を紹介します。


    // ヘッダーと横のカラム数を定義する配列
    var header =
        [ "名前", "性別", "年齢" ];

    // グリッドに表示されるデータを指定する2次元配列
    var treelist =
    [
        [ "ルパン",   "男", 40 ],
        [ "峰不二子", "女", 30 ],
        [ "銭型警部", "男", 50 ]
    ];

    // TREELISTを使ったダイアログボックスのテンプレート
    var template =
    [
        { type: "TREELIST", width: 200, height: 100, header: header, value: treelist }
    ];
    customBox("グリッド表示", template);

2次元配列が2次元のグリッドにそのまま表示されています。 カラムの数は、headerにより決定されます。 データがカラム数より少ないときは、足りないカラムは空白となります。 逆に多い場合は、表示されません。

ツリーで分類
次はツリー機能を使い、追う側と終われる側に分けて表示してみましょう。 以下横1行をアイテムよび、アイテムに対応しているデータをツリーアイテムオブジェクトと呼ぶことにします。 変更するのは、treelistの2次元配列だけです。 以下は修正したtreelistと、表示されるダイアログボックスです。 【注意】ダイアログボックスは、カラム幅とツリーのノードを表示後に調整しています。


    // 追われる側と追う側に分けてツリー表示する
    var treelist =
    [
        { 0: "追われる", children: [    // 親ツリーアイテムオブジェクト
            [ "ルパン",   "男", 40 ],   // 子ツリーアイテムオブジェクト
            [ "峰不二子", "女", 30 ]]},
        { 0: "追いかける", children: [
            [ "銭型警部", "男", 50 ]]}
    ];

オブジェクトと配列がほとんど同じ物であったことを思い出してください。 ツリーアイテムオブジェクトの場合、 [ "ルパン", "男", 40 ] と、 { 0: "ルパン", 1: "男", 2: 40 } はほぼ等価と考えて差し支えありません。 配列中treelist中の最初のオブジェクト { 0: "追われる", ... } には、 数値0のプロパティ、つまり配列要素の0に "追われる" という文字列が設定されているので、 これが最初のカラムの[+]/[-]ボタンの直後に表示されています。 1: ... 2: ... などを追加すれば、 それぞれ第2/3カラムに表示されます。

このオブジェクトのchildrenプロパティの値は、 このアイテムの子になるアイテムの配列です。

【ヒント】
この例では、オブジェクトと配列がほとんど同じものであることを利用し、 子持ちアイテムをオブジェクトリテラル、子無しアイテムを配列リテラルを使って記述していますが、 このように記述しなければならないという決まりはありません。 表示されるカラムの数は、ヘッダーを定義する配列により決まり、 0の要素が第1カラム、1の要素が第2カラム、...へ表示されることと、 childrenの要素が子アイテムとして扱われるだけです。 記述し易い方を使ったにすぎません。
【ヒント】
childrenは、何重にでも入れ子にすることがで、それだけ深いツリーが表示されます。
【ヒント】
ツリーリストが表示される段階で、 子ツリーアイテムオブジェクトから親を参照するためのparentプロパティが自動的に作成されます。 parentプロパティを使うことで、順次上のツリーアイテムへと辿ることが可能です。 ただしルートのツリーアイテムオブジェクトには、parentプロパティは作成されません。

ツリーを最初から開いた状態にするには、expandプロパティを使います。


    // 追われる側と追う側に分け、最初から展開したツリーを表示する
    var treelist =
    [
        { 0: "追われる", expand: true, children: [
            [ "ルパン",   "男", 40 ],
            [ "峰不二子", "女", 30 ]]},
        { 0: "追いかける", expand: true, children: [
            [ "銭型警部", "男", 50 ]]}
    ];

見栄えの調整
ところでいちいちカラム幅を調整しないと名前が完全に表示されないのは面倒でしかたありません。 最初から適当なカラム幅を設定するには、header配列を工夫します。 ついでに、右寄せやセンタリングの揃え位置指定もしてみましょう。


    // 初期幅や位置揃えも指定したヘッダー定義
    var header =
        [ [ "名前", 100 ], [ "性別", 50, /* センタリング */ 2 ], [ "年齢", /* 初期幅を省略 */, /* 右揃え */ 1 ] ];

header定義配列の個々の要素が配列であると、 [ 表示文字列, 初期幅(ドット単位), 揃え位置 ] と解釈します。 揃え位置は、0が左揃え、1が右そろえ、2がセンタリングで、省略すると左揃えと見なします。 揃え位置を指定すると、そのカラムのヘッダーとデータのすべてが指定に従って表示されます。

以上をまとめると、以下のようになるはずです。


    // 初期幅や位置揃えも指定したヘッダー定義
    var header =
        [ [ "名前", 100 ], [ "性別", 50, /* センタリング */ 2 ], [ "年齢", /* 初期幅を省略 */, /* 右揃え */ 1 ] ];

    // 追われる側と追う側に分け、最初から展開したツリーを表示する
    var treelist =
    [
        { 0: "追われる", expand: true, children: [
            [ "ルパン",   "男", 40 ],
            [ "峰不二子", "女", 30 ]]},
        { 0: "追いかける", expand: true, children: [
            [ "銭型警部", "男", 50 ]]}
    ];

    // TREELISTを使ったダイアログボックスのテンプレート
    var template =
    [
        { type: "TREELIST", width: 200, height: 100, header: header, value: treelist }
    ];
    customBox("ツリー表示", template);

グリッドオブジェクト
ここまでの例では、グリッド内に表示するデータがすべて単純な文字列や数値を使っていましたが、 代わりにオブジェクトを使うことで、個々に色を付けたり、値を編集するなどの操作が可能になります。 これをグリッドオブジェクトと呼ぶことにします。 最も簡単な例として、色を変えてみましょう。


    // 性別の男を青色、女を赤色で表示する
    var treelist =
    [
        { 0: "追われる", expand: true, children: [
            [ "ルパン",   { value: "男", color: 0xff0000 }, 40 ],
            [ "峰不二子", { value: "女", color: 0x0000ff }, 30 ]]},
        { 0: "追いかける", expand: true, children: [
            [ "銭型警部", { value: "男", color: 0xff0000 }, 50 ]]}
    ];

1つのグリッドに対応するデータがオブジェクトの場合、 valueプロパティが表示データ、 colorプロパティの値をRGB値(0x00BBGGRR)としてその色で表示されます。

【ヒント】
ツリーアイテムオブジェクトのcolorプロパティでは、1行全体の表示色を指定することができます。 ツリーアイテムオブジェクトとグリッドオブジェクトの両方にcolorプロパティがあるときは、 グリッドオブジェクトの指定が優先します。

データの編集
グリッド内のデータを編集することができます。 グリッドのデータを編集できるようにするには、 まずグリッドのデータをグリッドオブジェクトで表し値をvalueプロパティにセットし、 次にeditプロパティにtrueをセットします。 以下は、年齢を修正できるようにした例です。


    // 年齢を編集可能にする
    var treelist =
    [
        { 0: "追われる", expand: true, children: [
            [ "ルパン",   { value: "男", color: 0xff0000 }, { value: 40, edit: true } ],
            [ "峰不二子", { value: "女", color: 0x0000ff }, { value: 30, edit: true } ]]},
        { 0: "追いかける", expand: true, children: [
            [ "銭型警部", { value: "男", color: 0xff0000 }, { value: 50, edit: true } ]]}
    ];

編集されたデータの受け取り
データが修正できても、取り出せなければ意味がありません。 ツリーリストはそれ自体が構造を持っているので、 [OK]ボタンがクリックされたときに複数のデータを返す方法が必要です。 しかし、すべてのグリッドの値が必要というわけでもありません。 編集される可能性のあるグリッドだけ取得できれば十分なはずです。

[OK]ボタンがクリックされたときに値を受け取るには、 グリッドのデータをグリッドオブジェクトで表し、 nameプロパティで名前を付けます。 これを名前付きグリッドオブジェクトと呼びことにします。 ツリーリスト全体の名前も忘れずにTLとしておきましょう。


    // 初期幅や位置揃えも指定したヘッダー定義
    var header =
        [ [ "名前", 100 ], [ "性別", 50, /* センタリング */ 2 ], [ "年齢", /* 初期幅を省略 */, /* 右揃え */ 1 ] ];

    // グリッドに名前を付けて値を取得できるようにする。
    var treelist =
    [
        { 0: "追われる", expand: true, children: [
            [ "ルパン",   { value: "男", color: 0xff0000 }, { value: 40, name: "age1", edit: true } ],
            [ "峰不二子", { value: "女", color: 0x0000ff }, { value: 30, name: "age2", edit: true } ]]},
        { 0: "追いかける", expand: true, children: [
            [ "銭型警部", { value: "男", color: 0xff0000 }, { value: 50, name: "age3", edit: true } ]]}
    ];

    // TREELISTを使ったダイアログボックスのテンプレート
    var template =
    [
        { type: "TREELIST", width: 200, height: 100, header: header, value: treelist, name: "TL" }
    ];

    // ツリーを表示して結果を受け取る
    var res = customBox("ツリー表示", template);
    if (res)
    {
        writeln("ルパン " + res.TL.age1 + "才");
        writeln("峰不二子 " + res.TL.age2 + "才");
        writeln("銭型警部 " + res.TL.age3 + "才");
    }

【注意】
編集したデータは、すべてstringとしてvalueプロパティに戻されます。 もとのデータ型が何であってもすべて文字列に変換されてしまうので注意してください。
【注意】
グリッドオブジェクトの名前として"length"は使わないようにしてください。 結果を受け取るとき、選択されていたアイテム数を表すlengthプロパティと名前が衝突してしまいます。
【ヒント】
アイテムの入れ子関係とグリッドオブジェクトの名前は独立しています。 ツリーの根元にあるアイテムの名前付きグリッドオブジェクトも、 ツリーの奥深くにあるアイテムの名前付きグリッドオブジェクトも同じように取り出されます。 そのためグリッドオブジェクト名は、1つのツリーリスト全体でユニークなものでなければなりません。

選択アイテムの取得
[OK]ボタンがクリックされたとき選択されていたアイテムは、 対応するツリーアイテムオブジェクトがインデックス0に設定されています。


    // ツリーを表示して結果を受け取る
    var res = customBox("ツリー表示", template);
    if (res)
    {
        writeln("ルパン " + res.TL.age1 + "才");
        writeln("峰不二子 " + res.TL.age2 + "才");
        writeln("銭型警部 " + res.TL.age3 + "才");

        writeln(res.TL[0][0], "が選択されていました.");
    }

res.TL[0]で選択されていたツリーアイテムオブジェクトを参照し、 res.TL[0][0]でその先頭のデータ、つまり名前を取り出しています。

【ヒント】
ツリーリストでは、TLS_MULTIPLESELスタイルを指定することで複数アイテムが選択可能になります。 複数アイテムが選択されているときは、res.TL[0]res.TL[1]、... と順番に選択されているアイテムがセットされ、選択数はlengthプロパティに設定されます。

グリッドにチェックを付ける
個々のグリッドにはチェックボックスを付けることができます。 チェックボックスを付けるには、グリッドオブジェクトにcheckプロパティを追加します。


    // 初期幅や位置揃えも指定したヘッダー定義
    var header =
        [ [ "名前", 100 ], [ "性別", , 2 ], [ "年齢", , 1 ], "物欲" ];

    // グリッドに名前を付けて値を取得できるようにする。
    var treelist =
    [
        { 0: "追われる", expand: true, children: [
            [ "ルパン",
              { value: "男", color: 0xff0000 },
              { value: 40, name: "age1", edit: true },
              { check: true, name: "greedy1" } ],
            [ "峰不二子",
              { value: "女", color: 0x0000ff },
              { value: 30, name: "age2", edit: true },
              { check: true, name: "greedy2" } ]]},
        { 0: "追いかける", expand: true, children: [
            [ "銭型警部",
              { value: "男", color: 0xff0000 },
              { value: 50, name: "age3", edit: true },
              { check: false, name: "greedy3" } ]]}
    ];

    // TREELISTを使ったダイアログボックスのテンプレート
    var template =
    [
        { type: "TREELIST", width: 200, height: 100, header: header, value: treelist, name: "TL" }
    ];

    // ツリーを表示して結果を受け取る
    var res = customBox("ツリー表示", template);
    if (res)
    {
        writeln("ルパン "   + res.TL.age1 + "才, 物欲", res.TL.greedy1_check ? "あり" : "なし");
        writeln("峰不二子 " + res.TL.age2 + "才, 物欲", res.TL.greedy2_check ? "あり" : "なし");
        writeln("銭型警部 " + res.TL.age3 + "才, 物欲", res.TL.greedy3_check ? "あり" : "なし");
    }

チェックの結果は、nameプロパティで指定したグリッドオブジェクト名に、 _check接尾文字が付いたものとなります。 この例では、名前付きグリッドgreedy1のチェック状態をres.TL.greedy1_checkで取り出しています。

【ヒント】
グリッドオブジェクトにvalueプロパティとcheckプロパティの両方がある場合、 グリッドにはチェックと値の両方が表示されます。 1つのグリッドに2つの値が存在するため、 valueプロパティの値はnameプロパティで指定した名前で、 チェックの結果はさらに_check接尾語を付けた名前で返すようになっています。
【ヒント】
名前付きグリッドのチェック状態は、Dialog.getCheck()Dialog.setCheck()メソッドを使って取得、設定することも可能です。
【注意】
ヘッダーで右寄せ、センタリングを指定したカラムであっても、 チェックは常に左寄せで表示されます。

3ステートチェックを付ける
通常チェックは、「チェックあり」、「チェック無し」の2つの状態が交互に切り替わりますが、 それらに加えて「どちらでもない」という状態を持つ3ステートチェックとしても使えます。 チェックを3ステートにするには、checkプロパティの値を整数0〜2で指定します。 下表にチェックの値をまとめます。

モードチェック状態
2ステートtrueあり
falseなし
3ステート0なし
1あり
2どちらでもない

子アイテムの動的作成
最初はとりあえず子アイテムがあるかのように[+]ボタンを表示しておき、 [+]が最初にクリックされた時点で子アイテムを作成することができます。 それには、childrenプロパティに配列ではなく、 子ツリーアイテムオブジェクトの配列を返す関数オブジェクトをセットします。


    // [+]ボタンが押されたときに動的に子アイテムを作成する
    var treelist =
    [
        { 0: "追われる", expand: true, children: [
            [ "ルパン",
              { value: "男", color: 0xff0000 },
              { value: 40, name: "age1", edit: true },
              { check: true, name: "greedy1" } ],
            [ "峰不二子",
              { value: "女", color: 0x0000ff },
              { value: 30, name: "age2", edit: true },
              { check: true, name: "greedy2" } ]]},
        { 0: "追いかける", expand: true, children: [
            [ "銭型警部",
              { value: "男", color: 0xff0000 },
              { value: 50, name: "age3", edit: true },
              { check: false, name: "greedy3" } ]]},
        { 0: "エキストラ",
            children: function(dialog, control, parent)
            {
                // [+]が押されたので乱数により子アイテムを作成して返す。
                var array = [];
                var n = Integer(Math.random() * 10);
                for (var i = 1; i <= n; i++)
                {
                    array.push([ "通行人" + i,
                                 { value: "男", color: 0xff0000 },
                                 Integer(Math.random() * 50 + 5) ]);
                }
                return array;
            }
        }
    ];

関数が呼び出されるときは、引数としてダイアログオブジェクト(dialog)、 ツリーリストのコントロールオブジェクト(control)と、 childrenプロパティがあったツリーアイテムオブジェクト(parent)の3つが渡されます。 関数が動的に作成した子アイテムの配列を返すと、それがparentの子アイテムとして展開表示されます。

【ヒント】
関数により子アイテムが作成されると、 childrenプロパティの値が作成された配列で置き換えられます。 つまり、関数により子アイテムが作成されるのは、 最初に[+]ボタンがクリックされたとき1回だけです。 [-]ボタンをクリックして閉じたときに子アイテムが破棄されることはありません。
【注意】
childrenプロパティに関数がセットされているときは、 expandプロパティにtrueセットしても展開されません。 最初に[+]ボタンをクリックしたときに関数が呼び出され、その結果に従って展開されます。

名前付きツリーアイテムオブジェクト
ツリーアイテムにもnameプロパティにより名前を付けることで、 ツリーリストを操作する関数から直接指定することができるようになります。 これを「名前付きツリーアイテムオブジェクト」と呼ぶことにします。 ツリーリストを操作するDialogオブジェクトのほとんどのメソッドは、 ツリーアイテムを名前でも指定することができるようになっています。

以下のサンプルでは、「エキストラ」ツリーアイテムに"extra"という名前を付け、 「エキストラ追加」ボタンをクリックすることで、性別・年齢不詳の「通行人X」を追加できるようにしたものです。 ボタンをクリックしたときに呼び出されるコールバック関数の中から、 Dialog.expandItem()Dialog.addItem()Dialog.setCurSel() メソッドにより名前付きツリーアイテム"extra"を操作しています。


    // 名前付きツリーアイテムを使ってアイテムを操作する。
    var treelist =
    [
        { 0: "追われる", expand: true, children: [
            [ "ルパン",
              { value: "男", color: 0xff0000 },
              { value: 40, name: "age1", edit: true },
              { check: true, name: "greedy1" } ],
            [ "峰不二子",
              { value: "女", color: 0x0000ff },
              { value: 30, name: "age2", edit: true },
              { check: true, name: "greedy2" } ]]},
        { 0: "追いかける", expand: true, children: [
            [ "銭型警部",
              { value: "男", color: 0xff0000 },
              { value: 50, name: "age3", edit: true },
              { check: false, name: "greedy3" } ]]},
        { 0: "エキストラ",
            name: "extra",
            children: function(dialog, control, parent)
            {
                // [+]が押されたので乱数により子アイテムを作成して返す。
                var array = [];
                var n = Integer(Math.random() * 10);
                for (var i = 1; i <= n; i++)
                {
                    array.push([ "通行人" + i,
                                 { value: "男", color: 0xff0000 },
                                 Integer(Math.random() * 50 + 5) ]);
                }
                return array;
            }
        }
    ];

    // TREELISTを使ったダイアログボックスのテンプレート
    var template =
    [
        { type: "TREELIST", width: 200, height: 100, header: header, value: treelist, name: "TL" },
        { type: "BUTTON", label: "エキストラ追加",
          onClick: function(dialog, control)
          {
              // "extra" アイテムにエキストラを追加してそれを選択する。
              dialog.expandItem("TL", 2, "extra");
              var index = dialog.addItem("TL", [ "通行人X", "?", "?" ], "extra", -1);
              dialog.setCurSel("TL", "extra", index);
          }
        }
    ];

【ヒント】
ツリーアイテムオブジェクトとグリッドオブジェクトの名前は別々に管理されるため、 同じ名前を付けても区別できます。
【ヒント】
ツリーリストを扱うDialogオブジェクトのメソッドのほとんどは、 ツリーオブジェクト名以外に、ツリーオブジェクトそのものや親オブジェクトからのインデックスを使って操作できます。

ツリーリストのスタイル
ツリーリストのControlオブジェクトにstyleプロパティを追加することで、 ツリーリストのカスタマイズできます。 スタイルには以下の5種類があり、必要なものをOR演算子(|)で組み合わせて指定できます。

スタイル機能
TLS_NOHGRID0x0001水平罫線を表示しない
TLS_NOVGRID0x0002垂直罫線を表示しない
TLS_NOHEADER0x0004ヘッダーを表示しない
TLS_MULTIPLESEL0x0008複数選択を許す
TLS_NOBUTTONSPACE0x0010第1カラムの[+]/[-]ボタンがないアイテムを左端から表示する


    // スタイルを指定したTREELISTダイアログボックスのテンプレート
    var template =
    [
        { type: "TREELIST", width: 200, height: 100, header: header, value: treelist, name: "TL",
          style: TLS_NOHGRID | TLS_NOVGRID }
    ];

ツリーコントロールとして使う
ツリーコントロールを単なるツリーとして使いたい場合は、 TLS_NOHGRID | TLS_NOVGRID指定し、 TREELIST Controlオブジェクトheaderプロパティを指定しません。 headerプロパティを省略すると、ヘッダーが非表示となり、横幅いっぱいのカラムが1つ作成されます。


    // ツリーとして使う例
    var template =
    [
        { type: "TREELIST", width: 200, height: 100, /* header: header, */ value: treelist, name: "TL",
          style: TLS_NOHGRID | TLS_NOVGRID }
    ];

リストとして使う
逆にリストとして使いたい場合は、TLS_NOBUTTONSPACEスタイルを指定して、 左端に[+]、[-]ボタンのための空白を空けないようにします。


    var treelist =
    [
        [ "ルパン",   "男", 40 ],
        [ "峰不二子", "女", 30 ],
        [ "銭型警部", "男", 50 ]
    ];

    // 左端を詰めればマルチカラムのリストとしても使える
    var template =
    [
        { type: "TREELIST", width: 200, height: 100, header: [ "名前", "性別", "年齢" ],
          value: treelist, style: TLS_NOBUTTONSPACE }
    ];

    customBox("グリッド表示", template);

ツリーリストのまとめ
以下にツリーリストに関連するオブジェクトとそのプロパティをまとめておきます。

オブジェクトプロパティ
ツリーアイテムオブジェクト
(横1行を表す)
children array または function 子ツリーアイテムオブジェクトの配列、または
子ツリーアイテムオブジェクトの配列を返す関数
expand boolean true .... 展開する
false .... 閉じる
name※1 string ツリーアイテムオブジェクトに付けるユニークな名前
color※1 integer ツリーアイテムの表示色を指定するRGB値
parent object 親ツリーアイテムオブジェクト (自動的に作成される)
0, 1, 2, ... 基本データ型、または
グリッドオブジェクト
各グリッドに表示されるデータ
グリッドオブジェクト
(グリッド内のデータを表す)
value 任意 各グリッドに表示されるデータ
name string グリッドオブジェクトに付けるユニークな名前
color※1 integer グリッドに表示されているデータの色を指定するRGB値
(※1 両方指定されている場合はグリッドオブジェクトのcolorプロパティが優先)
edit boolean true .... 編集を許可する
false .... 編集を許可しない
check boolean または
integer
グリッドにチェックを付ける
true .... チェックあり(2ステート)
false .... チェックなし(2ステート)
0 .... チェックなし(3ステート)
1 .... チェックあり(3ステート)
2 .... どちらでもない(3ステート)


ダイアログシート

ダイアログシート(DialogSheet)オブジェクトを使うことで、 複数のダイアログボックスを一度に表示し、タブで表示を切り替えることが可能になります。 ダイアログシート上に表示されるDialogオブジェクトのことを「ページ」と呼ぶことにします。


    // ダイアログシート上に表示されるページ(Dialog オブジェクト)を作成する。
    // 第1ページ。
    var temp1 = [ { type: "EDIT", name: "ADDR", label: "住所: ", x: 50 },
                  { type: "EDIT", name: "TEL", label: "電話: ", x: 50 } ];
    var page1 = new Dialog("住所", temp1);

    // 第2ページ。
    var temp2 = [ { type: "EDIT", name: "WORD", label: "職業: ", x: 50 },
                  { type: "EDIT", name: "TEL", label: "電話: ", x: 50 } ];
    var page2 = new Dialog("職業", temp2);

    // 第3ページ。
    var temp3 = [ { type: "EDIT", name: "HOBBY", label: "趣味: ", x: 50, width: 80 } ];
    var page3 = new Dialog("趣味", temp3);

    // Dialog オブジェクトの配列を作成する。
    var pages = [ page1, page2, page3 ];

    // DialogSheet オブジェクトを作成する。
    var sheet = new DialogSheet("個人データ", 150, 40, pages);

    // シートを表示する。
    if (sheet.open() == IDOK)
    {
        // [OK] ボタンがクリックされた

    }

DialogSheetオブジェクトを作成するには、 その中に表示されるページが表示されるに十分な横・縦サイズをダイアログ単位で指定しなければなりません。 この例では、横を150ダイアログ単位、縦を40ダイアログ単位としています。

【ヒント】
ダイアログシートには、自動的に[OK]、[キャンセル]、[ヘルプ]ボタンが用意されます。 そのため、ダイアログシート上に表示するDialogオブジェクトには、 Dialog.ADD_OKCANCELLINEなどでこれらのボタンを追加しないでください。

ダイアログシートから値の取り出し
結果は、Dialogオブジェクトを単独で表示させたときと同じように、 それぞれのDialogオブジェクトのresultsプロパティにセットされます。 それに加えて、DialogSheetオブジェクトにもresultsという名前の配列が作成され、 その数値インデックス0〜...に、ページ1〜...の各々のDialogオブジェクトのresultsプロパティがコピーされます。 ただし、一度も表示されていないページは、 [OK]ボタンがクリックされた場合でも対応するDialogオブジェクトにresultsプロパティが作成されないので注意してください。 DialogSheetオブジェクトのresultsプロパティの該当要素も、未定義(undefined)となります。


    // シートを表示する。
    if (sheet.open() == IDOK)
    {
        // [OK] ボタンがクリックされたので結果を取り出す。
        var res;

        // ページ1の値を取得する。
        if (res = sheet.results[0])
            writeln("住所と電話: ", res.ADDR, ", ", res.TEL);

        // ページ2の値を取得する。
        if (res = sheet.results[1])
            writeln("職業と電話: ", res.WORD, ", ", res.TEL);

        // ページ3の値を取得する。
        if (res = sheet.results[2])
            writeln("趣味: ", res.HOBBY);
    }

【実行結果】

住所と電話: 自由が丘1-1, 123-4567
職業と電話: お菓子屋, 987-6543
趣味: 宇宙旅行

【重要】
一度も表示されていないページは、[OK]ボタンがクリックされた場合でも対応するDialogオブジェクトにresultsプロパティが作成されないので注意してください。 DialogSheetオブジェクトのresultsプロパティの該当要素も、未定義(undefined)となります。 そのため、上の例ではif文で結果があるかどうかを判断しながらアクセスしています。
【ヒント】
異なるページであれば、コントロールに同じ名前があっても区別できます。 上の例では、ページ1と2に名前がTELのコントロールがありますが、 ちゃんと区別して結果を取り出すことができています。






プロジェクト

Peggyのプロジェクトへアクセスするには、グローバル変数projectを使います。 プロジェクトが開かれているときは、projectにProjectオブジェクトが設定されていて、 そこからプロジェクトの内部を調べたり、ファイルの追加・削除などが行えます。

プロジェクトが開かれていないときは、nullが設定されています。 この仕組みを使ってプロジェクトが開かれていない状態を安全に処理できます。


    if (!project)
        error("プロジェクトが開かれていません.");


プロジェクトファイルを開く

プロジェクトファイルを開くには、 openProject()関数を使います。 同じように、新規プロジェクトを作成するには、 newProject()関数を使います。 どちらも成功するとProjectオブジェクト、それ以外はnullを返します。 これらの関数が返すProjectオブジェクトは、グローバル変数projectで参照するものと同じなので、 わざわざ変数を用意して保存する必要はありません。


    var path = "D:\\work\\myproj.peg";
    if (!openProject(path))
        error("プロジェクト " + path + " が開けません.");


保存する・閉じる

MocaScriptからプロジェクトを保存したり閉じたりするにも、グローバル変数projectを使います。 詳しくは、Project.save()メソッドProject.close()メソッドを参照してください。


    if (!project)
        error("プロジェクトが開かれていません.");

    project.save();


アイテムとクラス

プロジェクトには、ファイル、グループ、文字列検索、ファイル比較など何種類かの異なるアイテムを混在して登録できます。 MocaScriptからこれら種類の異なったアイテムを扱うために、アイテムの種類毎に専用のクラスを用意しました。 プロジェクトやその中のアイテムに対する操作は、これらクラスのオブジェクトに対してメソッドを呼び出すという形になります。

アイテムの種類クラス詳細
ファイル FileItem 登録されているファイルを表す。
グループ GroupItem グループを表す。
プロジェクト Project プロジェクトウインドウの先頭にあるプロジェクト自身(全体)を表す。
ゴミ箱 TrashItem プロジェクトウインドウのゴミ箱を表す。
文字列検索 FindItem プロジェクトに保存された文字列検索の条件を表す。
ファイルから検索 GrepItem プロジェクトに保存されたファイルから検索の条件を表す。
ファイル・フォルダの比較 DiffItem プロジェクトに保存されたファイル・フォルダの比較条件を表す。
キーボードマクロ KeyboardMacroItem プロジェクトに保存されたキーボードマクロを表す。

さらにオブジェクト指向の考えに基づき、クラスを親子関係とプロトタイプによる継承を使って、 親クラスのメソッドを共用するようになっています。 またそのために、実際には存在しないProjectItemやToolItemを作成してクラスを分類し、 メソッドの継承を系統立てています。 以下がプロジェクトアイテムのクラス階層です。


            プロジェクトアイテムのクラス階層

                       Object
                         ┃
                         ┃
                     ProjectItem
                         ┃
         ┏━━━━━━━╋━━━━━━━┓
         ┃              ┃              ┃
      FileItem       GroupItem        ToolItem
                         ┃              ┃
                   ┏━━┻━━┓        ┃
                   ┃          ┃        ┃
                Project     TrashItem    ┃
                                         ┃
                       ┏━━━━━┳━━┻━━┳━━━━━┓
                       ┃          ┃          ┃          ┃
                    FindItem    GrepItem    DiffItem   KeyboardMacroItem

ProjectItemはMocaScriptのObjectクラスから派生し、すべてのプロジェクトアイテムの基礎となるクラスです。 ProjectItemのプロトタイプには、select()insert()remove()といった基本的なメソッドが定義されていて、 派生したすべてのアイテムからでも利用できるようになっています。 同様にGroupItemのプロトタイプで定義されているメソッドは、 それを継承しているProjectクラス、TrashItemクラスのオブジェクトからも呼び出すことができます。

【ヒント】
Projectオブジェクトは、その名が示すようにプロジェクト全体を表していますが、 クラス階層の中ではGroupItemから派生したクラスとして定義されています。 これはProject自身にもファイルなどが登録できるため、 グループとしての側面も持っているためです。 ゴミ箱(TrashItem)も同じです。
【ヒント】
ProjectItem、ToolItemなどアイテム分類のためだけに存在するクラスのオブジェクトは、 プロジェクト中に存在しません(MocaScriptの場合故意に作ることはできますが、何の役にも立ちません)。 これらはいわば、C++/Javaなどの本格的なオブジェクト指向言語の「抽象クラス」に相当します。


グループ中のアイテム

グループに登録されているアイテムを取得するには、 GroupItem.enumItems()メソッドを使います。 以下の例では、プロジェクトの直下に登録されているすべてのアイテムを収めた配列を返します。


    if (!project)
        error("プロジェクトが開かれていません.");

    // ヒント: Project は GroupItem の機能を継承しています。
    var items = project.enumItems();
    messageBox(items);

【ヒント】
GroupItem.enumItems()メソッドには、引数を指定することでアイテムを絞り込んで取得する機能があります。 詳しくは、リファレンスを参照してください。
【ヒント】
GroupItemクラスには、GroupItem.getItem()など、 グループ中や入れ子になったグループからアイテムを探すためのメソッドが用意されています。 詳しくは、GroupItemのメソッド一覧を参照してください。


アイテム種別の判定

プロジェクトには色々な種類のアイテムが混在しています。 そのため、GroupItem.enumItems()が返す配列にも、色々なアイテムが含まれてしまいます。 取り出したアイテムの種類を調べるには、instanceof演算子を使います。 instanceof演算子は、左辺に調べたいオブジェクト、右辺にコンストラクタ(クラス)を指定することで、 そのオブジェクトがクラス(または親クラス)のものかどうかを判定して、 true、またはfalseを返します。


    if (!project)
        error("プロジェクトが開かれていません.");

    // プロジェクト直下にあるファイルアイテムの数をカウントします。
    var count = 0;
    var items = project.enumItems();
    for (var i = 0; i < items.length; i++)
    {
        // オブジェクトが FileItemのオブジェクトであれば、ファイルアイテムである。
        if (items[i] instanceof FileItem)
            count++;
    }
    messageBox("ファイルアイテムは " + count + " 個ありました.");

【ヒント】
GroupItem.enumItems()メソッドには、引数を指定することでアイテムを絞り込んで取得する機能があります。 アイテムの種類を指定することも可能です。 詳しくは、リファレンスを参照してください。
【ヒント】
instanceof演算子はプロジェクトアイテムの判定だけでなく、 Dateやユーザ定義のオブジェクトの判定にも使えます。


選択されているアイテム

プロジェクト内で選択されているアイテムを処理したいことはよくあります。 選択されているアイテムは、 project.selectedプロパティで取得できます。


    if (!project)
        error("プロジェクトが開かれていません.");

    // 選択されているアイテムを表示する。
    var item = project.selected;
    messageBox("選択されているアイテムは " + item + " です.");

【ヒント】
アイテムを選択したい場合は、ProjectItem.select()メソッドを使います。

選択されているアイテムと同じグループに登録されているアイテムをまとめて処理したいこともよくあります。 アイテムが登録されているグループを取得するには、 parentプロパティを使います。 ただし、選択されているのがグループ自身である可能性もあるので、少々工夫が必要です。


    if (!project)
        error("プロジェクトが開かれていません.");

    // 選択しているアイテムがグループでなければ、その親アイテム(必ずグループ)を取得する。
    var group = project.selected;
    if (group && !(group instanceof GroupItem))
        group = group.parent;

    // 取得したアイテムをリストボックスで表示する。
    var items = group.enumItems();
    listBox("グループ", "グループ " + group + " に登録されているアイテム", items);

【ヒント】
親グループだけでなく、 前後のアイテムを取得するnextprevious、 グループの最初の子アイテムを取得するchildなどのプロパティもあります。 これらのロパティを組み合わせることで、グループ内を独自の方法で巡回することが可能になります。


アイテムの挿入

新しいアイテムをプロジェクトに挿入するには、 ProjectItem.insert()メソッドを使います。 insert()メソッドは、プロジェクトのアイテム全体の基礎となるProjectItemクラスで定義されているので、 どの種類のアイテムに対しても呼び出すことができます。 insert()メソッドは、アイテムの直前に新しいアイテムを挿入します。


    if (!project)
        error("プロジェクトが開かれていません.");

    // 現在選択されているアイテムの直前にPeggyのreadme.txtをプロジェクトに挿入する。
    var item = project.selected;
    item.insert("C:\\Program Files\\Anchor\\Peggy\\readme.txt");

【ヒント】
insert()メソッドをグループに対して使ったときは、 そのグループの直前ではなく、グループ中の最後にアイテムが登録されます。 エクスプローラからファイルをドラッグし、グループ上へドロップした場合と同じです。
【ヒント】
insert()メソッドに渡すパスは、フルパスでも相対パスでもかまいませんが、 実在しないファイルは登録できません。 実在するファイルを指定すると、プロジェクトに挿入され、そのアイテムを表すFileItemオブジェクトを返します。 実在しないファイルを渡すと、プロジェクトには挿入されず、nullを返します。 詳しくは、リファレンスを参照してください。


いろいろなアイテムの挿入

他の種類のアイテムを挿入するのにもProjectItem.insert()メソッドを使います。 ファイルパスの代わりに挿入したいオブジェクトを作成し、insert()メソッドへ渡します。


    if (!project)
        error("プロジェクトが開かれていません.");

    // 現在選択されているアイテムの直前にグループを挿入する。
    var item = project.selected;
    var group = new GroupItem("スクリプトから作ったグループ");
    item.insert(group);

グループアイテム(GroupItem)以外にも、 文字列検索(FindItem)、 ファイルから検索(GrepItem)、 ファイル・フォルダの比較(DiffItem)、 キーボードマクロ(KeyboardMacro)アイテムを作成して、 同じようにプロジェクトへ挿入できます。 それぞれのアイテムは、new演算子でオブジェクトを作成するときに、 その種類に応じた引数でアイテムの値を指定することができます。 詳しくは、それぞれのコンストラクタ関数のリファレンスを参照してください。

【ヒント】
ファイルは、FileItemオブジェクトを作成してから挿入することもできます。
    item.insert(new FileItem("C:\\Program Files\\Anchor\\Peggy\\readme.txt"));
【注意】
抽象クラスProjectItem、ToolItemのオブジェクト、それにProject、TrashItemのオブジェクトを挿入することはできません。


アイテムの削除

アイテムを削除するには、 ProjectItem.remove()メソッドを使います。 削除したいアイテムに対して、remove()メソッドを呼び出すだけです。


    if (!project)
        error("プロジェクトが開かれていません.");

    // 選択しているアイテムを削除する。
    var item = project.selected;
    item.remove();

【重要】
グループを削除したときは、グループ以下のすべてのアイテムをまとめて削除します。
【注意】
プロジェクト全体を表すprojectと、ゴミ箱は削除できません。
【ヒント】
プロジェクトウインドウでDeleteキーを押し選択されているアイテムを削除するのと同じように、 remove()メソッドも削除したアイテムをゴミ箱へ移動させます。 ゴミ箱へ移動させず完全に削除してしまいたいときは、remove(true)とします。 詳しくは、リファレンスを参照してください。


ファイルアイテムの特定

Peggyのプロジェクトには、同じファイルを複数回登録することができるという特徴があります。 その場合、どのアイテムからでもファイルを開くことができます。 また、アイテムに別々の言語を設定しておくことで、 同じファイルをHTMLとして開いたり、Perlとして開くようなことが可能です。 Peggy内部では言語などいろいろな判定処理のため、 最後にどのアイテムを使ってファイルを開いたかを区別できるようになっています。

複数登録と使用履歴までを考慮してプロジェクト中のファイルアイテムを特定するには、 Project.identify()メソッドを使います。


    if (!project)
        error("プロジェクトが開かれていません.");

    // 編集しているファイルに相当するプロジェクトアイテムを探して選択する。
    var item = project.identify(view);
    if (item)
        item.select();

【ヒント】
identify()メソッドを使ってアイテムを特定できるのは、ファイルアイテムだけです。 その他のアイテムは、GroupItem.enumItems()メソッドの引数を工夫することで検索できます。 詳しくは、リファレンスを参照してください。


ファイルを開く・閉じる

プロジェクトに登録されているファイルアイテムから、ファイルを開いたり閉じたりできます。 ファイルを開くにはFileItem.open()メソッド、 閉じるにはFileItem.close()メソッド、 開いているかどうか確かめるには、FileItem.isOpened()メソッドを使います。


    // 編集しているファイルと同じグループのファイルをすべて閉じる。
    if (!project)
        error("プロジェクトが開かれていません.");
    if (!view)
        error("編集ウインドウが開かれていません.");

    var item = project.identify(view);
    if (!item)
        error("編集中のファイルはプロジェクトに登録されていません.");

    var group = item.parent;
    var items = group.enumItems(false, FileItem);
    for (var i = 0; i < items.length; i++)
    {
        item = items[i];
        if (item.isOpened() && !item.close())
            break;
    }

【ヒント】
ファイルアイテムのプロパティに登録されている方法で実行ファイルやスクリプトを実行するFileItem.execute()メソッドも利用できます。
【ヒント】
FileItem.close()メソッドを使ってファイルを閉じる場合、 変更あれていれば閉じる前に保存するかどうかを問い合わせます。 この問い合わせなしで変更箇所を破棄して強制的に閉じるには、 close(true)とします。 詳しくは、リファレンスを参照してください。


グループに対応するフォルダ

プロジェクトのグループは、ファイルシステムのフォルダには直接対応していません。 しかしドラッグ&ドロップによりフォルダごとプロジェクトに登録したような場合、 グループとフォルダは非常に近い関係となります。 そのような場合、GroupItem.guessFolder()メソッドを使うことにより、対応するフォルダを推測できます。


    if (!project)
        error("プロジェクトが開かれていません.");

    // 選択されている、もしくは選択しているアイテムを含んでいるグループのフォルダを推測する。
    var group = project.selected;
    if (group && !(group instanceof GroupItem))
        group = group.parent;

    var folder = group.guessFolder();
    messageBox("フォルダはおそらく " + folder + " です.");

【ヒント】
グループに対応するフォルダの推測は、 そのグループ中に登録されているファイルをヒントにします。 そのためファイルがまったく登録されていないグループのフォルダは推測できません。 また、グループにはいろいろなフォルダのファイルを集めて登録できます。 それはそれで便利なのですが、一意にフォルダを決定することができません。 そこで[フォルダ選択]ダイアログボックスが表示され、どのフォルダかを選ぶようになっています。 リスト中のフォルダは、ファイルが多い順にソートしてあります。






Search オブジェクト

Searchオブジェクトは、検索ツールなどの検索条件にアクセスするためのオブジェクトです。 Searchオブジェクトにより、検索テキスト、大文字小文字の区別、 単語として検索などの検索条件を取り出し・設定が行えます。

Search オブジェクト - プロパティ一覧

プロパティ名 読み書き 意味
text string R/W 検索する文字列
matchCase boolean R/W 大文字小文字を区別する
matchWord boolean R/W 単語として検索する
regexp boolean R/W 正規表現
highlight boolean R/W 検索強調表示をする
replace string R/W 置換文字列
folder string R/W ファイルから検索のフォルダ
fileType string R/W ファイルタイプ
recursive boolean R/W サブフォルダも検索
exclude string R/W 除外フォルダ
grepFolder boolean R/W 指定したフォルダのファイルを検索する
grepProject integer R/W プロジェクトから検索する(0:しない、1:グループ、2:グループ以下、3:プロジェクト全体)
getHistory() function - 検索履歴を取得する
clearHistory() function - 検索履歴をクリアする
clearHighlight() function - 検索強調表示をクリアする
getFindToolRect() function - 検索ツールのスクリーン座標を取得する


    // "Peggy" を大文字小文字を区別しながら単語として検索するようセットする。
    Search.text = "Peggy";
    Search.matchCase = true;
    Search.matchWord = true;
    Search.regexp = false;







Event オブジェクト

Eventオブジェクトに予め定められた名前で関数オブジェクトを登録しておくと、 そのイベントが発生したとき自動的に呼び出されます。 ハンドラの登録は、Startup.ms などで行うと便利です。

以下に、イベントとイベントハンドラの関数名と引数の一覧を示します。 現バージョンでは、下表にある25種類のイベントハンドラを定義できます。

Event オブジェクト - イベントとハンドラ関数

イベント 関数名 引数の説明 用途
Peggyの終了直前 Event.onExit() なし 次回のPeggy起動まで残して起きたいデータの保存など
新規編集を始めた直後 Event.onFileNew(theView) theView .... 編集ウインドウ 新規作成した編集ウインドウに何か追加の設定などをしたい場合
ファイルを開いた直後 Event.onFileOpen(theView) ファイルを開いた直後に何かの追加処理をしたいとき
ファイルを保存する直前 Event.onFileSave(theView) ファイルを保存する直前に何かの追加処理をしたいとき
ファイルを保存した直後 Event.onFileSaved(theView) ファイルの保存に成功した直後に何かの追加処理をしたいとき
ファイルを閉じる直前 Event.onFileClose(theView) ファイルを閉じる直前に何かの追加処理をしたいとき
編集ウインドウでEnterキーが押されたとき Event.onEnter(theView) 編集ウインドウでEnterキーが押されたタイミングで何か処理をしたいとき
Event.onEnter()からfalseが返されると、 改行など通常のEnterキーの処理をしますが、 trueが返されるとEnterキーの処理が完了したとみなし何もしません。 これにより、Enterキーの動作を完全に置き換えることができます。
編集ウインドウでF1キー(ヘルプ)が押されたとき Event.onHelp(theView) 編集ウインドウでF1キー(ヘルプ)が押されたタイミングで何か処理をしたいとき呼び出されます。 Event.onHelp()からfalseが返されると、 通常の状況依存ヘルプとして処理を続けますが、 trueが返されるとヘルプ処理が完了したとみなし何もしません。 これにより、言語モードやカーソル位置の状況を判断しながら柔軟なヘルプの呼び出しが可能になります。
編集ウインドウにフォーカスがセットされたとき Event.onSetFocus(theView) 編集ウインドウにフォーカスがセットされた後に呼び出されます。
【注意】複数回呼び出されることがあるので注意してください。
編集ウインドウのテキストエリアがダブルクリックされたとき Event.onDoubleClick(theView, point) theView .... 編集ウインドウ
point .... マウスの位置
編集ウインドウのテキストエリアがダブルクリックされたとき、 ダブルクリックによるテキスト選択を行った直後に呼び出されます。 これによりダブルクリックの動作をカスタマイズできます。
編集ウインドウ上へファイルがドロップされたとき Event.onDropFiles(theView, files) theView .... 編集ウインドウ
files .... ファイルパスの配列
編集ウインドウ上にCtrlキーを押しながらファイルをドロップしたときに呼び出されます。 これによりドロップされたファイルパスを加工してから編集ウインドウへ挿入することができます。
編集ウインドウで単語補完コマンドが実行されたとき Event.onCompleteWord(theView, hint) theView .... 編集ウインドウ
hint .... 補完ヒント文字列
配列を返すと、配列の要素が単語補完候補として追加されます。 また連想配列を返すと、連想配列のキーが単語補完候補として追加されます。
タグジャンプに成功したとき Event.onTagJump(theView, outputPage) theView .... 編集ウインドウ
outputPage .... アウトプットウインドウのページ
アウトプットウインドウからのタグジャンプが成功したときに呼び出されます。
ただし、比較によるジャンプでは呼び出されません。 編集ウインドウからジャンプしたときは、 thePageが未定義(undefined)になります。
アウトプットウインドウでEnterキーが押されたとき Event.onEnterOutput(outputPage) outputPage ... アウトプットウインドウのページ
アウトプットウインドウでEnterキーが押されたタイミングで何か処理をしたいとき
Event.onEnterOutput()からfalseが返されると、 改行など通常のEnterキーの処理をしますが、 trueが返されるとEnterキーの処理が完了したとみなし何もしません。 これにより、Enterキーの動作を完全に置き換えることができます。
アウトプットウインドウがダブルクリックされたとき Event.onDoubleClickOutput(outputPage, point) outputPage ... アウトプットウインドウのページ
point ... マウスの位置
アウトプットウインドウがダブルクリックされたときに呼び出されます。
Event.onDoubleClickOutput()からfalseが返されると、 改行など通常のダブルクリック処理をしますが、 trueが返されるとダブルクリック処理が完了したとみなし何もしません。 これにより、Enterキーの動作を完全に置き換えることができます。
新規プロジェクトを作成した直後 Event.onProjectNew(theProject) theProject .... Projectオブジェクト 新規プロジェクト作成と同時に何かの追加処理をしたいとき
プロジェクトを開いた直後 Event.onProjectOpen(theProject) プロジェクトを開いた直後に何かの追加処理をしたいとき
プロジェクトを保存する直前 Event.onProjectSave(theProject) プロジェクトを保存する直前に何かの追加処理をしたいとき
プロジェクトを閉じる直前 Event.onProjectClose(theProject) プロジェクトを閉じる直前に何かの追加処理をしたいとき
文字列検索をする直前 Event.onFind(theView) theView .... 編集ウインドウ
編集ウインドウで検索を行う直前に何かの追加処理をしたいとき
文字列検索でヒットしたとき Event.onFound(theView) 編集ウインドウで検索を行いヒットしたとき
ファイルから検索を始める直前 Event.onGrep() なし ファイルから検索を実行する直前に何かの処理をしたいとき
フォルダ/ファイル比較をする直前 Event.onDiff() なし フォルダ/ファイル比較をする直前に何かの処理をしたいとき
プログラム解析結果をアウトライン表示する直前 Event.onOutlineProgram() theView ...... アウトラインの対象となった編集ウインドウ
root ...... アウトライン解析結果を収めた配列
アウトライン表示をカスタマイズしたいとき
一定時間ごと Event.onTimer() なし setTimer()グローバル関数で指定した一定時間間隔に何らかの処

ハンドラの登録は、Startup.ms などで行うと便利です。 ハンドラを登録するには、関数をEventオブジェクトのプロパティとして代入します。 代入文なので、最後にセミコロン(;)を付けるのを忘れないでください。


    // Peggy の終了直前にメッセージを表示します。
    Event.onExit = function()
    {
        messageBox("Peggy を終了します.");
    };

【ヒント】
MocaScriptのライブラリ<share\script\AddHandler.msl>を使うと、 1つのイベント要因に複数のハンドラを登録できるようになります。






逆引き編

ここからは、プログラミング上のワンポイントヒントを数多く紹介してゆきます。

スクリプトの実行を止めるには?

Escキーを押してください。

関数の機能を調べるには?

言語モードがMocaScriptになっていれば、 機能を知りたい関数上にカーソル移動し、F1キーでヘルプを表示することができます。

正確な関数名が思い出せないときは?

言語モードがMocaScriptになっていれば、 関数の最初の数文字をタイプし、Ctrl+/ をタイプします。 関数名と簡単な説明の一覧が表示されるので、そこから選んで入力できます。

関数へ渡す引数の意味を忘れてしまったら?

関数上へマウスカーソルを移動してください。 簡単な動作説明と引数がチップ表示されます。 もっと詳しく知りたいときは、関数名の上にカーソルを移動させてF1キーをタイプし、ヘルプを呼び出してください。

文字列を探すには? (1)

文字列を探すには、View.findForward()View.findBackward()を使います。 これらのメソッドは、 キーボードから文字列を検索するように現在カーソルのある位置からテキストの終りまたは先頭に向かって検索を始め、 文字列が見つかると、選択範囲をセットして、見つかった部分の文字数を返します。


    // カーソル位置からテキストの終りに向かって"MocaScript"を検索する。
    var str = "MocaScript";
    if (findForward(str) >= 0)
    {
        messageBox(str + " が見つかりました.");
    }

文字列の代わりに正規表現を指定すると、 複雑なパターンも検索できます。


    // カーソル位置からテキストの先頭に向かってHTMLの見出しタグを検索する。
    if (findBackward(/<h[1-6]>.+<\/h[1-6]>/i) >= 0)
    {
        messageBox("HTMLの見出しが見つかりました.");
    }


文字列を探すには(2)?

カーソル位置とは無関係に文字列を探すには、 View.search()メソッドを使います。 文字列が見つかると、文字列の先頭と終りを示す2つのPointオブジェクトを要素とする配列を返します。 見つからない場合は、nullを返します。


    // カーソルを移動させずに「第XX章」という文字列を正規表現により検索する。
    var found;
    if (found = search(/第\d+章/))
    {
        messageBox(RegExp.lastMatch + " が " + found[0].line + " 行目に見つかりました.");
    }

【メモ】
正規表現を使った検索では、マッチした部分の詳細な情報がRegExpクラスの静的プロパティとして保存されます。


範囲を指定して文字列を検索

View.search()メソッドによる検索では、 範囲をしてできます。


    // 100〜200行の間で「第XX章」を探す。
    var found;
    if (found = search(/第\d+章/, 100, 201))
    {
        messageBox(RegExp.lastMatch + " が " + found[0].line + " 行目に見つかりました.");
    }

検索開始位置、検索終了位置にnullを指定すると、 それぞれテキストの先頭、最後と解釈します。 その他に、1EOFとしても先頭、最後を指定できます。

【メモ】
search()メソッドに検索範囲を指定する場合、終点の直前までが検索対象となります。 従って100〜200を検索する場合の終点は、201としなければなりません。


文字列をすべて探す

テキスト中の文字列をすべて探すには、 View.search()メソッドを繰り返し使います。


    // テキスト中のすべての「第XX章」を探す。
    // null とセットすることにより、検索開始位置をファイルの先頭にする。
    var found = null;
    while (found = search(/第\d+章/, found))
    {
        // 見つかった。
        // found[0]が見つかった部分の先頭、found[1]が最後を指している。

        ... 省略 ...

        // 次の検索開始位置をセットする。
        found = found[1];
    }


<h1>、<h2>...の一覧を作成するには?

<h1>、<h2>...の一覧は、 View.search()メソッドで作成できます。


    // すべての<h1>、<h2>...を抜き出しアウトプットウインドウへ出力する。
    var found = null;
    while (found = search(/<h([1-6])>.*<\/h[1-6]>/i, found))
    {
        writeln(spc, RegExp.lastMatch);
        found = found[1];
    }

アウトプットウインドウへ出力するのではなく、後で加工するために文字列として配列に保存するには、 以下のようにします。


    // すべての<h1>、<h2>...を抜き出し配列へ保存する。
    var array = new Array();
    var found = null;
    while (found = search(/<h([1-6])>.*<\/h[1-6]>/i, found))
    {
        array.push(RegExp.lastMatch);
        found = found[1];
    }

【メモ】
見出し<h1>、<h2>に付けられたname="..."からhref="#..."を自動生成して一覧に付け加えれば、ジャンプ可能な目次が出来上がります。 見出しの大小番号(1〜6)によりインデントを付けると、より完成度の高い目次が作成できます。
【メモ】
getFilePath()メソッドで取得したパス名と行番号を付けて出力すれば、 タグジャンプ可能な見出しリストが作成できます。


選択範囲の中を捜す

選択範囲を解除せずに選択範囲の中に特定の文字列があるかどうか探したい場合は、 View.search()メソッドと、 ViewオブジェクトのAPCPプロパティを使います。


    // 選択範囲を解除せずにその中の文字列「第XX章」を探す。
    var found = search(/第\d+章/, AP, CP);
    if (found)
    {
        // 見つかった。
        ... 省略 ...
    }


指定行のテキストを取得する

指定行のテキスト取得するには、View.getLine()メソッドを使います。


    // 100行のテキストを取得する。
    var text = getLine(100);

【メモ】
取得する行のテキストに改行文字まで含めるには、getLine(lineNumber, true)とします。


カーソル行のテキストを取得する

指定行のテキスト取得するには、View.getLine()メソッドを引数無しで呼び出します。


    // カーソル行のテキストを取得する。
    var text = getLine();


カーソル位置の文字を取得する

カーソル位置の文字を取得するには、View.getText()メソッドを使います。


    // カーソル位置の1文字を取得する。
    var ch1 = getText();

    // カーソル位置から2文字取得する。
    var ch2 = getText(2);

    // カーソル位置直前の3文字を取得する。
    var ch3 = getText(-3);


選択範囲のテキストを取得する

選択範囲のテキストを取得するには、View.getSelectedText()メソッドを使います。


    // 選択範囲のテキストを取得する。
    var text = getSelectedText();

【メモ】
矩形選択をしているときにgetSelectedText()を呼ぶと、 矩形選択部分の各行を改行で連結した文字列を返します。


指定行にテキストを挿入するには?

指定した行にテキストを挿入するには、View.insertTextAt()メソッドを使います。


    // 100行目の先頭にテキストを挿入する。
    insertTextAt(100, "ここは100行の先頭です");


テキストの最後の文字を挿入するには?

View.insertTextAt()メソッドとEOFプロパティを使うことで、 常にテキストの最後に文字を挿入することができます。


    // テキストの最後に文字を挿入する。
    insertTextAt(EOF, "最後に追加される文字列");


選択範囲の前後に文字列を挿入するには?

HTMLタグなどを選択範囲の前後に挿入する機能は便利です。 View.getSelection()View.setSelection()View.insertTextAt()メソッドを使い以下のようにすると、 選択範囲の前後に文字列を挿入できます。


    // 選択範囲の前後に"<center>"と"</center>"を挿入する。
    var str1 = "<center>";
    var str2 = "</center>";
    var sel = getSelection();

    // 後ろからテキストを挿入する。
    insertTextAt(sel[1], str2);
    insertTextAt(sel[0], str1);

    // 選択範囲を調整して、元のテキストを選択する。
    sel[0].index += str1.length;
    if (sel[0].line == sel[1].line)
        sel[1].index += str1.length;
    setSelection(sel);

【注意】
この方法は、前後に挿入する文字列に改行が含まれていない場合に有効です。 改行が含まれる可能性がある場合は、挿入後の選択範囲をもっと慎重に決める必要があります。


文字列を置換するには?

テキストから文字列を探して置換するには、 View.replace()メソッドを使います。


    // 「である。」を検索して「です。」に置換します。
    replace("である。", "です。");

【ヒント】
replace()メソッドは、最初に見つけた文字列を1つだけ置換します。 すべを置換した場合は、グローバルオプションを指定してreplace()メソッドを実行します。


文字列をすべて置換するには?

文字列をすべて置換するには、グローバルオプション付きでView.replace()メソッドを実行します。


    // 「である。」を検索してすべて「です。」に置換します。
    replace("である。", "です。", null, null, "g");

範囲を指定してすべて置換した場合は、nullnullの部分に置換範囲の先頭と終りを示すポイントを指定します。


    // 100〜200行の間の「である。」を検索してすべて「です。」に置換します。
    replace("である。", "です。", 100, 201, "g");

検索する文字パターンを正規表現で指定する場合は、 正規表現にグローバルオプション(g)を設定します。


    // すべての「第XX章」を「Chapter XX」で置換します。
    replace(/第(\d+)章/g, "Chapter $1");

【ヒント】
replace()メソッドは、置換文字列合成関数を使った高度な置換もサポートしています。


選択範囲の&などをすべてエンティティ(&amp;)に変換するには?

View.htmlConvertToEntity()メソッドにより選択範囲のエンティティであるべき文字(&、<、>、")をエンティティ(&amp;、&lt;、&gt;、&quot;)に変換できます。 その他にも、View.replace()メソッドを使うことで任意の変換が作成できます。


    // 選択範囲内の文字をエンティティに変換する。
    replace(/[&<>"]/g, convertCharToEntity, AP, CP);

    function convertCharToEntity($0)
    {
        // replaceメソッドから呼び出される文字列変換関数
        if ($0 == "&")
            return "&amp;";
        if ($0 == "<")
            return "&lt;";
        if ($0 == ">")
            return "&gt;";
        if ($0 == "\"")
            return "&quot;";
        return $0;
    }

その逆の変換は、以下のようにすることで可能です。


    // 選択範囲内のエンティティを文字に変換する。
    replace(/&(amp|lt|gt|quot)\>;?/g, convertEntityToChar, AP, CP);

    function convertEntityToChar($0)
    {
        // replaceメソッドから呼び出される文字列変換関数
        if ($0 == "&amp" || $0 == "&amp;")
            return "&";
        if ($0 == "&lt" || $0 == "&lt;")
            return "<";
        if ($0 == "&gt" || $0 == "&gt;")
            return ">";
        if ($0 == "&quot" || $0 == "&quot;")
            return "\"";
        return $0;
    }

【ヒント】
MocaScriptでは、入れ子の関数定義を許していません。 そのため、これらの変換を関数として定義するときは、 convertEntityToChar()convertCharToEntity()を関数リテラルとして記述するとよいでしょう。


編集ウインドウを更新するには?

スクリプトが走っている間は画面が更新されません。 すぐに終了するスクリプトであれば問題になりませんが、 処理が終わるまで数秒以上かかるスクリプトでは不安になってきます。 そのような場合、処理の途中でときどきView.update()メソッドを呼び出すと、 処理中の画面を更新し、途中経過を確認できます。


    for (var i = 1; i <= 10000; i++)
    {
        // 時間のかかる処理
        ... 省略 ...

        // 100回に1回の割合で画面を更新する。
        if (i % 100 == 0)
            update();
    }

【ヒント】
あまり頻繁に画面を更新すると、遅くなる可能性があります。 このサンプルのように適当な間隔をおいて更新すると、 処理スピードを犠牲にすることなく画面を更新できます。


ファイルのパスを取得するには?

ファイルのパスは、View.getFilePath()メソッドで取得できます。


    // アクティブな編集ウインドウのファイルパスを取得する。
    var path = getFilePath();


ファイルの行数を調べるには?

ファイルの行数は、View.getLineCount()メソッドView.getViewLineCount()メソッドや、 EOFプロパティで調べることができます。


    // アクティブなドキュメントのファイル行数を取得する。
    var numberOfFileLines = getLineCount();

    // 表示行数を取得する。
    var numberOfViewLines = getViewLineCount();

    // EOFからファイル行数、表示行数を取得する。
    var fileLines = EOF.fileLine;
    var viewLines = EOF.viewLine;


ある部分の文字を置き換えるには?

テキストの特定の場所から指定文字数を他の文字に置き換えるには、 View.replaceTextAt()、 またはView.replaceTextBetween()メソッドを使います。


    // 100行の11文字目から5文字を置き換えます。
    var pt = EP(100, 10);
    replaceTextAt(pt, 5, "これに置き換わります");

    // 2つのポイント間のテキストを置き換えます。
    var p1 = EP(110, 10);
    var p2 = EP(111, 15);
    replaceTextBetween(p1, p2, "2ポイント間のテキストがこれに置き換わります");

【ヒント】
replaceTextAt()replaceTextBetween()は、カーソル位置に影響を与えません。


スクリプトで行った編集操作を一発でUndoするには?

スクリプトで行った編集操作を一発でUndoするには、 まとめたい編集操作をView.enterUndoGroup()View.leaveUndoGroup()メソッドで囲みます。


    // 1つのUndoで元に戻せるようにする。
    enterUndoGroup();
    insertText("編集操作1回目\r\n");
    insertText("編集操作2回目\r\n");
    insertText("編集操作3回目\r\n");
    leaveUndoGroup();

【メモ】
enterUndoGroup()leaveUndoGroup()は入れ子にできます。 つまり、enterUndoGroup()と同じ回数だけleaveUndoGroup()を呼び出した時点でUndo操作のグループ化の終りとなります。 入れ子の深さに関係なくUndoのグループ化を強制的に終了させるには、leaveUndoGroup(true)としてください。


カーソルが見えるようにスクロールするには?

カーソルが見えるようにスクロールさせるには、 View.ensureVisible()メソッドを使います。


    // カーソルが見えるようにスクロールする。
    ensureVisible();


ファイルの漢字コードを知るには?

ファイルの漢字コードを知るには、 View.getEncoding()メソッドを使います。 getEncoding()メソッドが返す値は、File.guessEncoding()を参照してください。


    // ファイルの漢字コードを調べる。
    var encoding = getEncoding();
    if (encoding == File.ENCODING_SJIS)
        messageBox("Shift JIS");
    else if (encoding == File.ENCODING_EUC)
        messageBox("EUC");
    else // (それ以外)
        messageBox("Shift JIS でも EUC でもありません.");



タブ間隔を知るには?

タブ間隔は、View.getTabStop()メソッドで取得できます。


    // タブ間隔を取得する。
    var tabstop = getTabStop();



編集禁止かどうかを知るには?

編集禁止かどうかを調べるには、 View.getReadOnly()メソッドを使います。


    // まず編集ウインドウが開いていることを調べる
    if (!view)
        error("ウインドウが開いていません.");
    if (getReadOnly())
        error("編集禁止です.");

    ... 編集操作 ...



プロジェクトに登録されているすべての*.cファイルを取得するには?

ある条件に従ってプロジェクトのアイテムを探すには、 GroupItem.enumItems()メソッドを使います。 このメソッドと正規表現を組み合わせることで、プロジェクト中のすべての*.cファイルのアイテムを取得できます。


    // 正規表現 /\.c$/i で拡張子 .c を指定します。最後表す $ と、
    // 大文字小文字の区別しない「i」オプションを忘れないでくださいね。

    var items = project.enumItems(true, FileItem, /\.c$/i);

    for (var i = 0; i < items.length; i++)
    {
        // 個々の.cファイル対する処理
        ...
    }

この応用で、拡張子が.c、.h、.cppのファイルアイテムを取り出すには、 正規表現を以下のように修正するだけです。


    var items = project.enumItems(true, FileItem, /\.(c|h|cpp)$/i);