上一篇:
Eclipse RCP详解(01):Hello World以及Eclipse RCP和OSGi的简单展示 先来谈谈我对GUI编程的体会:
GUI编程的本质和难题:
GUI编程一直都不简单,不管在什么平台、用什么语言。而且项目越大就越难驾驭。如果追本溯源,会发现GUI编程的本质就是窗口和消息循环。(本人比较了解Windows平台和MFC框架,却从未在X Window中使用任何库编写GUI程序,故以下探讨皆来源于Windows系统的理论,如有不当,欢迎指正。)GUI不同于CLI,程序运行时没有固定的流程,程序界面必须能随时响应用户的任何操作,当然,也要响应操作系统或其它程序对本程序的操作,因此程序结构中必然存在一个循环,在该循环中不停地获取消息和响应消息。在Windows系统中,一切界面元素,不管是对话框、文本框,还是菜单栏、工具栏按钮,其本质都是窗口。如果使用EnumWindow这个API枚举一下操作系统中的窗口,会发现系统中的窗口数量远远多于我们见到的窗口数量。每一个窗口都能接受发送给它的消息,每一个类型的窗口都有一个消息处理函数用来处理发送给它的消息。窗口之间通过互相发送消息而通讯。
以上所描述的GUI系统机制看似脉络清晰、简单自然,但是却没有多少人愿意用这些反扑归真的底层API去编写自己的GUI程序。因为其中蕴含着两个难题:1、直接使用底层API非常麻烦,必须经过适当的封装;2、程序越大,则窗口对象越多,它们之间的通讯越复杂,如果不经过精心组织,则非常难以管理。
为了解决以上难题,不同平台、不同编程语言及不同的程序库都会对底层API进行封装。这些封装基本上都会从两个方面着手:1、将不同的窗口封装为不同的控件,大的如Frame、Dialog,小的如Button、Label,并且都提供方便的方法来设置这些控件的外观;2、提供某种事件机制响应用户对GUI的操作。
有人可能会说Java中的Swing没有使用底层API,它所有的控件都是自己画出来的。但是Java对GUI的封装依然离不开我上面说的两点。
不同编程语言特性带来的难度:
由于编程语言自身的特性,在使用封装好的GUI组件时仍然有不同的难易性。最简单的应该算是VB之类,拖拖拉拉就可以创建一个GUI界面,要处理哪个控件的事件就设置哪个控件的事件处理函数,其次是JavaScript,先使用HTML定义好界面,然后定义各个控件的事件处理函数即可。不过上面所说的简单也只能算是入门简单,如果要写大规模的程序,它们仍然避免不了大量的控件无法合理组织管理这样的难题。
最难的应该算是C++,因为它最底层。MFC对各种控件都做了不错的封装,也提供了SDI、MDI和基于对话框的程序这样的框架,大大简化了使用C++开发GUI程序的难度,但它依然还是最难的。在MFC中是通过消息映射来处理事件的,如果要处理某个窗口的事件,必须得创建自己的窗口类,并定义大量的事件处理函数。对于大规模的程序,仍然有大量对象不好合理组织的难题。
用Java语言编写GUI程序介于以上两种之间。Java语言的面向对象封装做得非常好,GUI编程理解起来不难。其流程也是先使用诸如JFrame、JButton、JText之类的控件构建一个GUI界面,然后再处理各个控件的事件。但是也正是因为Java语言在面向对象特性方面的过分完美,给GUI编程带来的额外的复杂性。其表现体现在两个方面:1、在Java中一切皆是对象,所以在设置一个控件的事件处理器时,传递给它的必须也是一个对象,而前面提到的几种编程语言都可以直接设置事件处理器为一个函数,可以把函数或函数指针当参数传递。所以在使用Java编写GUI程序时往往需要编写大量的EventHandler类,哪怕编写这些类只是为了定义一个函数,然后实例化出一个对象。当然,随着Java语言的不断改进,我们可以通过内部类、匿名类、Lambda表达式来减少编写EventHandler类的工作,在Java8中,甚至可以直接传递方法引用。个人认为,以上语言特性在提供了方便的同时,有点破坏Java面向对象编程的完美性。2、对象可见性的问题,不像VB、JavaScript,定义一个全局变量后则全世界都可以访问到,在Java中则不行,一个对象要访问另一个对象学问可就深了,特别是为了解决上一个问题而出现的内部类、匿名类,它们想要访问外部类外面的对象容易吗?Java中的闭包就是为了解决这样的问题,但我认为这不是完美的解决方案。
设计模式在GUI编程中的应用:
在前面的探讨中,我反复提到一旦程序规模过大,则大量的对象难以管理,这些对象之间的相互通讯更是错综复杂。这个时候就必须靠设计模式来解决问题了。首先,几乎所有的GUI程序都是按照树形结构组织起来的,父控件包含子控件,子控件再包含更多的子控件,这样一级一级的包含下去。最后只需要把顶层的控件暴露出来,就可以顺着路径找到其它的控件。其次,MVC模式在GUI编程中广泛使用。比如MFC中的SDI和MDI,都有一个Document对象,修改Document对象的状态后,再刷新视图的显示。最后,还有众多的诸如工厂模式、单例模式、监听器模式、过滤器模式等等在GUI编程中广泛使用。
编写GUI程序不能从零开始,要适当使用框架: 这一点应该不需要仔细论证。这也是我们使用Eclipse RCP编程的初衷。
Eclipse的Runtime和UI: 我们可以认为Eclipse RCP是一个很适合我们使用的编写GUI程序的起点。在本篇博文中,我正是要对Eclipse的Runtime和UI进行探讨。因为不可能一下子写得很深入,所以在标题中称为“初探”。Eclipse的Runtime提供了很多功能,比如对配置首选项的支持、对ContentType的支持、对安全存储的支持、对结构化文本的支持、对网络和代理的支持、甚至还有一个多线程框架(貌似已经deprecated),这些功能我统统不感冒。(当然,如果程序的功能正好需要这些底层支持,用一下也无妨。)Eclipse的Runtime最引人注目的当然是它的插件机制了。插件机制可以带来这样的优点:开发项目时,我们可以先编写一个核心的程序实现最基本的功能,然后把程序需要实现的其它功能进行划分,每一个功能单独做成一个Plugin插入到核心中即可。不难看出,使用这样的结构的程序添加和删除功能都非常方便,可以大大降低项目失败的风险。Eclipse的UI更是提供了独具一格的界面风格和程序框架,使用Eclipse RCP,我们当然不需要从头开始实现一个窗口,只需要添添补补,就可以让程序拥有完美的菜单、工具栏、视图、编辑器等一系列的界面元素。
要使用Eclipse的Runtime和UI提供的功能,我们需要熟悉两个工具类,它们是:
org.eclipse.core.runtime.Platform
org.eclipse.ui.PlatformUI
我们先来熟悉一下Eclipse的UI是怎么组织的,以及其中的一些基本概念。在上一篇中,我展示了一个HelloWorld程序,通过该程序,应该不难了解什么是视图。除了视图之外,在HelloWorld程序中还有一个编辑器区域,但是我们没有实现任何编辑器。视图和编辑器放在一个称为Workbench Page的容器中,一个Workbench Page可以容纳多个编辑器和视图。Workbench Page又是放在一个称为Workbench Window的容器中。Workbench Window则是我们程序的主窗口,Workbench Window中还可以包含菜单栏、工具栏及状态栏。不同于其它框架开发出的程序,Eclipse还有一个比较有特色的UI特性,那就是每一个视图都可以有自己的菜单栏和工具栏。经常使用Eclipse的开发者对以上概念应该不陌生。Eclipse各个组件的层次关系如下图:
Workbench Window其实还有上一级,那就是Workbench。理论上讲,一个Workbench可以有多个Workbench Window,但是同一时刻只能有一个Workbench window是Active的,也只有一个是可见的。事实上,我们见到的程序一般都只有一个Workbench Window。同样,一个Workbench Window可以有多个Workbench Page,但同一时刻,也只有一个是Active的和可见的。以上情况不同于视图,因为多个视图可以同时显示。我们把一组视图和编辑器的初始布局称为透视图(Perspective)。Workbench Window可以切换Page,每一个Page初始显示时都是以透视图为模板来组织视图和编辑器。
当我们需要使用Workbench、Workbench Window、Workbench Page的功能时,可以使用如下接口:
org.eclipse.ui.IWorkbench
org.eclipse.ui.IWorkbenchWindow
org.eclipse.ui.IWorkbenchPage
以上接口不需要我们自己去实现,这些接口指向的对象也不需要我们去实例化,只需要调用PlatformUI.getWorkbench()、PlatfromUI.getWorkbench().getActiveWorkbenchWindow()以及PlatfromUI.getWorkbench().getActiveWorkbenchWindow().getActivePage()即可分别得到以上对象。(当然,也可以通过Id获得不是Active的Window和Page。)
除了窗口、视图和编辑器,Eclipse UI中的另外一部分就是菜单和工具栏了。在SWT中,菜单栏、菜单项、工具栏、工具栏按钮属于最基本的控件。直接使用这些控件是可行的,但是肯定很麻烦,所以JFace对它们做了一次封装。封装后就成了Action。在我五年前写的文章中,菜单和工具栏就是使用Action实现的。但是Action还是没有把菜单、工具栏的外观和行为彻底分开,所以Eclipse中又出现了command和handler机制。
在Helloworld程序中,大家已经看到了HelloWorldView是怎么通过定义org.eclipse.ui.views扩展添加到程序界面中的。菜单和工具栏也可以通过定义相应的扩展而增加到Workbench Window中。在我五年前写的文章中我们使用了org.eclipse.ui.actionSets扩展。今天,我们不使用Action,而是使用全新的Command机制。
不同于Action,Commands通过三步有效的达到界面表现和内部实现的分离:首先,通过 org.eclipse.ui.commands 扩展点创建命令和类别(Category),并且可以把某些命令放在一个类别(Category)中;然后,通过 org.eclipse.ui.menus 指定命令出现在界面的哪个区域(视图菜单/主菜单/上下文菜单);最后通过 org.eclipse.ui.handlers 指定命令的实现。因此,Eclipse 推荐使用Commands 来创建菜单。在定义org.eclipse.ui.menus扩展时,需要使用到locationURI来指定菜单出现的位置,locationURI的格式不难,大家可以看Eclipse的参考文档。
下面开始看实例。为了展示Eclipse的底层功能,我设计了这样一个例子:在程序中有两个视图和两个菜单项,其中一个视图中是一个SWT的List控件,当点击菜单项“Show Bundles”时,在这个List中显示所有的Bundle,另一个视图中是一个SWT的Tree控件,当点击菜单项“Show Extensions”时,在这个Tree中显示所有的扩展点和扩展。
第一步,先实现两个视图的界面,如下图:
这两个视图中一个是List控件,一个是Tree控件,使用AbsoluteLayout。界面很简单,根本不需要动用WindowBuilder这样的神器,直接写代码即可,如下:
这个程序的界面和HelloWorld相比有两处不同,一是窗口变大了,二是没有显示编辑器区域。是通过修改ApplicationWorkbenchWindowAdvisor.java和Perspective.jave文件实现的,如下图:
怎么在plugin.xml中通过Extension来显示这两个视图,我就不截图了。下面来看怎么定义菜单。下面是定义完Commands、Bindings和Menus的界面
:
可以看到,locationURI使用的是menu:org.eclipse.ui.main.menu?after=additions,就是把菜单添加到主窗口的菜单栏中。
下面是定义完菜单程序的运行效果,因为没有实现Handler,所以菜单项显示为灰色:
下面,来实现Handler。首先实现显示Bundles的功能。在上一篇中我讲过,Bundles是OSGi的概念,如果想知道我们的程序有哪些Bundles,可以通过Activator的start方法的context参数得到。所以,我们先改写Activator类,在里面添加一个static的List<String>,在start方法中获取所有的bundles,并将它们的SymbolicName保存到List<String>中,如下图:
然后,添加一个ShowBundlesHandler类,继承自org.eclipse.core.commands.AbstractHandler,代码很简单,如下:
在plugin.xml中定义org.eclipse.ui.handlers扩展,如下图:
再来实现另外一个菜单项的功能。先添加一个ShowExtensionsHandler类,仍然继承自org.eclipse.core.commands.AbstractHandler。代码如下:
以上代码展示了怎么使用Platform类来获得所有的扩展点和扩展。同时也展示了SWT的Tree控件使用起来也很简单。
下面是程序的运行效果:
补充内容: Eclipse在Ubuntu下的表现并不完美,甚至有些Bug让我走了不少弯路。
Eclipse在Ubuntu中的菜单项是不能显示图标的,如下两图,左边是Ubuntu中Eclipse的菜单,没有图标,右边是Win7中的菜单,有图标:(同样的Eclipse 3.8.1版)
更坑爹的是,Ubuntu中的RCP程序经常不显示菜单栏,只有显示Splash Screen的程序才能正常显示菜单栏。如下两图,都是用Eclipse自带的向导生成的程序,Ubuntu中没有菜单栏,Win7中有:
所以,我上面的示例程序并不是从Hello RCP模板扩展而来的,而是从RCP Mail Template创建,然后删掉不要的功能,再然后扩展而来的。如下图:
下一篇:
Eclipse RCP详解(03):SWT的相关概念以及一个连连看游戏的实现