我曾在早期的博文中介紹過IronRuby。在文章中,我擴展了IronRuby的基礎 知識,來解釋需要在Rail應用程序所做的額外工作,好讓大家繼續深入.NET所實 現Ruby語言,但這方面的內容並不夠。所以現在我想深入地談談IronRuby與項目 的兼容性,以便開發全新的應用程序來說明IronRuby和.NET之間的互操作性。實 際上,我們會使用WPF(Windows Presentation Foundation),它是.NET Framework的組件,我們可以用它創建富媒體和圖形界面。
WPF基礎
再次申明,WPF是.NET Framework組件之一,負責呈現富用戶界面和其他媒體 。它不是.NET Framework中唯一可完成該功能的函數庫集,Window Form也可以 完成類似工作,在我們需要創建炫目效果的時候,WPF會顯得十分有用。無論是 演示文檔、視頻、數據錄入表格、某些類型的數據可視化(這是我最希望做的, 尤其用IronRuby完成,後面的故事更精彩)抑或用動畫把以上的都串聯起來,你 很可能會發現在給Windows開發這些應用程序的時候 WPF可以滿足你的需求。
舉例說明。某一天午飯時間,我創建了基於WPF的類似於時鐘的應用程序—— 我喜歡參考WPF的“Hello,Wold”應用程序——於是決定使用IronRuby。
注:學習本示例的過程中,需要參考WPF文檔。
示例程序
require 'WindowsBase'
require 'PresentationFramework'
require 'PresentationCore'
require 'System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
class Clock
CLOCK_WIDTH = 150
CLOCK_HEIGHT = 150
LABEL_HEIGHT = CLOCK_HEIGHT / 7
LABEL_WIDTH = CLOCK_WIDTH / 7
RADIUS = CLOCK_WIDTH / 2
RADS = Math::PI / 180
MIN_LOCATIONS = {}
HOUR_LOCATIONS = {}
def run!
plot_locations
# build our window
@window = System::Windows::Window.new
@window.background = System::Windows::Media::Brushes.LightGray
@window.width = CLOCK_WIDTH * 2
@window.height = CLOCK_HEIGHT * 2
@window.resize_mode = System::Windows::ResizeMode.NoResize
@canvas = System::Windows::Controls::Canvas.new
@canvas.width = CLOCK_WIDTH
@canvas.height = CLOCK_HEIGHT
# create shapes to represent clock hands
@minute_hand = System::Windows::Shapes::Line.new
@minute_hand.stroke = System::Windows::Media::Brushes.Black
@minute_hand.stroke_thickness = 1
@minute_hand.x1 = CLOCK_WIDTH / 2
@minute_hand.y1 = CLOCK_HEIGHT / 2
@hour_hand = System::Windows::Shapes::Line.new
@hour_hand.stroke = System::Windows::Media::Brushes.Black
@hour_hand.stroke_thickness = 3
@hour_hand.x1 = CLOCK_WIDTH / 2
@hour_hand.y1 = CLOCK_HEIGHT / 2
# .. and stick them to our canvas
@canvas.children.add(@minute_hand)
@canvas.children.add(@hour_hand)
plot_face # draw a clock face
plot_labels # draw clock numbers
plot_hands # draw minute / hour hands
@window.content = @canvas
app = System::Windows::Application.new
app.run(@window)
# the Application object handles the lifecycle of our app
# including the execution loop
end
# determine 2 sets of equidistant points around the circumference of a circle
# of CLOCK_WIDTH and CLOCK_HEIGHT dimensions.
def plot_locations
for i in (0..60) # 60 minutes, and 12 hours
a = i * 6
x = (RADIUS * Math.sin(a * RADS)).to_i + (CLOCK_WIDTH / 2)
y = (CLOCK_HEIGHT / 2) - (RADIUS * Math.cos(a * RADS)).to_i
coords = [x, y]
HOUR_LOCATIONS[i / 5] = coords if i % 5 == 0 # is this also an 'hour' location (ie. every 5 minutes)?
MIN_LOCATIONS[i] = coords
end
end
# draws a circle to represent the clock's face
def plot_face
extra_x = (CLOCK_WIDTH * 0.15) # pad our circle a little
extra_y = (CLOCK_HEIGHT * 0.15)
face = System::Windows::Shapes::Ellipse.new
face.fill = System::Windows::Media::Brushes.White
face.width = CLOCK_WIDTH + extra_x
face.height = CLOCK_HEIGHT + extra_y
face.margin = System::Windows::Thickness.new(0 - (extra_x/2), 0 - (extra_y/2), 0, 0)
face.stroke = System::Windows::Media::Brushes.Gray # give it a slight border
face.stroke_thickness = 1
System::Windows::Controls::Canvas.set_z_index(face, -1) # send our circle to the back
@canvas.children.add(face) # add the clock face to our canvas
end
# at each point along the hour locations, put a number
def plot_labels
HOUR_LOCATIONS.each_pair do |p, coords|
unless p == 0
lbl = System::Windows::Controls::Label.new
lbl.horizontal_content_alignment = System::Windows::HorizontalAlignment.Center
lbl.width = LABEL_WIDTH
lbl.height = LABEL_HEIGHT
lbl.content = p.to_s
lbl.margin = System::Windows::Thickness.new (coords[0] - (LABEL_WIDTH / 2), coords[1] - (LABEL_HEIGHT / 2), 0, 0)
lbl.padding = System::Windows::Thickness.new(0, 0, 0, 0)
@canvas.children.add(lbl)
end
end
end
def plot_hands
time = Time.now
hours = time.hour
minutes = time.min
if !@minutes || minutes != @minutes
@hours = hours >= 12 ? hours - 12 : hours
@minutes = minutes == 0 ? 60 : minutes
# Dispatcher.BeginInvoke() is asynchronous, though it probably doesn't matter too much here
@minute_hand.dispatcher.begin_invoke (System::Windows::Threading::DispatcherPriority.Render, System::Action.new {
@minute_hand.x2 = MIN_LOCATIONS[@minutes][0]
@minute_hand.y2 = MIN_LOCATIONS[@minutes][1]
@hour_hand.x2 = HOUR_LOCATIONS[@hours][0]
@hour_hand.y2 = HOUR_LOCATIONS[@hours][1]
})
end
end
end
clock = Clock.new
timer = System::Timers::Timer.new
timer.interval = 1000
timer.elapsed { clock.plot_hands }
timer.enabled = true
clock.run!
查看GitHub站點的實例效果
世上沒有完美的事物,但我認為本實例使用數據可視化來說明IronRuby與WPF 間的互操作。我相信你會細心研究以上代碼,但我仍要逐步解析它的關鍵之處。 (順便提一下,通過ir來運行本實例可第一時間看到效果)。
現在,我們使用的是IronRuby,並非我之前提到的那樣純使用Ruby代碼並用 ir(IronRuby解析器)運行代碼來以證明它的兼容性。本文的主旨在於說明.NET 命名空間和Ruby模塊,.NET類和Ruby類之間的明顯相似性。在這方面我覺得無需 多說,你也許已經能夠熟練地應用Ruby 的繪圖函數。
以上例子中,我們實例化.NET對象,但使用的是標准的Ruby對象的.new方法 ,即Object#new。我們調用這些對象(和類)的方法(例如,對 System.Windows.Controls.Canvas.SetZIndex()調用)可為Ruby語言建立相應的 小寫規則。無縫集成讓我們可在.NET CLR之上運行動態語言(公共語言運行時需 要動態語言運行時來支持動態語言)。這對於我們來說是完全抽象的,僅用於創 建軟件。---www.bianceng.cn
注:使用IronRuby的時候,.NET堆棧確實在各級別上集成。有一個地方要注 意的是所有的IronRuby對象並非真正意義上的Object而是System.Object。
事件
事件是開發.NET客戶端應用程序的重要一環,在其它開發環境下也同樣如此 。萬一你沒有注意到這一點,事件驅動編程實質上也需要在不可預知的情況下調 用方法或者其它代碼塊(比如:委托)。你永遠無法預測用戶什麼時候點擊按鈕 ,敲擊按鍵或者執行任何輸入,所以事件驅動編程必須處理GUI事件。
我最喜歡Ruby語言的原因之一就在於它的“blocks”確實能夠幫助我們。例 如在傳統的C#語言中,你需要通過以下一種或兩種方式來訂閱事件(即在事件發 生時執行所分配的代碼塊):把引用傳遞給指定的方法,或者提供匿名代碼塊。 你正好可以看到Ruby中的類似概念“block”“Proc”和 “lambda”。最後在相 對簡單的代碼中說明這些概念,我們會使用.NET的System.Timers.Timer來嘗試 每秒鐘更新該時鐘(我知道這並非最佳做法,僅用於示范)
注:和我之前說的稍有不同,時鐘的運行是可預期的,然而我們仍使用Timer 事件進行更新,這是在主線程之外完成任務的眾多方式的一種。
接下來,你會看到為處理事件所需編寫的代碼僅是向CLR提供處理事件的函數 名。這種方式的缺點在於它對每個事件僅允許委托一個代碼塊。我們需要使用 add方法讓該事件訂閱多個處理程序,即把處理函數放到隊列的末端。如下所示 :
def tick
puts "tick tock"
end
timer.elapsed.add method(:tick)
timer.elapsed.add proc { puts "tick tock" }
tick_handler = lambda { puts "tick tock" }
timer.elapsed.add(tick_handler)
創建代碼塊作為事件處理程序的能力使得IronRuby向優秀的動態語言又邁進 了一步。小寫規范減少了樣板代碼的數量。當然,匿名方法在其它傳統的.NET語 言——像C#和VB——中也可用,但是在IronRuby則讓人感覺更加優雅和自然。
注:無論方法是已命名還是匿名,處理事件的委托代碼都可以接收參數,一 般來說,參數會包括一個sender 對象和一些args。
XAML和IronRuby
XAML是微軟用於定義CLR對象及其屬性的類XML語言,主要在WPF和 Silverlight應用程序中使用。有了它,我們可以用描述的方式來創建整個UI, 在程序性代碼中關聯事件並在運行時綁定數據、創建圖形、甚至為那些圖形創建 具有故事情節的動畫。我不准備深入探討XAML的架構,如果你有任何使用基於 XML語言的經驗的話,你就會了解其中發生的事情。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<Rectangle x:Name="mySquare" Width="50" Height="50">
<Rectangle.Fill>
<SolidColorBrush Color="Green" />
</Rectangle.Fill>
</Rectangle>
<TextBlock Text="Hello, world">
<TextBlock.Foreground>
<SolidColorBrush Color="Red" />
</TextBlock.Foreground>
</TextBlock>
</StackPanel>
</Window>
注:Window、StackPanel、TextBlock、SolidColorBrush和Rectangle 都是 WPF類。XAML代碼可以輕松地用C#、VB或者IronRuby編程實現。
以上代碼會顯示一個中等尺寸的獨立窗體。該窗體中有StackPanel對象,它 是WPF控件,用於定義其子控件采取流布局樣式。在 StackPanel中有兩個不同對 象:一個文本框和一個矩形。在XAML定義的對象皆可被命名以供後續引用亦可匿 名(我們的Rectangle對象就命名為mySquare,盡管TextBlock未被命名)。這些 對象的屬性可以通過兩種方式進行賦值:利用XML元素屬性(例如:Width="50" ),或者所期望的值非初級類型它們的子元素(例如:預期 <Rectangle.Fill> 為Brush或者派生自Brush)。
不要陷入WPF和XAML的謎團當中,因為任何人都可以輕松地編寫大量代碼,讓 我們用IronRuby運行這些代碼。
require 'PresentationFramework'
require 'PresentationCore'
@window = System::Windows::Markup::XamlReader.parse(File.open ('my_xaml.xaml', 'r').read)
System::Windows::Application.new.run(@window)
WPF方法Application.Run需要Window作為其中一個參數。如果我們回頭看之 前的XAML代碼,就會發現根元素其實就是 Window,那也是語法分析後所返回的 對象。所有在XAML中定義的控件都會作為反射XAML文檔結構的控件樹返回, Window是根元素,StackPanel作為Window的唯一子元素,Rectangle和TextBlock 則作為StackPanel的子元素等等。我們可以通過以下方式添加控件:
@window.find_name("mySquare").class # => "System::Windows::Shapes::Rectangle"
關於CLR類繼承的解析
我們提到兼容性、互操作性卻忽略了可擴展性。我已經清楚解釋了IronRuby 與.NET間如何無縫繼承,甚至你可以用繼承來擴展CLR類。以下是一個示例,讓 我們再來看一看之前寫的文章中用C#創建的Person類。
namespace MyClassLibrary
{
public class Person
{
public string Name { get; set; }
public string Introduce()
{
return String.Format("Hi, I'm {0}", Name);
}
}
}
讓我們用Ruby來擴展它,並借此培養程序員的思維習慣。
require 'MyClassLibrary.dll'
class Programmer < MyClassLibrary::Person
ACCEPTABLE_DRINKS = [:coffee, :tea, :cola, :red_bull]
def drink(liquid)
if ACCEPTABLE_DRINKS.include? liquid
puts "Mmm... #{name} likes code juice!"
else
raise "Need caffeine!"
end
end
end
me = Programmer.new
me.name = "Edd"
puts me.introduce
me.drink(:coffee)
關於代碼冗長
老實說,我不介意使用繁瑣的代碼引用,只要它不影響程序的性能即可,就 像在之前的代碼顯示的那樣。我喜歡簡潔的代碼,冗長的尋址和對象描述的簡化 會產生某種安全感,盡管這和本句形成了鮮明的對比。然而,在使用IronRuby的 時候,我已厭倦輸入 System::Whatever::Something。不管使用何種語言,總有 一些開發人員喜歡設定命名空間並忘掉它們。不用擔心,IronRuby 也有這種人 。
由於.NET命名空間在IronRuby中是模塊,所以在調用include後,完全可以把 .NET命名空間引入IronRuby代碼,就像要引入一個Ruby組織模塊一樣。
class Clock
include System::Windows::Shapes
include System::Windows::Media
include System::Windows::Threading
# and so on...
這樣做可以減少調用 System::Windows::Shapes::Ellipse.new,代之以 Ellipse.new,或通過 System::Windows::Threading::DispatcherPriority.Render引用 DispatcherPriority.Render。
在.NET Framework中,另一個簡化IronRuby代碼以及處理這些冗長代碼的方 法就是通過給命名空間取別名來完成。
require 'System.Windows.Forms'
WinForms = System::Windows::Forms
WinForms::Form.new
WinForms::Label.new
結論
到此為止,我希望你能更好的了解IronRuby與.NET間的互操作,以及如何利 用.NET Framework的動態屬性和Ruby的優雅語法。
Ruby的風格和用法讓數據處理變成一種樂趣,當然,在IronRuby也一樣,它 結合了 WPF生成圖像的功能。我希望大家能具體看到使用這兩種技術進行數據可 視化的可能性。使用IronRuby來創建數據和信息圖的視覺描述是多麼的振奮人心 。盡管在這個項目中,我們僅展現了一些簡單的信息——時間——但潛在的可能 性是巨大的。