【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

用 IronRuby 创建 WPF 应用程序

  • 2010-09-21
  • 本文字数:6385 字

    阅读完需:约 21 分钟

我曾在早期的博文中介绍过 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 之上运行动态语言(公共语言运行时需要动态语言运行时来支持动态语言)。这对于我们来说是完全抽象的,仅用于创建软件

注:使用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 来创建数据和信息图的视觉描述是多么的振奋人心。尽管在这个项目中,我们仅展现了一些简单的信息——时间——但潜在的可能性是巨大的。


感谢侯伯薇对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2010-09-21 00:002194
用户头像

发布了 87 篇内容, 共 20.3 次阅读, 收获喜欢 1 次。

关注

评论

发布
暂无评论
发现更多内容

以太坊L2跨链明星——Arbitrum 为开发者带来区块链入门方案

TinTinLand

区块链

这些年,使用缓存踩过的坑

鲸品堂

缓存

时间轮算法

领创集团Advance Intelligence Group

算法 时间轮算法

毕设不会做,怎么办?

图灵教育

机器学习 深度学习 毕设

清晰明了!人人都能懂的Python自动发送邮件实战教程

Python全栈库

Python 编程 程序员 面试 全栈开发

大咖说*图书分享-Node布道师狼叔|三卷书详解Node.js

大咖说

前端 后端 代码

“超级计算机”——GPU云服务器

Finovy Cloud

gpu GPU服务器 GPU算力

摆平各类目标检测识别AI应用,有它就够了!

华为云开发者联盟

计算机视觉 CANN 昇腾 目标检测识别 YoloV3

Hacker 资讯 | 5 月上旬区块链黑客松活动汇总

TinTinLand

区块链

邀您填写调研问卷 | 2022中国 AIOps 现状调查全面启动!

博睿数据

AIOPS 博睿数据

netty系列之:HashedWheelTimer一种定时器的高效实现

程序那些事

Java Netty 程序那些事 5月月更

vuejs中的普通方法/计算属性computed与监听属性watch四者的比较

itclanCoder

JavaScript Vue 前端开发

JAVA OOM异常可观测最佳实践

观测云

可观测性 可观测

国内首个开源物联网边缘工业协议网关软件,Neuron v2.0产品解读

EMQ映云科技

开源 物联网 IoT 5月月更 neuron

讨论两种Redis中Token的存储方式

华为云开发者联盟

Token key 存储token userid

HTTP请求转发那些事:你可能不知道的Hop-by-hop Headers和End-to-end Headers

华为云开发者联盟

HTTP 请求转发 web 容器 F5

柏拉图会成为元宇宙风险标吗?PlatoFarm的机会很大

西柚子

批量作业调度引擎 TASKCTL 安装与实例部署

TASKCTL

程序员 DevOps 分布式 ETL 自动化运维

【刷题第十天】21. 合并两个有序链表

白日梦

5月月更

等保和分保的区别是什么?哪个更厉害?

行云管家

网络安全 等保 等级保护 分保

【LeetCode】后继者Java题解

Albert

LeetCode 5月月更

第三代区块链DFINITY布局DeFi,开发者如何抓住机遇

TinTinLand

区块链

招募 | 加入DFINITY 进阶开发,人人都是下一个“张一鸣”

TinTinLand

区块链

KeyDB重量发布6.3.0开源版

华为云开发者联盟

redis 开源 多线程 分布式缓存 KeyDB

vuejs中的默认插槽-具名插槽-作用域插槽三者的比较

itclanCoder

JavaScript Vue 前端开发

vuejs中的mixin混入-局部混入/全局混入

itclanCoder

Vue 前端开发

使用APICloud AVM多端框架开发企业移动OA办公的项目实践

YonBuilder低代码开发平台

企业应用 APP开发 APICloud 多端开发 avm.js

小型企业需要CRM系统的理由

低代码小观

CRM 企业管理系统 中小企业 CRM系统 客户关系管理系统

开源生态在中国:播撒种子,待成雨林

科技热闻

《安全大讲堂》 第十四期|不破不立:软件供应链的威胁与方案

腾讯安全云鼎实验室

供应链 安全大讲堂

云计算的云是指什么?最简单的解释是什么?

行云管家

云计算 云服务 私有云 混合云

用IronRuby创建WPF应用程序_.NET_Edd Morgan_InfoQ精选文章