GUI开发可能是一项艰巨的任务。 GUI框架的文档记录并不总是很好,所需的代码量可能会快速增长,从而减慢了开发流程。 最重要的是,支持这些GUI框架的拖放工具和IDE通常可以引导GUI软件开发人员创建难以管理且难以阅读的代码。 这会进一步模糊业务逻辑和描述GUI的代码之间的界线,从而使软件维护更加困难。
这是说明性UI语言变得很方便的地方。 UI语言描述的是“什么”,而不是“如何”。 例如,HTML描述的是显示的内容,而不是用于呈现内容的呈现功能。 通过不使用声明性语言指定“方式”,控制流也被省去了。 尽管这种损失听起来像是一种限制,但由于消除了诸如修改全局状态(例如变量)或调用其他函数或方法之类的副作用,它成为一种优势。 选择声明性语言还具有将UI代码与应用程序代码分离的好处。 这种解耦可以带来未来的好处,例如在项目角色和团队角色之间明确区分,甚至可以减少将业务逻辑与多个视图或视图技术集成的成本。
如今,使用了许多声明性XML UI的示例。 使用GNOME桌面环境的Linux®和UNIX®操作系统具有Glade。 Microsoft®Windows®用户具有可扩展应用程序标记语言(XAML),该语言支持多种功能,包括在XML中插入代码。 Adobe®Flex®Framework的MXML格式描述了Adobe Shockwave(SWF)播放器的GUI,还包括代码插入。 请参阅相关主题的更多信息的链接。
Java技术中的基本声明性UI框架的一组要求可能是:
考虑到这些要求,是时候创建声明性XML了。
清单1中的XML格式的第一次尝试展示了一个简单的窗口,一个面板和一个按钮。 清单1中的属性代表了基本必需的属性,例如坐标,尺寸和引用单个内存中组件的唯一标识符。
声明性的XML UI将XML元素映射到Java Swing框架 ,该框架提供了最大的可移植性,因为Swing保证在所有当前Java运行时环境中都可用。 许多Swing组件将具有XML格式内的代表性XML元素。
该框架使用XML模式。 XML模式允许在模式实例内强制执行指定的顺序,基数和数据类型。 这个很重要; 框架将期望以指定的顺序指定类型的一组XML元素。 清单2演示了XML模式实例中层次结构的初始元素和属性。
详细查看架构。 首先,XML声明必须位于所有内容之前,甚至必须位于XML建议中指出的空格和注释之前。 接下来, schema
元素包含其他元素:
elementFormDefault="qualified"
声明所有元素必须具有名称空间-前缀或默认名称空间。 targetNamespace="http://xml.bcit.ca/PurnamaProject/2003/xui"
指定目标名称空间URI。 xmlns:xs="http://www.w3.org/2001/XMLSchema"
)。 xmlns:xui="http://xml.bcit.ca/PurnamaProject/2003/xui"
标识另一个名称空间及其附带的前缀。 在XSD中使用名称空间很重要:消除名称空间冲突。 当来自两种或多种XML格式的两个或多个元素具有相同的名称时,就会发生名称空间冲突 。 这种冲突会引起对对其相应标签集感兴趣的任何应用程序的困惑。 通过使用名称空间和随附的名称空间前缀,可以完全避免此问题。
接下来,根级别数据类型元素XUI
指出:
Window
元素的一个序列,最后允许一个Resource
元素。 这两个都是稍后在架构实例中找到的引用元素。 id
属性,该属性是必需的,并且必须为anyURI
类型。 XUI
元素(可能)包含许多Window
元素,尽管基于minOccurs
属性中值为0的XUI
元素可能没有Window
元素。 至于Resource
元素:
xs:sequence
元素为空,因此它的内容模型为空。 type
属性,从XSD的定义类型( token
)创建派生的简单类型,其中限制方面是enumeration
,允许枚举的java
和groovy
文字文本值。 Resource
元素的目的是向Java框架提供资源的URI(在本例中为JAR),该URI包含可在运行时加载并绑定到的已编译Java类。 此资源依赖于将被调用的特定类( class
属性的值),本质上提供了一个公开类,该类将响应从GUI生成的所有事件。
Window
元素:
GridLayout
序列,在BasicDialog
, OpenFileDialog
, SaveFileDialog
, CustomDialog
, Panel
, SplitPane
和TabbedPane
元素之间无限选择,最后包含零个或一个MenuBar
。 xs
前缀)。 Window
可以包含许多不同的顶层和中间层容器。 Window
元素引用GridLayout
元素。 GridLayout
元素指定组件可以占据的单元格网格的尺寸。 GridLayout
提供了与Java环境中的java.awt.GridBagLayout
相似的布局功能,只是没有所有的复杂性。
不用再看了,很明显XML Schema实质上是表达性的。 清单3显示了更多元素。
...
...
请注意,没有存储易失性状态信息,仅存储可能有助于GUI组件重建的状态信息。 一个示例是CustomDialog
元素的状态信息:
Panel
元素数 Panel
是一个中间容器,并允许包含大量的原子成分。 返回清单3 , Panel
仅有一个GridLayout
并且可以选择不将任何原子组件放置在Panel
内部或根据需要添加任何原子组件。 Panel
本身具有x和y坐标。 但是, Panel
使用桌面上的像素(如CustomDialog
那样),而是使用x和y坐标来引用父容器的GridLayout
。 就像俄罗斯的娃娃一样,这种嵌套的构图非常类似于Swing的布局规则。 有了所有这些功能之后,就该解决软件实现了。
让我们从所提议的Java框架的概述开始。 清单4中的代码显示了应用程序程序员创建应用程序必须遵循的步骤。
try {
// Gain access to a XUI builder through factory
// In this framework the term XUI is going to represent the custom DOM
XUIBuilder builder = XUIBuilderFactory.getInstance().getXUIBuilder(); // (1)
// Validate and parse (unmarshal) the XML document
builder.parse("browser.xml"); // (2)
// Build a custom DOM
XUI xui = builder.getXUIDocument(); // (3)
// Create 1:1 GUI component mapping to custom DOM
xui.visualize(); // (4) (5)
// Create bindings to data model (i.e. JAR file from Resource element)
xui.bind(); // (6)
// Get root node from the XUI document
XUINode root = xui.getRoot();
// Save a copy of the DOM to file (marshal)
xui.marshalXUI("browser-marshalled.xml");
} catch (XUIParseException xpe) {
xpe.printStackTrace();
} catch (XUIBindingException xbe) {
xbe.printStackTrace();
} catch (IOException ioe) {
ioe.printStackTrace();
}
清单4中的步骤定义了功能的明确分离,并允许进一步完善框架的组件。 可视化该流程的尝试如图1所示。 尽管代码演示了两个额外的步骤( 图1中每个带圆圈的数字与清单4中每个注释的数字都一致)(检索对XUI
根节点的引用并将DOM编组到文件中)。 这些步骤是:
图1说明了以下步骤
Builder
从检索BuilderFactory
。 Builder
首先确保XML文档已经过验证和解析。 如果解析或验证失败,则将发生XUIParseException
,并且框架将中止文档加载。 Builder
创建DOM,对象在其中反映已读入的XML元素。 XUI
对象内部调用的Realizer
对象被实例化,并准备好执行下一步。 这个六步调用流程易于使用,但包含大量消息和内部的对象实例化,接下来值得探讨。 该框架的核心是步骤5和6 。
在图1中 , 步骤5创建了一个组件模型。 这允许将XML节点(现在是内存中的对象)与GUI组件配对。 此配对需要以下事件的非常严格的同步:
XUINode
(代表任何XML元素的内存对象), XUIComponent
必须创建一个XUIComponent
来包含XUINode
。 XUIComponent
,必须创建一个GUI对等项,例如javax.swing.JFrame
。 XUIComponent
实例(或其子类型之一,例如XUIButton
)时(例如,更改尺寸时), XUIComponent
都会确保XUINode
和GUI对等体同时被同等地更新。 通过满足上述要求,该框架允许程序员阅读(取消编组)XML文档,修改DOM,并将更改保存回XML文档(编组)。 程序员甚至可以以编程方式创建新的DOM并将其封送。
为了使XUINode能够将自己封送为XML,提供了toString
方法的自定义实现(在清单5中 )。 根节点可以包含许多子节点。 每个子节点可以包含自己的子节点集,依此类推。 通过调用根级节点的toString
方法,框架可以轻松封送整个XML文档。 添加了名称空间,并使每个元素了解其在层次结构中的level
(通过level
变量)。 这样,在调用toString
方法时,它提供了缩进,以便于手动读取这些文档。
toString
方法实现 @Override
public String toString() {
StringBuffer sb = new StringBuffer();
String namespacePrefix = "";
// insert indenting ... 2 spaces for now.
if(isRoot) {
sb.append(XMLPI + "\n");
sb.append(API_COMMENT + "\n");
} else {
sb.append("\n");
for(int s = 0; s < level; s++) {
sb.append(" ");
}
}
sb.append("<");
// get namespaces for this node
Enumeration keys = nameSpaces.keys();
String names = "";
while(keys.hasMoreElements()) {
String uri = (String)keys.nextElement();
String prefix = (String)nameSpaces.get(uri);
/* if its the xsi namespace (XML Schema Instance),
* ignore it, this isn't part of that namespace but it is
* needed for the XML Schema validator to work. */
if(!(prefix.equals("xsi"))) {
sb.append(prefix + ":");
namespacePrefix = prefix;
}
names += (" " + "xmlns:" + prefix + "=\"" + uri + "\"");
}
if(beginOfNamespace) {
sb.append(name + names);
} else {
sb.append(name);
}
// do attributes if there are any
if(attributes.getLength() > 0) {
int length = attributes.getLength();
for(int i = 0; i < length; i++) {
String attributeValue = attributes.getValue(i);
String attributeQName = attributes.getQName(i);
sb.append(" " + attributeQName + "=\"" + attributeValue + "\"");
}
}
sb.append(">");
sb.append(cdata);
int size = childNodes.size();
for(int i = 0; i < size; i++) {
XUINode e = (XUINode)childNodes.get(i);
sb.append(e.toString());
}
if(size > 0) {
sb.append("\n");
for(int s = 0; s < (level); s++)
sb.append(" ");
}
if(namespacePrefix.length() > 0) {
sb.append("" + namespacePrefix + ":" + name + ">");
} else {
sb.append("" + name + ">");
}
return sb.toString();
}
另一部分值得探讨是容器类型XUIWindow
,这是一个间接子类型的XUIComponent
。 XUIWindow
实现表示一个javax.swing.JFrame
组件,因此必须允许将子组件添加到布局中。 清单6演示了实现。 第一步是确保只能将某些类型的组件添加到XUIWindow
。 如果是这样,则XUIComponent
的DOM节点表示形式XUINode
,以便访问该组件的属性。 请注意,这要求所有XUIComponent
的构造函数初始化这些值。
进一步检查以确保该组件是中间容器(例如XUIPanel
),并且该中间容器适合XUIWindow
的行和列网格。 最后,该组件可以被添加到XUIWindow
确保组件已启用,在布局网格内的正确位置设置,并且XUIWindow
的XUINode
(的win
变量)被赋予新的子组件的参考XUINode
- addChildNode()
调用。
addComponent
方法的实现 public void addComponent(XUIComponent component) throws XUITypeFormatException {
if(component instanceof XUIBasicDialog
|| component instanceof XUIOpenFileDialog
|| component instanceof XUICustomDialog
|| component instanceof XUIMenuBar
|| component instanceof XUIPanel
|| component instanceof XUISplitPanel
|| component instanceof XUITabbedPanel
|| component instanceof XUISaveFileDialog) {
// get the node
XUINode node = component.getNodeRepresentation();
if(!(component instanceof XUIMenuBar)) {
int x = Integer.parseInt(node.getAttributeValue("x"));
int y = Integer.parseInt(node.getAttributeValue("y"));
int width = Integer.parseInt(node.getAttributeValue("width"));
int height = Integer.parseInt(node.getAttributeValue("height"));
// can't add dialogs so need to check for type here.
if(component instanceof XUIBasicDialog
|| component instanceof XUIOpenFileDialog
|| component instanceof XUICustomDialog
|| component instanceof XUISaveFileDialog) ; // nothing
else {
// check to make sure it fits within the grid.
Dimension localGrid = this.getGrid();
if(width > localGrid.getWidth() || height >
localGrid.getHeight()) {
throw new XUITypeFormatException(node.getName()
+ " (id: " + node.getAttributeID()
+ ") must be within this window's grid width and"
+ "height (w: " + localGrid.getWidth()
+ " + h: " + localGrid.getHeight() + ")");
}
Rectangle rect = new Rectangle(y, x, width, height);
component.getPeer().setEnabled(true);
frame.getContentPane().add(component.getPeer(), rect);
// for mapping components to the regions they occupy
childComponentMappings.put(component, rect);
}
component.setComponentLocation(x, y);
} else {
// do specifics for a menubar
frame.setJMenuBar((JMenuBar)component.getPeer());
}
frame.invalidate();
frame.validate();
// add the component's node
int level = win.getLevel();
node.setLevel(++level);
if(win.getParent() == null)
win.addChildNode(node);
} else {
StringBuffer sb = new StringBuffer();
sb.append("Type not supported in XUIWindow. ");
sb.appen("The following types are supported:\n");
for(int i = 0; i < supportedComponents.size(); i++) {
String s = (String)supportedComponents.get(i);
sb.append("- " + s + "\n");
}
throw new XUITypeFormatException(sb.toString());
}
}
值得检查的代码的最后一个领域是运行时绑定的处理。 调用XUI
对象的bind
方法时,将调用BindingFactory
的实例。
BindingFactory
的doBinding
方法(在清单7中 )必须做几件事才能将模型代码绑定到构造的GUI:
JarURLConnection
类JarURLConnection
JAR,并使用自定义且独立的类加载器加载类。 Resource
元素的class
属性名称匹配的class
。 该类是模型的入口点。 init
方法。 init
方法在概念上与典型Java类的main
方法相似,因为它们都是入口点。 BindingFactory
的doBinding
方法 public void doBinding(XUINode resource, XUI xui) throws XUIBindingException,
MalformedURLException, IOException {
if(resource.getAttributeValue("type").equals("java")) {
String className = resource.getAttributeValue("class");
String aURLString = resource.getAttributeValue("uri");
URL url = null;
// get the url ... if it's not a valid URL, then try and grab
// it as a relative URL (i.e. java.io.File). If that fails
// re-throw the exception, it's toast
try {
url = new URL("jar:" + aURLString + "!/");
} catch (MalformedURLException mue) {
String s = "jar:file://" + new File(aURLString)
.getAbsolutePath().replace("\\", "/") + "!/";
url = new URL(s);
if(url == null) {
// it really was malformed after all
throw new
MalformedURLException("Couldn't bind to: "
+ aURLString);
}
}
// get a jar connection
JarURLConnection jarConnection = (JarURLConnection)url.openConnection();
// get the jar file
JarFile jarFile = jarConnection.getJarFile();
// jar files have entries. Cycle through the entries until finding
// the class sought after.
Enumeration entries = jarFile.entries();
// the class that will be the entry point into the model
JarEntry modelClassEntry = null;
Class modelClass = null;
XUIClassLoader xuiLoader =
new XUIClassLoader(this.getClass().getClassLoader());
while(entries.hasMoreElements()) {
JarEntry remoteClass = (JarEntry)entries.nextElement();
// load the classes
if(remoteClass.getName().endsWith(".class")) {
// have to get the second last word between period marks. This
// is because the convention allows for:
// org.purnamaproject.xui.XUI
// that is, the periods can represent packages.
StringTokenizer st =
new StringTokenizer(remoteClass.getName(), ".");
String previousToken = st.nextToken();
String currentToken = "";
String nameOfClassToLoad = previousToken;
while(st.hasMoreTokens()) {
currentToken = st.nextToken();
if(currentToken.equals("class"))
nameOfClassToLoad = previousToken;
else {
nameOfClassToLoad += currentToken;
}
}
// get an output stream (byte based) attach it to the
//inputstream from the jar file based on the jar entry.
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream is = jarFile.getInputStream(remoteClass);
final byte[] bytes = new byte[1024];
int read = 0;
while ((read = is.read(bytes)) >= 0) {
baos.write(bytes, 0, read);
}
Class c = xuiLoader.getXUIClass(nameOfClassToLoad, baos);
// check for the class that has the init method.
if(remoteClass.getName().equals(className + ".class")) {
modelClassEntry = remoteClass;
modelClass = c;
}
} else {
String imageNameLowerCase = remoteClass.getName().toLowerCase();
if(imageNameLowerCase.endsWith(".jpeg")
|| imageNameLowerCase.endsWith(".jpg")
|| imageNameLowerCase.endsWith(".gif")
|| imageNameLowerCase.endsWith(".png")) {
// add resources (images)
XUIResources.getInstance().addResource(remoteClass, jarFile);
}
}
}
// now instantiate the model.
try {
// create a new instance of this class
Object o = modelClass.newInstance();
// get the method called 'init'. This is part of the API
// requirement
Method m = modelClass.getMethod("init", new Class[] {XUI.class});
// at last, call the method up.
m.invoke(o, new Object[] {xui});
} catch(InstantiationException ie) {
ie.printStackTrace();
} catch(IllegalAccessException iae) {
iae.printStackTrace();
} catch(NoSuchMethodException nsm) {
nsm.printStackTrace();
} catch(InvocationTargetException ite) {
System.out.println(ite.getTargetException());
ite.printStackTrace();
}
} else {
throw new XUIBindingException(
"This platform/API requires Java libraries.");
}
}
在研究了该框架的机制之后,是时候将该框架用于测试驱动并展示其中一个示例应用程序了。
项目框架(请参阅下载 )包含几个示例。 Web浏览器示例的功能相当详尽。
此示例提供了一个合理的实际示例,说明您可能希望将其放置在声明性XML UI文档中。 如清单8所示 ,主Window
具有指定的x和y坐标以及一个id
值。 所有元素必须具有用于业务逻辑的唯一ID值,才能引用这些组件。
Window
元素包含几个子元素,包括:
Panel
OpenFileDialog
SaveFileDialog
用于保存当前查看的网页 CustomDialog
CustomDialog
Window
顶部并提供菜单项功能的MenuBar
Resource
所包含的组件(例如Button
)的所有坐标都引用网格内的位置。 所包含组件的所有尺寸是指每个组件在网格中有多少个单元格宽和高。 组件的定义是高度声明性的,因为它们定义属性,而不是有关如何使用或创建这些属性的逻辑。 本文档中的其他一些兴趣点是:
MenuItem
可以具有快捷键,例如Ctrl-X可以退出应用程序。 Window
具有对话框,但是默认情况下,这些对话框在用户调用它们之前是不可见的。 Panel
)必须具有布局,并且必须指定该布局中的行数和列数。
html
htm
html
htm
http://www.w3c.org
http://www.agentcities.org
http://www.apache.org
http://www.gnu.org
当然,接下来没有用户交互,这都没有任何价值。
在清单8中 , Resource
元素包含充当应用程序模型入口点的类的名称。 给定的名称是BrowserModel
,因此在Java端,已编译类的名称必须匹配。 这包括名称空间,在本例中为默认名称空间。
因此,任何类都可以充当应用程序模型部分的入口点,只要其名称与Resource
元素的class
属性值相同即可。 为了在运行时正确地进行用户交互,实现类必须遵循其他几条规则:
public void init(XUI document)
。 XUIButton
实现的ActionModel
)。 id
值来引用GUI组件。 (这可以使用XUI
类中提供的几种不同方法来完成。) XUIButton
类实现)都实现XUIEventSource
,因此会生成UI事件。 在清单9中 , BrowserModel
类在init
方法中执行其初始化。 这包括通过id
值获取对组件的引用,创建包含Web URL书签的菜单项以及通过addEventListener
方法将其自身作为组件的侦听器添加。 所述BrowserModel
本身可以添加为监听器,因为它是一个XUIModel
( ActionModel
是一个子类型的XUIModel
)。 还值得一提的是, XUIComponentFactory
类提供了许多创建XUI组件的方法。
...
import org.purnamaproject.xui.binding.ActionModel;
...
public class BrowserModel implements ActionModel, TextModel, WindowModel,
ListActionModel {
...
private XUI xui;
...
public void init(XUI document) {
xui = document;
...
bookmarksList = (XUIList)xui.getXUIComponent("list_0");
homeButton = (XUIButton)xui.getXUIComponent("button_1");
...
List bookmarks = bookmarksList.getItems();
for(int i = 0; i < bookmarks.size(); i++) {
String url = (String)bookmarks.get(i);
XUIMenuItem aMenuItem = XUIComponentFactory.makeMenuItem(url);
bookmarksMenu.addMenuItem(aMenuItem);
linkModel.addSource(aMenuItem);
aMenuItem.addEventListener(linkModel);
}
...
homeButton.addEventListener(this);
...
}
...
}
深入研究, 清单10展示了各种组件的事件处理代码。 例如:
openMenuItem
将导致出现一个fileDialog
(一个模式对话框,用于打开本地存储的Web页面)。 homeButton
和popuphomeMenuItem
(在窗口中单击鼠标右键以访问它们)都调用doHome
方法,该方法将浏览器定向到HypertextPane
元素的uri
属性值(来自清单8 )。 fileDialog
将加载一个新文件,然后递增应用程序queue
使用的index
变量,以跟踪以前访问的网页。 public void action(XUIComponent component)
{
if(component == openMenuItem) {
fileDialog.setVisible(true);
} else if(component == homeButton || component == popuphomeMenuItem) {
doHome();
} else if(component == prevButton || component == popupprevMenuItem) {
doPrevious();
} else if(component == nextButton || component == popupnextMenuItem) {
doNext();
} else if(component == fileDialog) {
if(fileDialog.getSelectedFile() !=null)
hyperTextPane.setURL(fileDialog.getSelectedFileAsURL());
index++;
if(index != queue.size()) {
nextButton.setEnabled(false);
popupnextMenuItem.setEnabled(false);
for(int i = index; i < queue.size(); i++) {
queue.remove(i);
}
}
queue.add(hyperTextPane.getURL());
prevButton.setEnabled(true);
popupprevMenuItem.setEnabled(true);
} else if(component == saveDialog) {
try {
FileOutputStream fos = new FileOutputStream(saveDialog.getSelectedFile());
hyperTextPane.getDocument().writeTo(fos);
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
} catch (IOException ioe) {
ioe.printStackTrace();
}
} else if(component == popupsaveasMenuItem || component == saveMenuItem) {
saveDialog.setVisible(true);
} else if(component == popupbookmarkMenuItem || component == bookmarkMenuItem) {
doBookmark(hyperTextPane.getURL());
} else if(component == notDontExit) {
exitDialog.setVisible(false);
browserWindow.setVisible(true);
} else if(component == yesExit) {
System.exit(0);
} else if(component == exitMenuItem) {
exitDialog.setVisible(true);
} else if(component == manageBookmarksMenuItem) {
bookmarksDialog.setVisible(true);
}
}
最终应用程序( 图2中 )演示了一个基本的Web浏览器,该浏览器使您可以显示本地页面,基于Web的页面和以前访问过的Web页面,以及管理书签的功能。
您可以在本文的下载中找到其他几个示例应用程序。
尽管该解决方案令人兴奋,但该方法相当理想:该框架内的安全问题已被忽略。 回忆一下API如何从任何URI无害地加载JAR文件。 回想一下清单8中所示的Resource
元素。 类型实际上是anyURI
。 这意味着本地文件,网络上的文件,Internet上的文件。 任何地方。 应用程序应该从任何地方信任业务逻辑吗? 显然,您想考虑某种安全模型来限制不可信资源的加载。 解决此问题的一种方法是限制URI,是引用查找表。 另一种(更清洁的)解决方案是使用数字证书。
最后,考虑在此声明性XML UI格式中加载其他XML格式。 由于需要使用名称空间,因此XML Schema支持此功能。 例如,您可以嵌入单独的XML格式来表示XML文档中的可缩放矢量图形。
本文介绍了一种声明性XML UI语言以及它的外观。 它引入了随附的Java框架以及示例应用程序-Web浏览器。 最后,它带来了潜在的安全问题和担忧。
创建声明性XML UI确实不是什么新鲜事。 但是,它是软件开发领域中日趋成熟并变得越来越普遍的领域。 加号之一是创建声明性XML UI有助于促进软件重用和模块化。
翻译自: https://www.ibm.com/developerworks/java/library/x-decxmlui/index.html