Introduction to UIL(第23章)

本章对用户界面语言(UIL)和Motif资源管理器(Mrm)进行了基本介绍。本章描述了UIL和Mrm,讲解了如何使用它们,并探讨了使用它们创建用户界面的优缺点。此外,本章还展示了一个"Hello, World"应用程序,旨在让您对如何同时使用UIL和Mrm来开发基于Motif的应用程序有一个基本的了解。

在本章中,我们将介绍OSF/Motif用户界面语言(UIL)和Motif资源管理器(Mrm)。首先,我们会解释UIL和Mrm的用途及功能。然后,通过详细讲解传统的"Hello World"应用程序,描述UIL模块和使用Mrm的程序的结构。这个示例将让您大致了解如何用UIL描述用户界面以及如何用Mrm创建用户界面。在很大程度上,我们会侧重于整体情况,而对底层细节的讨论将留到第24章"使用UIL创建用户界面"中进行。

在所有关于UIL的章节中,我们假设您已经熟悉X工具包(Xt)的基础知识以及本书前面讨论的Motif编程概念。至少,您必须理解窗口小部件的创建过程和窗口小部件资源的概念。(Xt编程在第四卷《X工具包内部编程手册》中有介绍,您可以在第五卷《X工具包内部参考手册》以及第六卷B《Motif参考手册》中分别找到关于Xt和Motif的参考资料。)

23.1 Overview of UIL and Mrm(UIL和Mrm概述)

UIL是一种基于文本的语言,用于描述由Motif(及其他)控件组成的用户界面。与C程序类似,UIL描述是一个纯文本文件,您可以使用标准编辑器对其进行编辑。然而,不同于支持循环和条件语句等动态结构的结构化编程语言,UIL是一种严格意义上的静态描述语言。它旨在与Motif控件集和数据类型配合使用,不过您也可以融入其他基于Xt的控件。UIL文件看起来有点像C语言,因为大括号被广泛用于分组。

Mrm是一个C例程库,它读取已编译的UIL文件,以便在运行时创建用户界面。Mrm包含用于打开和关闭已编译的UIL文件、创建控件、检索值(如字符串和图标)以及声明回调的函数。大致来说,UIL描述是Mrm所"管理"的"资源",这也是其名称的由来。

23.1.1 Using UIL and Mrm(使用UIL和Mrm)

使用UIL的应用程序由一个或多个UIL源模块和应用程序源代码组成。一个UIL模块包含描述应用程序用户界面的组件声明。应用程序源代码通常用C语言或其他高级语言编写,它用于创建用户界面并实现应用程序的功能。根据自身需求,应用程序可能还会使用其他类型的文件。

使用UIL和Mrm的过程包括三个主要步骤:

  • 在一个或多个UIL文件中描述用户界面。该描述针对的是文本编辑器。编写此描述需要定义界面的控件层次结构,并指定资源和回调设置。(一些用户界面构建工具也会生成界面描述,但其操作超出了本书的范围。)
  • 使用UIL编译器编译这些文件。该编译器会解析并验证这些文件。如果没有错误,编译器会为每个UlIL源文件生成一个用户界面描述(UID)文件。UID文件类似于编程语言中的目标文件,它包含界面的二进制描述,供Mrm读取。
  • 在运行时使用Mrm创建用户界面。在进行标准的Xt初始化调用后,应用程序只需调用几次Mrm即可创建并显示界面。Mrm通过调用Xm、Xt和X库中的例程,几乎处理了创建界面的所有工作。
  • 该图展示了UIL和Mrm如何与您的应用程序以及Xm、Xt和X库相适配。
  • 使用UIL和Mrm创建用户界面

23.1.2 Advantages and Disadvantages of UIL(UIL的优缺点)

  • 在决定是否在应用程序中使用UIL之前,要考虑该语言的优缺点。接下来的两个部分将讨论支持和反对使用UIL的理由。UIL提供了一种相对简单的语法,用于根据小部件层次结构来指定用户界面。无论是新手还是有经验的Motif程序员,都能快速学会UIL语法。相比之下,要学会创建代码界面所需的所有Motif和Xt函数调用则更加困难且耗时。此外,由于UIL模块只是一个静态的界面描述,它不受小部件创建顺序约束的影响,而这种约束通常会影响通过编程方式创建的小部件的布局。

    UIL编译器提供的全面错误检查也能加快开发速度,并有助于确保应用程序更健壮。编译器了解每个小部件支持的资源和回调、每个资源可以设置的值的类型,以及每个小部件允许的子部件(如果有的话)。你所犯的任何错误都会在运行前被发现。对于用C代码创建的界面,大部分此类错误检查都无法实现。尽管ANSIC编译器会检查每个函数调用的语法,但你可能会设置不被支持的小部件资源,或者在不支持该子部件(或任何子部件)的小部件父级下创建子部件。其中一些错误会在运行时被捕获,但在大多数情况下,需要你自己去发现界面在外观或行为上何时出现异常。

  • UIL的复杂程度远低于C这样的动态语言。因此,编译UIL中的接口描述所需的时间,只是编译和链接完全用代码创建的同类接口所需时间的一小部分。设计-编译-测试周期所需的时间大幅缩短。通过将UIL用作原型设计工具,你可以尝试多种备选接口,而不会在那些你不使用的接口上浪费太多时间。

    如今市面上的许多图形用户界面构建器(UIB)都能读写UIL文件,如果你目前正在使用这类构建器,或者将来可能会使用,这会是一个优势。UIL的语法足够简单,这些产品可以读取任何UIL文件,包括不是用该构建器编写的文件。然而,目前还没有工具能够读取用任意C代码创建的接口。大多数构建器会生成应用程序代码,但与UIL文件不同,你无法修改生成的C代码并让UIB导入它。

    使用UIL对应用程序进行国际化往往比仅用C语言编写的应用程序国际化更容易,因为界面中使用的几乎所有字符串都存储在应用程序关联的UIL模块中。对应用程序进行国际化,只需将依赖于语言的字符串隔离在单个模块中,并为应用程序支持的每种语言编写该模块的单独版本即可(第26章的#suili 18n部分会介绍这种技术的示例)。如果你决定使用UIL,就必须花些时间学习如何使用它。这对那些已经知道如何用C语言创建接口的有经验的Motif和Xt程序员来说主要是个问题。对于完全不熟悉Motif的人来说,使用UIL通常能更快上手,因为需要学习的内容比对应的C语言接口要少。虽然UIL试图模仿C语言,但使用UIL的语法可能会很困难,因为在某些情况下,它过于冗长,并且需要不必要的关键字和分隔符。例如,开始使用UIL的C程序员经常会忘记在闭合的花括号后添加必需的分号。显然,其语法设计的目的是让编译器易于解析,而不是让人们易于使用。

    由于UIL追求简单性,它缺少动态编程语言的许多优势。因此,接口的动态性越强,UIL在描述它时的用处就越小。接口的动态方面,如更改控件的敏感度、执行拖放操作,或者"实时"创建和销毁接口的某些部分,都必须在应用程序代码中处理。在这些情况下,可能无法将接口描述完全外部化。因此,你会发现代码和接口描述之间存在更多的依赖关系,更改其中一个可能也需要更改另一个。当涉及UIL时,这些限制会使动态接口的处理变得更加复杂。

    在Motif 1.2版本之前,使用UIL的最大缺点是不稳定性,这是由许多漏洞导致的。虽然大多数漏洞现在已经修复,但UIL仍然名声不佳。到Motif 1.2版本时,UIL在一些较复杂的功能上仍然存在问题,但随着Motif每个新版本的发布,更多未解决的漏洞得到了修复。为了给你提供帮助,我们会指出许多这样的漏洞,并在可能的情况下,说明如何规避它们。如果你使用的是较早版本的UIL,可能会遇到一些这里没有提到的额外漏洞。

23.2 The

  • 了解基本的UIL和Mrm编程模型的一个好方法是研究一个简单的应用程序。我们在这里展示的是经典的"Hello, World"程序的一个版本,它说明了我们前面列出的三个步骤。我们将重点关注第一步和第三步:用UIL描述界面,以及在运行时使用Mrm创建界面。我们还会简要介绍如何编译UIL模块,但关于UIL编译器的详细讨论将留到第23章"使用UIL编译器"中进行。

    "Hello, World"应用程序只需要几个基本的UIL结构来描述界面,以及几个Mrm函数调用来创建它。该应用程序由一个包含界面描述的UIL模块和一个C程序组成,这个C程序初始化Xt,用Mrm创建界面,并实现一个回调函数。该应用程序的输出如图所示。它包含一个地球图标标签和一个按钮,按钮上有字符串"Hello, World!"。

  • 图标标签和按钮包含在一个表单中,该表理它们的位置。与典型的Motif程序一样,层次结构根部的应用程序外壳包含这个表单。该层次结构的图示见图。

23.3 Describing an Interface With UIL(用UIL描述接口)

  • UIL模块中的接口描述包含三个部分:
  • a. 部件层次结构
  • b. 各个部件的初始资源设置,这些设置定义了界面
  • c. 响应用户操作而调用的回调函数,这些函数定义了应用程序的"触感"
  • "Hello, World"应用程序的UIL模块如源代码所示。该模块定义了从Form开始的部件层次结构。它的父部件ApplicationShell由C程序创建,我们稍后会对其进行研究。这种划分对于使用UIL创建界面的应用程序来说很典型,因为程序必须至少创建一个部件,作为UIL定义的部件的父部件。
cpp 复制代码
/* hello_world.uil -- Illustrate basic UIL programming concepts */
module hello_world
  objects = { XmPushButton = gadget; }

  value
    form_margin : 3;   ! Value for all-around form margins.

  value
    hello_string : "Hello, World!";
    hello_font   : font ('-adobe-helvetica-medium-r-*-*-*-140-*');
    world_icon   : icon (
      ' *******  ',
      ' *     * ',
      ' * *** * ',
      ' *  *  * ',
      ' * *** * ',
      ' *     * ',
      ' *******  '
      /* 注:原代码中icon的星号矩阵较长,此处保留示例格式 */
    );

  procedure
    quit (string);

  object hello_main : XmForm {
    controls {
      XmLabel      world;
      XmPushButton hello;
    };
    arguments {
      XmShadowThickness = 0;
      XmResizePolicy    = XmRESIZE_GROW;
      XmMarginHeight    = form_margin;
      XmMarginWidth     = form_margin;
    };
  };

  object world : XmLabel {
    arguments {
      XmLabelType     = XmPIXMAP;
      XmLabelPixmap   = world_icon;

      ! Form constraint resources
      XmLeftAttachment   = XmATTACH_FORM;
      XmTopAttachment    = XmATTACH_FORM;
      XmBottomAttachment = XmATTACH_FORM;
    };
  };

  object hello : XmPushButton {
    arguments {
      XmLabelString   = hello_string;
      XmFontList      = hello_font;
      XmMarginHeight  = 2;
      XmMarginWidth   = 3;

      ! Form constraint resources
      XmLeftAttachment   = XmATTACH_WIDGET;
      XmLeftWidget       = world;
      XmTopAttachment    = XmATTACH_FORM;
      XmBottomAttachment = XmATTACH_FORM;
      XmRightAttachment  = XmATTACH_FORM;
    };
    callbacks {
      XmNActivateCallback = procedure quit ("Goodbye!");
    };
  };
end module;

UIL模块的整体结构相当简单。一个模块以名称开头,紧接着是一些可选设置。模块的主体通常由一个或多个描述用户界面的部分组成。这种结构在图中有所展示。
*

23.3.1 Starting and Ending a Module(模块的开始与结束)

  • 除空行和注释外,每个UIL模块都必须以一条模块语句开头,该语句用于为模块命名。本质上,这条语句是UIL编译器要求的一种语法形式。它由字符串"module" followed by 一个你自行选择的名称构成。这个名称没有特殊意义,但必须是一个UIL标识符(UIL标识符的语法在#suilsyntax部分有说明)。我们的示例以如下模块语句开头: module hello_world

该名称通常与模块的文件名相同,只是没有.uil后缀。在为模块选择名称时,该名称不能在模块中重复用于命名其他任何内容,例如变量或小部件。如果不小心重复使用了模块名称,UIL编译器会生成一条错误消息。同样,您必须使用以下语句明确指示每个UIL模块的结束:end module

与模块开头的module语句一样,为了UIL编译器,这条语句也是必需的。在Motif 1.2的早期版本及之前的版本中,如果不在end module语句末尾添加换行符,编译器就会产生错误。尽管这个问题已经得到修复,但为了让所有版本的编译器都能正常工作,你还是应该尽量加上最后的换行符。

hello_world.uil模块的结构

23.3.2 Specifying Module−wide Options(指定模块范围内的选项)

如果模块存在选项,这些选项会紧跟在模块名称之后。这些选项能让你告知UIL编译器,对于在规则中遇到的特定信息应如何处理。Motif 1.2编译器支持以下三个选项:names(用于设置大小写敏感性)、character_set(用于设置默认字符集)以及object(用于指示某些对象默认使用的是部件变体还是小工具变体)。你可能会在较旧的UIL模块中看到version选项。Motif 1.2中支持该选项是为了向后兼容,但未来版本可能会将其移除。与其他选项不同,version设置不会影响对模块的解释,它用于将版本字符串与模块相关联。你不应使用version选项,而应在注释中或值部分的变量中放置版本信息。

names选项可以设置为case_sensitive(大小写敏感)或case_insensitive(大小写不敏感)。正如这些设置所暗示的,该选项决定了UIL编译器如何解释程序员定义的名称(如部件名称)和内置关键字。如果不设置此选项,其默认值为case_sensitive。例如,在启用case_sensitive设置的情况下,编译器会将snowba11、SnowBa11和SNOWBALL视为不同的名称。然而,当指定case_insensitive时,这些相同的名称会被视为相同。当names设为case_sensitive时,内置关键字必须以小写形式出现;而当names设为case_insensitive时,内置关键字可以是小写、大写或混合大小写形式。不过需要注意的是,module、names、case_sensitive和case_insensitive这几个关键字必须始终以小写形式出现。

我们建议你坚持使用默认的case_sensitive设置,原因有二。首先,大小写不敏感很容易让习惯了大小写敏感的C程序员产生困惑。其次,当设置为case_insensitive时,所有程序员定义的名称都会被转换为大写并保存,这反过来就要求在应用程序中使用不便的大写引用来进行引用。如果你决定使用case_sensitive设置,它必须是模块名称后设置的第一个选项,如下例所示。

按照我们的建议,hello_world.uil模块没有设置names选项,因此它使用默认的区分大小写设置。character_set选项允许你设置UIL模块中出现的复合字符串、字体和字体集的默认字符集。(我们将在第#suiltext节中讨论如何定义这些值以及默认字符集对它们的影响。)当你开发的界面所使用的语言,其字符集与你的母语所使用的字符集不同时,通常会用到这个选项。

我们的示例应用程序使用英语。由于这与我们的计算环境所使用的语言相同,因此没有必要在我们的模块中指定character_set选项。如果我们是在非英语环境中构建该应用程序,但希望它能在英语环境中运行,那么该模块的开头会是:

当未设置character_set选项时,如果设置了LANG环境变量,字符集会默认采用该变量中代码集部分;否则,会默认采用特定供应商的XmFALLBACK_CHARSET。由于默认字符集取决于环境和供应商设置,因此你应该确保为可能在不同环境中编译的模块选择合适的字符集。

表面上看,似乎总是可以使用 character_set 选项来设置默认字符集,而不必担心 LANG 的设置。但遗憾的是,设置这个选项会产生一个副作用,即会禁用特定于区域设置的复合字符串解析,这对于包含多字节字符字符串的模块来说非常重要。目前,避免这个问题的唯一方法是在 LANG 环境变量中指定字符集。在这个示例中,我们可以在模块中安全地设置字符集,因为我们没有使用任何多字节字符串。(有关多字节字符串解析的更多信息,请参见第 #suilcomps 节和第 #suiltext 节。)objects 选项允许您选择默认情况下使用 Label(标签)、PushButton(按钮)、ToggleButton(切换按钮)、Cascade Button(级联按钮)和 Separator(分隔符)对象的 gadget 版本还是 widget 版本。每种对象类型的 widget 或 gadget 变体是独立指定的。在我们的示例中,我们使用以下设置来获取 gadget 版本的 PushButton。

每个对象的默认值是微件,因此,只有当你希望默认创建小工具时,才需要指定objects选项。设置此选项并不会阻止你在对象定义中明确使用控件的微件或小工具变体。我们建议,当你要对某一类型的所有或大多数对象使用小工具时,设置objects选项。