Qt in Scala(JVM)开发梗概

曾建凯 发布于 2010/08/31 08:03
阅读 3K+
收藏 7

前言

一直有人问Qt的开发情况,希望有个感性的认识。一直也有整理这方面资料的冲动,但时间也很紧迫,所以长话短说。目前将目标锁定Qt Jambi版本吧,熟悉了Qt以后,我对其C++版本的兴趣也十分浓厚,所以,将来再慢慢整理吧。

本篇文章涉及的开发环境如下:windows xp sp2,JDK6u21,Scala 2.8.0 final,Qt Jambi LGPL 4.5.2_01,IDE选NetBeans吧!

列出Qt的文档中心的几个重要资源的链接入口:

Qt Jambi 4.5.2_01的文档索引
API Javadoc
CSS样式说明(内容彼此交叉,十分详尽,你只需要有一点点的CSS基础就能明白)
官方实例
Qt Jambi 4.5.2_01Binary for Windows 32-bit(点击将自动下载)

关于Qt的点滴,我会以注释的形式写在代码里面,毕竟都是写代码的,对注释会比较敏感。

Hello Qt in Scala

package qt.demo

import com.trolltech.qt.gui._

object HelloQt {

  def main(args: Array[String]): Unit = {
    QApplication.initialize(args)
    (new QLabel("Hello Qt")).show
    QApplication.exec
  }
}

可能上述代码还能再度简化一下,比如去掉new QLabel两边的括号(但可能会很怪异了)。这个很简单吧,输出结果如下图:

QApplication是Qt的一个全局单例类,就把他看作是一个总控制中心吧。他是一个static类,通过调用QApplication.instance()方法,可获得当前运行过程中的app实例。

QApplication.instance是一个全局控制实例,这里所定义的内容(可以定义的东西,详细请看手册),除非在实例具体某个对象时有具体设置,否则全局都按照instance的设置进行。当然,其实多数时候,我会用他来控制全局的样式定义。

好吧,上述的例子实在简单的有些恶心了,我们来些实际一点的东东:

package qt.demo

import com.trolltech.qt.gui._
import com.trolltech.qt.core.Qt._

object CustomWindow {

  val globalStyle = """
* { font-family: Mircosoft Yahei; font-size: 12px; color: #333; }
#mainWindow { border: 40px solid #ccc; border-image: url(classpath:qt/demo/resource/window.png) 40 stretch; }
"""

  def main(args: Array[String]): Unit = {
    QApplication.initialize(args)
    QApplication.instance.setStyleSheet(globalStyle)
    val frame = new QFrame() {
      this.setObjectName("mainWindow")
      // 以下为窗体展现定制,应该在show之前调用
      // show以后再调用,会令窗体crash,你需要再次show
      this.setWindowFlags(WindowType.FramelessWindowHint)
      this.setAttribute(WidgetAttribute.WA_TranslucentBackground, true)
      // 由于设定了不使用windows窗体,所以,请手动结束多余的进程
    }
    frame.show()
    QApplication.exec
  }
}

截图效果如下:

怎么样,开始有点意思了吧?30行代码连样式,其实想做漂亮的界面,也不是那么难吧!

安装Qt Jambi

回到最初点,首先还是要把环境搭建起来。先去上面的地址下载Qt Jambi 4.5.2_01Binary for Windows 32-bit,随便解压吧。解开目录,里面有几个值得一看的东西:

 

qtjambi.exe 这个仅仅是运行一个Demo示例,看看吧,里面很多东西都会给你带来不错的启发。但不得不说,Qt原版的Demo,那叫一个炫啊,Java真受冷落。

designer.bat 这个是打开设计器的,实际设计器在这个目录下的bin目录里面。

qtjambi-4.5.2_01.jar

qtjambi-win32-msvc2005-4.5.2_01.jar 这两个jar包是你在实际开发中需要使用的,你需要将这两个库引入到你的项目中。并且从文件名我们可以发现,他是使用vc2005(vc80),如果你没安装vs2005补丁,快去装一个吧。

好了,准备功夫就这么点,我们可以开始进一步的工作了!

填充基础界面

好了,我们该开始往这样一个界面里面加东西了,首先,他要有个标题栏,中间是他的视图展示部分,当然了,我们还可以加一个底部的状态条。实际上,使用设计器和标准的QMainWidget,我们能做得很好。但一方面我是代码控,而另一方面,我觉得通过代码,能展示更多感性方面的东西(透过Qt Jambi的Eclipse的插件,一个界面设计完成,它会自动帮你转换为一个Java的类,所以,你无需过分担心后续实际开发的复杂度。)。而且说实在的,他的设计器和Vs比,就差很多很多了。

既然有那么多想法,我们可以考虑给它添加一个Layout,哦,忘记说了,Qt遵循较为严格的对象机制,所有界面构造元素都继承自QWidget,而Layout,则继承自QLayout,而QLayout和QWidget则都来自QObject,当然,细分还有很多,但Layout和QWidget毕竟是大头。

刚才展示的两个例子中,QFrame和QLabel都继承自QWidget,所以你可以对他们对Show,而不用考虑窗口,父容器,你对哪个QWidget的实例调用了show方法,它就会是一个独立窗体,逻辑十分清晰。

Layout有两种声明方式(当然,new实例只有一种方式),所谓声明是指和QWidget产生关联。new QHBoxLayout(anyQWidget),或者anyQWidget.setLayout(anyQLayout)。一个QWidget实例不能进行(关联)两次以上布局,当你第二次对它进行布局关联时,系统会给你一个警告,但不会令程序或者窗体Crash。

package qt.demo

import com.trolltech.qt.gui._
import com.trolltech.qt.core.Qt._

object CustomWindow {

  //=======================// 全局样式 //=======================//

  val globalStyle = """
* { font-family: Microsoft Yahei; font-size: 12px; color: #333; }
#mainWindow { border: 32px solid #ccc; border-image: url(classpath:qt/demo/resource/window.png) 32 stretch; }
#title { font-size: 13px; font-weight: bold; color: #000; }
#body { border: 1px solid #ccc; }
"""

  //=======================// 主窗体 //=======================//
  //
  // 我们将frame变量转移到这里
  // lazy关键字,表示调用时才实现该变量值
  lazy val frame = new QFrame() {
    this.setObjectName("mainWindow")
    // 以下为窗体展现定制,应该在show之前调用
    // show以后再调用,会令窗体crash,你需要再次show
    this.setWindowFlags(WindowType.FramelessWindowHint)
    this.setAttribute(WidgetAttribute.WA_TranslucentBackground, true)
    // 由于设定了不使用windows窗体,所以,请手动结束多余的进程
  }

  // 为了进一步分工明确,我又把layout也挪出来了
  // 这里采用声明关联
  // 一个layout会默认撑满整个容器
  lazy val frameLayout = new QVBoxLayout(frame) {
    this.setMargin(0)  // Layout四边的margin,当然你也可以用setContentsMargin来设定四边的具体值
    this.setSpacing(0) // 设定元件之间的间距
  }

  //=======================// 标题栏 //=======================//
  //
  // 要构造界面,未必需要全部都是widget
  // layout也可以插入到layout中
  lazy val titleLayout = {
    val _layout = new QHBoxLayout // 这是一个内部变量,不在类变量中
    // 让他垂直居中,左对齐
    _layout.setSpacing(5)
    _layout.setAlignment(new Alignment(AlignmentFlag.AlignLeft, AlignmentFlag.AlignVCenter))
    _layout.addWidget(titleIcon)
    _layout.addWidget(titleText)
    _layout // 这里等同于这个block return _layout
  }

  // 标题的元素构成
  lazy val titleIcon = new QLabel {
    this.setPixmap(getIcon.pixmap(18))
    // 指定他的具体宽高
    this.setFixedSize(20, 20)
    // 让该容器按照该宽高占位
    this.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
  }
  lazy val titleText = new QLabel("这是一个标题栏") {
    this.setObjectName("title")
    // 只指定具体高度
    this.setFixedHeight(20)
    // 他的水平方向会100%的撑开,垂直方向按指定的高度占位
    this.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
  }

  //=======================// body部分 //=======================//

  lazy val body = {
    val widget = new QWidget
    widget.setObjectName("body")
    widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
    widget
  }

  //=======================// foot部分 //=======================//

  lazy val foot = new QLabel("状态栏") {
    this.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
    this.setAlignment(new Alignment(AlignmentFlag.AlignRight, AlignmentFlag.AlignBottom))
  }

  //=======================// 快捷函数 //=======================//

  def app = QApplication.instance

  def getIcon = frame.style.standardIcon(QStyle.StandardPixmap.SP_MessageBoxInformation)

  def main(args: Array[String]): Unit = {
    QApplication.initialize(args)
    app.setStyleSheet(globalStyle)
    frameLayout.addLayout(titleLayout)
    frameLayout.addWidget(body)
    frameLayout.addWidget(foot)
    frame.resize(370, 270) // 这里要计算样式中border-width的宽度,实际上我们期望这个窗体的内容大小在300, 200
    frame.show()
    QApplication.exec
  }
}

效果截图:

从30行激增到100行,可能需要点耐性去读。但整个窗体的初步效果已经看出来了,而且,很显然,类似这样的窗体,我们完全可以将他封装成一个单独的类。当然模拟一个完整的窗口,也许需要更多更多的代码,但通过良性的封装,能很好的解决代码冗余的问题。

Qt的信号槽和事件

信号槽,是Qt中一个有趣的设定,这个也体现了Qt在Ui方面的一种经验的积累。

首先给出最最简单的信号槽的代码样例。

class ClickEvent(widget: QWidget) {
	
  def doClicked(checked: Int) {
    // 若干操作
  }
}

val btn = new QPushButton()
btn.clicked.connect(anyInstance, "doClicked(int)")

好了,当你点击了btn以后,他会自动执行doClicked方法。也许你会说,切,这有什么了不起的呢?如果要细说,恐怕不是一时半会能说的清楚的,大家也许还焦急着进一步完善刚才做出来的窗口。OK,我长话短说。

在Qt所有类的上层,Event接口,是由QObject去定义的,而QWidget实际上是继承自QObject的。QObject::installEventFilter(QObject),实际上是注册事件的总入口。当然,在你每次实例一个QObject的时候,他已经默认的帮你为当前实例installEventFilter。这个方法实际上就是指派给具体哪个实例作为该对象的事件观察者。

从installEventFileter以后,事件首先经过evnetFilter进行分发,event应该是同级的事件分发。在这两个方法里,需要指定返回Boolean类型,实际上是对事件的拦截。

再其次,到各种种类繁多,因应具体类而产生的event入口,比如showEvent等等。在这个层面,事件经由上层的分发,已经不会再等待你返回结果(他是返回无类型的),虽然对于QEvent实例,有accept或者igroe,但实际上这个过程里,你已无法保证绝对控制事件的整体了,你只能控制经由上层分到你这一级以后的事情,可是上层做了什么,你不知道,也管不了。

在这个事件机制的基础之下,信号槽似乎是处于整个事件机制的最低端,但信号槽机制又有其主要特点。他是单向的,不管理该实例的全局状态,他只关心他最关心(当然也是指派给他)的对象,这个(或若干个)对象往往是整个事件机制中最关键的一个状态,他提供给你一个既不用单独注册一个QObject以从头接管实例的全部事件,也不用利用override的方式重载事件声明,即可利用信号槽,去做其他关联操作的触发。而无论他有或者无,不会对实例本身的状态进行改变,所以他是一个松耦合、无歧义的接口。

而且,Qt提供给你自己扩展信号槽的机会,除了常规的Qt类可获得信号槽以外,在你自定义的类中,也可以通过继承自QSignalEmitter,来获得信号槽机制的使用。

好了废话一大堆,让我们继续干活,这次,我们添加一个关闭按钮,并且,让这个窗口可以被自由的拖动,而这也将是最后将要完成的工作:

package qt.demo

import com.trolltech.qt.gui._
import com.trolltech.qt.core._
import com.trolltech.qt.core.Qt._

object CustomWindow {

  //=======================// 全局样式 //=======================//

  val globalStyle = """
* { font-family: Microsoft Yahei; font-size: 12px; color: #333; }
#mainWindow { border: 32px solid #ccc; border-image: url(classpath:qt/demo/resource/window.png) 32 stretch; }
#title { font-size: 13px; font-weight: bold; color: #000; }
#body { border: 1px solid #ccc; }
#closeBtn { border: 0; background: none; font-weight: bold; color: red; }
"""

  //=======================// 主窗体 //=======================//
  // 其实我们可以将这个Frame作为一个单独类封装一下
  class DemoFrame extends QFrame {

    this.setObjectName("mainWindow")
    // 以下为窗体展现定制,应该在show之前调用
    // show以后再调用,会令窗体crash,你需要再次show
    this.setWindowFlags(WindowType.FramelessWindowHint)
    this.setAttribute(WidgetAttribute.WA_TranslucentBackground, true)

    val frameLayout = new QVBoxLayout(this) {
      this.setMargin(0)  // Layout四边的margin,当然你也可以用setContentsMargin来设定四边的具体值
      this.setSpacing(0) // 设定元件之间的间距
    }

    private def frame = this

    //=======================// 标题栏 //=======================//

    val titleLayout = new QHBoxLayout {
      setSpacing(5)
      setAlignment(new Alignment(AlignmentFlag.AlignLeft, AlignmentFlag.AlignVCenter))
    }

    // 标题的元素构成
    val titleIcon = new QLabel {
      this.setPixmap(getIcon.pixmap(18))
      // 指定他的具体宽高
      this.setFixedSize(20, 20)
      // 让该容器按照该宽高占位
      this.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
    }
    val titleText = new QLabel("这是一个标题栏") {
      this.setObjectName("title")
      // 只指定具体高度
      this.setFixedHeight(20)
      // 他的水平方向会100%的撑开,垂直方向按指定的高度占位
      this.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
      // 我们从这里注册title拖动
      // 其实最好是title首先是个QWidget,那么我们可以对整个title进行拖动,请原谅我的懒惰
      val dragPosition = new QPoint()
      // 鼠标单击放下
      override def mousePressEvent(event: QMouseEvent) {
        if (event.button() == MouseButton.LeftButton) {
          val topLeft = frame.frameGeometry.topLeft
          dragPosition.setX(event.globalPos.x - topLeft.x)
          dragPosition.setY(event.globalPos.y - topLeft.y)
          event.accept
        }
      }

      override def mouseMoveEvent(event: QMouseEvent) {
        if (event.buttons().isSet(MouseButton.LeftButton)) {
          val topLeft = frame.frameGeometry.topLeft
          val p = new QPoint(event.globalPos().x() - dragPosition.x,
                             event.globalPos().y() - dragPosition.y)
          frame.move(p)
          event.accept
        }
      }
    }

    val closeBtn = new QPushButton("X")
    closeBtn.setObjectName("closeBtn")
    closeBtn.setFixedSize(15, 15)
    // 其实这里可以写成 this.clicked.connect(QApplication.instance, "quit()")
    // close表示关闭frame窗口,quit则是整个程序退出
    // qt内部会做调整,当你close窗口,又没有其他窗口在show或者准备show,他也会自动进行quit
    closeBtn.clicked.connect(this, "close()")

    override def showEvent(event: QShowEvent) {
      val g = this.frameGeometry
      val (w, h) = (g.width + 70, g.height + 70)
      this.setFixedSize(w, h) // 根据样式重设大小
      closeBtn.move(w - 45, 30)
    }

    //=======================// body部分 //=======================//

    val body = {
      val widget = new QWidget
      widget.setObjectName("body")
      widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
      widget
    }

    //=======================// foot部分 //=======================//

    val foot = new QLabel("状态栏") {
      this.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
      this.setAlignment(new Alignment(AlignmentFlag.AlignRight, AlignmentFlag.AlignBottom))
    }

    this.init()

    def init() {
      titleLayout.addWidget(titleIcon)
      titleLayout.addWidget(titleText)
      frameLayout.addLayout(titleLayout)
      frameLayout.addWidget(body)
      frameLayout.addWidget(foot)
      closeBtn.setParent(this) // closeBtn最后填充,因为他不在布局模式中。
    }

    def getIcon = frame.style.standardIcon(QStyle.StandardPixmap.SP_MessageBoxInformation)
  }

  def app = QApplication.instance

  lazy val frame = new DemoFrame

  def main(args: Array[String]): Unit = {
    QApplication.initialize(args)
    app.setStyleSheet(globalStyle)
    frame.resize(300, 200) // 我们可以自由的设定这个部件的大小了,调整元件的大小被我们送去showEvent里面去了
    frame.show()
    QApplication.exec
  }
}

好了,弄了这么半天,这个窗口也终于算是做成了,这是最终的效果,虽然和之前的版本没有什么差别,可是能拖动,效果却很不一样了。最后来个留影合照哈!

后记

Qt由于是基于C++的,所以除了UI界面方面,在很多细节都进行封装,比如有QString,QDevice,QByteArray等。而在HTTP、图形渲染、opengl、数据库驱动等方面类库,Qt也都一丝不苟的移植到了Qt Jambi上。事实上,Qt似乎包揽了作为客户端开放的方方面面,当然,其实Java自身也包含了JDBC、Swing等,权当是做一个比较吧。

附上示例的代码(点击将会下载,注意运行环境)

加载中
0
红薯
红薯

自从你介绍了Qt后,我就下了个 Qt Creator 玩了下下,感觉真的很不错,看来以后如果有需要开发GUI应用的话,的确可以考虑了,呵呵:)

0
曾建凯
曾建凯

引用来自#2楼“红薯”的帖子

自从你介绍了Qt后,我就下了个 Qt Creator 玩了下下,感觉真的很不错,看来以后如果有需要开发GUI应用的话,的确可以考虑了,呵呵:)

呵呵,老大来捧场,一定要回帖啊!

0
DEC_LIU
DEC_LIU

好贴,不得不顶,能不能说一下qt和其他gui开发库有什么优点???

0
老盖
老盖

想知道楼主是干什么工作的,怎么什么都懂,呵呵

0
曾建凯
曾建凯

就目前的经验来说,我觉得Qt使用的资源会比较直观些,比如CSS,都是较为成熟的技术。但是其实还会期望他做得更多,比如letter-spacing,text-shadow。

我不了解Swing做一套theme要多久,但我搜集的资料反应,似乎是个高成本的事情。而且大多数时候你只能换换皮肤而已。

SWT先阶段仍然没有对样式有统一定制的支持,好像eclipse 4在按这个计划进行着。

GTK,未来的Gnome3也将支持CSS,而且他支持将动画效果的语法在CSS中实现,所以也很值得期待。

Qt目前实现特效级的东西,需要用到专门的类。我用很简单的模式实现了渐变、滑动,闪动等效果,但没做大量的监测不晓得消耗如何。

.net 3.5以前的我不太熟悉。但WPF和Silverlight也做过一点小东西,WPF有类似CSS的思想,而且还很前卫很超前,但就和多年前的DHTML一样,完全和别人走不同的路子。这个DHTML大家是经历过的,现在HTML5 css3大张旗鼓,而软件行业,纷纷援引CSS来辅助UI,最早的就是Flex和Air,现在越演越烈了,SWT GTK都假如到这个行列了,可是微软还这样搞,让人觉得。。

但WPF也有其优势,一方面是他能利用新时代的计算机硬件特性进行加速,但开发人员却不需要太多操心,这也是微软的老路子。其次Win7普遍好评,所以WPF也具有一定的前瞻性和代表性吧。

0
曾建凯
曾建凯

引用来自#5楼“戏水”的帖子

想知道楼主是干什么工作的,怎么什么都懂,呵呵

苦命IT啥都干,啥都操心,结果啥都懂点了。

0
JavaGG
JavaGG

界面不错哦,怪不得tield新版本都改用qt了

0
大东哥
大东哥

就冲Qt这名字,我就想研究他,不要问为什么,没理由。

0
0
v
vipyami

楼主,swing和jambi可以混用吗?比如把qwebkit放在JPanel里?

返回顶部
顶部