引言
Java 自从 1995 年发布以来,其图形界面一直为世人所诟病。无论是早期的 AWT,还是后来的 Swing 应用程序不能像本地应用程序一样执行,外观也不一样,响应的速度也不快。SWT 吸收了 AWT 和 Swing 实现的最好的部分 : 当可以得到本地组件时调用本地实现,当不能得到本地组件时使用 Java 实现。这就同时保证了与本地窗口部件相当的外观,又提高了响应速度。
目前 SWT 已被广泛应用于开发 JAVA 富客户端,但是基于 SWT/RCP 的应用程序界面都是经典的 Eclipse 界面风格 : 蓝色的标题栏、灰色的工具栏和状态栏、四方形的视图和编辑器、还是四方的控件,这些界面过于朴素,缺乏吸引力。
其实我们可以基于 SWT/RCP,编写漂亮 GUI 的界面。自定义窗口的形状,通过图片背景来美化 SWT/RCP 窗体界面。窗体可以是多边形,如矩形、圆形、以及这些形状的叠加。按钮控件也可以任意多边形。
下图是经典的 Eclipse 风格界面和美化后界面的比较:左面是一个经典的 SWT/RCP 窗体界面;右面是美化后的窗体,黑色的外框由一个矩形和一个圆形的叠加而成、圆形的播放按钮、圆弧形的退出按钮、不规则的放映视图等。
图 1. 经典窗体与美化后窗体的比较
本文首先介绍了 Eclipse 中图形和界面的一些基本知识, 如何定义多边形,如何把 SWT 的窗体设置成多边形,以及如何定制多边形的 SWT 窗口和控件;然后介绍如通过图片来获得多边形外形,如何使用图片来设置 SWT 窗体背景。最后通过剖析 Eclipse 的工作台(工作台就是 Workbench)的启动运行过程,介绍如何编写 RCP 的定义多边形窗口,以及窗体上控件的多边形外形。
SWT 图形和窗体基础
在开始介绍之前,我们先熟悉一下如何创建一个图形的。SWT 是通过 org.eclipse.swt.graphics.Region来定义图形的。 我们可以通过定义一个整型数组来定义各个点,这些点连接在一起就是一个图形。坐标的顺序是先 X 轴后 Y 轴,逆时针连接的。 例如下面定义的 4 个点数组 int[] rect = {Xa, Ya, Xb, Yb, Xc, Yc, Xd, Yd},A->B->C->D 连接而成就是一个矩形。int[] rect2 = {Xe, Ye, Xf, Yf, Xg, Yg, Xh, Yh},E->F->G->H 连接而成就是另外一个矩形。把这两个矩形叠加在一起就是一个多边形的外形。
图 2. 多边形
生成两个矩形的函数如下:
清单 1. 生成两个矩形的函数
int[] getBackRect1(){
int [] rect = new int[2*4];
//A
rect[0] = 0;
rect[1] = 0;
//B
rect[2] = 0;
rect[3] = 320;
//C
rect[4] = 520;
rect[5] = 320;
//D
rect[6] = 520;
rect[7] = 0;
//
return rect;
}
int[] getBackRect2(){
int [] rect = new int[2*4];
//E
rect[0] = 50;
rect[1] = 320;
//F
rect[2] = 50;
rect[3] = 370;
//G
rect[4] = 470;
rect[5] = 370;
//H
rect[6] = 470;
rect[7] = 320;
//
return rect;
}
Region 类里面也提供了直接生成矩形的函数 add(Rectangle rect)和 add(int x, int y, int width, int height)。如果要生成不规则的窗体,如圆形,或者其他形状的窗体,Region 不直接提供函数,只能通过坐标数组来实现。
如图所示,圆形是通过一组逆时针连接而成的点构成,各个点可以有园半径,圆心坐标推算而成。A1 的坐标是 (Xo-r, Yo). An 的 X 坐标是 XAn=Xo-r+n,Y 坐标是 YAn=Yo+R。
图 3. 园
生成圆形坐标数组的函数代码如下:
清单 2. 生成圆形坐标数组的函数代码
int[] circle(int r, int offsetX, int offsetY) {
int[] ring = new int[8 * r + 4];
// x^2 + y^2 = r^2
for (int i = 0; i < 2 * r + 1; i++) {
int x = i - r;
int y = (int) Math.sqrt(r * r - x * x);
ring [2 * i] = offsetX + x;
ring [2 * i + 1] = offsetY + y;
ring [8 * r - 2 * i - 2] = offsetX + x;
ring [8 * r - 2 * i - 1] = offsetY - y;
}
return ring;
}
有了这些定义图形的函数,我们就可以创建一个 Region 对象用于定义窗体的外形。首先我们定义一个缺省的 Region 对象,然后加入定义好的图形。这些图形可以叠加在一起形成多边形外形;也可以剔除一块图形。
下面是示例的完整代码。首先生成一个 Display 对象,然后用 Display 对象创建一个 Shell。注意需要定义成无装饰风格的窗口。再后就是创建 Region 对象来定义一个图形形状,通过 shell.setRegion 来设置窗口外形,最后是显示 Shell。Region 在使用完后必须要跟 Display 对象一样被释放。
清单 3. 示例的完整代码
public static void main(String[] args) {
final Display display = new Display();
final Shell shell = new Shell(display, SWT.NO_TRIM);
//
Region region = new Region();
region.add(getBackRect1());
region.add(getBackRect2());
//
region.subtract(circle(20, 400, 345));
//
shell.setRegion(region);
Rectangle size = region.getBounds();
shell.setSize(size.width, size.height);
//
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch())
display.sleep();
}
region.dispose();
display.dispose();
}
通过运行这段代码,可以得到自己定义的多边形窗口界面。
图 4. 多边形窗口
Control(控件)多边形外形只有在最近的 Eclipse 版本支持该功能。下面代码是通过另外一种非常规的手段来实现多边形小部件的,通过图片来显示小部件边界。程序首先定义一个 BUTTON 按钮,然后设置按钮的图片,让按钮大小约大于图片。为了只让图片显示,而不显示按钮,我们需要创建一个 Region,设置相应的偏移,使 Region 刚好覆盖要显示的图片。
清单 4. 实现多边形小部件
Button startBt = new Button(shell, SWT.PUSH);
startBtRegion = new Region();
startBtRegion.add(circle(35, 40, 40));
ImageData startData = startBtImage.getImageData();
startBt.setRegion(startBtRegion);
startBt.setSize(startData.width+10, startData.height+10);
startBt.setLocation(575, 297);
startBt.setImage(startBtImage);
startBt.setToolTipText("Play media");
图 5. 多边形 Control
图 1.5 说明了其中原因,如果要创建一个圆形多边形按钮,我们首先要定义个圆形部分的 Region,然后设置偏移 X1,Y1,再后设置 button 的 Region 为我们创建的 Region。这样 Button 显示给我们的就是一个圆形的 Button。而且只有该圆形区域,按钮点击才有响应。
图 6. 圆形控件示意图
值得注意的是,如果不用图片,圆形的 button 没法显示相应的边界,用户很难分辨圆形按钮和父窗口,因而用处不大。如果想创建一个像 Button 的多边形按钮,我们需要继承 Button,重载图形绘制部分,自己绘制 Region 的边界以显示一个真正的多边形按钮。
多边形控件只有在 Eclipse3.4 中才开始支持,有对方面感兴趣的朋友可以自己实现多边形控件,然后定义几个特例,比如说圆形,环形等。如果运气好的话,说不定这些实现可能被 Eclipse 采纳。
通过图片来定义窗口界面
在介绍通过图片来获得图形外形前,我们先介绍一下图像方面的一点基础知识。
在计算机里,图像是通过像素来显示的 , 像素也叫图像分辨率。正如 WIKI 里面定义的,像素是图像显示的基本单位,是英文单词 picture 和 element 的组合而成。一幅图像中的像素可以在任何尺度上看起来都不像分离的点或者方块;但是在很多情况下,它们采用点或者方块显示。每个像素可有各自的颜色值,可采三原色显示,因而又分成红、绿、蓝三种子像素(RGB 色域),或者青、品红、黄和黑(CYMK 色域,印刷行业以及打印机中常见)。照片是一个个采样点的集合,故而单位面积内的像素越多代表分辨率越高,所显示的图像就会接近于真实物体。
如下图所示,图像被定义成 N * M 个方格,每个方格表示一个像素。每个像素都有自己的颜色值。蓝×××像就是大量蓝色小方格组成的。
图 7. 像素示意图
在 SWT 中,图像模型是 ImageData,它用来保存图像信息,如图像高度,宽度以及像素相关信息; 它不像 Image,是一个设备无关类。目前 SWT 支持 JPG, PNG, BMP 等图片格式。在这里我们遍历整个图片,获取每个像素值,如果像素值不为 0,表示该像素位于图片内。这些点组成的图形,就是图片的图像。
清单 5. 代码
ImageData mask = data.getTransparencyMask();
Rectangle pixel = new Rectangle(0, 0, 1, 1);
for (int y = 0; y < mask.height; y++) {
for (int x = 0; x < mask.width; x++) {
if (mask.getPixel(x, y) != 0) {
pixel.x = data.x + x;
pixel.y = data.y + y;
region.add(pixel);
}
}
}
SWT 中有很多种方法得到 ImageData 实例。可以通过 ImageData 的构造函数初始化一个 ImageData 实例;也可以通过 ImageLoader 来得到 ImageData 实例;还可以通过已有的 Image 对象来获得。
通过图片可以获得如下窗体外形:
图 8. 图像外形多边形窗口
清单 6. 示例全部代码
private static Region getBackRegionFromImage(Display display, String p_w_picpath){
ImageLoader loader = new ImageLoader();
ImageData[] p_w_picpathData = loader.load(p_w_picpath);
Region region = new Region(display);
ImageData data = p_w_picpathData[0];
ImageData mask = data.getTransparencyMask();
Rectangle pixel = new Rectangle(0, 0, 1, 1);
for (int y = 0; y < mask.height; y++) {
for (int x = 0; x < mask.width; x++) {
if (mask.getPixel(x, y) != 0) {
pixel.x = data.x + x;
pixel.y = data.y + y;
region.add(pixel);
}
}
}
return region;
}
public static void main(String[] args) {
String backImage="icons/back.gif";
final Display display = new Display();
final Shell shell = new Shell(display, SWT.NO_TRIM | SWT.ON_TOP);
//
Region region = getBackRegionFromImage(display, backImage);
//
shell.setRegion(region);
Rectangle size = region.getBounds();
shell.setSize(size.width, size.height);
//
ImageLoader loader = new ImageLoader();
ImageData[] p_w_picpathData = loader.load(backImage);
Image p_w_picpath = new Image(null, p_w_picpathData[0]);
shell.setBackgroundImage(p_w_picpath);
//
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch())
display.sleep();
}
p_w_picpath.dispose();
region.dispose();
display.dispose();
}
美化 RCP 界面
RCP 程序的 GUI 可简单看成一个窗体,窗体里有菜单,工具栏,状态栏,以及由视图和编辑器组成的 Page。
图 9. RCP Demo
其实 RCP 远比看起来要复杂,我们看到的其实是一个工作台 Workbench。Workbench 提供 UI 建造部件,使得 Eclipse 应用程序容易编写,使用,升级,扩展。Workbench 非常强大,它是一个基于 OSGi 的插件容器,里面定义了多达 42 个扩展点和大约 350 个 API 类,我们可以基于这些扩展点和提供的 api 创建自己需要的应用程序界面。
Workbench 包含一个或多个 WorkbenchWindows, WorkbenchWindows 是界面元素,里面包含了 Shell,窗口中包含菜单,工具栏,状态栏,以及 WorkbenchPage 等信息。WorkbenchPage 其实是一组视图和编辑器的组合,一个 WorkbenchWindows 可以有多个 WorkbenchPage(但是通常我们只有一个 WorkbenchPage),WorkbenchPage 可以含有多个透视图。WorkbenchPage,ToolsBar 和 StatusLine 都是 Control,我们可以定义其外形。WorkbenchWindows 还可以添加很多其他的控件,如示例中的按钮。除去菜单以外,我们可以定义窗体内所有 Control 的外形,包括 WorkbenchWindows。
图 10. RCP 用户界面组成
注意我们不能定制 WorkbenchPage 中的视图和编辑器的外形,但是视图和编辑器中定义的 Control 可以,如 Button, Label, Link, ProgressBar, Sash, Scale, Scrollable, Slider 等。这些 Control 外形定制的方法跟前面 SWT 中介绍的一样。
Workbench 和 WorkbenchWindows 都是些内部类,我们想定制多边形窗体,并不能直接修改他们。不过我们可以继承 Eclipse 暴露出一些回调函数组成的 Advisor 来重载这些 Eclipse 定义好的回调函数来设置一些相关的信息。当 Workbench 和 WorkbenchWindows 启动运行是,这些回调函数就会被调用到。Workbench 的 Advisor 是 WorkbenchAdvisor,这是一个应用级别的建议者,Workbench 启动和关闭是被调用到,用来设置透视图等信息,跟 UI 没有太大关系。WorkbenchWindows 的 Advisor 是 WorkbenchWindowsAdvisor,WorkbenchWindowsAdvisor 比 WorkbenchAdvisor 承担了更多的 UI 方面的角色。我们基本上可以使用该 Advisor 来设置每个工作台窗口的显示,如窗体风格、标题、工具栏、状态栏等。我们要实现多边形的 RCP 窗体,就是继承该类,重载其中的 postWindowCreate 和 createWindowContents 来实现的。
Eclipse 中的 OSGi 机制是通过 Equinox 来实现的。插件 org.eclipse.equinox.app 中定义了一个普通应用程序的启动的扩展点,Application 是它的一个扩展。下面是 RCP 启动的一个时序图:
图 11. RCP 启动时序图
Application 调用 PlatformUI.createAndRunWorkbench 来创建一个工作台 . 工作台 Workbench 调用 WorkbenchAdvisor 来设置相关信息,创建 WorkbenchWindow。 WorkbenchWindow 又通过 WorkbenchWindowAdvisor 来设置窗口的一些信息。重载 preWindowOpen 来设置窗体风格;重载 createWindowContents 来创建窗体内的部件,如菜单、工具栏、状态栏、WorkbenchPage 等;重载 postWindowCreate 来设置窗体大小、外形。
定制 RCP 窗体外形前,Shell 对象已经初始化,所以我们只能是在窗体创建后。设置 RCP 窗体外形跟前面的 SWT 程序一样简单,首先通过通过窗体配置参数获得当前窗体 Shell 的引用,然后定义一个外形 Region,使用这个外形设置窗体就成。
窗体中的部件的外形定制跟其他的 Control 一样,先创建图形,然后使用图形设置 Control 外形。需要注意的是我们需要定义好 Control 的位置,因为父子部件是一层层叠加的,最顶层的部件可能会覆盖其父部件的外形。
通过定制多边形外形,我们可以制作一个界面美观的媒体播放器 :
图 12. 媒体播放器
实例的部分代码如下:
public void preWindowOpen() {
getWindowConfigurer().setShellStyle(
SWT.NO_TRIM | SWT.CENTER | SWT.NO_BACKGROUND);
}
首先是在窗体打开前设置窗体的风格,没有边界,没有背景。
清单 7. 设置窗体风格
public void postWindowCreate() {
IWorkbenchWindowConfigurer configurer = getWindowConfigurer();
//
final Shell shell = configurer.getWindow().getShell();
backRegion = getBackRegionFromImage(shell.getDisplay(), backImage, 0, 0);
ImageData data = backImage.getImageData();
shell.setRegion(backRegion);
shell.setSize(data.x+data.width, data.y+data.height);
//
Listener listener = new Listener() {
int startX, startY;
public void handleEvent(Event e) {
if (e.type == SWT.MouseDown && e.button == 1) {
startX = e.x;
startY = e.y;
}
if (e.type == SWT.MouseMove && (e.stateMask & SWT.BUTTON1) != 0) {
Point p = shell.toDisplay(e.x, e.y);
p.x -= startX;
p.y -= startY;
shell.setLocation(p);
}
if (e.type == SWT.Paint) {
ImageData data = backImage.getImageData();
e.gc.drawImage(backImage, data.x, data.y);
}
}
};
shell.addListener(SWT.MouseDown, listener);
shell.addListener(SWT.MouseMove, listener);
shell.addListener(SWT.Paint, listener);
//
}
窗体创建后,程序通过读取图片信息获得图片的大小,多边形外形等信息,然后设置窗体的大小外形。 同时添加窗体变化的 Listener,通过绘制图片来设置窗体的背景。
清单 8. 设置窗体的背景
public void createWindowContents(final Shell shell) {
IWorkbenchWindowConfigurer configurer = getWindowConfigurer();
//
page = configurer.createPageComposite(shell);
pageRegion = getBackRegionFromImage(shell.getDisplay(), pageImage, 0, 0);
ImageData pageData = pageImage.getImageData();
page.setRegion(pageRegion);
page.setBounds(45, 21, pageData.width, pageData.height);
//
Button startBt = new Button(shell, SWT.PUSH);
//startBtRegion = getBackRegionFromImage(shell.getDisplay(), startBtImage);
startBtRegion = new Region();
startBtRegion.add(circle(35, 40, 40));
ImageData startData = startBtImage.getImageData();
startBt.setRegion(startBtRegion);
startBt.setSize(startData.width+10, startData.height+10);
startBt.setLocation(575, 297);
startBt.setImage(startBtImage);
startBt.setToolTipText("Play media");
startBt.addSelectionListener(new SelectionAdapter(){
public void widgetSelected(SelectionEvent e) {
// TODO Auto-generated method stub
String [] ext = {"*.AVI", "*.mp3", "*.*"};
FileDialog dlg = new FileDialog(shell, SWT.OPEN);
dlg.setText("Select Media...");
dlg.setFilterExtensions(ext);
String file = "file://" + dlg.open();
//
String editorID = "RCPDemo.mediaplay";
try {
PlatformUI.getWorkbench().getActiveWorkbenchWindow()
.getActivePage().openEditor(new MediaPlayerInput(file), editorID);
} catch (PartInitException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
});
//
Button exitBt = new Button(shell, SWT.PUSH);
exitRegion = getBackRegionFromImage(shell.getDisplay(), exitImage, 5, 5);
ImageData exitData = exitImage.getImageData();
exitBt.setRegion(exitRegion);
exitBt.setSize(exitData.width+10, exitData.height+10);
exitBt.setLocation(615, 350);
exitBt.setImage(exitImage);
exitBt.setToolTipText("Exit");
exitBt.addSelectionListener(new SelectionAdapter(){
public void widgetSelected(SelectionEvent e) {
// TODO Auto-generated method stub
PlatformUI.getWorkbench().close();
}
});
}
这段代码是创建窗体上的控件,一个 video 播放 editor,一个播放按钮和一个退出按钮。video 播放 editor 是 Page 的一部分,而 Page 的外形通过图片来定义;一个播放按钮是通过一个文件对话框让用户来选择需要播放的 video 文件。
通过上面的介绍,我们可以看到,SWT/RCP 是一个非常优秀的应用界面 SDK,不光可以编写经典的窗体,还可以编写漂亮的多边形窗体及控件。希望读者通过本文的介绍,多使用 SWT/RCP 相关技术,开发出具有自己风格的应用程序。
本文地址 : http://www.fengfly.com/plus/view-179472-1.html