zoukankan      html  css  js  c++  java
  • JavaFX学习曲线日记2:声明用户接口

    avaFX学习曲线日记-2:声明用户接口

    作者:John O'Conner
    译者:cleverpig
    image


    我 已经使用Java语言定义用户接口近十年了,当我第一次体验JavaFX脚本时便马上感到了这两种不同环境之间的差异。尽管程序员在Java语言中使用过 程式代码来定义用户接口,而在JavaFX脚本语言中你能够使用声明语句来定义用户接口。这是两者之间最大的不同,要适应后者的确需要花费一定的时间和精 力。

    为了学习这种创建UI的全新声明风格,我决定将一个从前使用Java语言实现的应用UI移植到JavaFX脚本上。于是我挑选了一个在Java语言中心Swingworker教学中使用的图片浏览应用。原始应用演示了如何在JavaSE 6.0中使用Swingworker类,正因为其UI本身非常简单,所以我将它作为移植的“原料”。

    现存的用户接口

    现存的应用为用户提供了查询、列表、从Flickr站点下载并显示图片的功能。用户可以输出查询关键字,应用调用REST API来查询Flickr以获取匹配的缩略图片;而且用户还可以从缩略图片中进行选择,以便查看更大更细致的图片。现存应用的查询结果如下图:
            image
            图 1. 带有查询结果的应用UI

    这个UI由下列组件构成,按照从上至下的顺序:
            • 主框架窗体容器
            • 查询标签和查询文本输入栏
            • 查询匹配标签和处理进度条
            • 与简短描述(检索关键字)相匹配的缩略图列表
            • 选择标签和处理进度条
            • 显示被选择图片的标签

    此UI由以下常见的Swing组件构成:JFrame、Jlabel、JprogressBar、JscrollPane、JList。JList组件具有自定义渲染器,它能够显示缩略图和相应的简短描述。

    但 这还是一个相当简单的UI,我们用它来研究如何使用JavaFX脚本描述UI。下一步,我打算尝试使用JavaFX实现整个应用;但是目前,只要完成一个 对现存UI的近似实现就可以了。下面展示了一个毫无生气的UI,它代表了我使用JavaFX脚本进行UI描述来实现的最初目标:
            image
            图 2. 应用UI

    我使用NetBeans IDE和它的Matisse GUI实现了这个原始的UI。所以源代码都可从Swingworker教学中下载;下面列出了用于生成UI的主要代码。它告诉了我们如何在NetBeans中使用GroupLayout来创建UI。
    private void initComponents() {
        lblSearch = new javax.swing.JLabel();
        txtSearch = new javax.swing.JTextField();
        lblImageList = new javax.swing.JLabel();
        scrollImageList = new javax.swing.JScrollPane();
        listImages = new JList(listModel);
        lblSelectedImage = new javax.swing.JLabel();
        lblImage = new javax.swing.JLabel();
        progressMatchedImages = new javax.swing.JProgressBar();
        progressSelectedImage = new javax.swing.JProgressBar();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("Image Search");
        lblSearch.setText("Search");
        lblImageList.setText("Matched Images");

        // ...
        // event listeners, models, and cell renderers removed for this example
        //

        lblSelectedImage.setText("Selected Image");

        lblImage.setBorder(javax.swing.BorderFactory.createLineBorder(
                new java.awt.Color(204, 204, 204)));
        lblImage.setFocusable(false);
        lblImage.setMaximumSize(new java.awt.Dimension(500, 500));
        lblImage.setMinimumSize(new java.awt.Dimension(250, 250));
        lblImage.setOpaque(true);
        lblImage.setPreferredSize(new java.awt.Dimension(500, 250));

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
                    .addComponent(lblImage, javax.swing.GroupLayout.Alignment.LEADING,
                            javax.swing.GroupLayout.DEFAULT_SIZE, 462, Short.MAX_VALUE)
                    .addComponent(scrollImageList, javax.swing.GroupLayout.DEFAULT_SIZE,
                            462, Short.MAX_VALUE)
                    .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addComponent(lblImageList)
                            .addComponent(lblSelectedImage))
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addComponent(progressMatchedImages, javax.swing.GroupLayout.DEFAULT_SIZE,
                                    350, Short.MAX_VALUE)
                            .addComponent(progressSelectedImage, javax.swing.GroupLayout.DEFAULT_SIZE,
                                    350, Short.MAX_VALUE)))
                    .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()
                        .addComponent(lblSearch)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(txtSearch, javax.swing.GroupLayout.DEFAULT_SIZE,
                                411, Short.MAX_VALUE)))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(lblSearch)
                    .addComponent(txtSearch, javax.swing.GroupLayout.PREFERRED_SIZE,
                            javax.swing.GroupLayout.DEFAULT_SIZE,                                    
                            javax.swing.GroupLayout.PREFERRED_SIZE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(lblImageList)
                    .addComponent(progressMatchedImages, javax.swing.GroupLayout.PREFERRED_SIZE,
                            javax.swing.GroupLayout.DEFAULT_SIZE,
                            javax.swing.GroupLayout.PREFERRED_SIZE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(scrollImageList, javax.swing.GroupLayout.PREFERRED_SIZE, 235,
                        javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(lblSelectedImage)
                    .addComponent(progressSelectedImage, javax.swing.GroupLayout.PREFERRED_SIZE,
                            javax.swing.GroupLayout.DEFAULT_SIZE,
                            javax.swing.GroupLayout.PREFERRED_SIZE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(lblImage, javax.swing.GroupLayout.DEFAULT_SIZE, 305, Short.MAX_VALUE)
                .addContainerGap())
        );
        pack();
    }


    使 用NetBeans IDE进行UI布局非常简单——只需要鼠标的拖、拽操作便可。在本示例中,NetBeans通过使用javax.swing.GroupLayout生成 了UI代码,javax.swing.GroupLayout这个类为我们提供了跨平台的正确大小、位置、组件之间的间隔。虽然生成的代码并不便于阅读, 但从工具支持性来讲它确实是非常优秀的,而且无需任何人工编写代码。

    声明JavaFX脚本接口

    尽管JavaFX脚本并没有提供GUI工具,NetBeans也没有提供支持JavaFX脚本语言的UI设计界面,但我们可以使用JavaFXPad这个OpenJFX站点提供的演示应用。通过使用JFXPad,你能够人工输入UI代码,并马上见到代码所呈现的效果。尽管它是一个演示应用,但的确是一个超级好用的简单工具。

    本 接口所需的所有GUI组件都在javafx.ui包中。JavaFX脚本和Java语言一样支持包(package)结构和导入(import)语句。我 在学习过程中并不是导入整个包,而是每次导入要用到的组件类。这样可以让我们在查看代码时能够准确地识别出使用了哪些组件。

    JavaFX 脚本组件具有height、width、text、content等属性。其中content属性可以包含子组件,每个content属性可以包含被声明 为数组的多个组件。在声明图片检索应用的用户接口时,我选用并使用了适合的JavaFX脚本组件,并设置了它们的属性。

    例如,下面简短的脚本定义了一个不含有任何内容的空白框架。
    import javafx.ui.Frame;
    Frame {
        title: "JFX Image Search"
        height: 500
         500
        visible: true
    }


    在JavaFX 脚本中与Swing的Jframe等价的是javafx.ui.Frame。这里的title属性声明了window的标题。Height和width属 性则以像素为单元定义了整个框架的大小。最后,visible属性声明了框架是否可见,这与在Swing中的Jframe类的setVisible方法类 似。

    FlowPanels 和 Boxes

    javafx.ui包中包含了Label、SimpleLabel、Box、FlowPanel、ProgressBar、ScrollPane和ListBox这些组件。它们的名称听起来与Swing组件非常相似,因此我首先尝试使用它们来实现目标UI。

    尽 管JavaFX脚本支持GroupLayout和GroupPanel组件,但我还是尽量在首次尝试时避开这些组件。我并不认为GroupLayout有 多好,因为在Java语言中使用它时你不得不人工编写代码,从这一点出发我对在JavaFX脚本中使用GroupLayout存在着同样的顾虑。于是我选 择了更加简单的FlowPanel和Box组件来创建下面的JavaFXImageSearchUI1.fx代码:
    package com.sun.demo.jfx;

    import javafx.ui.Frame;
    import javafx.ui.Box;
    import javafx.ui.FlowPanel;
    import javafx.ui.SimpleLabel;
    import javafx.ui.Label;
    import javafx.ui.TextField;
    import javafx.ui.ScrollPane;
    import javafx.ui.ListBox;
    import javafx.ui.ProgressBar;
    import javafx.ui.LineBorder;
    import javafx.ui.EmptyBorder;
    import javafx.ui.Color;

    Frame {
        title: "JFX Image Search"
        content: Box {
            border: EmptyBorder {
                top: 10
                left: 10
                right: 10
                bottom: 10
            }
            orientation: VERTICAL
            content: [
            FlowPanel {
                alignment: LEADING
                content: [
                SimpleLabel {
                    text: "Search"
                },
                TextField {
                    preferredSize: { 425}
                }
                ]
            },
            FlowPanel {
                alignment: LEADING
                content: [
                SimpleLabel {
                    text: "Matched Images"
                },
                ProgressBar {
                    preferredSize: { 378}
                    
                }
                ]
            },
            ListBox {
                preferredSize: {height: 200 }
            },
            FlowPanel {
                alignment: LEADING
                content: [
                SimpleLabel {
                    text: "Selected Image"
                },
                ProgressBar {
                    preferredSize: { 382 }
                }
                ]
            },
            Label {
               opaque: true
                preferredSize: {
                    height: 250
                }
                border: LineBorder
            }
            ]
        }
        visible: true
    }


    在JavaFXPad 中输入以上内容后,上方的window中将显示相应的执行结果。如果使用NetBeans IDE创建项目时,请将项目配置为使用FXShell类执行JFXImageSearchUI1.fx文件;这样我们所期望的窗体框架将会被显示出来。虽 然我硬性编码了文本输入栏和进度条的宽度以使其看起来和原始UI一样大小,但对于第一次尝试用JavaFX创建UI来讲,其效果还算称得上成功。:-)
            image
            图 3. 复制好的UI

    框 架的内容是一个Box组件。这个组件的orientation(朝向)属性是VERTICAL,这意味着在Box中的内容将被垂直放置,而不是水平放置。 Box组件具有一个content属性。你可以在其content属性中放置多个组件。如果你插入了多个组件,那么则必须将这些组件以数组的形式写在方括 号中,而在数组中的组件之间使用逗号分割:
    content: [
        SimpleLabel {
            text: "Search"
        },
        TextField {
            preferredSize: { 425}
        }
    ]


    FlowPanel 组件常常用于创建一对Label组件。我使用多个FlowPanel来组织Label和其它的组件,例如TextField或者ProgressBar。 当将FlowPanel的alignment属性设置为LEADING时,框架中的面板将向左侧对齐,这样UI看上去最漂亮。

    尽管这个UI看上去已经很不错了,但其布局仍然需要硬性编写进度条的宽度,以使其充满整个框架。不幸的是,我们完全不可能非常精确地将进度条对齐到框架的右侧。你可以从上面的UI中发觉查询文本框和匹配图片进度条并没有完全对齐。

    GroupPanel 和 GroupLayout

    为了实现能够自动设置组件大小并使其充满容器空间的布局,我决定尝试javafx.ui.GroupPanel接口。这个接口使用了Swing的GroupLayout,因此它能够实现更加精确地表现布局。

    GroupPanel 组件使用行和列来定位在表格中的组件。它能够自动在组件和其容器之间提供平台特定(platform-specific)的间隔,以达到布局的目的。另 外,它能够很好地对齐组件。GroupPanel简化了Swing的GroupLayout,使其更加易于编写。

    下面是UI的第二个版本JFXImageSearchUI.fx:
    package com.sun.demo.jfx;
    import javafx.ui.Frame;
    import javafx.ui.GroupPanel;
    import javafx.ui.Row;
    import javafx.ui.Column;
    import javafx.ui.SimpleLabel;
    import javafx.ui.Label;
    import javafx.ui.TextField;
    import javafx.ui.ProgressBar;
    import javafx.ui.LineBorder;
    import javafx.ui.ListBox;

    Frame {
        title: "JavaFX Image Search"
        content:
            // main panel within the frame
            GroupPanel {
            // define the five rows and main column
            var searchRow = new Row
            var matchedProgressRow = new Row
            var thumbNailRow = new Row {resizable: true}
            var selectedProgressRow = new Row
            var imageRow = new Row {resizable: true}
            var mainCol = new Column {resizable: true}
            
            // declare the five rows and the column
            rows: [searchRow, matchedProgressRow, thumbNailRow, selectedProgressRow, imageRow]
            columns: mainCol
            // provide the array of components in the frame
            content: [
            // search text row
            GroupPanel {
                autoCreateContainerGaps: false
                row: searchRow
                column: mainCol
                var row = new Row
                var searchLabelCol = new Column
                var searchTextFieldCol = new Column {
                    resizable: true
                }
                rows: row
                columns: [searchLabelCol, searchTextFieldCol]
                content: [
                SimpleLabel {
                    text: "Search:"
                    row: row
                    column: searchLabelCol
                },
                TextField {
                    row: row
                    column: searchTextFieldCol
                    columns: 50
                }
                ]
            },
            // matching images progress panel row
            GroupPanel {
                autoCreateContainerGaps: false
                row: matchedProgressRow
                column: mainCol
                var row = new Row
                var lblCol = new Column
                var progressBarCol = new Column {resizable: true}
                rows: row
                columns: [lblCol, progressBarCol]
                content: [
                SimpleLabel {
                    text: "Matched Images"
                    row: row
                    column: lblCol
                },
                ProgressBar {
                    row: row
                    column: progressBarCol
                }
                ]
            },
            // thumbnail list row
            ListBox {
                preferredSize: {height: 200, 400}
                row: thumbNailRow
                column: mainCol
            },
            // selected image progress row
            GroupPanel {
                autoCreateContainerGaps: false
                row: selectedProgressRow
                column: mainCol
                var row = new Row
                var lblCol = new Column
                var progressBarCol = new Column {resizable: true}
                rows: row
                columns: [lblCol, progressBarCol]
                content: [
                SimpleLabel {
                    text: "Selected Image"
                    row: row
                    column: lblCol
                },
                ProgressBar {
                    row: row
                    column: progressBarCol
                }
                ]
            },
            // selected image display row
            Label {
                opaque: true
                preferredSize: {height: 300, 400}
                row: imageRow
                column: mainCol
                border: LineBorder
            }
            ]
        }
        visible: true
    }


    这段代码生成了更加完美的布局。框架中的组件之间进行了很好的分割,并且与框架的左右两侧分别对齐。
            image
            图 4. JFX图片查询UI

    虽然在这个框架中使用的组件前一个例子中相同,但这里使用了GroupPanel,而不是FlowPanel和Box。当你查看原始的UI时,你会发现五个不同的行:检索行、匹配图片进度行、列表行、选择图片进度行、被选择图片行。在这些组件被顺序放置在一个居中的列内。

    而我的第二个版本也具有五行和一列。框架的主要content是一个GroupPanel,这个组件包含了几个GroupPanel和其它组件。下面便让我们看一下在GroupPanel中是如何实现这五行和一列的:
    content: GroupPanel {
        var searchRow = new Row
        var matchedProgressRow = new Row
        var thumbNailRow = new Row {resizable: true}
        var selectedProgressRow = new Row
        var imageRow = new Row {resizable: true}
        var mainCol = new Column {resizable: true}
            
        rows: [searchRow, matchedProgressRow, thumbNailRow, selectedProgressRow, imageRow]
        columns: mainCo
    l

    第一个GroupPanel包括一个组件的数组和一个用于检索关键字输入的GroupPanel(这里称为第二个GroupPanel)。下面就是用于检索关键字输入的GroupPanel的行列属性设置:
    // search text row
    GroupPanel {      
        autoCreateContainerGaps: false
        row: searchRow
        column: mainCol


    GroupPanel 具有两个非常重要的属性:autoCreateGaps和autoCreateContainerGaps,它们定义了如何在组件和容器之间创建间隔。这 两个属性默认值为true,但由于这里已经在组件之间创建了间隔,在第一个GroupPanel和其中包含的第二个GroupPanel之间不需要额外的 间隔,因此这里将其autoCreateContainerGaps设置为false来取掉额外的间隔。否则,检索文本输入行将被插入不必要的边缘。要使 检索文本行所在的GroupPanel组件填充父容器的相应行、列,我们需要设置它的行和列属性为searchRow和mainCol。

    检索文本行所在的GroupPanel定义了它自己的行和列,在其中包含了检索标签和文本输入框:
                var row = new Row
                var searchLabelCol = new Column
                var searchTextFieldCol = new Column {
                    resizable: true
                }
                rows: row
                columns: [searchLabelCol, searchTextFieldCol]


    行、列定义被创建好后,让我们继续使用声明式语法定义content的数组:
    content: [
        SimpleLabel {
            text: "Search:"
            row: row
            column: searchLabelCol
        },
        TextField {
            row: row
            column: searchTextFieldCol
            columns: 50
        }
    ]


    余下的代码都遵循相应的模式。包含多个组件的行被封装在一个GroupPanel中。具有单个组件的行,例如下拉列表和图片标签,则使用row和column属性与外部的GroupPanel相关联。

    尽 管我最初对使用GroupPanel很担心,但JavaFX脚本将GroupLayout封装后使其变得非常易用,我在尝试GroupPanel后便打消 了担忧。另外NetBeans IDE的编辑器插件和JavaFXPad演示程序提供了上下文敏感的代码自动完成功能,它可以根据输入内容弹出相关可用的属性。通过使用简单的行列布局和 代码自动完成,本人感觉使用JavaFX脚本的GroupPanel并没有想象中那样困难。下面的图片展示了在IDE或者JavaFXPad中按下 CTRL+SPACE出现的弹出选项。
            image
            图 5. 弹出选项


    总结

    为 了探索在创建UI过程中如何使用声明式语法,我将现有应用的UI进行了大胆的移植。原始应用的UI使用了Swing的GroupLayout来定位、对齐 组件。尽管NetBeans IDE没有提供用于JavaFX脚本的图形化设计工具,但通过编写JavaFX代码进行布局并非我所想象的那样困难。通过使用GroupPanel和其它 组件,我实现了和原始应用完全相同的UI。方便的GroupPanel组合加上NetBeans IDE插件、上下文敏感的代码自动完成功能使工作变得轻松。
     
  • 相关阅读:
    451. Sort Characters By Frequency
    424. Longest Repeating Character Replacement
    68. Text Justification
    44. Wildcard Matching
    160. Intersection of Two Linked Lists
    24. Swap Nodes in Pairs
    93. 递归实现组合型枚举
    98. 分形之城
    97. 约数之和
    96. 奇怪的汉诺塔
  • 原文地址:https://www.cnblogs.com/lanzhi/p/6470737.html
Copyright © 2011-2022 走看看