您能编写跨多种平台编译、同时执行速度仍然像本机代码一样快的 Java 代码吗?这是一个困扰 Java 开发人员的难题,对于具有复杂 UI 的应用程序来说尤为如此。在本文中,开发人员 Vladimir Silva 建议了一种针对这个难题的有趣解决方案。您将学习如何使用 JNI 来访问 SLIK,后者是一个跨平台的 C API,它同时在 Windows 和 UNIX 上提供本机性能。
相对于诸如 C 和 C++ 这样的传统语言,Java 语言已主宰了中间层开发。Java 语言在开发社区得到普遍接受的原因是它的面向对象设计、平台无关性,以及丰富的支持 API。然而,它在设法将桌面作为一个真正的开发平台方面却很失败——或许这是由于使得它主宰中间层的相同原因吧。平台无关性意味着相对于本机 C 和 C++ 应用程序来说,性能会大大降低。

本文的目的是展示 Java 与简单皮肤接口(Simplistic Skin Interface,也称为 SLIK)的绑定(请参阅 参考资料,获取更多细节)。SLIK 是一个可用于构建跨平台 GUI 的 C API。通过使用 Java 本机接口(Java Native Interface,JNI),您可以将 Java 代码绑定到 SLIK,从而设计出像 C 和 C++ 一样执行的跨平台用户界面。

Java 语言和 C:动感二重唱

Java 技术支持与多种语言绑定,包括 C/C++、Fortran,等等。这一卓越特性为开发人员带来了无比的灵活性,允许他们编写需要某些操作系统相关功能的 API。

为了证明 SLIK 的高级用户界面功能,我将展示如何构建一个没有矩形窗口的 UI(也称为 皮肤或 带皮肤的窗口),它需要 Java 语言当前所没有提供的低级 API。Java 所提供的是使用 JNI 访问任何操作系统相关服务的手段。在本机端,C 是最佳的语言选择,而我将使用的 SLIK 的修改版本提供了实现本文目标的多平台 C API。

关于 JNI 的详细介绍超出了本文的范围;欲了解关于该主题的更多信息,请参考下面的 参考资料小节。务必要牢记的一件重要事情是,SLIK 是用 C 编写的,并且您需要 JNI 来从用 Java 语言编写的代码中访问它。

SLIK 概述

SLIK 是一个 C API,用于构建基于 WinAmp 风格的皮肤的高级用户界面。此类界面的例子包括像用于类 UNIX 系统的流行的 XMMS(X-Multimedia System,X-多媒体系统)和 XINE CD/DVD 播放器这样的音频和视频播放器。 使用这个 API 作为 GUI 的基础具有许多好处:

SLIK 包括一组增强的小部件,比如按钮、菜单、窗口、列表,以及当前的窗口工具包所没有提供的高分辨率图像。


该 API 可用于多平台设计,因为它的核心库可同时在 Microsoft Windows 和类 UNIX 系统下编译。


SLIK 可以实现为一个 Windows DLL 或 UNIX 共享对象库,这意味着它可以跨多个应用程序重用,并且易于维护。
SLIK 软件由 GQmpeg 小组在 GNU 公共许可证(GNU Public License,GPL)之下提供。GQmpeg 小组提供的 SLIK 版本仅在类 UNIX 系统上作为可执行文件来编译。我创建了一个修改后的版本,它可以同时工作在 Win32 和 UNIX 系统上;这个版本以二进制的形式随本文的源代码一起提供(同时包括 Windows DLL 和 UNIX 共享对象或 SO 版本)。

我还同时提供了用于 UNIX 和 Windows 环境的 makefile。请参阅 参考资料小节,获得这些源代码下载。


结合使用 SLIK 和 Java 语言

您可以在 SLIK C 实现的基础上将 SLIK-JNI 接口层实现为 DLL 或 SO(取决于底层操作系统)。消息通过 JNI 层发送,从 C 层激发的回调层级联返回 Java 层。高级 Java JNI 类提供了针对低级层的主要访问点。这些类负责加载该二进制的 DLL/SO,并提供访问低级 C 子例程的接口。SLIK JNI 层是一个平台无关的组件,它充当 Java 代码和操作系统相关的代码之间的粘合剂。SLIK C API 是 SLIK 服务的具体操作系统相关的实现;它需要 GIMP 工具包(GIMP Toolkit,GTK)才能操作。(GTK 是 Linux 和许多 UNIX 版本使用的窗口管理器;Windows 用户必须安装 GTK 2.x 运行库才能使用 SLIK。请参阅本文的 参考资料,获得相关链接。)最后一个组件是皮肤 规格文件,这是一个包含诸如图像、标签、XY坐标等窗口小部件信息的配置文件。


SLIK-Java 对象层次

UML 图描绘了带皮肤的窗口对象层次和 JSLIK 包的本机层。JSLIK 对象层次的顶部是 GtkSkin 类。它封装了一个带皮肤的窗口,并为这样一个窗口提供以下有用功能:加载、卸载、最小化、图标化,等等。它还能够监听诸如鼠标移动、单击、子窗口的派生以及抛出异常等事件。它不同于常规窗口的地方在于,其几何形状和它们的所有子部件都基于从规格文件中加载的高分辨率图像。 GtkSkinNatives 类提供了调用实际绘制窗口的低级本机方法的接口。



这些窗口小部件封装了皮肤规格文件中定义的属性,该文件默认被命名为 skindata ,并且连同必要的图像文件一起位于皮肤目录中。窗口小部件的属性包括几何 XY 坐标、透明度,以及背景图像。清单 1 包含了节选自本文的示例皮肤之一的规格文件的内容。


清单 1. 皮肤规格文件节选

[main] 

image = main-complete.png 

transparent = FALSE 

border = TRUE 

border_left = 116 

border_right = 63 

[button_exit] 

image = btn-12-exit.png 

x = 252 

y = 2 

prelight = TRUE





欲了解关于这个文件的格式的完全描述,请参阅随本文的源代码提供的 SKIN-SPECS 文档。

SLIK 和 AWT/Swing

在构建 UI 时,务必记住 SLIK 接口是与 AWT(Abstract Window Toolkit,抽象窗口工具包)或 Swing GUI 工具包不兼容的。因而,您不能在同一个窗口中同时使用 AWT/Swing 小部件和 SLIK 小部件。然而,在 SLIK 窗口能够派生 AWT/Swing 窗口的意义上,SLIK 是可以和 AWT 或 Swing 共存的。不过,如果您遵循这个策略,应用程序的资源占用将会更多,因为您需要在系统上同时安装 AWT/Swing 和 GTK。


小部件概述

SLIK 提供了 WinAmp 风格的皮肤小部件,比如按钮、滑块、拨号盘、菜单、子窗口、标签和列表。正如您已经看到的那样,诸如图形、标签等小部件属性是从一个默认名为 skindata 的皮肤规格文件中读取的。其中一些小部件包括拨号盘旋钮(专门为多媒体播放器设计)、一个高度可定制的图形列表(通常用于播放列表)、菜单、按钮、标签和滑块。这些小部件的基本版本如图 4 所示。


这些抽象小部件能够加载远远优于系统的默认窗口管理器的特定图形样式。然而,许多小部件没有在媒体播放器中广泛使用,它们当前也不是小部件集的组成部分。本文后面的 图 5给出了这些增强的图形样式的一个例子。该图描绘了一个具有许多美妙图形的类似 PDA 外观的应用程序。

这个 API 提供的完整属性集可从包括在本文代码包中的 SKIN-SPECS 文件中找到(请参阅 参考资料)。


用 Java 语言编写的一个示例皮肤客户机

SLIK 的 JNI 接口能够为 Java 客户机提供同时在 Windows 和 UNIX/Linux 环境中创建皮肤对话框的能力。像使用传统 GUI 工具包来构建的类一样,带皮肤的客户端将监听高级事件,比如窗口的关闭、最小化或最大化。然而,这个基于 SLIK 的类需要 SLIK 专用的接口来监听任何带皮肤的小部件的这些事件。清单 2 说明了这些接口。(以下所有清单都取自 SkinDemo.java,这是本文与本文配套的代码包的一部分。请 单击这里来获得这个文件的彩色语法显示的完整清单。)


清单 2 SLIK 事件接口

import jni.skin.slik.*; 

import jni.skin.slik.evt.*; 

import jni.skin.slik.widget.*; 

import jni.skin.util.Debug; 

public final class SkinDemo implements // listen for: 

 WindowEventListener, // window evts: Window closed, mouse clicked, mouse dragged 

 ButtonEventListener, // Button widget clicks 

 ListEventListener, // List evts: row clicked, etc... 

 DialEventListener, // Dial widget drags 

 SliderEventListener, // Slider drags 

 MenuEventListener // Menu selection events 

{ 

 ...




C 层中生成的 GTK 小部件事件将通过 JNI 级联地返回到 Java 层。大多数小部件属性,比如标签、XY 位置以及图形,都是在皮肤规格文件中定义的。


加载皮肤数据文件

为了从磁盘加载皮肤规格文件,需要首先初始化 GtkSkin 类,然后注册您希望监听其事件的小部件,并加载该文件,如清单 3 所示。


清单 3. 从磁盘加载皮肤规格文件

/* 

 * Main function 

 */ 

public SkinDemo(String[] args) throws GtkSkinException 

{ 

 ... 

 // init gtk (create skin window) + JNI debug flag (TRUE/FALSE) 

 skin = new GtkSkin("My App", GtkSkin.GTK_TRUE); 

 // Init all widgets: for skin demo purposes (Widgets typically initialized when needed...) 

 skin.initAllWidgets(); 

 // Create and register some widgets 

 // See the SPEC file for details 

 // example: [button_exit], [list_playlist], [number_song], .... 

 // IMPORTANT: All widgets must exist in SPEC file 

 // Widget data (imgs, lbls, etc. are defined in the spec file 

 skin.registerWidget(new WText("title"), "Title goes here!"); 

 skin.registerWidget(new WButton("exit")); 

 skin.registerWidget(new WButton("iconify" 

 ... 

 /** 

 * Listen for Skin Window main evts: KeyPress, Mouse motion and Mouse BTN press 

 */ 

 skin.addWindowEventListener(this); 

 // load/show 

 skin.loadSkin(skinPath); 

 skin.show(); 

}




GtkSkin("My App", GtkSkin.TRUE) 调用将以调试模式初始化 GIMP 运行库。 之后,皮肤规格文件中定义的所有 widget 都必须注册,这样 Java 层才会监听诸如鼠标点击等事件。除了 widget 事件之外,应用程序还必须监听窗口事件,比如最小化、最大化和关闭窗口命令。最后, loadSkin(path) 方法调用将从磁盘读取一个给定的文件路径,而 show() 将显示该路径。


使用小部件:拨号盘、滑块和弹出菜单

拨号盘和滑块小部件对于设计诸如视频或音频播放器等多媒体应用程序的用户界面很有用。与所有 SLIK 小部件一样,诸如位置坐标、像素映射和字体等大多数属性都是在皮肤规格文件中定义的。Java 代码注册这些对象,以便从这些对象接收事件,如清单 4 所示。


清单 4. 拨号盘、滑块和弹出菜单

// dial widget 

// see [dial_position] on the skin file for attributes 

position = new WDial("position"); 

position.addDialListener(this); 

skin.registerWidget(position); 

// More documentation can be found on the SKIN-SPECS file 

// in the src distribution of this article 

// slider test: maps to [slider_volume] in skindata 

volume = new WSlider("volume"); 

volume.addSliderListener(this); 

skin.registerWidget(volume); 

// popup menu sample 

String [] menus = 

 { 

 // menu #1 

 "Menu 1" + WMenu.SUBMENU_DIVIDER + "Submenu-11" + WMenu.SUBMENU_DIVIDER + 

 "Submenu-12", 

 // menu #2 

 "Menu2" + WMenu.SUBMENU_DIVIDER + "Submenu21" + WMenu.SUBMENU_DIVIDER + 

 WMenu.MENU_DIVIDER + WMenu.SUBMENU_DIVIDER + "SubM31" , 

 WMenu.MENU_DIVIDER, 

 "Menu3", 

 "Menu4" 

 }; 


popup = new WMenu("popup", menus, skinHandle); 

popup.addMenuListener(this); 

skin.registerWidget(popup, menus); 

...



拨号盘小部件(广播调谐器/播放器的典型组件)应该具有一个类似如下的声明:

WDial position = new WDial("position")



这里, position 是皮肤数据文件( skindata )中定义的一个键,该文件包含诸如几何形状、图像等属性。之后, position.addDialListener(this) 调用将设置主类来监听由这个小部件激发的事件。 最后, skin.registerWidget(position) 将从 C 层级联调用 Java 层,然后返回。相同的方法序列也适用于所有 JSLIK 小部件。


探索源代码

本文已经提供了关于 SLIK 和 Java 平台在实践中如何协同工作的介绍。为了真正理解它究竟是如何工作的,您需要运行并研究源代码。随本文分发的代码包包含以下文件夹结构:

Slik_jni 文件夹结构 文件夹名称 文件类型 

classes 已编译的 Java 类 

myskins 示例皮肤 

native Java JNI SLIK 库 Win32/Linux 

src Java/C 源代码



还有两个提供用于测试目的的 shell 脚本:rundemo.bat 和 rundemo.sh。双击针对您的平台的相应脚本将会运行演示程序,并加载默认的皮肤,如图 5 所示。


按 Play 按钮将打开皮肤编辑器,您可以使用它来浏览或修改构成此皮肤的不同小部件。按 Back 按钮将启动打开文件对话框,您可以使用它来加载所提供的许多示例皮肤。

可以容易地使用自己最喜欢的 IDE 来编译该 Java 源代码。代码包中还提供了二进制版本,不过如果想要自己编译 C 代码,相应的编译说明已包括在这个项目的 README 文件中。本文中的代码已在以下平台上测试过:

Microsoft Windows 2000 和 XP。
Red Hat Linux 8.x 和 9,以及 SuSE Linux Workstation 8.x,两者都是基于 x86 的体系结构。