永井@知能.九工大です.

Ruby/Tk で作成した GUI アプリケーションを外部に公開するために,
VNC と組み合わせた枠組みを考えています.
コンセプトは次のようなものです.

 ・外部公開に際しては VNC (RFB プロトコル) を用いる.

 ・作成したアプリケーションは safe (safe-Tk) ベースの
   スレーブインタープリタ ( 通常は $SAFE == 4 ) で動かす.
   必要なら,safe ベースではないスレーブで動かすことも可.

 ・リスク低減のため,ウィンドウマネージャの類は一切使わず,
   VNC サーバ以外では Ruby/Tk プロセスを一つだけ動かす.
   # 将来的には,Ruby/Tk 自体が RFB サーバ機能まで持ち,
   # VNC サーバを必要としなくなることも期待できる.

 ・マスターインタープリタのキャンバスウイジェットが
   ウィンドウマネージャ代りとなり,スレーブインタープリタの
   ルート/トップレベルを埋め込みウィンドウで管理する.

tclplugin を用いる場合に比べ,

 ・VNC viewer が既に手元にあるなら,それを使うことができる.

 ・ブラウザでの利用の際,JAVA 版 viewer を applet として
   渡せるため,利用者が plugin をインストールする必要がない.

 ・アプリケーションの実装において Tcl/Tk 拡張を用いていても,
   利用者がそれをインストールしておく必要がない.

 ・スクリプトのソースを送り出す必要がない.
   基本的には VNC によるイベント/画面のやり取りのみのため,
   セキュリティについての配慮のコストを減らすことができる.

といった利点もあるのではないかと思います.

欠点は

 ・ネットワークを介したリモート操作であるため,遅い.

 ・要求されるサーバの資源が多い.

というところでしょう.

コンセプトに基いて試験的に実装してみたスクリプトを添付します.
動作には 2005/06/08 以降の新しい ruby が必要です.
multi-tk を利用しているため,スレーブインタープリタで
実行しているスクリプトの記述が,通常の Ruby/Tk スクリプトと
違いがないことにも注目してください.

かなり遅いマシンであり設置期間も未定ではありますが,
実際に触れて試してみていただけるように
同スクリプトを使った実験サーバも用意しました.
VNC viewer を用いる場合は 131.206.154.81:33 で,
ブラウザを用いる場合は http://131.206.154.81/ で
アクセスしてみてください.
もちろん,利用者側には Ruby は必要ありません.

# ブラウザを使った場合には最初にパスワードが要求されますが,
# そこでは何も入力せずに OK ボタンを押してかまいません.

ご意見,コメント,開発への参加希望(^_^)等ありましたら,
ぜひお寄せ頂けますようお願い致します.

                                       永井 秀利 (九工大 知能情報)
                                           nagai / ai.kyutech.ac.jp
---------------------------------------------------------------
以下がテストスクリプトです.
===============================================================
#!/usr/bin/env ruby
#
#  Ruby/Tk+VNC :: concept example
#
#                             by  Hidetoshi NAGAI (nagai / ai.kyutech.ac.jp)
#
require 'multi-tk'
require 'singleton'

class VNC_Wall < TkCanvas
  include Singleton

  def initialize
    @new_relx = 0.4
    @new_rely = 0.4

    super(:width=>TkWinfo.screenwidth('.'), 
          :height=>TkWinfo.screenheight('.'))
    self.pack
  end

  ######################################

  class Window_Frame < TkcWindow
    def _title_bind
      @titlebar.bind('ButtonPress-1', 
                     proc{|rx, ry| 
                       @rx = rx; @ry = ry; @sx, @sy = self.coords
                       @base.raise
                     }, '%X', '%Y')

      @titlebar.bind('B1-Motion', 
                     proc{|rx, ry|
                       wx = @wall.winfo_rootx; wy = @wall.winfo_rooty
                       if rx > wx && rx < wx + @wall.width &&
                           ry > wy && ry < wy + @wall.height
                         self.coords = [@sx + (rx - @rx), @sy + (ry - @ry)]
                       end
                     }, '%X', '%Y')
    end
    private :_title_bind

    def initialize(wall, title, coords, keys={})
      @wall = wall
      @base = TkFrame.new(@wall, :borderwidth=>3, :relief=>:ridge)
      @titlebar = TkLabel.new(@base, :text=>" #{title} ", :relief=>:raised, 
                              :foreground=>'white', 
                              :background=>'midnight blue').pack(:fill=>:x)
      @container = TkFrame.new(@base, :container=>true).pack(:fill=>:both, 
                                                             :expand=>true)
      super(@wall, coords[0], coords[1], keys)
      self.window = @base
      _title_bind
    end

    def winid
      @container.winfo_id
    end

    def title(txt)
      @titlebar.text(txt)
    end
  end

  ######################################

  def new_window(title, coords, keys={})
    keys = _symbolkey2str(keys)
    keys['anchor'] = 'nw'
    Window_Frame.new(self, title, coords, keys)
  end

  def new_root(keys={})
    keys = _symbolkey2str(keys)
    coords = keys.delete('coords') || 
      [self.width * @new_relx, self.height * @new_rely]
    title  = keys.delete('title')  || 'root'
    new_window(title, coords, keys)
  end
  private :new_root

  ######################################

  class TOPLEVEL_ARG < Exception
    alias value message
  end

  ######################################

  def new_toplevel(slave_ip, top, 
                   coords=[self.width * @new_relx, self.height * @new_rely])
    w = new_window("toplevel(#{top})", coords)
    MultiTkIp.invoke_hidden(slave_ip, 'toplevel', top, '-use', w.winid)
  end

  def replace_toplevel_cmd(slave_ip)
    th = Thread.new(slave_ip){|ip|
      begin
        Thread.stop
      rescue TOPLEVEL_ARG => arg
        begin
          new_toplevel(ip, arg.value)
        rescue Exception => e
        end
        retry
      end
    }

    cmd = TkComm._get_eval_string(proc{|t|
                                    th.raise(TOPLEVEL_ARG.new(t))
                                    until slave_ip.eval_proc{TkWinfo.exist?(t)}
                                      Thread.pass
                                      Tk.update
                                    end
                                  })
    slave_ip._eval("proc toplevel {path} {#{cmd} $path}")
  end

  ######################################

  def new_slave(safe=nil, keys={}, &b)
    if safe.kind_of?(Hash)
      keys = _symbolkey2str(safe)
      safe = keys.delete('safe')
    end
    w = new_root(keys)
    ip = MultiTkIp.new_trusted_slave(safe, {:use=>w.winid}, &b)
    MultiTkIp.hide_cmd(ip, 'toplevel')
    replace_toplevel_cmd(ip)
    ip
  end
  alias new_trusted_slave new_slave

  def new_safe_slave(safe=4, keys={}, &b)
    if safe.kind_of?(Hash)
      keys = _symbolkey2str(safe)
      safe = keys.delete('safe') || 4
    end
    w = new_root(keys)
    ip = MultiTkIp.new_safe_slave(safe, {:use=>w.winid}, &b)
    replace_toplevel_cmd(ip)
    ip
  end
  alias new_safeTk new_safe_slave
end

#===============================================================

if $0 == __FILE__
  timeout = 60

  wall = VNC_Wall.instance
  wall[:background] = 'skyblue'

  #-----------------------------------------------------------

  TkcText.new(wall, 150, 50, :fill=>'navyblue', :font=>'courier -14', 
              :text=>"Ruby/Tk+VNC :: concept example")
  TkcText.new(wall, 370, 170, :text=><<EOT)
The root window of the safeTk IP is 
embedded in Master IP's frame widget.

You can move the root window by 
'Button-1 + Motion' on the titlebar.
Master IP works like as a window manager.

No window manager on the VNC server.
Running one Ruby/Tk process only.
EOT
#'
  TkcText.new(wall, 150, 300, 
              :text=>"This example will exit in #{timeout} seconds.")
  TkcText.new(wall, 350, 350, 
              :text=>"This is Master IP's canvas widget.\n\t($SAFE==#{$SAFE})")

  #-----------------------------------------------------------

  # ip = wall.new_trusted_slave(:title=>'slave root', :coords=>[50, 70]){
  # ip = wall.new_safeTk(3, :title=>'slave root', :coords=>[50, 70]){
  ip = wall.new_safeTk(:title=>'slave root', :coords=>[50, 70]){
    TkLabel.new(:text=>"safeTk interpreter's root").pack(:padx=>10, :pady=>5)

    top = nil
    cnt = 0

    b1 = TkButton.new(:text=>'create Toplevel')

    b2 = TkButton.new(:text=>'add label to the toplevel', :state=>:disabled, 
                      :command=>proc{
                        cnt += 1
                        TkLabel.new(top, 
                                    :text=>"Pressed(#{cnt})!! $SAFE=#{$SAFE}"
                                    ).pack
                      })

    b1.command = proc{
      top = TkToplevel.new
      b1[:state] = :disabled
      b2[:state] = :active
      TkLabel.new(top, 
                  :text=>'New toplevel of slaveIP').pack(:padx=>20, :pady=>30)
    }

    label = TkLabel.new(:foreground=>'red', :text=>"\n")
    timer = TkTimer.new(500, 1, proc{label.text = "\n"})
    TkButton.new(:text=>'BUTTON', 
                 :command=>proc{
                   timer.cancel
                   label.text = "button is pressed!!\n($SAFE==#{$SAFE})"
                   timer.start
                 }).pack(:padx=>5, :pady=>5, :fill=>:x)
    label.pack

    Tk.pack(b1, b2, :fill=>:x, :padx=>5, :pady=>5)
  }

  #-----------------------------------------------------------

  Tk.after(timeout * 1000){exit}
  Tk.mainloop
end