京山游侠

专注技术,拒绝扯淡
posts - 50, comments - 868, trackbacks - 0, articles - 0
  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理
上一篇
Eclipse RCP详解(02):Eclipse的Runtime和UI初探

  我在前面讲过:如果讲GUI编程一味只讲各个控件的使用方法,那么纯粹是浪费大家时间,如果出书,那绝对是骗钱的。所以我并不会详细地讲解SWT各个控件的具体使用方法。然而的众所周知,Eclipse的UI界面是建立在SWT基础之上的,如果一字不提SWT,似乎也不大可能。SWT是一个优秀的GUI编程框架,即使不要Eclipse的其它部分,SWT也可以单独使用。单独使用SWT编写GUI程序的最简单示例如下:
   public static void main (String [] args) {
      Display display 
= new Display ();
      Shell shell 
= new Shell (display);
      Label label 
= new Label (shell, SWT.CENTER);
      label.setText (
"Hello_world");
      label.setBounds (shell.getClientArea ());
      shell.open ();
      
while (!shell.isDisposed ()) {
         
if (!display.readAndDispatch ()) display.sleep ();
      }
      display.dispose ();
   }

  从上面的代码可以看出,使用SWT比使用其它GUI框架要多出一步,那就是先要创建一个Display。SWT与其它GUI框架的另外一个不同之处就是它的主窗口不叫窗口,而是叫Shell。除此之外,如果大家具有其它GUI框架的编程基础,使用SWT就没什么难度了。
  我在这里简单比较一下SWT和Swing的区别。在使用Swing时,一般会创建一个JFrame作为主窗口,然后向里面添加控件,而且不需要Display。Swing添加控件的时候是parent.add(child)。SWT需要先创建一个Display,然后再创建一个Shell作为主窗口,然后再添加控件。SWT添加控件的方法是把parent作为参数传递到child的构造函数中。SWT的主窗口关闭后,还得dispose掉Display。而它们对于事件的处理基本上是一样的,都是通过control.addXXXListener添加一个事件处理器来进行。
  SWT为什么要这么一个额外的Display呢?而这个Display又代表着什么呢?SWT底层究竟有着什么样的设计哲学呢?
  Eclipse自己的文档说Display是SWT和操作系统本地GUI之间的桥梁,Display的主要用途就是建立一个消息循环并进行消息的派发,另外一个用途就是帮助GUI线程和非GUI线程进行通讯。我认为,Display还有一个理解,那就是Display是计算机显示系统的一个抽象。看一下下面这个Drawable接口的继承关系:


  可以看到,Display实现了Drawable接口,说明我们可以随意在Display上进行绘图。另外,Display还是Device的子类,说明Display代表着一种设备,而另外一个同样也属于设备的是Printer。我们可以这样理解:如果要把图像显示在电脑屏幕上,就可以使用Display进行画图,如果要把什么内容打印到纸上,使用Printer就好,如果有其它的另类的显示设备,就只好自己实现Device了。
  SWT的widgets中包含很多Control,具体用法我就不讲了,大家可以自己参考SWT的文档,或者直接上SWT Designer或WindowBuilder pro。通过Control的addXXXListener方法可以为该控件添加事件处理器,从而处理响应的事件。下面的截图可以看到Control可以添加的一系列事件处理器:


  除了一般的鼠标键盘事件,每一个实现了Drawable接口的控件都可以随意进行绘制,只需要使用addPaintListener来添加一个PaintListener即可。绘图离不开下面这些东西:


  按我自己的理解,我一般将GUI程序分成三类。
  第一类就是一个对话框里面有许多文本框和按钮的那种,用户依次将各个文本框填满,点击一个按钮,然后就等着程序处理这些数据了。这种类型的GUI系统非常的简单,它的重点并不是GUI,而是GUI背后的业务逻辑。比如超市、银行用的终端,即使是使用全由字符界面组成的只显示黑白灰色的GUI系统,依然能够处理复杂的业务。还有现在和很多ERP系统、HIS系统等等,无非就是数据的采集、存储、显示,用几个文本框和按钮控件足以,没有什么技术含量。
  第二类是涉及到自己绘图的那种,比如绘制个什么波形图啊,绘制个什么2D游戏啊。这种类型的GUI程序不需要复杂的控件,重点在于绘图的操作。比如我后面即将展示的连连看游戏,就仅仅使用了一个Canvas而已。当然,要写个像Photoshop之类的巨型软件,难度还是很大的。
  第三类就是可以用来处理文档的那种。比如各种字处理程序、表格程序、浏览器程序,还有我们程序员最常用的IDE。在这些处理程序中,什么字体变大变小、颜色变红变白什么的,说到底还是画图,和第二类的画图差不多,但是其背后掩藏着复杂的算法。就拿我们常见的IDE来说,语法高亮、自动提示这样的功能怎么也离不开对文本内容的解析,还要计算每一个字符究竟显示在什么位置。要做得漂亮,难度也是很大的。
  如果以上三类GUI程序都能熟练编写,就可以算是GUI编程的达人了。今天,我先展示一个连连看游戏的例子。直到目前为止,我所讲述的Eclipse RCP都还只是建立在视图上。怎么写编辑器(Editor)还没有涉及。以后肯定会讲到编辑器的编写,当然是可以分析文本的编辑器,对于那种只靠文本框和按钮收集和编辑数据的,我觉得不好意思叫编辑器。今天的连连看程序仍然是一个单视图的程序。

  连连看游戏运行效果图:


  如果碰到困难,点击工具栏的放大镜图标,可以自动推荐两个可以匹配的方块,如下图:


  下面来谈谈这个程序的实现。首先是准备素材。大家可以看到这个游戏中用到的图片都是Java世界一些比较著名的开源项目,如Spring、Tomcat、Glassfish、MySQL等。我从网上找到它们的Logo,然后用GIMP稍微处理了一下,就成了游戏中需要用到的方块。从上面两个截图可以看到,每一个方块都有三个状态,分别是正常状态、被选择状态和被推荐状态,所以每张图片有三个样式,如下图:


程序的GUI结构:

  程序的GUI结构非常简单,就是一个单视图的Eclipse RCP程序。这个视图的标题是Game View。和前两篇博文讲的不一样的地方是这个程序没有用菜单,而是使用了视图的工具栏。使用视图的工具栏和使用菜单的流程是一样的:1、先添加一个org.eclipse.ui.commands扩展,定义两个Command;2、再添加一个org.eclipse.ui.menus扩展,定义一个MenuContribution,该MenuContribution可以控制我们添加的Command显示在菜单中还是显示在工具栏中;3、写两个Handler,分别处理两个Command命令即可。
  对MenuContribution设置不同的locationURI属性可以控制Command的显示位置。在前面一篇博文中,我们设置locationURI为menu:org.eclipse.ui.main.menu,所以就在主菜单中添加了一个菜单。在这个连连看游戏中,我将locationURI设置为toolbar:JavaLinkGame.views.game,也就是冒号前是toolbar,冒号后是Game View的Id,所以在Game View中添加了一个工具栏和两个按钮。
  在Game View中只使用了一个Canvas控件,Canvas控件用来画图和响应用户的操作。所以只需要设置Canvas的PaintListener和MouseListener即可。程序的框架非常简单,如下图:


  上面图片中显示的代码只是为了显示程序的结构,后面都经过了大量的更改和扩充。在上面的代码中,我在注释中问了两个一样的问题:这里用什么?如果是C或C++,就可以在这里传递一个函数指针,如果是函数式编程语言,就可以在这里直接传递一个函数。但是,我们用的是Java,所以canvas.addPaintListener()的参数只能是一个对象。要获得一个对象,就必须定义一个类,这正是Java语言的麻烦之处。
  好在这个麻烦有许多的解决办法。如果不想另外写一个.java文件,我们可以定义一个内部类;如果连类名都懒得想,可以用一个匿名类;如果真的不想定义一个类,那就用lambda表达式吧。在这里我使用的是匿名类。如下图:
  

  使用Java语言,除了时时刻刻要考虑定义一个类的问题外,还有一个问题,那就是field的可见性。一个类要如何才能访问到另一个类中的field?如上图,我们定义的匿名类中可以直接访问外面类中的canvas和spirits,也就是说内部类可以直接访问外部类中的field。是不是相当于闭包?假如我们不是用的匿名类,也不是用的内部类,而是另外定义一个类,那该如何访问到这里的canvas和spirits呢?只能通过构造函数把这两个field当参数传进去吧。

程序的数据组织:

  在这个程序中,除了Eclipse RCP必须的一个View和两个Handler类之外,我只额外写了两个类,一个类是Spirit,用它表示游戏中的一个方块。每一个Spirit对象保存了它需要的三幅图片、状态(是正常还是被选择还是被推荐)、坐标(在数组中的坐标,而不是像素的坐标)以及一个imageId,使用imageId的目的是为了方便判断用户选择的两个方块是否是相同的,相同的方块如果在转两个弯之内连得上的话就可以消去。另外一个类是GameAlgorithmUtil,它主要处理游戏中的算法。
  我用了一个Spirit[][]二维数组来保存方块,这个二维数组的边缘一圈填充的Spirit的imageId为0,中间的Spirit的imageId不为0。imageId为0的Spirit不会显示,所以边缘这一圈显示为空白,也是最开始时候连接方块的通道。如下图:


  另外,当两个方块可以消去时,我们还要显示他们之间的连线,所以需要保存他们之间连接的路径。这个简单,不需要另外写一个类,用一个LinkedList<Spirit>即可。

游戏中的算法:

  游戏中涉及的算法不多,只有两个。 第一个,是游戏开始时,需要把所有方块的位置打乱,所以需要一个洗牌算法。第二个,在用户选择了两个图片相同的方块时,需要判断它们是否可以连通,所以需要一个寻找路径的算法。
  洗牌算法:先在这n个方块中随机选择一个方块和第n个方块交换,再在前n-1个方块中随机选择一个和第n-1个交换,再在前n-2个方块中随机选择一个和第n-2个交换……一直递归到第一个。
  寻找路径算法:我没有用《编程之美》中讲到的广度优先搜素算法,而是用了另外一个分类扫描算法。将两个方块可以连通的情况分为三类。第1类,两个方块可以通过一条直线连通,没有转角;第2类,两个方块需要通过两条直线连通,有一个转角,第2中情况可以递归为判断这个转角处的元素可以和这两个方块分别通过一条直线连通;第3类,两个方块需要通过三条直线连通,有两个转角,很显然其中一个转角肯定要么和第一个方块同行,要么同列,然后递归为判断这个转角是否能够和第二个方块通过两条直线连接。

下面开始贴代码:
1、Spirit类的代码:
Spirit.java

2、视图类的代码:
GameView.java

3、GameAlgorithmUtil类的代码,算法的实现都在这里面了,有详细的注释:
GameAlgorithmUtil.java

上面三个就是这个游戏的主要实现了。另外三个代码如下:
4、plugin.xml,就只定义了一个视图,两个Command及其menuContributions和Handler:
plugin.xml

5、GameStartHandler类的代码,点击工具栏左边那个带旗子的图标,开始游戏:
GameStartHandler.java

6、SpiritSearchHandler类的代码,也就是点击右边那个放大镜图标时,自动寻找一对能够连通的方块:
SpiritSearchHandler.java


完整的项目压缩文件如下:
JavaLinkGame.zip

该项目是在Ubuntu下写的,下载后使用Eclipse可以直接导入。如果是在Windows下使用的话,一定记得在项目的属性中将字符编码改成UTF-8,换行风格改成Unix风格。否则出现乱码。

在Windows 7中运行的截图:


  该游戏在Ubuntu中运行很流畅,但是在Windows7有点闪烁,要解决这个问题需要用到double buffer。另外,由于不想增加额外的复杂性,我没有使用多线程,所以方块的消除是在下一次点击鼠标时完成的,用户体验略差。
  如果想用多线程,就得更改程序的结构,不能直接在MouseListener中处理鼠标点击事件,而是应该另外建立一个队列,将所有的操作,包括定时器到期的操作,都发送到队列中,然后在队列另一端使用一个消费者消费这些事件。由于我不是在讲并发编程,这里就不详细展开了。我以前用MFC做了一个俄罗斯方块小游戏,就是把所有的操作都发送到队列中,大家可以参考,博客在这里:写个小游戏练一练手

下一篇:
  Eclipse RCP详解(04):Eclipse RCP相关的学习资料及国内相关图书点评

评论

# re: Eclipse RCP详解(03):SWT的相关概念以及一个连连看游戏的实现  回复  更多评论   

2014-02-15 10:50 by 魏五锁业
支持博主分享

# re: Eclipse RCP详解(03):SWT的相关概念以及一个连连看游戏的实现[未登录]  回复  更多评论   

2014-02-24 11:19 by 浩子
赞一个

# re: Eclipse RCP详解(03):SWT的相关概念以及一个连连看游戏的实现  回复  更多评论   

2014-04-09 16:24 by 最代码
你好,我本地运行不了,不知道是什么缘故,出错信息:
java.lang.UnsatisfiedLinkError: Cannot load 32-bit SWT libraries on 64-bit JVM

另外我转载到了最代码网站,地址:http://www.zuidaima.com/share/1772672482675712.htm

# re: Eclipse RCP详解(03):SWT的相关概念以及一个连连看游戏的实现  回复  更多评论   

2014-04-18 21:30 by 海边沫沫
@最代码
很显然,我的代码是用64位虚拟机编译的,你的机器上是32位的虚拟机。
要解决问题很简单,你先把项目clean一下,然后再build一下即可。

# re: Eclipse RCP详解(03):SWT的相关概念以及一个连连看游戏的实现  回复  更多评论   

2014-08-11 14:10 by RCP开发
首选很感谢楼主能够给大家提供这种技术分享,不过对其还是表示有点遗憾,Demo已经试运行过,在一些界面表现还是很不到位,算法上我不发表评论,但界面的实现上还有待提高,像每次点击都要刷新整个屏幕就觉得研究的不是很深,其真正的好的是可以做到局部刷新,配合后台算法逻辑处理,思想就是在每个图标上加个一层父层,要消除图标只要dispose图标,然后刷新父层。还有就是分享的代码请能够确保还运行,提供的代码都是拷贝的,没有进行手工整理,所以无法正常运行,请其它学习者参考时,先自己建立一个工程,然后将其主要代码和扩展点进行复制过去。有些地方还得整理,就是报错的地方是有些代码没分行被屏蔽了。

# re: Eclipse RCP详解(03):SWT的相关概念以及一个连连看游戏的实现  回复  更多评论   

2014-09-03 10:52 by 京山游侠
@RCP开发
你的回复是到目前为止我见到的最高水平的回复。
局部刷新确实是GUI开发中最重要的理念,它的重要性不仅仅只是性能,更重要的是它能解决界面闪烁的问题。
以后我会注意这一点。

# re: Eclipse RCP详解(03):SWT的相关概念以及一个连连看游戏的实现  回复  更多评论   

2015-04-20 14:21 by cyberhero
首先感谢博主的分享,要注意资源的释放,这里指的资源是SWT对应操作系统操作的资源,比如GC,Color,Image等,如果在使用后不及时释放的话,会造成句柄泄露,博主的代码直接运行的话是会报错的,需要在所有GC和Color使用之后及时释放资源就没有问题了。

只有注册用户登录后才能发表评论。


网站导航: