こんにちは、高木です。
前回まででTkのウィンドウを出すことができるようになりました。今回はそのウィンドウにボタンを配置してみます。そして、ボタンをクリックすれば何らかのC++のコードを呼び出せるようにしてみたいと思います。
ウィンドウにボタンを配置するには次のようなスクリプトを書けばOKです。
| 0 1 2 | pack [button .b -text test -command test] | 
これで、ボタンをクリックすれば「test」というコマンドを呼び出すことができます。testというTcl/Tkのコマンドはありませんので、このままでは同名の外部コマンドが呼び出されてしまいます。そこで、独自に「test」という名のコマンドを実装してみることにしましょう。
Tclで独自のコマンドを登録するにはTcl_CreateObjCommand関数を使います。この関数には、インタープリター、コマンド名、コマンドを評価したときに呼び出すコールバック関数、コールバック関数に渡すデータ、コマンドの削除時に呼び出すコールバック関数を引数として渡します。
単純にこの関数に一枚被せるだけでもいいんですが、できればラムダ式などC++特有の機能を使いたいですよね。そこでワンクッション置くことにしましょう。また、コールバック関数の引数もC++らしいものに直して上げた方が使い勝手が向上します。
というわけで、ユーザーが定義するコールバック関数(あるいはファンクター)は、いったんstd::functionで受けることにします。引数はすこしすっきりさせて、interpreterとstd::vecrtor<obj>のconst参照ということにしましょう。
この方針を踏まえた上でcommandクラスを作ってみました。
| 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |   class command   {   public:     using proc_type = std::function<int(interpreter, const std::vector<obj>&)>;     template <typename Proc>     command(interpreter interp, const char8_t* name, Proc proc)       : interp_(interp),         token_(Tcl_CreateObjCommand(interp.get(), reinterpret_cast<const char*>(name), command_proc, new proc_type(proc), delete_proc))     {     }     ~command()     {       Tcl_DeleteCommandFromToken(this->interp_.get(), this->token_);     }   private:     // Tcl C APIの形式に合わせたコールバック関数     static int command_proc(ClientData clientData, Tcl_Interp* interp, int objc, Tcl_Obj* const objv[])     {       if (auto proc = static_cast<proc_type*>(clientData))       {         std::vector<obj> args(objv + 0, objv + objc);         return (*proc)(interp, args);       }       return TCL_ERROR;     }     // 後始末関数     static void delete_proc(ClientData clientData)     {       auto proc = static_cast<proc_type*>(clientData);       delete proc;     }     interpreter interp_;     Tcl_Command token_;   }; | 
どうするのがいいのかはいろいろ迷うところではあります。ここでは、std::function<int(interpreter, const std::vector<obj>&)>型に定義したproc_typeへのポインターをClientDataとしています。
commandクラスへのポインターをClientDataとして渡して、実際のコールバック関数はcommandクラスの派生クラスでオーバーライドしたメンバー関数にするという手もあります。その方がオブジェクト指向らしいのでしょうが、ラムダ式を使いたいという方針からは離れてしまいます。
迷った結果、上記のような形になりました。このcommandクラスでは、コンストラクターでコマンドの登録を行い、デストラクターでコマンドの登録解除を行っています。登録解除にはTcl_DeleteCommandFromToken関数を使っています。
Tclには、Tcl_DeleteCommand関数というのもあって、こちらはコマンド名を渡して登録解除を行います。コマンド名を保持するにはTcl_CreateObjCommand関数が返すトークンを保持するよりコストが大きいので、今回はトークンを使うようにしました。
commandクラスを使って、testコマンドを登録するには次のようにします。
| 0 1 2 3 4 5 6 | int test(tcl::interpreter interp, const std::vector<tcl::obj>& args) {   std::cout << __func__ << std::endl;   return TCL_OK; } | 
まずはコールバック関数を用意する必要があるので、上記のような関数を定義します。この関数は単に関数名を標準出力に書き出すだけのものです。
次に、以下のようにしてcommandクラスのオブジェクトを作成します。
| 0 1 2 | tcl::command cmd(interp, u8"test", test); | 
ここでinterpは事前に作成しておいたインタープリターです。interpreter::root()を使ってルートとなるインタープリターを呼び出してもいいでしょう。また、testの部分はラムダ式で書くこともできます。
ここまで準備を整えたら、interpreter::evaluateメンバー関数で最初のスクリプトを評価すればOKです。プログラムを実行すると以下のような小さなウィンドウが表示されます。ボタンを押せば標準出力にtestという文字列が出力されることでしょう。

ここまでできればTcl/TkとC++を組み合わせたプログラムを書くことができます。次回はもう一歩進めて、Tclの変数をC++から設定・取得する方法について考えてみることにします。

![[迷信] 引用符で囲んだヘッダ名はカレントディレクトリから探索する](https://www.kijineko.co.jp/wp-content/uploads/2021/06/5064715_s.jpg)

![[迷信] コンパイラはプログラマの心を察してくれる](https://www.kijineko.co.jp/wp-content/uploads/2021/06/709817_s.jpg)

![[迷信] 2の累乗による割り算と右シフトは等価](https://www.kijineko.co.jp/wp-content/uploads/2021/06/3109898_s.jpg)
