Tcl/TkでDDE?
 ここではWindows上で動くアプリケーション、 特にExcelと、PowerBuilderで作ったアプリケーションと Tcl/Tk のアプリケーション同士でDDE(Dynamic Data Exchange) という機構を使ってデータをやりとりしてみようということをやっています。 Excelは、Windows上の今日代表的な表計算アプリケーションソフトの一種で、 マクロ言語としてVisual Basic for Applications(VBA)を載せているので、 なかなか高機能なことをやらせることができます。


DDEとは?
 DDE(Dynamic Data Exchange)とは、 ムッチャ簡単には Windows 上のアプリケーション同士がデータや命令などを互いに通信できるしくみです。 データを要求したり、処理を実行するよう命令する側をクライアント (またはデスティネーション)、要求に応じてデータを返したり、 処理を行ったりする側をサーバ(またはソース)と呼びます。

 市販のアプリケーションソフトの中ではDDEが利用できるものとできないものがあり、 できないソフトはどうしようもありませんが、WordやExcelなど、 簡単に言えば「リンクはりつけ」できるアプリケーションは DDE通信でデータを相手に送る機能をもっており、DDEソースになることができます。 また、VisualBasic、Delphi、PowerBuilderなどの言語処理系で書いたプログラムは、 DDEソースにもDDEデスティネーションにもなることができます。 特殊な例としてWordやExcelはマクロ言語がDDE処理を記述できる言語VBA なので、アプリケーションとしてDDEソースになれるほか、 マクロがDDEデスティネーションになれます。 そして、ここで扱う我らがTcl/Tkで書いたプログラムも、少し制限はありますが、 DDEソースにもDDEデスティネーションにもなれます。


DDEでTcl/TkからExcelに接続する
 手始めにExcelとwishを起動して、wish(Tcl/Tk)側から接続してみます。 Excelのほうは新規作成のブックでも、 既存のブックでもいいので適当に開いておきます。 で、Tcl/Tkのスクリプトで
  package require dde
  set r [dde request Excel "\[Book1\]Sheet1" R5C5]
などとdde requestサブコマンドを使うと、 指定した位置のセルの値をExcelからとってくることができます。 \[Book1\]Sheet1 の部分は、前半がブックファイル名、 後半がシート名です。 逆に、dde pokeサブコマンドを使うと、 Excelの指定したセルに文字列をセットさせることもできます(*1)。
  dde poke Excel "\[Book1\]Sheet1" R5C5 {Hello}
(*1)dde pokeサブコマンドはTcl 8.2でサポートされたので、以前の版では使えません。
また、
  # 新規ブック作成
  dde execute Excel System {[NEW(1)]}
  # ブックを開く
  dde execute Excel System {[OPEN("\usr\me\data.xls")]}
  # マクロを実行する
  dde execute Excel System {[RUN("data.xls!macro1")]}
  # 指定したセルを選択状態にする
  dde execute Excel System {[SELECT("R5C5")]}
  # セルを挿入する
  dde execute Excel System {[INSERT(1)]}
  # Excel上で指定したキーの並びが押されたかのような動作をさせる
  dde execute Excel System {1243{ENTER}}
というようなもろもろのことができます。
 ちなみに、dde executeコマンドはDDEソース(ここではExcel) が応答するまで固まって待ちます。 そうではなく、Tcl/Tkスクリプトの動作を途切れさせずにウラでDDE通信をやらせたい場合は -async というオプションをつけます。


DDEでExcel VBAからTcl/Tkに接続する
 今度は立場が一転、Tcl/Tkの方がDDEソースとなって Excelにデータを返したり、処理を行ってさしあげたりします。 ExcelのマクロといえばExcel VBA(Visual Basic for Applications) 、というわけで、DDE通信を書く言語はVisualBasicです。 「ツール」→「マクロ…」→「VisualBasicエディタ」でさっそくヤツを起動します。 でVisualBasicエディタが出てきたら「挿入」→「ユーザーフォーム」 でウィンドウ編集画面を出し、そのウィンドウをいきなりダブルクリック、 または「表示」→「コード」で準備はOKです。
Private Sub UserForm_Activate()
    Dim fd As Integer

'まず通信相手と接続します。
    fd = DDEInitiate("TclEval", "TclTkDDESample")
'Tcl/Tkから情報をとってきて、Excelのセルに書きこみます。
    result = DDERequest(fd, "SampleValue")
    For i = LBound(result) To UBound(result)
        Worksheets("Sheet1").Cells(i, 2).Formula = result(i)
    Next
'Tcl/Tk上でコマンドを実行させます。
    'DDEExecute fd, "tk_messageBox -message hello"
    'DDEExecute fd, "SampleProc 1"
'Tcl/Tkスクリプトにキーを送ります。
    AppActivate "TclScriptWindowName"
    SendKeys "x{ENTER}"

    DDETerminate (fd)
End Sub
DDEInitiate関数では接続相手のTcl/Tkのサービス名(後で出てきます) を "TclTkDDESample" のように指定します("TclEval"は固定)。 VBA側からできることは、DDERequest関数を使って、 Tcl/Tk中の変数の値をとってくることと、 DDEExecute関数を使って、 任意のTclコマンドを実行させること、 それにSendKeysステートメントを使って、 Tcl/Tkスクリプトにキーを送ることです。 これらに応じるTcl/Tkスクリプトはこんなかんじになります。 とりあえず、要求を受けるために外部変数SampleValue とプロシージャSampleProcを定義しておきます。また、キーが送られてきたときそれに反応するコードが書きたければ、 通常のbindコマンドと全く同じようにbindコマンドを使って記述すればOKです。 重大な注意として、VBA側からのDDEExecute関数を使った場合、 通信相手のTcl/Tkスクリプトが終了するまで VBA側はビジー状態(コールドスリーピーな状態)になってしまいます。 これはTcl/Tkの罪ではなく、VBAのマニュアルにも書いてある現象です。 とはいってもこれではとても不便なので、現状ではDDEExecute は使わないほうがよいでしょう。
  package require dde
  dde servername TclTkDDESample
  set SampleValue 12345
  proc SampleProc {args} {
    # ...
  }
  wm title . TclScriptWindowName
  bind .  {
    # ...
  }
  # ...
Tcl/TkがDDEソースになるためには、必ず dde servername コマンドで自分のサービス名を適当に決めて宣言する必要があります。


PowerBuilderで書いたDDEサーバにTcl/Tkから接続する
 PowerBuilderはTcl/Tk以上に知られていない言語処理系ですが、 簡単に言うとVisualBasicの親玉みたいなやつで、 特にOracleなどのデータベースサーバに接続するアプリケーションインターフェースの作成に使われる処理系です。 マウス操作でウィンドウ作成ができる統合開発環境が完備している点や、 トラッドテイストあふれる実行文主体の文法やオブジェクト指向風味なコントロールのコントロールなど、 どこから見てもVisualBasicの親玉としか言いようがありません。 ここではDDEの扱いが比較的すっきり整備されている PowerBuilderを使いますが、 VisualBasicやExcel VBA で作ったアプリケーションなりマクロなりとも同じようなかんじで通信ができるはずです。

 まず、PowerBuilderのウィンドウペインタで次のような画面を作ります。 PowerBuilderはVisualBasic同様、 マウス操作でこういう画面が作れてこれはこれでとてもベンリです。

 ウィンドウの「プロパティ」で名前を「window1」、 シングルラインエディット(Tcl/Tkで言うエントリ、VisualBasicで言うテキストボックスのこと)の方も「プロパティ」で名前を「text1」 としておきます。 そして、ウィンドウに焦点を合わせ「スクリプト」でコードを編集します。

// window1.Open():
StartServerDDE("PowerBuilder", "PBDDESERV", "Value")
まずはこのウィンドウが出てきたときのイベントOpenです。 StartServerDDE関数の引数ですが、 最初と2番目は適当に好きな名前にすればいいです。 その後ろに、公開しているアイテムの名前を書きます(オプション)。

// window1.RemoteExec():
String command, clientname
GetCommandDDEOrigin(clientname)
GetCommandDDE(command)
if left(command, 3) = "SET" then
    window1.text1.text = mid(command, 5)
    RespondRemote(TRUE)
else
    RespondRemote(FALSE)
end if
// end.
今度はRemoteExecイベントで、デスティネーションここではTcl/Tkスクリプトに dde executeコマンドを食らったときに呼び出されます。 Tcl/Tkがどんな命令を送りつけてきたのかは GetCommandDDE関数で知ることができ、その内容に応じて適当に処理します。 命令は文字列で、プログラムを書く人がどのようにでも好きに決めることができます。 上の例では、Tcl/Tk側から送ってきた命令が「SET 」で始まるものであれば、 その後ろの部分をシングルラインエディットtext1に書きこみ、 そうでなければ無視しています。

// window1.RemoteRequest():
SetDataDDE(window1.text1.text)
// end.
最後にRemoteRequestイベントで、 こちらは dde request によるデータ要求を受け取ったときに呼び出されます。 SetDataDDE関数を使って、ほしがっているらしいデータを適当に送り返してやればOKです。

さて、PowerBuilderでアプリケーションが書けたら、 それを実行状態にしたまま、wish(Tcl/Tk)を起動してコマンドを下のように叩き、 接続してみます。

# DDEは外部パッケージとして提供されるので、最初にこのコマンドが必要
# 意味がわからなくても、とりあえず毎回これでOKですよ
package require dde
# OK。上のRemoteRequestイベントに書いた通り、シングルラインエディット
# text1の内容が送られてくる
set v [dde request PowerBuilder PBDDESERV Value]
# エラー。
set v [dde request PowerBuilder PBDDESERV DetaramenaAtai]
# OK。上のRemoteExecイベントに書いた通り、シングルラインエディットに
# 指定した文字列が書き込まれる。
dde execute PowerBuilder PBDDESERV {SET Take the long way home}
# エラーではないが、何も起こらない…
dde execute PowerBuilder PBDDESERV {DetarametaCommand}
Tcl/Tkのddeコマンドは、「接続」「処理」「切断」 という流れ的な概念がなく、dde requestまたはdde executeコマンドの引数で毎回、 アプリケーション名(ここではPowerBuilder) とトピック名(ここではPBDDESERV)を指定して、 コマンド単位で接続と切断を繰り返します。
Tcl/Tkで書いたDDEサーバにPowerBuilderから接続する
 またまた立場が逆転、今度はTcl/TkがDDEサーバ(ソース) となってPowerBuilderで書かれたDDEデスティネーションから接続を受けつけます。 今度はもっと大掛かりです。PowerBuilderで書いたプログラムの窓に Tclコマンドを入力すると、DDEでつながったwishに接続して実行してくれるというものです。
まずウィンドウの上側のボタンが押されたときのイベントハンドラ button1.Clicked です。これは、WISHDDESERV という名前で待機しているTcl/Tkアプリケーションに接続し、 その左側のシングルラインエディットに入力された文字列を Tclコマンドとして実行させます。
// window1.button1.clicked()
int handle, r
handle = OpenChannel("TclEval", "WISHDDESERV")
if handle < 0 then
    MessageBox("Error", "DDE接続に失敗しました", Exclamation!)
    return
end if
r = ExecRemote(text1.text, handle)
if r < 0 then MessageBox("Error", "失敗しました。")
CloseChannel(handle)
// end.


今度はウィンドウの下側のボタンが押されたときのイベントハンドラ button2.Clicked です。 これは、WISHDDESERVという名前で待機しているTcl/Tkアプリケーションに接続し、 その左側のシングルラインエディットに入力された文字列の名前のTcl変数の値をもらってきて、一番下のマルチラインエディット(Tcl/Tkで言うテキストです) に書き出します。

// window1.button2.clicked():
int handle, r
handle = OpenChannel("TclEval", "WISHDDESERV")
if handle < 0 then
    MessageBox("Error", "DDE接続に失敗しました", Exclamation!)
    return
end if
r = GetRemote(text2.text, text3.text, handle)
if r < 0 then MessageBox("Error", "失敗しました。")
CloseChannel(handle)
// end.
以上でPowerBuilder側、つまりクライアントの準備は完了です。 サーバ側たるTcl/Tk側ですが、 ここでは普通のwishを起動して、 次の2行を実行させたところで待機状態にしときます。
package require dde
dde servername WISHDDESERV
で、PowerBuilderのアプリケーションの窓から 「tk_messageBox -message Hello」とか「source daaaaa.tcl」 などとコマンドを実行させてみたり、 「tcl_version」や「env(PATH)」などTcl変数の値を受けとってみたりできます。

やねうらの top
(first uploaded 1999/07/03 last updated 1999/11/03, Urano398)