作者:Michael Powers
2004 年 6 月

下载项目源代码: midterm-fonts.zip

在本文中,我们将通过利用 MIDP 的呈现自定义字体的功能来增加一些修饰和对外观的一些急需的控制。

首先我们讨论对字体的内建支持的优势和限制,并且确定哪种应用程序可能需要自定义字体。然后介绍一下实现您自己的字体的技巧,并将其正确地应用于终端模拟器 MIDlet 中。

内置字体

使用 MIDP 的 Graphics 类,可以调用其 setFont() 来指定字体,然后调用该字体的一种 drawChar() drawString() 方法来在 canvas 或后台图像上呈现字体。MIDP 仅提供了一套有限的字体选项,但是:字体可以是等宽的或成比例的,大小可以是小、中或大,并且样式可以是平铺或粗体、斜体和下划线的任意组合。运行库实现工具可能没有满足 您的标准的字体,它会将自由返回其确定的最相近的字体。

大多数的应用程序都能容许这些限制。不管怎样,应用程序开发人员都希望将其溶入主机平台的外观和风格中。为使操作简单,MIDP 2.0 提供了一个新的 getFont() 方法,提供一般地用于绘制静态文本(如标题和标签)的字体,以及用于绘制用户在文本框中输入的字符的字体。您可以确信这些字体存在,尺寸正确并与应用程序的其余部分一致。当调节给定字体的高度、宽度和基线, 以及在运行时适当调整用户界面时,应用程序应能够在不同种类的设备上正确地运行。

然而一些应用程序需要对它们的外观进行更多的控制。例如,大多数游戏需要一种与其图形其他部分的风格相一致的字体。一些平台可能存在特定的问题:当需要等宽字体时可能返回 成比例字体,最小的字体可能太大了,并且一些字体可能不吸引人甚至无效。这些差异增大了创建和维护跨平台软件的难度。

MIDTerm 就是这种情况。我们需要确保其字体等宽的、足够小以便在屏幕上能绘制最多的行和列并且清楚可读,但在这一点上,绘制终端内容的 canvas 取决于设备实现。如果给定字体过大终端将不可用,并且如果字体不是等宽的,应用程序根本就不能工作。在我们的项目的这一点上,MIDP 模拟器返回的字体是等宽的且大小合适,但字体不是特别容易阅读。

实现自定义字体将提供所需的字体控制,以便增强对 MIDterm 可用性的信心。同样重要的是,因为不再依赖于每个平台的本地字体实现方式,其跨不同的 MIDP 设备的性能将更具可预见性。

完成后,就会看到改进:

Before: Built-in Font
After: Custom Font

创建自定义字体

字体有两个来源:

  • Outline 字体:包含绘制每一个字符所需的指令。这些指令是精确的、冗长的且复杂的,但它们允许操作系统以任何尺寸和任何样式来呈现字体。
  • Bitmap 字体:仅通知操作系统对于每个字符哪些像素处于开状态,哪些像素处于关状态。它们是简单明了,但对于所支持的字体的每一种大小和样式都需要一个独立的指令集。

移动设备有限的处理能力使其无法使用 outline 字体。这些设备自然要使用 bitmap 字体,也正是我们将使用的。操作系统呈现我们的实现时不像内置字体那样快,但为达到对字体外观的绝对控制,这样的时间牺牲能够为人们所接受。

bitmap 字体的绘制指令是位到图形环境中的位置的逐一映射。bitmap 字体中的每一个字符对应于从内存拷贝到屏幕的像素排列。换一种说法,每一个字符是通过拷贝到屏幕的一个后台图像来绘制的。从这个角度看,立即就能得到字体的定义(包含用于 呈现字体的指令的文件)本身就可以是一个压缩格式(如 PNG 或 JPEG)的图像文件。我们可以把这个图像读到内存中,然后在需要绘制我们的字体的字符时,将它的各有关部分拷贝到屏幕。

那么第一步就是要创建一个这样的图像。在台式计算机,使用您最喜欢的图像编辑器来创建一个新的文档。该文档的高度应当就是字体所需的高度,并且足够宽来容纳要呈现的字符集中的每一个字符。那是多宽呢?

Java 技术通常采用 Unicode Character Standard。Unicode 字符集有 95,000 多个字符,比我们能提供的要多,并远远超出我们的需要!因此 MIDTerm 的 Telnet 协议的实现方式是基于 7 位 ASCII 字符集的基础上,该字符集只包含 128 个字符。因为 MIDTerm 需要等宽字体,文档宽度应为一个字符宽度的 128 倍。例如,如果每一个字符高为 12 像素宽为 10 像素,为容纳所有的 128 个字符,则需要高为 12 宽为 1,280 的图像。

现在有了用于放置对应于 ASCII 代码 0 到 127 的 128 个位置。为了能查明哪个字符放到哪个 空位中,可能需要 ASCII 表 。请注意前 31 个字符是“控制字符”并且不能看到,可以保留为空白。

可以一个像素一个像素地地手动把每一个字符填充到槽中,但使用计算机中的某个字体的字符会更容易一些。确保这个字体是您自己的或有使用许可。我使用的是一种称为 Anonymous 的字体,具有十分清晰的 6-point 显示且免费使用。您可以从 Mark Simonson Studios 下载。

使用 Anonymous,我的字体图像看起来是这样的:

从定义来讲,因为一个位只有 on 和 off 两种状态,因此一个 bitmap 字体仅需要使用两种颜色。可以使用一种颜色来定义状态为“开”的像素,用另一种不同的颜色来定义 所有其他。由于 MIDP 2.0 设备需要支持透明的图像,应将图像编辑器的“关”的颜色标记为透明。透明的像素是不可见的,允许任何像素通过计算机中已有的应用程序 canvas 绘制。

您可能想把图像文件存储为 PNG 格式。一个单“位”的周围区域颜色一致的图像恰是这种图像,这正是基本 PNG 压缩算法所为之设计的。您的字体图像文件将会压缩得非常紧密:Anonymous 的 PNG 文件的大小只有 803 个字节大小。

自定义字体类

现在我们需要编写一些软件来读取图像文件并将其各部分呈现在屏幕上,我们将用到叫作 CustomFont 的一个新的类。

为了使 CustomFont 易于学习和使用,应尽可能地使其具有像标准 MIDP Font 类一样的外观和风格。由于为了使实现者可使用原生代码进行优化,Font 已经被声明为 final,因此不能为其定义子类,但 我们能够模拟其公共界面。Font 的每个方法在 CustomFont 中也有,并且只有静态工厂方法 getFont() 具有不同的签名,它采用文件名字来替代惯用的字体类型:

public static CustomFont getFont( 
String inName, int inStyle, int inSize );

在许多 MIDP 应用程序中,通常通过调用 setFont() 方法来将一种字体传递到 Graphics 对象,然后调用该对象的 drawChars() drawString() 方法来呈现文本。但这里我们不能采用同样的模式。因为 CustomFont 不是 Font 的子类,我们不能 将其传递到 Graphics.setFont() 。相反,CustomFont 提供了公共的方法来绘制字符,这些方法模拟 Graphics 类的方法,每一种都用 Graphics 实例作为其第一个参数。

public void drawChar( 
Graphics g, char character,
int x, int y, int anchor );

public void drawChars(
Graphics g, char[] data,
int offset, int length,
int x, int y, int anchor );

public void drawString(
Graphics g, String str,
int x, int y, int anchor );

public void drawSubstring(
Graphics g, String str,
int offset, int length,
int x, int y, int anchor );

使用 CustomFont 的应用程序需要修改其绘制代码来调用自定义字体类的这些方法,而不是 Graphics 对象的相应方法。

现在让我们看一下字符位图怎样进入屏幕。可考虑两种方法。

显而易见的方法是把源图像分解成为 128 个的图像,一个图像对应一个字符,如下所示:

...
images = new Image[ 128 ];
for ( int i = 0; i < 128; i++ )
{
images[i] = Image.createImage(
image, i*width, 0, width, height, 0 );
}
...

从外部资源创建的图像认为是不可变的。因为像素不能改变,运行库能够优化内存分配。使用 createImage() 方法从不可改变的图像创建的图像,也不能够改变。因此,当有 128 个小图像和一个大图像时,可以期望运行库足够职能,能够注意到二者都是不可修改的,并且在内存中只保存位图的一份拷贝。即使这样做,128 个图像实例的纯系统开销也是一个小型设备难于应付的。

另一种方法是只用一个大图像并像 IBM Selectric 打字机的连动球(typeball) 一样使用。作为当时的革新,连动球(typeball)用球面上排列的提升起来的字符取代了用于每个字符的单个连动杆(typebar)。连动球根据每一次按键转动且倾斜,使 Selectric 达到令人惊叹的速度(每秒钟几乎15个字母)。连动球可能早已废弃不用,但我们可以借用它的设计思想来使 MIDTerm 更具有高效率。为了绘制字符,点住图像区域,左右移动图像以便使所需的字符出现在剪辑区内,然后绘制图像。每一个 Graphics 实例始终保持一个剪辑区,并且只有影响剪辑区的命令才真正执行。在这个例子中,可以借助运行库来仅绘制适合于剪辑区的像素。方法如下:

public void drawChar( 
Graphics g, char character, int x, int y, int anchor )
{
int clipX = g.getClipX();
int clipY = g.getClipY();
int clipW = g.getClipWidth();
int clipH = g.getClipHeight();

g.setClip( x, y, width, height );
g.drawImage(
image, x - width*character, y, anchor );

g.setClip( clipX, clipY, clipW, clipH );
}

这种方法的不利之处在于不得不在改变剪辑区前记住现存的剪辑区,以使能够返回到  Graphics 创建时的状态。每一次调用绘制方法之前都要存储剪辑区,显著的代价是:每次必须进行 4 次调用并分配 4 个 int。

我们陷入常见的时间-空间的权衡问题中:为节省内存,CustomFont 占用了更多的处理器时间,使用剪辑来取代对每一个字符分配单个图像实例。在内存小的设备上,运行慢总不根本不能运行要强得多。

初始化时,有一项小任务 CustomFont 必须完成,这就是计算字体的基线。在字体渲染中,与字体的高度和宽度一样,字体的基线也是另一个重要的决定因素,因为它定义了一个字体的字符位于一个文本行的哪个位置。如果一 个文本行有不同的字体,需要将其基线对齐,以使其出现在同一个水平线上。

我们可以改变 CustomFont.getFont() 来要求调用者指定所选字体的基线,但有一种更好的方法。MIDP 2.0 使我们能够动态检测基线。getRGB() 方法使我们可直接访问一个图像的单个像素,并且一个简单的探试就能确定哪行像素就是基线。

...
// determine background color: assume it's at 0, 0
image.getRGB( row, 0, 1, 0, 0, 1, 1 );
background = row[0];

// here's the heuristic: find the row on the bottom
// half of the image with the most non-background pixels
for ( int y = height/2; y < height; y++ )
{
total = 0;
image.getRGB( row, 0, imageWidth, 0, y, imageWidth, 1 );
for ( int x = 0; x < imageWidth; x++ )
{
if ( row[x] != background ) total++;
}
if ( total > max )
{
max = total;
result = y;
}
}
...

CustomFont 假定图像中左上角像素为背景颜色。CustomFont 计算图像的下半部分中的每一行的非背景像素,并且判断具有最多前景像素的那一行作为基线。在运行时确定基线,使您在创建自定义字体位图时无需为此事担忧。

动态样式设置

Font.getFont() and CustomFont.getFont() 的签名的第一个参数有所不同:标准的方法采用字体类型,而这里的方法采用的是图像文件的名称;但是二者都接受大小和样式参数。CustomFont 忽略了所要求的字体大小,因为如果对每一个字体大小都在内存中提供一个独立的图像的话,代价太高,并且如果试图在运行时缩放位图,就可能造成最终结果难以辨识。但是,自定义字体类确实 试图尊重所要求的样式,途径是采用了一些简单而高效的技术,这些技术改变了它在运行时绘制字符图像的方式。

最简单的是下划线。就是在基线下两个像素的位置,在字符下画一条线。

...
if ( ( style & Font.STYLE_UNDERLINED ) != 0 )
{
g.drawLine(
x, y + baseline + 2, x + width, y + baseline + 2 );
}
...

+加粗只是稍微复杂了一点。为了加粗字符,CustomFont 多绘制了一次字符,在右侧加一列像素。因为背景是透明的,因此两行像素重叠使字符加黑。

...
if ( ( style & Font.STYLE_BOLD ) != 0 )
{
// draw an additional time, one pixel to the right
g.drawImage(
image, x - width*character + 1, y, anchor );
}
...

做斜体字时,CustomFont 使用剪辑来将字符的上半部分绘制得向右移动一个像素。效果出人意料地好。

...
if ( ( style & Font.STYLE_ITALIC ) != 0 )
{
g.setClip( x + 1, y, width, height/2 );
g.drawImage(
image, x - width*character + 1, y, anchor );
g.setClip( x, y+height/2, width, height/2 );
g.drawImage(
image, x - width*character, y, anchor );
}
...

下面是实际中的样式:

STYLE_PLAIN
STYLE_UNDERLINED
STYLE_BOLD
STYLE_ITALIC

在绘制特定颜色的字符时会稍难一些。当 Graphics 用当前颜色渲染字体时,CustomFont 只能把像素拷贝到整个的字体图像中,因此字符以原始图像的采用的非透明的颜色出现。

MIDP 2.0 提供了一种从 getRGB() 返回的像素创建新图像的方法。您可以复制一个字符的像素,扫描并修改每一个非背景像素的颜色,建立新的图像,并将该图像绘制屏幕上;但这种 方法要占用大量的处理器资源而明显地很慢。如果事先知道需要哪种颜色,在创建字体时可以在前景中使用该颜色。MIDTerm 采用是黑色背景,因此整个字体图像含有白色字符,背景则是透明的。

使自定义的字体工作起来

因为 CustomFont 与 MIDP 的 Font 类非常接近于一致,MIDTerm 的 TelnetCanvas 类仅需要很少的改动就可以实现自定义字体的优势。

Using MIDP Fonts Using Custom Font
font = Font.getFont( 
Font.FACE_MONOSPACE,
Font.STYLE_SMALL,
Font.SIZE_PLAIN );
font = CustomFont.getFont( 
"/mono.png", // the font bitmap file
Font.SIZE_SMALL, // ignored
Font.STYLE_PLAIN ); // no styling
g.setFont( font );
g.drawChar( (char) b,
insetX + x*fontWidth,
insetY + y*fontHeight,
g.TOP | g.LEFT );
 
font.drawChar( g, (char) b,
insetX + x*fontWidth,
insetY + y*fontHeight,
g.TOP | g.LEFT );

进行这几项改动后,应用程序会像以前一样运行,而且更出色:更为清晰(如果运行稍慢一点),并且在多种的 MIDP 实现方面更具有可预见性。您同样可以在其他应用程序中经过少量改动或不改动而使用 CustomFont。

结束语

我们采用了一种简单而高效的技术来为 MIDP 应用程序提供了对文本绘制的绝对控制。您可以使用简单的图像编辑器来创建属于自己的字体,并且,使用 MIDP 2.0 的图像和图像功能,可以以可接受的性能在屏幕上呈现您的字体。您可以设置自己所希望的字体样式,甚至是在运行时是动态地设置。对于 MIDTerm ,自定义字体改进了可读性和可用性,并减少了确保应用程序在多种平台上顺利运行所需的工作。

参考文献

这里再次列出本文中的链接。

 关于作者:Michael Powers 是 mpowers LLC (一家台式和无线平台的软件顾问公司)的主席。自从 Java 技术出现以来,他一直在其多个方面进行工作。他屡获殊荣的 Piranha Pricecheck MIDlet 风靡全球,可以从 mpowers.net 免费下载。