首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

12.4  QtRuby

Qt是Trolltech创建并发布的一个GUI工具包。Qt旨在成为多平台工具包,为Windows、Mac和UNIX操作系统提供一致的编程接口。开发人员只需编写代码一次,就可以在这三种平台上编译,而不需要做任何修改。

Qt是以双重许可方式发布的——GPL或商业购买许可协议。其他公司也使用双重许可协议,如MySQL,这种发布方式允许开源项目使用工具包,以便从其提供的众多功能中受益。对于希望使用不像GPL那么严格的许可协议的客户,可以商业许可方式购买,从而给Trolltech带来收益。

12.4.1  概述

QtRuby绑定是很多人努力的结果,其中贡献最大的是Richard Dale。Ashley Winters、Germain Garand和David Faure负责完成生成绑定代码的后端(称为SMOKE)的大部分编写工作,还有很多人报告了错误和修正方案。

QtRuby绑定不仅提供丰富的GUI相关类,还提供程序员经常需要的一整套应用程序插件(如XML和SQL库),它支持整个Qt工具包。

在过去几年,QtRuby绑定主要围绕Qt工具包第3版。2005年年底发布了Qt第4版。现在有Qt3和Qt4两个版本的QtRuby绑定,但它们是不同的包。由于没有用于Windows平台Qt3开源包,因此本书只介绍Qt4的QtRuby绑定。然而,这里的代码和例子通常也使用Qt3版,这里的所有代码都可使用Qt4 QtRuby绑定在Windows、Linux和Mac平台上运行。

Qt的一个重要方面(因此也是QtRuby的重要方面)是信号和插槽(slot)的概念。信号是异步事件,在某些自发情况发生时触发,如按下鼠标或在文本框中输入文本。插槽是在特定信号发生时调用的响应方法。使用connect方法可将信号和插槽关联起来。

为使用信号和插槽以及QtRuby的众多其他特性,所有自定义类都使用Qt::Object类。另外,创建的GUI类必须继承基类Qt::Widget,后者继承了Qt::Object。

12.4.2  一个简单的窗口应用程序

QtRuby程序首先必须请求(require)Qt库。QtRuby通过Qt模块提供其功能,这意味着Qt类名都使用前缀Qt::。由于所有Qt类名都以字母Q打头,因此从Qt转换到QtRuby时删除了这个Q,例如,基于Qt的QWidget类在QtRuby中变成了Qt::Widget。

require 'Qt'

app = Qt::Application.new(ARGV)

str = Time.now.strftime("Today is %B %d, %Y")

label = Qt::Label.new(str)

label.show

app.exec

下面详细介绍上述代码。首先调用Qt::Application.new来启动基于Qt的应用程序,它初始化窗口系统,为程序员创建要使用的窗口部件做好准备。

然后创建了一个Qt::Label,这是一种向用户显示文本的简单方式。在这个例子中,文本被初始化为前一行创建的字符串。下一行将标签显示在屏幕上。

最后,调用app.exec开始应用程序事件循环。这个方法在应用程序终止时才返回,应用程序终止通常是由用户单击窗口的“关闭”导致的。

12.4.3  使用按钮

文本框:  
图12.7  Qt按钮
使用QtRuby创建按钮很简单,只需创建新的Qt::PushButton实例即可(见程序清单12.14和图12.7)。很可能希望在按钮被单击时执行某种操作,这是使用QtRuby信号和插槽处理的。

程序清单12.14  Qt按钮

require 'Qt'

class MyWidget < Qt::Widget

  slots 'buttonClickedSlot()'

  def initialize(parent = nil)

    super(parent)

    setWindowTitle("QtRuby example");

    @lineedit = Qt::LineEdit.new(self)

    @button = Qt::PushButton.new("All Caps!",self)

    connect(@button, SIGNAL('clicked()'),

              self, SLOT('buttonClickedSlot()'))

    box = Qt::HBoxLayout.new

    box.addWidget(Qt::Label.new("Text:"))

    box.addWidget(@lineedit)

    box.addWidget(@button)

    setLayout(box)

  end

  def buttonClickedSlot

    @lineedit.setText(@lineedit.text.upcase)

  end

end

app = Qt::Application.new(ARGV)

widget = MyWidget.new

widget.show

app.exec

在这个例子中,创建了一个名为MyWidget的窗口部件类,它继承了所有自定义窗口部件类都继承的Qt::Widget类。

在initializer方法前,创建了要在该类中定义的插槽列表。插槽是常规的Ruby类方法,但必须通过名称指定它们,让QtRuby运行环境知道程序员希望能够将其用做插槽。类方法slots接受一系列字符串作为参数,如下所示:

slots 'slot1()', 'slot2()'

该类的初始化方法有一个parent参数,几乎所有的Qt窗口部件类都接受这样的参数。parent参数指定哪个窗口部件将拥有要创建的窗口部件,将其设置为nil意味着它是顶级窗口部件,没有拥有者。“拥有”概念可能在C++中更有意义。在C++中,父窗口部件拥有子窗口部件,因此,当父窗口部件被删除时,其子窗口部件也将被删除。

MyWidget类创建了一个Qt::LineEdit,让用户能够输入文本,还创建一个Qt::PushButton,其显示的文本为All Caps!。注意,对于这两个窗口部件,将参数parent都设置为self。这意味着MyWidget实例被创建时,它将“收养”这两个窗口部件。

接下来使用了Qt工具包的一个关键部分:将信号和插槽关联起来。Qt::Pushbutton类定义了clicked信号,该信号在按钮被单击时发出。可将该信号同一个插槽关联起来,这里将其关联到常规方法buttonClickedSlot。插槽的名称无关紧要,有时使用后缀Slot强调这是插槽。

最后,创建了Qt::HBoxLayout类的一个实例。添加窗口部件时,这个类能够自动调整布局。

12.4.4  使用文本框

如程序清单12.14所示,QtRuby提供了Qt::LineEdit类用于简单的单行输入;而Qt::TextEdit类用于多行编辑。

程序清单12.15使用了一个多行编辑框。当内容发生变化时,文本的长度将显示在窗口底部的标签中,如图12.8所示。

程序清单12.15  简单的Qt编辑器

require 'Qt'

class MyTextWindow < Qt::Widget

  slots 'theTextChanged()'

  def initialize(parent = nil)

    super(parent)

    @textedit = Qt::TextEdit.new(self)

    @textedit.setWordWrapMode(Qt::TextOption::WordWrap)

    @textedit.setFont( Qt::Font.new("Times", 24) )

    @status = Qt::Label.new(self)

    box = Qt::VBoxLayout.new

    box.addWidget(@textedit)

    box.addWidget(@status)

    setLayout(box)

    @textedit.insertPlainText("This really is an editor")

    connect(@textedit, SIGNAL('textChanged()'),

              self, SLOT('theTextChanged()'))

  end

  def theTextChanged

    text = "Length: " + @textedit.toPlainText.length.to_s

    @status.setText(text)

  end

end

app = Qt::Application.new(ARGV)

widget = MyTextWindow.new

widget.setWindowTitle("QtRuby Text Editor")

widget.show

app.exec

图12.8  一个简单的Qt编辑器

和前面的按钮示例一样,创建了自定义的窗口部件。这里创建了Qt::TextEdit的一个实例和一个用于更新状态的Qt::Label。

首先要注意的是,@textedit的字体被设置为24点的Times字体。继承Qt::Widget的每个类(包括Qt::TextEdit)都有可读写的font属性。

接下来创建一个垂直方框布局(Qt::VBoxLayout)来存储子窗口部件,将一些文本插入到窗口部件@textedit中,然后将编辑器窗口部件的textChanged信号同自定义的TextChanged插槽关联起来。

在TextChanged插槽中,获取编辑器中的文本并查询其长度,然后更新@status标签以显示该长度。

注意,所有信号和插槽动作都是异步的。应用程序进行事件循环(app.exec)后,GUI事件循环将获得控制权,这就是信号和插槽如此重要的原因。程序员可以定义发生的动作(信号)及这些动作发生时要采取的措施(插槽)。

12.4.5  使用其他窗口部件

Qt还提供了很多其他的GUI通用窗口部件,如单选按钮、复选框和其他用于显示的窗口部件。程序清单12.16演示了其中的一些,图12.9是程序运行时的屏幕截图。

程序清单12.16  其他Qt窗口部件

require 'Qt'

class MyWindow < Qt::Widget

  slots 'somethingClicked(QAbstractButton *)'

  def initialize(parent = nil)

    super(parent)

    groupbox = Qt::GroupBox.new("Some Radio Button",self)

    radio1 = Qt::RadioButton.new("Radio Button 1", groupbox)

    radio2 = Qt::RadioButton.new("Radio Button 2", groupbox)

    check1 = Qt::CheckBox.new("Check Box 1", groupbox)

    vbox = Qt::VBoxLayout.new

    vbox.addWidget(radio1)

    vbox.addWidget(radio2)

    vbox.addWidget(check1)

    groupbox.setLayout(vbox)

    bg = Qt::ButtonGroup.new(self)

    bg.addButton(radio1)

    bg.addButton(radio2)

    bg.addButton(check1)

    connect(bg, SIGNAL('buttonClicked(QAbstractButton *)'),

    self, SLOT('somethingClicked(QAbstractButton *)') )

    @label = Qt::Label.new(self)

    vbox = Qt::VBoxLayout.new

    vbox.addWidget(groupbox)

    vbox.addWidget(@label)

    setLayout(vbox)

  end

  def somethingClicked(who)

    @label.setText("You clicked on a " + who.className)

  end

end

app = Qt::Application.new(ARGV)

widget = MyWindow.new

widget.show

app.exec

图12.9  其他Qt窗口部件

在这个新类中,首先创建了一个Qt::GroupBox,这是一个带边框和可选标题的分组框,可以存储其他窗口部件。然后创建两个Qt::RadioButton和一个Qt::CheckBox,并将分组框设置为它们的父窗口部件。

接下来创建了一个Qt::VBoxLayout,用于存储单选按钮和复选框,并将分组框设置为使用这种布局。

接下来很重要的一点是创建一个Qt::ButtonGroup,并将复选框和单选按钮添加到其中。Qt::ButtonGroup用于将按钮、复选框和单选按钮进行逻辑编组,对这些窗口部件的视觉布局没有影响,而是将它们进行逻辑组合以提供互斥等功能(单击某些按钮时取消选择其他一些按钮)。在这个例子中,将按钮组作为buttonClicked信号的源,该信号在组中的按钮被单击时发出。

该信号的发出方式与前面介绍的信号稍有不同,因为它还发出一个参数,这里为用户单击的对象。请注意其中的C++式语法,即使用参数QAbstractButton *。前面说过,Qt是一个C++工具包,因此,有些类型的参数不可避免地使用了C++表示法(但将来的版本可能解决这种问题)。

connect语句最终的结果是,每当按钮被单击时,该按钮都将作为参数传递给somethingClicked插槽。最后,创建了一个Qt::Label和一个Qt::VBoxLayout,并排列所有窗口部件。

在somethingClicked插槽的定义中,每当按钮被单击时都将修改标签的文本,这里显示的是导致信号发出和插槽被调用的对象的类名。

如果使用内置的窗口部件无法满足需求,Qt提供了功能强大的绘图系统供程序员创建自定义窗口部件。程序清单12.17是一个小示例,演示一些这样的特性。

程序清单12.17  自定义的TimerClock窗口部件

require 'Qt'

class TimerClock < Qt::Widget

    def initialize(parent = nil)

          super(parent)

          @timer = Qt::Timer.new(self)

          connect(@timer, SIGNAL('timeout()'), self, SLOT('update()'))

          @timer.start(25)

          setWindowTitle('Stop Watch')

          resize(200, 200)

    end

    def paintEvent(e)

    fastHand = Qt::Polygon.new([Qt::Point.new(7, 8),

                                      Qt::Point.new(-7, 8),

                                      Qt::Point.new(0, -80)])

    secondHand = Qt::Polygon.new([Qt::Point.new(7, 8),

                                        Qt::Point.new(-7, 8),

                                        Qt::Point.new(0, -65)])

    secondColor = Qt::Color.new(100, 0, 100)

    fastColor = Qt::Color.new(0, 150, 150, 150)

    side = [width, height].min

    time = Qt::Time.currentTime

    painter = Qt::Painter.new(self)

    painter.renderHint = Qt::Painter::Antialiasing

    painter.translate(width() / 2, height() / 2)

    painter.scale(side / 200.0, side / 200.0)

    painter.pen = Qt::NoPen

    painter.brush = Qt::Brush.new(secondColor)

    painter.save

    painter.rotate(6.0 * time.second)

    painter.drawConvexPolygon(secondHand)

    painter.restore

    painter.pen = secondColor

    (0...12).each do |i|

        painter.drawLine(88, 0, 96, 0)

        painter.rotate(30.0)

    end

    painter.pen = Qt::NoPen

    painter.brush = Qt::Brush.new(fastColor)

    painter.save

    painter.rotate(36.0 * (time.msec / 100.0) )

    painter.drawConvexPolygon(fastHand)

    painter.restore

    painter.pen = fastColor

    (0...60).each do |j|

        if (j % 5) != 0

          painter.drawLine(92, 0, 96, 0)

        end

        painter.rotate(6.0)

    end

    painter.end

  end

end

app = Qt::Application.new(ARGV)

wid = TimerClock.new

wid.show

app.exec

在这个例子中,也创建了一个自定义的窗口部件,这次名为TimerClock。在其初始化方法中,创建了Qt::Timer的一个实例,可以将其设置为定期地发出信号。这里将其timeout信号同TimerClock的update插槽管理起来。update插槽是内置的,导致窗口部件重绘。

通过调用start方法启动定时器,其参数指定每隔25毫秒就超时(并发出timeout信号),这意味着窗口部件的update插槽将每25毫秒执行一次。

接下来创建了paintEvent方法,它重写了Qt::Widget提供的同名方法。当窗口部件需要重绘时(这里是每当Qt::Timer过期时)将调用该方法。通过重写这个方法,可以定制窗口部件如何在屏幕上绘制自己。在这个方法中,代码负责处理窗口部件的原始绘画例程。

从现在开始,就全部是几何学的内容了。创建几个Qt::Polygon来表示要绘制的时钟的指针。注意,多边形的方向无关紧要,因为以后可以修改。

接下来设置要用到的一些属性。为时钟指针定义了两个Qt::Color。Qt::Color的初始化方法的参数是RGB值和可选的alpha透明度值。

由于要绘制的定时器放在正方形中,但窗口可能是矩形,这可能导致窗口部件的形状很怪异。这里使用side变量来存储窗口部件显示在屏幕上时较短一边。另外,还使用Qt::Time.currentTime来存储当前时间。

文本框:  
图12.10  TimerClock窗口部件
接下来,创建一个Qt::Painter,并使用它来开始执行绘图例程。绘画时使用消除锯齿功能使边缘看起来光滑些,还通过调用painter.translate(width/2, height/2)将Qt::Painter的起始坐标设置为绘图区域的中央。同时将Qt::Painter缩放为200:200引用范围,这意味着所有绘图命令都将在200:200单位内进行绘画。如果绘画区域变大或变小,将自动调整每个单位的长度。

接下来执行一系列绘图操作,在执行变换(如旋转)时,这些操作将放在painter.save和painter.restore之间。save将当前的painter属性存储在栈中,以方便恢复它们。

代码将时钟指针旋转到表示正确时间的角度,然后绘制它们;并沿钟面的外边缘以特定的间隔绘制一些刻度。

最后通知绘图器结束绘图(通过调用painter.end)。最后使用4行代码创建Qt::Application和定时器窗口部件,再开始事件循环。图12.10显示了最终的结果。

12.4.6  其他说明

Qt是一个C++工具包,由于该语言的限制,必须在该工具包中使用一些习惯用法。有时到Ruby的转换并非完全自然的,因为Ruby完成相同任务的方式可能略有不同。因此,在有些地方需要改变原来的方式,以便以Ruby方式执行QtRuby中的操作。

例如,对于Qt方法命名约定“首字母大写”,可改为使用下划线。下面两种写法是等价的:

Qt::Widget::minimumSizeHint

Qt::Widget::minimum_size_hint

在Qt中,所有setter的名称都以set打头,如Qt::Widget::setMinimumSize。在Ruby中可以删除set,并使用赋值符。这就意味着下列三条语句是等价的:

widget.setMinimumSize(50)

widget.minimumSize = 50     # same

widget.minimum_size = 50    # same

类似地,Qt中的一些Boolean方法以is或has开头,如Qt::Widget::isVisible。QtRuby再一次提供了一种更具Ruby风格的方式来调用此方法:

a.isVisible

a.visible?         # same

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论