From: Hidetoshi NAGAI Date: 2005-10-07T18:06:47+09:00 Subject: [RFC] framework of Ruby/Tk + VNC Hi, 4 months ago, I wrote about the framework of Ruby/Tk + VNC ([ruby-talk:145031]). And now, I've exhibited a new demo to try its concept. The previous demo has the client script embedded in the main script. But the main script of this demo can load the external file as the client script which can work as a standalone Ruby/Tk script. It means that steps from developpng on local to opening to the net are seamless. You can also try its interactivity. One of the client script is an animation demo quoted from "Ruby/Tk Widget Demo". Please access 131.206.154.81:5933 by VNC viewer, or http://131.206.154.81/ by web browser. In both case, your firewall must pass through the packets to IP addr: 131.206.154.81 and port: 5933. # When by a web browser, a dialog to require password is shown. # Please ignore it and press the "OK" button with empty password. Any comments (or wish to participate in the development ;-)) are welcome. Hidetoshi NAGAI (nagai@ai.kyutech.ac.jp) --------------------------------------------------------------- The following is the sample script. The main script expects that the main and the 2 client scripts are placed on the same directory. =======< wrap-test-main2.rb >========================================= #!/usr/bin/env ruby require 'multi-tk' require 'tk/canvas' 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 def initialize(wall, title, *args) @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) @container.bind('Destroy'){self.destroy} super(wall, *args) self.window = @base _title_bind end def winid @container.winfo_id end end def new_toplevel(wall, ip, top) w = Window_Frame.new(wall, "toplevel(#{top})", 200, 150, :anchor=>:nw) MultiTkIp.invoke_hidden(ip, 'toplevel', top, '-use', w.winid) end class TOPLEVEL_ARG < Exception def self.new(path, *args) obj = super(path) obj.instance_variable_set('@args', args) obj end alias value message attr_reader :args end def replace_toplevel_cmd(wall, slave_ip) th = Thread.new(wall, slave_ip){|w, ip| begin Thread.stop rescue TOPLEVEL_ARG => arg begin new_toplevel(w, ip, arg.value, *(arg.args)) rescue Exception => e p e end retry end } cmd = TkComm._get_eval_string(proc{|t, *args| th.raise(TOPLEVEL_ARG.new(t, *args)) until slave_ip.eval_proc{TkWinfo.exist?(t)} Thread.pass Tk.update end }) MultiTkIp.hide_cmd(slave_ip, 'toplevel') unless slave_ip.safe? slave_ip._eval("proc toplevel {path args} {eval \"#{cmd} $path $args\"}") slave_ip._eval("proc wm {args} {}") end ############################################## timeout = 60 wall = TkCanvas.new(:background=>'skyblue').pack #wall = TkCanvas.new(:width=>500, :height=>400, :background=>'skyblue').pack #wall = TkCanvas.new(:width=>800, :height=>600, :background=>'skyblue').pack wall.xscrollbar(h_scroll = TkScrollbar.new(:width=>10)) wall.yscrollbar(v_scroll = TkScrollbar.new(:width=>10)) TkGrid.rowconfigure(Tk.root, 0, 'weight'=>1, 'minsize'=>0) TkGrid.columnconfigure(Tk.root, 0, 'weight'=>1, 'minsize'=>0) wall.grid(:row=>0, :column=>0, :sticky=>'news') h_scroll.grid(:row=>1, :column=>0, 'sticky'=>'ew') v_scroll.grid(:row=>0, :column=>1, 'sticky'=>'ns') TkcText.new(wall, 150, 50, :fill=>'navyblue', :font=>'courier -14', :text=>"Ruby/Tk+VNC :: concept example") TkcText.new(wall, 150, 300, :text=>"This example will exit in #{timeout} seconds.") TkcText.new(wall, 350, 340, :text=>"This is Master IP's canvas widget.\n\t($SAFE==#{$SAFE})") Tk.update Tk.root.geometry('500x380') Tk.after(timeout * 1000){exit} ev_thread = Thread.new{Tk.mainloop} #=========================================================# demos = [ ['wrap-test-target.rb', 'Simple Demo: create a new toplevel'], ['pendulum.rb', 'Test of interactivity (Animation demo)'], ] v = TkVariable.new('') sel_frame = TkFrame.new(wall, :relief=>:flat, :borderwidth=>3) TkLabel.new(sel_frame, :text=>'Please select the demo which you want to try.', :foreground=>'red').pack(:padx=>10, :pady=>5) demos.each{|file, label| TkButton.new(sel_frame, :text=>label, #:anchor=>:w, :command=>proc{ v.value = file sel_frame.destroy }).pack(:fill=>:x) } TkcWindow.new(wall, 40, 100, :anchor=>:nw, :window=>sel_frame) Tk.update sel_frame.wait #p v.value exit if v.value.empty? dirname = File.dirname(File.expand_path(__FILE__)) #target_file = ARGV.shift.dup #target_file = File.expand_path(ARGV.shift.dup) target_file = File.join(dirname, v.value) #=========================================================# wall.scrollregion = [0, 0, 1024, 768] TkcText.new(wall, 370, 170, :text=><:nw) w.instance_variable_get(:@container).bind_append('Destroy'){Tk.exit} #ip = MultiTkIp.new_trusted_slave(:use=>w.winid) #ip = MultiTkIp.new_trusted_slave(3, :use=>w.winid) #ip = MultiTkIp.new_trusted_slave(4, :use=>w.winid) #ip = MultiTkIp.new_safeTk(0, :use=>w.winid) #ip = MultiTkIp.new_safeTk(3, :use=>w.winid) ip = MultiTkIp.new_safeTk(:use=>w.winid) replace_toplevel_cmd(wall, ip) #p ['on main', __FILE__] #=========================================================# target_file.untaint #ldr = proc{|f| $SAFE=ip.safe_level; load f,false} xp = proc{|val| p val} alias __require__ require @req = proc{|f| # p ['@req', f] __require__(f)} def require(f) @req.call(f) end #xp.call '-------------------' ip.bg_eval_proc{ # xp.call self begin # xp.call target_file #ldr.call target_file load target_file, true rescue => e xp.call e if $DEBUG end } #xp.call '-------------------' =begin Tk.after(5000){ w.instance_variable_get(:@container).configure(:width=>450, :height=>250) } Tk.after(10000){ w.instance_variable_get(:@container).configure(:width=>500, :height=>600) } =end #p 'join' ev_thread.join ====================================================================== =======< wrap-test-target.rb >======================================== require 'tk' p ['on target', __FILE__] if $SAFE < 4 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.mainloop ====================================================================== =======< pendulum.rb >================================================ #!/usr/bin/env ruby # # based on Tcl/Tk8.5a2 widget demos # require 'tk' #TkRoot.new(:title=>'Pendulum Animation Demonstration', # :iconname=>"pendulum") # create label msg = TkLabel.new { font 'Helvetica -12' wraplength '4i' justify 'left' text 'This demonstration shows how Ruby/Tk can be used to carry out animations that are linked to simulations of physical systems. In the left canvas is a graphical representation of the physical system itself, a simple pendulum, and in the right canvas is a graph of the phase space of the system, which is a plot of the angle (relative to the vertical) against the angular velocity. The pendulum bob may be repositioned by clicking and dragging anywhere on the left canvas.' } msg.pack('side'=>'top') # animated wave class PendulumAnimationDemo def initialize(frame) TkFrame.new(frame) {|f| TkButton.new(f, :command=>proc{Tk.root.destroy}, :text=>'Dismiss').pack(:side=>:left, :expand=>true) }.pack(:side=>:bottom, :fill=>:x, :pady=>'2m') # Create some structural widgets pane = TkPanedWindow.new(frame #, :width=>350, :height=>200 ).pack(:fill=>:both, :expand=>true) @lf1 = TkLabelFrame.new(pane, :text=>'Pendulum Simulation') @lf2 = TkLabelFrame.new(pane, :text=>'Phase Space') # Create the canvas containing the graphical representation of the # simulated system. @c = TkCanvas.new(@lf1, :width=>320, :height=>200, :background=>'white', :borderwidth=>2, :relief=>:sunken) TkcText.new(@c, 5, 5, :anchor=>:nw, :text=>'Click to Adjust Bob Start Position') # Coordinates of these items don't matter; they will be set properly below @plate = TkcLine.new(@c, 0, 25, 320, 25, :width=>2, :fill=>'grey50') @rod = TkcLine.new(@c, 1, 1, 1, 1, :width=>3, :fill=>'black') @bob = TkcOval.new(@c, 1, 1, 2, 2, :width=>3, :fill=>'yellow', :outline=>'black') TkcOval.new(@c, 155, 20, 165, 30, :fill=>'grey50', :outline=>'') # pack @c.pack(:fill=>:both, :expand=>true) # Create the canvas containing the phase space graph; this consists of # a line that gets gradually paler as it ages, which is an extremely # effective visual trick. @k = TkCanvas.new(@lf2, :width=>320, :height=>200, :background=>'white', :borderwidth=>2, :relief=>:sunken) @y_axis = TkcLine.new(@k, 160, 200, 160, 0, :fill=>'grey75', :arrow=>:last) @x_axis = TkcLine.new(@k, 0, 100, 320, 100, :fill=>'grey75', :arrow=>:last) @graph = {} 90.step(0, -10){|i| # Coordinates of these items don't matter; # they will be set properly below @graph[i] = TkcLine.new(@k, 0, 0, 1, 1, :smooth=>true, :fill=>"grey#{i}") } # labels @label_theta = TkcText.new(@k, 0, 0, :anchor=>:ne, :text=>'q', :font=>'Symbol 8') @label_dtheta = TkcText.new(@k, 0, 0, :anchor=>:ne, :text=>'dq', :font=>'Symbol 8') # pack @k.pack(:fill=>:both, :expand=>true) # Initialize some variables @points = [] @theta = 45.0 @dTheta = 0.0 @length = 150 # init display showPendulum # animation loop @timer = TkTimer.new(15){ repeat } # binding @c.bindtags_unshift(btag = TkBindTag.new) btag.bind('Destroy'){ @timer.stop } btag.bind('1', proc{|x, y| @timer.stop; showPendulum(x, y)}, '%x %y') btag.bind('B1-Motion', proc{|x, y| showPendulum(x, y)}, '%x %y') btag.bind('ButtonRelease-1', proc{|x, y| showPendulum(x, y); @timer.start }, '%x %y') btag.bind('Configure', proc{|w| @plate.coords(0, 25, w, 25)}, '%w') @k.bind('Configure', proc{|h, w| @psh = h/2; @psw = w/2 @x_axis.coords(2, @psh, w-2, @psh) @y_axis.coords(@psw, h-2, @psw, 2) @label_theta.coords(@psw-4, 6) @label_dtheta.coords(w-6, @psh+4) }, '%h %w') # add to pane Tk.update pane.add(@lf1, @lf2) # must be called after setting 'Configure' binding # animation start @timer.start(500) end # This procedure makes the pendulum appear at the correct place on the # canvas. If the additional arguments x, y are passed instead of computing # the position of the pendulum from the length of the pendulum rod and its # angle, the length and angle are computed in reverse from the given # location (which is taken to be the centre of the pendulum bob.) def showPendulum(x=nil, y=nil) if x && y && (x != 160 || y != 25) @dTheta = 0.0 x2 = x - 160 y2 = y - 25 @length = Math.hypot(x2, y2) @theta = Math.atan2(x2,y2)*180/Math::PI else angle = @theta*Math::PI/180 x = 160 + @length*Math.sin(angle) y = 25 + @length*Math.cos(angle) end @rod.coords(160, 25, x, y) @bob.coords(x-15, y-15, x+15, y+15) end # Update the phase-space graph according to the current angle and the # rate at which the angle is changing (the first derivative with # respect to time.) def showPhase @points << @theta + @psw << -20*@dTheta + @psh if @points.length > 100 @points = @points[-100..-1] end (0...100).step(10){|i| first = - i last = 11 - i last = -1 if last >= 0 next if first > last lst = @points[first..last] @graph[i].coords(lst) if lst && lst.length >= 4 } end # This procedure is the "business" part of the simulation that does # simple numerical integration of the formula for a simple rotational # pendulum. def recomputeAngle scaling = 3000.0/@length/@length # To estimate the integration accurately, we really need to # compute the end-point of our time-step. But to do *that*, we # need to estimate the integration accurately! So we try this # technique, which is inaccurate, but better than doing it in a # single step. What we really want is bound up in the # differential equation: # .. - sin theta # theta + theta = ----------- # length # But my math skills are not good enough to solve this! # first estimate firstDDTheta = -Math.sin(@theta * Math::PI/180) * scaling midDTheta = @dTheta + firstDDTheta midTheta = @theta + (@dTheta + midDTheta)/2 # second estimate midDDTheta = -Math.sin(midTheta * Math::PI/180) * scaling midDTheta = @dTheta + (firstDDTheta + midDDTheta)/2 midTheta = @theta + (@dTheta + midDTheta)/2 # Now we do a double-estimate approach for getting the final value # first estimate midDDTheta = -Math.sin(midTheta * Math::PI/180) * scaling lastDTheta = midDTheta + midDDTheta lastTheta = midTheta + (midDTheta+ lastDTheta)/2 # second estimate lastDDTheta = -Math.sin(lastTheta * Math::PI/180) * scaling lastDTheta = midDTheta + (midDDTheta + lastDDTheta)/2 lastTheta = midTheta + (midDTheta + lastDTheta)/2 # Now put the values back in our globals @dTheta = lastDTheta @theta = lastTheta end # This method ties together the simulation engine and the graphical # display code that visualizes it. def repeat # Simulate recomputeAngle # Update the display showPendulum showPhase end end # Start the animation processing PendulumAnimationDemo.new(Tk.root) Tk.mainloop ======================================================================