本文将讲述Qt in Scala(JVM)开发梗概。
前言
一直有人问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的文档索引
◆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等,权当是做一个比较吧。
【编辑推荐】