作者:Davanum Srinivas
Java 技巧 86:JDK1.3 中的本地绘制支持 -- JavaWord
在不牺牲本地 GUI 代码性能的条件下获得可移植性并缩短开发周期
(本文选自IBM developerWorks中文网站)
摘要
假如您正在使用 JDK1.3,则您可以借助于 Java 2 AWT 本地接口,使用 C 或 C++ 方法来绘制基于 AWT 的 Canvas 对象。因此,本地 GUI 组件可以嵌入到 Java 应用程序中;同时,正像 JDK1.3 以前的其他版本一样,您可以使用 Java 本地接口从 Java 应用程序中调用本地方法。Davanum Srinivas 解释了如何在 Java 中使用现有的本地用户界面库。他还提供了 Win32 操作系统下的一个具体示例。
在 JDK1.3 出现以前,您仅能将 Java 本地接口用于非用户界面的工作。JDK 1.3 引入了新的 Java 2 AWT 本地接口,这使您可以在 Java 程序中使用非 Java 的 GUI 组件,尽管这样做会失去纯 Java 解决方案的可移植性。在使用 J2AWT 时,您必须针对要使用它的每个平台制作本地动态连接库或共享库。
下面这段话摘自 JDK1.3 的某个头文件,它说明了这种新的 API 的开发背景及原因:
AWT 支持使用本地 C 或 C++ 应用程序访问 AWT 的本地结构。这是为了便于将原有的 C 或 C++ 应用程序移植到 Java 并满足需要 ... [这些应用程序] 出于性能方面的原因在画布上自行进行本地绘制
在 JDK1.3 以前,Java 编程没有明确的方法来访问基层的同等 GUI 组件的句柄。在 JDK 1.3 中, Sun 公司创建了一种标准机制,通过这种机制,开发人员可以使本地 GUI 应用程序和库在 Java AWT Canvas 对象中进行绘制。这意味着现在有一种正式的、有保证的方法来获得支持这一功能的信息。当 JDK 1.3 与其他操作平台对接时,所有的接口都提供相同的信息 -- 而不管使用的是什么系统。JDK 1.3 的 Windows 版本和 Solaris 版本是首先提供这种支持的实现。
Sun 公司引入这一功能组件有几方面的原因。首先, JDK 1.3 使得人们可以将依靠第三方产品的复杂原有软件移植到 Java 上,而不必等到第三方产品本身完成移植以后。第二个原因即性能;假如本地的 GUI 代码经过人们长时期的努力得到优化,则原样保留这些软件具有重要的商业价值。
在本文中,我将介绍一些该功能部件的基本概念。我将逐步开发一个窗口小部件样例,该窗口小部件使用Win32 API 进行绘制。下图是最终的窗口小部件的快照,一个带有笑脸的圆形窗口。
运行中的窗口小部件
分步概览
第一步,定义一个 Java 类 -- 比如说,Mywindow -- 使其继续 Canvas 类并重载 paint 方法。您使用 paint 方法执行 AWT 对象的绘制操作,并在覆盖该方法时加上 native 要害字。覆盖方法使您能够使用自己的本地代码。您必须构建自己的本地代码并把它编译成一个动态连接库,就像我们处理其他的 Java 本地接口应用程序一样,在本例中,我们将调用 MyWindow.DLL 库。在 Solaris 和 Linux 上则为共享对象或共享库。您还需要用 System.loadLibrary("MyWindow") 调用将 MyWindow.DLL 库加载到您的名为 MyWindow 的 Java 类中。
完成这一示例需要二个部分:其一是 MyWindow.Java ,它提供 Canvas 类的子类,其二是 MyWindow.CPP ,它包含基于 Java 本地接口的绘制子程序的入口点。 在参考资源部分可找到 MyWindow.Java、MyWindow.CPP 及自动执行编译的批处理文件 BUILD.BAT。
第一步: 创建 MyWindow Java 类
J2AWT 用于这种方法时有一个主要的局限性:本地代码只能对 java.awt.Canvas 类的子类进行操作。这正是 MyWindow 继续 Canvas 类的原因。在 Java 应用程序中,您可以像使用 Canvas 的其它子类那样使用 MyWindow;在本例中,我将 MyWindow 添加到 Jwindow 中。
import java.awt.*;
import javax.swing.*;
public class MyWindow extends Canvas {
static {
//加载包含 paint 代码的库。
System.loadLibrary("MyWindow");
}
//绘制操作的本地入口点
public native void paint(Graphics g);
public static void main( String[] argv ){
Frame f = new Frame();
f.setSize(300,400);
JWindow w = new JWindow(f);
w.setBackground(new Color(0,0,0,255));
w.getContentPane().setBackground(new Color(0,0,0,255));
w.getContentPane().add(new MyWindow());
w.setBounds(300,300,300,300);
w.setVisible(true);
}
}
请注重:您是在静态块中加载 MyWindow.DLL。这正是 Java 应用程序访问本地代码的方式。(我稍候就会开发这段本地代码。)同时还应注重:paint 方法是用 native 要害字声明的,并且没有提供任何实现;这样做是为了让虚拟机知道,应该从在静态块中加载的动态连接库中调用该本地方法。
第二步:生成该类的 JNI 头文件
要为以上定义的类生成 Java 本地接口头文件,需使用 javah MyWindow.class 命令。首先应确保这个类文件在您的 CLASSPATH 中。以下是所生成的 MyWindow.h 的一部分,给出了函数声明。
/*
* Class: MyWindow
* Method: paint
* Signature: (Ljava/awt/Graphics;)V
*/
JNIEXPORT void JNICALL Java_MyWindow_paint
(JNIEnv *, jobject, jobject);
第三步:开发完整的 MyWindow.CPP
以下是完整的 MyWindow.CPP,其中包含 MyWindow.Java 中所需要的绘图程序的本地代码。
#include <windows.h>
#include <assert.h>
#include "jawt_md.h"
#include "MyWindow.h"
#define X(x) (int)(xLeft + (x)*xScale/100) // 缩放宏
#define Y(y) (int)(yTop + (y)*yScale/100) // 以使尺度在 0-100 之间
#define CX(x) (int)((x)*xScale/100)
#define CY(y) (int)((y)*yScale/100)
void DrawSmiley(HWND hWnd, HDC hdc);
HRGN hrgn = NULL;
JNIEXPORT void JNICALL
Java_MyWindow_paint(JNIEnv* env, jobject canvas, jobject graphics)
{
JAWT awt;
JAWT_DrawingSurface* ds;
JAWT_DrawingSurfaceInfo* dsi;
JAWT_Win32DrawingSurfaceInfo* dsi_win;
jboolean result;
jint lock;
// 获取 AWT
awt.version = JAWT_VERSION_1_3;
result = JAWT_GetAWT(env, &awt);
assert(result != JNI_FALSE);
// 获取绘图界面
ds = awt.GetDrawingSurface(env, canvas);
if(ds == NULL)
return;
// 锁定绘图表面
lock = ds->Lock(ds);
assert((lock & JAWT_LOCK_ERROR) == 0);
// 获取绘图表面的信息
dsi = ds->GetDrawingSurfaceInfo(ds);
// 获取特定平台的绘图信息
dsi_win = (JAWT_Win32DrawingSurfaceInfo*)dsi->platformInfo;
HDC hdc = dsi_win->hdc;
HWND hWnd = dsi_win->hwnd;
//////////////////////////////
// !!! 在此处进行绘图 !!! //
//////////////////////////////
if(hrgn == NULL)
{
RECT rcBounds;
GetWindowRect(hWnd,&rcBounds);
long xLeft = 0; // 用于缩放宏
long yTop = 0;
long xScale = rcBounds.right-rcBounds.left;
long yScale = rcBounds.bottom-rcBounds.top;
hrgn = CreateEllipticRgn(X(10), Y(15), X(90), Y(95));
SetWindowRgn(GetParent(hWnd),hrgn,TRUE);
InvalidateRect(hWnd,NULL,TRUE);
} else {
DrawSmiley(hWnd,hdc);
}
// 释放绘图表面的信息
ds->FreeDrawingSurfaceInfo(dsi);
// 为绘图表面解锁
ds->Unlock(ds);
// 释放绘图表面
awt.FreeDrawingSurface(ds);
}
void DrawSmiley(HWND hWnd, HDC hdc)
{
RECT rcBounds;
GetWindowRect(hWnd,&rcBounds);
long xLeft = 0; // 用于缩放宏
long yTop = 0;
long xScale = rcBounds.right-rcBounds.left;
long yScale = rcBounds.bottom-rcBounds.top;
// 基于控制大小的画笔宽度
int iPenWidth = max(CX(5), CY(5));
HBRUSH brushBlack;
HBRUSH brushYellow;
HPEN penBlack = CreatePen(PS_SOLID, iPenWidth, RGB(0x00,0x00,0x00));
// 用于绘制填充椭圆的空画笔
HPEN penNull = CreatePen(PS_NULL, 0, (COLORREF)0);
brushBlack = CreateSolidBrush(RGB(0x00,0x00,0x00));
brushYellow = CreateSolidBrush(RGB(0xff,0xff,0x00));
HPEN pPenSave = (HPEN)SelectObject(hdc, penBlack);
HBRUSH pBrushSave = (HBRUSH)SelectObject(hdc,brushYellow);
Ellipse(hdc,X(10), Y(15), X(90), Y(95)); // 头部
Arc(hdc,X(25), Y(10), X(75), Y(80), // 嘴部(微笑)
X(35), Y(70), X(65), Y(70));
SelectObject(hdc,&penNull); // 无绘图宽度
SelectObject(hdc,&brushBlack);
Ellipse(hdc,X(57), Y(35), X(65), Y(50));
Ellipse(hdc,X(35), Y(35), X(43), Y(50)); // 右眼
Ellipse(hdc,X(46), Y(50), X(54), Y(65)); // 鼻子
SetBkMode(hdc,TRANSPARENT); // 使用前景颜色
SelectObject(hdc,pBrushSave);
SelectObject(hdc,pPenSave);
}
这里的要害数据结构是 JAWT,它是在 jawt.h 中定义的(通过 jawt_md.h 包含在内)。它使程序可以访问本地代码在基于 Java 的 GUI 组件上绘图所需的所有信息。本地方法的第一部分是套式:置入 JAWT 结构,获得一个 JAWT_Win32DrawingSurfaceInfo 结构,锁定表面(请一次只使用一种绘图工具!),然后,获取一个 JAWT_DrawingSurfaceInfo 结构,该结构包含特定平台下绘图所必需的指针(在 platformInfo字段中)。它也包含绘图界面的矩形界限框及当前剪切区域。有关具体信息,请查看 jawt.h 和 jawt_md.h (请参阅下面标题为 “构建环境”的部分)。
Java_MyWindow_paint 是一个入口点,JVM 通过调用它来绘制 MyWindow。辅助函数 DrawSmiley 使用 Win32 调用来完成实际的绘制工作。要在您的应用程序中包含 GetDrawingSurfaceInfo,请使用外部库 jawt.lib(请参阅 “构建环境”)。
第四步:编辑 BUILD.BAT
在运行 BUILD.BAT 之前首先对它进行编辑,并像如下所示的那样,为您的 Visual C++ 及 JDK1.3 设置路径。BUILD.BAT 对 MyWindow.java 进行编译,生成 MyWindow.h,然后将 MyWindow.CPP 编译为 MyWindow.DLL。
SET DEVSTUDIO=D:Program FilesMicrosoft Visual StudioVC98
SET JDK13=D:JDK1.3
好了,一切预备就绪。在运行该样例之前,请确保 MyWindow.DLL、JDK1.3BIN 及 JDK1.3JREBIN 都在 PATH 内,还要保证当前目录在 CLASSPATH 中;这将确保 MyWindow.class 会被成功加载。在确信 PATH 和 CLASSPATH 都设置妥当后,在命令行输入 java MyWindow 来运行此应用程序。为方便您的使用,window.zip 中包含了一个批处理文件 RUN.BAT(请参阅参考资源)。要为 JDK 1.3 设置PATH 和 CLASSPATH,请编辑 RUN.BAT。
构建环境
头文件:在 JDK 的 include 目录中新增了专用于 Windows 的 C 头文件。它们是:
include/jawt.h.
include/win32/jawt_md.h.
依据 JavaSoft 网站的说明,这些头文件并不是 Java 2 平台正式规范的组成部分;提供这些头文件只是为希望用一种标准化方法访问本地绘图功能的开发人员提供一种便利。我认为这表示将 JDK 移植到其它平台的厂商可以不提供这个 API。
库:一个以 jawt.lib 命名的新库已添加到 SDK 的库目录中。如前所述,这个库包含一个用于把 J2AWT 包含到您的应用程序中所需要的入口点。例如,要链接到 GetDrawingSurfaceInfo 入口点,您需要在您的程序中包含 jawt.lib。
工具:javah 工具用来为 Java 类的本地函数生成 C/C++ 头文件,javac 工具用来编译 Java 源文件。
小结
将原有软件系统移植到 Java 中并不轻易,尤其是当原有软件包含高性能的绘图器时。Java 2 AWT 本地接口使得分阶段移植变得较为轻易,它答应您首先移植对性能要求不高的代码,然后再移植要害的绘制代码。它同时使第三方窗口小部件开发厂商更能严厉地看待针对 Java 产品的开发。有了 Java 2 AWT 本地应用程序接口,您就可以移植原有的 GUI 代码,并更快地完成开发,这样就不会牺牲您为提高本地代码要害部分的性能而作的投资。
作者简介
Davanum Srinivas 目前在 Computer Associates 公司从事软件开发工作。
参考资源
本压缩文件包含本文章涉及的所有源代码和一个批处理文件:
window.zip
Sun 公司对 AWT 本地接口的介绍:
http://java.sun.com/prodUCts/jdk/1.3/AWT_Native_Interface.Html
SUN 公司在 Java 2 SDK,版本 1.3 中所作的增强:
http://java.sun.com/products/jdk/1.3/docs/guide/awt/enhancements.html
Sun 公司对本地绘图 JAWT 接口的改进,post-Kestrel(只需免费注册):
http://developer.java.sun.com/developer/bugParade/bugs/4281429.html
"Enhance your Java application with Java Native Interface (JNI)," TalLyron(JavaWorld,1999 年 10 月):
http://www.javaworld.com/javaworld/jw-10-1999/jw-10-jni.html
"Java Tip 23: Write native methods," John D. Mitchell (JavaWorld):
http://www.javaworld.com/javaworld/javatips/jw-javatip23.html