Lightweight User Interface Toolkit (LWUIT) 为 Java ME UI 开发人员带来了许多令人印象深刻的功能。Style(风格)、Theme(主题)和 Painter 正是这样三种功能,它们可以方便开发非常吸引人且独立于设备的视觉元素。在本文中,我们将介绍如何使用它们,并探讨一些细微的问题。
我们已经使用 Sprint Wireless Toolkit 3.3.1 开发了演示应用程序。此工具包不仅提供了对 LWUIT 的良好支持,它还提供了许多非常有趣的设备仿真器,比如 HTC Touch 和 Samsung Instinct 的仿真器。如果您希望尝试使用 LWUIT,我强烈建议您在计算机上安装 Sprint WTK。
Style(风格)的概念是 theming(主题)构建的基础。Style 的本质是集中定义每个组件的视觉特征。除了其物理设计(比如其形状),窗口小部件的外观可以根据若干共同属性来定义:
Background and foreground colors(背景色和前景色):每个组件有四个颜色属性:两个用于背景,两个用于前景。当组件准备好激活时,该组件被认为处于选中状态。当按钮接受焦点时,例如,它处于选中状态,可以通过单击操作来激活它。一个组件可以使用两种背景颜色来分别表示选中状态和未选中状态。同样,也可以根据两种状态分别定义前景色(通常是用于组件上的文本的颜色)。
Text fonts(文本字体):可以使用该平台支持的标准字体样式显示文本,也可以使用 bitmap 字体。每个组件的字体可以通过其风格对象设置。
Background transparency(背景透明度):组件背景的透明度可以设置为从完全不透明(默认设置)到完全透明。整数值 0 对应完全透明,而 255 对应完全不透明。
Background image(背景图片):默认情况下,组件的背景没有任何图片。但是,该设置可用于指定用作背景的图片。
Margin and padding(边距和填充):组件的可视布局(源自 CSS Box 模型)定义边距和填充。图 1 显示了 LWUIT 环境中术语 margin(边距)和 padding(填充)的意思。注意 content area(内容区域)用于显示基本内容,比如文本或图片。Style
(风格)允许分别设置四个方向(顶部、底部、左边和右边)的 margin(边距)和 padding(填充)。
Background painter(背景 painter):特殊用途的 painter 对象可用于自定义一个或一组组件的背景。
Style
类代表在应用程序中使用的每个组件的这些属性的集合,并且拥有相应的存取(accessor)方法。另外,当特定组件的 style 对象被更改时,该类还可以通知已注册的侦听器。
当组件被创建时,一个默认的 Style
对象会与之关联。对于任何重要应用程序,都需要修改视觉属性。一种方法是使用单独的 setter 方法。如果,例如,组件(thiscomponent
)的背景色必须更改为红色,则可以使用以下代码:
thiscomponent.getStyle().setFgColor(0xff0000);
第二种修改默认风格设置的方法是创建一种新的风格,然后将其挂接到相关的组件。Style
类拥有允许指定大多数属性的构造函数。以下代码片段设置了一个组件的新风格。
Font font = Font.createSystemFont
(Font.FACE_SYSTEM,Font.STYLE_BOLD,Font.SIZE_LARGE);
byte tr = (byte)255;
Style newstyle = new Style
(0x00ff00, 0x000000, 0xff0000, 0x4b338c, font, tr);
thiscomponent.setStyle(newstyle);
该代码设置了新的前景和背景色、文本的字体以及背景透明度。此处使用的构造函数格式如下:
Style(int fgColor, int bgColor, int fgSelectionColor,
int bgSelectionColor, Font f, byte transparency)
该构造函数还有另外一种形式,除了上述属性之外,它还允许设置图片。该构造函数不支持的属性将需要通过各自的 setter 方法来设置。
最后,通过使用 主题,还可以设置组件中所有类(比如,应用程序中的所有标签)的视觉属性,详见下文。
现在,我们应该实现了一个简单的显示效果。接下来,我们介绍如何使用风格来指定组件的外观。我们的应用程序将提供一个组合框,如图 2 所示:
图 2. 一个简单的组合框
此处显示的该组合框的所有属性都是默认值。唯一的例外是前景选择颜色,必须更改该颜色,以提高选中项的可见性。同样,包含该组合框的窗体只修改了一个属性——它的前景色。以下代码显示了如何创建该窗体:
.
.
.
//create a form and set its title
Form f = new Form("Simple ComboBox");
//set layout manager for the form
//f.setLayout(new FlowLayout());
//set form background colour
f.getStyle().setBgColor(0xd5fff9);
.
.
.
代码的头两行是不言自明的,AWT/Swing 开发人员应该非常熟悉。第三行设置窗体的背景色属性。
该组合框也使用类似的方式来实现:
// Create a set of items
String[] items = { "Red", "Blue", "Green", "Yellow" };
//create a combobox with String[] items
ComboBox combobox = new ComboBox(items);
ComboBox
是 List
的子类,需要使用支持的数据结构。这里我们使用一个字符串数组来代表该数据结构。
准备好组合框后,我们要更改其前景选择颜色,以提高可读性。所以像对窗体所做的一样,我们写了如下一行代码:
combobox.getStyle().setFgSelectionColor(0x0000ff);
但是,当我们编译代码并运行的时候,结果非常让人吃惊——前景色保持不变!它对窗体行得通,那为什么对组合框行不通呢?为了解释这个问题,我们需要记住 LWUIT 的基本结构。像 Swing 一样,LWUIT 是围绕 MVC 概念设计的。所以呈现组件的实体和组件本身逻辑上是分离的。同样,组合框的呈现对象需要是 Component
的子类,这表示它将拥有其自己的风格
。每个组合框创建时都有其默认的呈现器,它是 DefaultListCellRenderer
的一个实例。当绘制组合框时,使用的风格属于该呈现器,这就是在组合框的 style
对象中设置前景选择颜色行不通的原因。为了使该设置生效,我们必须修改呈现器的 Style
对象:
//set foreground selection colour for
//the default combobox renderer
//this will work
DefaultListCellRenderer dlcr =
(DefaultListCellRenderer)combobox.getRenderer();
dlcr.getStyle().setFgSelectionColor(0x0000ff);
这一次,当编译该代码时,就可以正常工作了。
在以上部分,我们看到了如何为组件设置单个视觉属性。在有大量 UI 组件的应用程序中,为每个组件设置属性将会是一项庞大的任务,而且容易出错。Theme
(主题)允许我们(在一个地方)设置组件的所有类的属性。这不仅简化了为某个特殊类型的所有组件设置属性的任务,而且可以确保任何新添加的组件和应用程序中所有其他同类组件看起来一样。因此 Theme
跨越应用程序的所有屏幕建立了一致的视觉效果。
Theme
是由 key-value 对(其属性为 key,其值为相应的值)组成的列表。列表中的任何项都类似以下格式:
Form.bgColor= 555555
该项指定应用程序中所有窗体的背景色为 RGB 格式的 (hex) 555555。每个主题都被打包为一个 resource(资源)文件,它也可以承载其他项,像用于字体的图片和位图。LWUIT 下载束包含一个 resource editor(资源编辑器),它提供了一种简单的方法来创建主题,并将其打包为一个资源文件。该编辑器可以在该束的 util 目录中找到。双击图标启动它,该编辑器将会如下所示打开。Resource Editor 也集成到了 Sprint WTK 3.3.1 中,可以通过选择 File -> Utilities -> LWUIT Resource Editor 来访问,如图 3 所示。
图 3. Resource Editor
要创建一个主题,双击左窗格上的 +
按钮,则会打开一个用于输入主题名称的对话框。如图 4 所示。
图 4. 创建新主题
当您单击 OK 时,新主题的名称会显示在左窗格中。单击此主题标签,在有窗格中会出现一个空白主题,如图 5 所示。
图 5. 空白主题
要填充该空白主题,单击 Add 按钮,则会打开 Add 对话框。您可以在此对话框顶部的组合框中选择一个组件和属性。在图 6 中,选中的组件是一个窗体,选中的属性是背景色。该颜色的 RGB 值可以作为十六进制字符串输入提供的空白处。您也可以单击该空白旁边的颜色盒来输入颜色值。这会打开一个 color chooser(颜色选择器),选中颜色的值可以直接从中输入对话框中。
图 6. 将项添加到主题
单击 OK 按钮,该项会出现在主编辑器窗口的右窗格。注意:可以通过使用相应的按钮编辑或删除项。所有项都完成后,您可以通过选择 File -> Save As 将其保存。如果您使用的是 Sprint WTK,那么应用程序的资源文件必须位于其 res 文件夹。
既然我们已经知道了如何创建主题,下面我们来看一个展示其使用的演示。本部分的演示也有组合框,但是比我们刚才看到的将更加精美。图 7 展示了此演示屏幕。注意:现在该窗体有一个背景图片,并且组合框是围绕复选框构建的。同样,标题栏(在窗体顶部)和菜单栏(在底部)也有与默认颜色(白色)不同的背景颜色。
在查看引起外观差异的主题前,让我们快速检查一下用于制作该屏幕的代码。
//initialise the LWUIT Display
//and register this MIDlet
Display.init(this);
try
{
//open the resource file
//get and set the theme
Resources r = Resources.open("/SDTheme1.res");
UIManager.getInstance().
setThemeProps(r.getTheme("SDTheme1"));
}
catch (java.io.IOException e)
{
//if there is a problem print a message on console
//in this case default settings will be used
System.out.println
("Unable to get Theme " + e.getMessage());
}
//create a form and set its title
Form f = new Form("ComboBox Example");
//set layout manager for the form
f.setLayout(new FlowLayout());
//create two sets of items
String[] items = { "Red", "Blue", "Green", "Yellow" };
String[] items2 =
{"Sky", "Field", "Ocean", "Hill", "Meadow"};
//create two comboboxes with these items
ComboBox comboBox = new ComboBox(items);
ComboBox comboBox2 = new ComboBox(items2);
//create new instances of CbPainter
//and set them to combo boxes
//so that a checkbox will be
//the basic building block
CbPainter cbp = new CbPainter();
comboBox.setListCellRenderer(cbp);
CbPainter cbp2 = new CbPainter();
comboBox2.setListCellRenderer(cbp2);
//add the two combo boxes to the form
f.addComponent(comboBox);
f.addComponent(comboBox2);
//create an "Exit" command and add it to the form
f.addCommand(new Command("Exit"));
//set this form as the listener for the command
f.setCommandListener(this);
//show this form
f.show();
<!-- co: the passive voice in the last sentence makes things confusing. if all components should have this method called, then who calls it? does UIManager.setThemeProps() call it on all existing components, or do I have to keep references to all my components and call refreshTheme() on each one or what? ca --><!-- co: I agree this is badly expressed. I have changed the text. bs -->
最开始的时候,我们看到如何从资源文件中提取主题。然后为 UIManager
实例设置该主题。这里我们已经安装了开始的主题。但是当匆忙设置该主题后,屏幕上窗体的一些组件可能会看不见,而且在这些组件上设置主题的效果无法预测。为了确保即使看不见的组件也适当地更新了它们的风格,您应该调用 refreshTheme
方法:
Display.getInstance().getCurrent().refreshTheme();
窗体和组合框是和 前一部分 中的例子一样创建的。没有代码向此演示添加视觉效果,因为所有属性都是在主题
中指定的。这里不同的是,我们设置了我们自己的呈现器,而不是通过默认的呈现器来绘制组合框。这在代码中用突出的部分来显示。这些自定义呈现器使组合框看起来不一样。
呈现器本身非常简单。它所要做的是就是实现接口 ListCellRenderer 中指定的方法。由于我们想要我们的组合框封装复选框,该呈现器扩展了 CheckBox
。DefaultLookAndFeel
类的 drawComboBox
方法使用该呈现器获得用于绘制组合框的组件。在这种情况下,这样获得的组件是一个复选框,如以下代码所示。
//objects of this class will be used to paint the combo boxes
class CbPainter extends CheckBox implements ListCellRenderer
{
public CbPainter()
{
super("");
}
//returns a properly initialised component
//that is ready to draw the checkbox
public Component getListCellRendererComponent
(List list,Object value,int index,boolean isSelected)
{
setText("" + value);
if (isSelected)
{
setFocus(true);
setSelected(true);
}
else
{
setFocus(false);
setSelected(false);
}
return this;
}
//returns the component required for drawing
//the focussed item
public Component getListFocusComponent(List list)
{
setText("");
setFocus(true);
setSelected(true);
return this;
}
}
组合框可以具有多种样式,而不仅仅是纯列表或复选框。他可以围绕一些其他的标准组件,或者甚至是有自己独特外观的新组件来构建。图 8 展示了一个拥有单选按钮呈现器的组合框
图 8. 具有单选按钮呈现器的组合框
<!-- co: what app is figure 9 taken from? the sprint developer tools? ca --><!-- co: This the theme as seen on Resource Editor. I have expalined here. bs -->要查看定义我们的演示的外观的主题,您将需要在计算机上安装 Resource Editor。启动 LWUIT 附带 或者集成到 Sprint Toolkit 中的 Resource Editor。打开 Resource Editor 后,选择 File -> Open 找到并打开资源文件。Resource Editor 将在左窗格的 Themes 下面显示 SDTheme1。单击 SDTheme1 将在右窗格中显示该主题的详细信息,如图 9 所示。
图 9. 该演示的主题
首先要注意的一点是,在顶部有一个项是以白色文字显示的。所有这样的项都是默认的。在我们的例子中,唯一特定于组件的字体设定是用于软件按钮的——左下角的 Exit 按钮。窗体标题和组合框字符串的字体没有定义。这些字体将根据默认设置呈现。
在我们较早的例子中,我们看到文本的选择颜色必须在呈现器中设置。在当前的例子中,我们知道呈现实际上是由复选框呈现器完成的。所以已经为复选框定义了背景和前景色,实际上,呈现文本的颜色和文本背景(都有接受焦点和不接受焦点两种状态)是根据这些定义。如图 10 所示。
图 10. 前景和背景颜色
在上图中,我们可以看到复选框透明度值 127 的效果(半透明)。由于该设置,下拉列表中的三个未选中的项呈中灰色。您可以更改该值,看看这些背景如何改变。顺便说以下,当您更改主题时,没必要重新构建应用程序。只需保存资源文件并单击 Run。
新主题安装好后,除了通过使用 Style
类的某个 accessor 方法手动更改的那些属性(之前 讨论过的)外,所有的可用的风格都会更新。但是,如果您想要新的主题对那些甚至手动更改的属性有效,那么使用 Style
中的某个 setter,它有两个参数,第二个是一个布尔变量。例如:
setFgColor(int fgColor, boolean override);
如果当属性被手动更改时,该布尔参数被设置为 true
,那么在新主题中指定的值也将覆盖手动设置的值。代码如下所示:
thiscomponent.getStyle().setFgColor(0xff0000, true);
Painter
界面允许组件的背景按您所想的显示。回忆一下我们关于风格的讨论,我们看到其中一个属性是 background painter(背景 painter)。在这部分,我们将看到如何使用一个简单的背景 painter。
参考我们的演示截屏,绘制文本的背景颜色无法通过风格或主题更改。当我们分析组合框(如图 11 所示)的结构以及呈现它的顺序时,原因变得很清楚。
图 11. 组合框的结构
当组合框需要重新绘制时(比如,由于它刚接受了焦点),以下事件就会按顺序进行。
现在我们看到第一步之后,没有重新绘制组合框背景。所以这部分仍为完全透明的层,显示窗体的背景。您可以在主题中更改窗体的背景色,并且您将看到此颜色也将变为组合框的背景颜色。
如果我们现在想要组合框背景有一个不同的颜色(或者图案、图片),我们需要使用 Painter
。我们应该看到一个简单的 painter 的样子,以及如何使用它。
public class ComboBgPainter implements Painter
{
private int bgcolor;
public ComboBgPainter(int bgcolor)
{
this.bgcolor = bgcolor;
}
public void paint(Graphics g, Rectangle rect)
{
//probably redundant
//but save the current colour anyway
int color = g.getColor();
//set the given colour
g.setColor(bgcolor);
//get the position and dimension
//of rectangle to be drawn
int x = rect.getX();
int y = rect.getY();
int wd = rect.getSize().getWidth();
int ht = rect.getSize().getHeight();
//draw the filled rectangle
g.fillRect(x, y, wd, ht);
//restore colour setting
g.setColor(color);
}
}
该代码非常简单——它所做的是,使用传递给构造函数的颜色绘制一个填充的矩形。该矩形在 rect
定义和位置和方向绘制。
我们现在要做的是,将该 painter 钩挂到需要描绘其背景的组合框。我们通过在实例化两个组合框的代码后添加突出显示的行来完成。注意:将只描绘一个组合框的背景。
//create two comboboxes with these items
ComboBox combobox = new ComboBox(items);
ComboBox combobox2 = new ComboBox(items2);
//set the painter
combobox.getStyle().setBgPainter
(new ComboBgPainter(0x4b338c));
图 12 显示左侧的组合框的背景已经按预期进行了绘制。如果还希望描绘另一个组合框的背景,我们将使用同一个 painter。实际上,我们可以创建 painter 的实例,然后在所有组合框中设置同一实例。
图 12. 绘有背景的组合框
我们已经了解了如何借助 LWUIT 平台,使用 Style
、Theme
和 Painter
来创建一组有视觉吸引力的统一化组件。最近 LWUIT 已经开放了源代码。详细研究这些源代码将会是一种令人陶醉的体验,而且可以帮助您合理地利用该库并实现丰富的用户体验。