[ruby-list:40868] [RFC] framework of Ruby/Tk + VNC
From:
Hidetoshi NAGAI <nagai@...>
Date:
2005-06-10 03:43:24 UTC
List:
ruby-list #40868
永井@知能.九工大です.
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