我曾在早期的博文中介绍过IronRuby。在文章中,我扩展了IronRuby的基础知识,来解释需要在Rail应用程序所做的额外工作,好让大家继续深入.NET所实现Ruby语言,但这方面的内容并不够。所以现在我想深入地谈谈IronRuby与项目的兼容性,以便开发全新的应用程序来说明IronRuby和.NET之间的互操作性。实际上,我们会使用WPF(Windows Presentation Foundation),它是.NET Framework的组件,我们可以用它创建富媒体和图形界面。
再次申明,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之上运行动态语言(公共语言运行时需要动态语言运行时来支持动态语言)。这对于我们来说是完全抽象的,仅用于创建软件。
注:使用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是微软用于定义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"
我们提到兼容性、互操作性却忽略了可扩展性。我已经清楚解释了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来创建数据和信息图的视觉描述是多么的振奋人心。尽管在这个项目中,我们仅展现了一些简单的信息——时间——但潜在的可能性是巨大的。