48、Java Swing布局与界面优化全解析

Java Swing布局与界面优化全解析

在Java的Swing开发中,布局管理和界面优化是构建高质量用户界面(UI)的关键环节。本文将深入探讨Swing中的布局管理器,如GridBagLayout和SpringLayout,以及如何通过各种技术手段提升界面的美观性和用户体验。

1. GridBagLayout布局管理器

GridBagLayout是一种高度可配置但相对复杂的布局管理器,它与GridLayout类似,都能将组件组织在矩形网格中。不过,GridBagLayout提供了更多的控制能力。每个矩形的大小并非固定,通常根据其包含组件的默认或“首选”大小来确定,并且组件可以跨越多行或多列。

以下是使用GridBagLayout布局标签和字段的示例代码:

JPanel createFieldsPanel() {
    GridBagLayout layout = new GridBagLayout();
    JPanel panel = new JPanel(layout);
    int columns = 20;
    JLabel departmentLabel =
        createLabel(DEPARTMENT_LABEL_NAME, DEPARTMENT_LABEL_TEXT);
    JTextField departmentField =
        createField(DEPARTMENT_FIELD_NAME, columns);
    JLabel numberLabel =
        createLabel(NUMBER_LABEL_NAME, NUMBER_LABEL_TEXT);
    JTextField numberField =
        createField(NUMBER_FIELD_NAME, columns);
    layout.setConstraints(departmentLabel,
        new GridBagConstraints(
            0, 0,  // x, y
            1, 1,  // gridwidth, gridheight
            40, 1, // weightx, weighty
            LINE_END, //anchor
            NONE, // fill
            new Insets(3, 3, 3, 3), // top-left-bottom-right
            0, 0)); // padx, ipady
    layout.setConstraints(departmentField,
        new GridBagConstraints(1, 0, 2, 1, 60, 1,
            CENTER, HORIZONTAL,
            new Insets(3, 3, 3, 3), 0, 0));
    layout.setConstraints(numberLabel,
        new GridBagConstraints(0, 1, 1, 1, 40, 1,
            LINE_END, NONE,
            new Insets(3, 3, 3, 3), 0, 0));
    layout.setConstraints(numberField,
        new GridBagConstraints(1, 1, 2, 1, 60, 1,
            CENTER, HORIZONTAL,
            new Insets(3, 3, 3, 3), 0, 0));
    panel.add(departmentLabel);
    panel.add(departmentField);
    panel.add(numberLabel);
    panel.add(numberField);
    return panel;
}

创建GridBagLayout并将其设置到面板后,需要为每个要添加的组件调用 setConstraints 方法。该方法接受两个参数:一个 Component 对象和一个 GridBagConstraints 对象。 GridBagConstraints 对象包含了组件的多个约束条件,如下表所示:
| 约束条件 | 描述 |
| ---- | ---- |
| gridx/gridy | 组件开始绘制的单元格位置,左上角单元格为(0, 0) |
| gridwidth/gridheight | 组件应跨越的行数或列数,默认值为1,表示组件占用单个单元格 |
| weightx/weighty | 用于确定在一行或一列中所有组件布局完成后,如果还有剩余空间,应分配给该组件多少额外空间 |
| anchor | 当组件小于其显示区域时,用于确定组件在单元格内的放置方式,默认值为 GridBagConstraints.CENTER |
| fill | 当组件小于其显示区域时,指定组件应如何扩展以填充显示区域,取值有NONE(不调整大小)、HORIZONTAL、VERTICAL和BOTH |
| insets | 使用 Insets 对象指定组件与其显示区域边缘之间的间距 |
| ipadx/ipady | 指定要添加到组件最小大小的额外空间 |

使用GridBagLayout的最佳策略是先在纸上或白板上绘制一个网格,确定组件的相对大小和位置,然后集中处理每个组件的 anchor fill 方面。根据绘制的规范编写布局代码,并在必要时进行修改。最后,可以尝试调整 insets weightx/weighty ipadx/ipady 约束条件,以微调组件之间的间距。

以下是经过适度重构的代码:

JPanel createFieldsPanel() {
    GridBagLayout layout = new GridBagLayout();
    JPanel panel = new JPanel(layout);
    int columns = 20;
    addField(panel, layout, 0,
        DEPARTMENT_LABEL_NAME, DEPARTMENT_LABEL_TEXT,
        DEPARTMENT_FIELD_NAME, columns);
    addField(panel, layout, 1,
        NUMBER_LABEL_NAME, NUMBER_LABEL_TEXT,
        NUMBER_FIELD_NAME, columns);
    return panel;
}

private void addField(
    JPanel panel, GridBagLayout layout, int row,
    String labelName, String labelText,
    String fieldName, int fieldColumns) {
    JLabel label = createLabel(labelName, labelText);
    JTextField field = createField(fieldName, fieldColumns);
    Insets insets = new Insets(3, 3, 3, 3); // top-left-bottom-right
    layout.setConstraints(label,
        new GridBagConstraints(
            0, row,  // x, y
            1, 1,  // gridwidth, gridheight
            40, 1, // weightx, weighty
            LINE_END, //anchor
            NONE, // fill
            insets, 0, 0)); // padx, ipady
    layout.setConstraints(field,
        new GridBagConstraints(1, row,
            2, 1, 60, 1, CENTER, HORIZONTAL,
            insets, 0, 0));
    panel.add(label);
    panel.add(field);
}

由于代码存在大量冗余,建议进行进一步的重构。可以考虑使用简化的实用构造函数来替换 GridBagConstraints 对象的重复构造。如果需要表示多个字段和相关标签,可以使用数据类来表示每对字段和标签,然后在表格中表示整个字段集,并通过迭代来创建布局。

2. SpringLayout布局管理器

从Java 1.4开始,Sun引入了SpringLayout类。该布局管理器主要用于GUI组合工具。SpringLayout的基本概念是通过使用称为“弹簧”的约束将组件的边缘连接在一起来定义布局。

例如,在CoursesPanel中,可以创建一个弹簧将部门文本字段的左侧边缘连接到部门标签的右侧边缘,弹簧的大小固定为5个像素。另一个弹簧可以将部门文本字段的右侧边缘连接到面板本身的右侧边缘,同样使用固定长度的弹簧。当面板宽度增加时,部门文本字段也会相应地增长。

手动创建SpringLayout对于只有几个组件的面板来说相对容易,但对于更复杂的布局可能会非常困难和令人沮丧。在大多数情况下,建议使用布局工具来完成这项工作。

3. 界面美观性优化

除了布局管理,界面的美观性也是提升用户体验的重要因素。以下是一些可以用于提升现有CoursesPanel界面美观性的技术:

3.1 JScrollPane滚动面板

当使用 sis.ui.Sis 添加多个课程时, JList 可能无法显示所有课程。为了解决这个问题,可以将 JList 包装在滚动面板中。滚动面板充当列表的视口,当列表模型包含的信息超过当前 JList 大小所能显示的范围时,滚动面板会显示滚动条,允许用户水平或垂直移动视口以查看隐藏的信息。

以下是在CoursesPanel中添加滚动面板的示例代码:

private void createLayout() {
    JLabel coursesLabel =
        createLabel(COURSES_LABEL_NAME, COURSES_LABEL_TEXT);
    JList coursesList = createList(COURSES_LIST_NAME, coursesModel);
    JScrollPane coursesScroll = new JScrollPane(coursesList);
    coursesScroll.setVerticalScrollBarPolicy(
        ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
    setLayout(new BorderLayout());
    add(coursesLabel, BorderLayout.NORTH);
    add(coursesScroll, BorderLayout.CENTER);
    add(createBottomPanel(), BorderLayout.SOUTH);
}
3.2 边框(Borders)

课程列表和相关标签直接与面板边缘相邻,使用边框可以在面板边缘和其组件之间创建一个缓冲区。可以使用 BorderFactory 类创建多种不同类型的边框,大多数边框用于装饰目的,而空边框则用于创建间距。还可以使用 createCompoundBorder 方法组合边框。

以下是使用不同边框类型的示例代码:

private void createLayout() {
    JList coursesList = createList(COURSES_LIST_NAME, coursesModel);
    JScrollPane coursesScroll = new JScrollPane(coursesList);
    coursesScroll.setVerticalScrollBarPolicy(
        ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
    setLayout(new BorderLayout());
    final int pad = 6;
    Border emptyBorder = 
        BorderFactory.createEmptyBorder(pad, pad, pad, pad);
    Border bevelBorder = 
        BorderFactory.createBevelBorder(BevelBorder.RAISED);
    Border titledBorder = 
        BorderFactory.createTitledBorder(bevelBorder, COURSES_LABEL_TEXT);
    setBorder(BorderFactory.createCompoundBorder(emptyBorder,
                                                 titledBorder));
    add(coursesScroll, BorderLayout.CENTER);
    add(createBottomPanel(), BorderLayout.SOUTH);
}

使用标题边框可以消除单独使用 JLabel 来表示“Courses:”文本的需要,但需要注意修改相关测试代码。

3.3 添加标题

SIS框架窗口的标题栏中没有显示文本,可以通过更新 SisTest 中的 testCreate 方法来解决这个问题:

public void testCreate() {
    final double tolerance = 0.05;
    assertEquals(Sis.HEIGHT, frame.getSize().getHeight(), tolerance);
    assertEquals(Sis.WIDTH, frame.getSize().getWidth(), tolerance);
    assertEquals(JFrame.EXIT_ON_CLOSE,
                 frame.getDefaultCloseOperation());
    assertNotNull(Util.getComponent(frame, CoursesPanel.NAME));
    assertEquals(Sis.COURSES_TITLE, frame.getTitle());
}

Sis 类中,可以使用 JFrame 的构造函数传入标题栏文本:

public class Sis {
    ...
    static final String COURSES_TITLE = "Course Listing";
    private JFrame frame = new JFrame(COURSES_TITLE);
    ...
}
3.4 图标(Icons)

可以为窗口添加图标,默认情况下会显示Java咖啡杯图标。可以通过 getIconImage 方法获取窗口的图标,并使用 ImageUtil.create 方法加载图标。

以下是相关的测试代码:

public void testCreate() {
    final double tolerance = 0.05;
    assertEquals(Sis.HEIGHT, frame.getSize().getHeight(), tolerance);
    assertEquals(Sis.WIDTH, frame.getSize().getWidth(), tolerance);
    assertEquals(JFrame.EXIT_ON_CLOSE,
                 frame.getDefaultCloseOperation());
    assertNotNull(Util.getComponent(frame, CoursesPanel.NAME));
    assertEquals(Sis.COURSES_TITLE, frame.getTitle());
    Image image = frame.getIconImage();
    assertEquals(image, ImageUtil.create("/images/courses.gif"));
}

ImageUtil 类的实现如下:

package sis.util;
import javax.swing.*;
import java.awt.*;
public class ImageUtil {
    public static Image create(String path) {
        java.net.URL imageURL = ImageUtil.class.getResource(path);
        if (imageURL == null)
            return null;
        return new ImageIcon(imageURL).getImage();
    }
}

需要注意的是,传递给 getResource 方法的图像文件名应以斜杠开头,表示从每个类路径条目的根目录开始查找资源。

4. 用户体验优化

除了界面美观性,用户体验也是至关重要的。以下是一些可以用于提升用户体验的技术:

4.1 键盘支持

GUI的一个基本原则是用户必须能够使用键盘或鼠标完全控制应用程序。虽然存在一些例外情况,但在大多数情况下,忽略仅使用键盘或仅使用鼠标的用户需求是不恰当的。

默认情况下,Java提供了大部分必要的支持,例如可以通过鼠标点击或使用Tab键切换到按钮并按下空格键来激活按钮。

4.2 按钮助记符(Button Mnemonics)

可以使用Alt键组合来激活按钮。通常,另一个键是按钮文本中出现的字母或数字,称为“助记符”。例如,“Add”按钮的合适助记符是字母“A”。

CoursesPanelTest 中可以测试按钮的助记符:

public void testCreate() {
    assertEmptyList(COURSES_LIST_NAME);
    assertButtonText(ADD_BUTTON_NAME, ADD_BUTTON_TEXT);
    assertLabelText(DEPARTMENT_LABEL_NAME, DEPARTMENT_LABEL_TEXT);
    assertEmptyField(DEPARTMENT_FIELD_NAME);
    assertLabelText(NUMBER_LABEL_NAME, NUMBER_LABEL_TEXT);
    assertEmptyField(NUMBER_FIELD_NAME);
    JButton button = panel.getButton(ADD_BUTTON_NAME);
    assertEquals(ADD_BUTTON_MNEMONIC, button.getMnemonic());
}

CoursesPanel 中设置按钮的助记符:

public class CoursesPanel extends JPanel {
    ...
    static final char ADD_BUTTON_MNEMONIC = 'A';
    ...
    JPanel createBottomPanel() {
        addButton = createButton(ADD_BUTTON_NAME, ADD_BUTTON_TEXT);
        addButton.setMnemonic(ADD_BUTTON_MNEMONIC);
        ...
        return panel;
    }
    ...
}
4.3 必需字段(Required Fields)

一个有效的课程需要同时包含部门和课程编号。为了避免用户在未输入这些信息的情况下按下“Add”按钮,可以对应用程序进行修改。

一种解决方案是在用户按下“Add”按钮后,检查部门和课程编号字段是否包含非空字符串。如果不包含,则显示一个消息弹出框解释要求。然而,这种解决方案会给用户带来不便,更好的解决方案是在用户输入信息之前主动禁用“Add”按钮。

可以通过监控两个字段的输入情况,每次用户按下一个字符时,测试字段内容并相应地启用或禁用“Add”按钮。

以下是相关的测试代码:

public void testEnableDisable() {
    panel.setEnabled(ADD_BUTTON_NAME, true);
    JButton button = panel.getButton(ADD_BUTTON_NAME);
    assertTrue(button.isEnabled());
    panel.setEnabled(ADD_BUTTON_NAME, false);
    assertFalse(button.isEnabled());
}

public void testAddListener() throws Exception {
    KeyListener listener = new KeyAdapter() {};
    panel.addFieldListener(DEPARTMENT_FIELD_NAME, listener);
    JTextField field = panel.getField(DEPARTMENT_FIELD_NAME);
    KeyListener[] listeners = field.getKeyListeners();
    assertEquals(1, listeners.length);
    assertSame(listener, listeners[0]);
}

CoursesPanel 中实现相应的方法:

void setEnabled(String name, boolean state) {
    getButton(name).setEnabled(state);
}

void addFieldListener(String name, KeyListener listener) {
    getField(name).addKeyListener(listener);
}

在应用程序级别进行测试时,可以使用 java.awt.Robot 类来模拟用户输入。以下是相关的测试代码:

public void testKeyListeners() throws Exception {
    sis.show();
    JButton button = panel.getButton(CoursesPanel.ADD_BUTTON_NAME);
    assertFalse(button.isEnabled());
    selectField(CoursesPanel.DEPARTMENT_FIELD_NAME);
    type('A');
    selectField(CoursesPanel.NUMBER_FIELD_NAME);
    type('1');
    assertTrue(button.isEnabled());
}

private void selectField(String name) throws Exception {
    JTextField field = panel.getField(name);
    Point point = field.getLocationOnScreen();
    robot.mouseMove(point.x, point.y);
    robot.mousePress(InputEvent.BUTTON1_MASK);
    robot.mouseRelease(InputEvent.BUTTON1_MASK);
}

private void type(int key) throws Exception {
    robot.keyPress(key);
    robot.keyRelease(key);
}

Sis 类中添加键盘监听器:

public class Sis {
    ...
    private void initialize() {
        createCoursesPanel();
        createKeyListeners();
        ...
    }
    ...
    void createKeyListeners() {
        KeyListener listener = new KeyAdapter() {
            public void keyReleased(KeyEvent e) {
                setAddButtonState();
            }
        };
        panel.addFieldListener(CoursesPanel.DEPARTMENT_FIELD_NAME,
                               listener);
        panel.addFieldListener(CoursesPanel.NUMBER_FIELD_NAME, listener);
        setAddButtonState();
    }
    void setAddButtonState() {
        panel.setEnabled(CoursesPanel.ADD_BUTTON_NAME,
            !isEmpty(CoursesPanel.DEPARTMENT_FIELD_NAME) &&
            !isEmpty(CoursesPanel.NUMBER_FIELD_NAME));
    }
    private boolean isEmpty(String field) {
        String value = panel.getText(field);
        return value.trim().equals("");
    }
}
4.4 字段编辑(Field Edits)

为了使用户界面更加有效,应尽量避免用户输入无效数据。可以通过验证和修改文本字段中的数据来实现这一目标。

例如,课程部门必须只包含大写字母。可以创建一个自定义的 DocumentFilter 类来实现这一功能。以下是一个将输入字符转换为大写字母的示例:

package sis.ui;
import javax.swing.text.*;
public class UpcaseFilter extends DocumentFilter {
    public void insertString(
        DocumentFilter.FilterBypass bypass,
        int offset,
        String text,
        AttributeSet attr) throws BadLocationException {
        bypass.insertString(offset, text.toUpperCase(), attr);
    }

    public void replace(
        DocumentFilter.FilterBypass bypass,
        int offset,
        int length,
        String text,
        AttributeSet attr) throws BadLocationException {
        bypass.replace(offset, length, text.toUpperCase(), attr);
    }
}

为了测试这个过滤器,可以编写以下测试代码:

package sis.ui;
import javax.swing.*;
import javax.swing.text.*;
import junit.framework.*;
public class UpcaseFilterTest extends TestCase {
    private DocumentFilter filter;
    protected DocumentFilter.FilterBypass bypass;
    protected AbstractDocument document;
    protected void setUp() {
        bypass = createBypass();
        document = (AbstractDocument)bypass.getDocument();
        filter = new UpcaseFilter();
    }
    public void testInsert() throws BadLocationException {
        filter.insertString(bypass, 0, "abc", null);
        assertEquals("ABC", documentText());
        filter.insertString(bypass, 1, "def", null);
        assertEquals("ADEFBC", documentText());
    }
    public void testReplace() throws BadLocationException {
        filter.insertString(bypass, 0, "XYZ", null);
        filter.replace(bypass, 1, 2, "tc", null);
        assertEquals("XTC", documentText());
        filter.replace(bypass, 0, 3, "p8A", null);
        assertEquals("P8A", documentText());
    }
    protected String documentText() throws BadLocationException {
        return document.getText(0, document.getLength());
    }
    protected DocumentFilter.FilterBypass createBypass() {
        return new DocumentFilter.FilterBypass() {
            private AbstractDocument document = new PlainDocument();
            public Document getDocument() {
                return document;
            }
            public void insertString(
                int offset, String string, AttributeSet attr)  {
                try {
                    document.insertString(offset, string, attr);
                }
                catch (BadLocationException e) {}
            }
            public void remove(int offset, int length) {}
            public void replace(int offset, 
                int length, String string, AttributeSet attrs) {
                try {
                    document.replace(offset, length, string, attrs);
                }
                catch (BadLocationException e) {}
            }
        };
    }
}

Sis 类中为部门文本字段添加过滤器:

private void initialize() {
    createCoursesPanel();
    createKeyListeners();
    createInputFilters();
    ...
}
private void createInputFilters() {
    JTextField field = 
        panel.getField(CoursesPanel.DEPARTMENT_FIELD_NAME);
    AbstractDocument document = (AbstractDocument)field.getDocument();
    document.setDocumentFilter(new UpcaseFilter());
}

除了自定义过滤器,还可以使用 JFormattedTextField 类来管理字段编辑。可以为字段附加格式化器,确保内容符合指定的格式,并以适当的对象类型检索字段内容。

通过以上技术,可以构建出布局合理、美观且用户体验良好的Java Swing应用程序。在实际开发中,应根据具体需求选择合适的布局管理器和界面优化技术,不断提升应用程序的质量和用户满意度。

5. 自定义过滤器与字段长度限制

在前面提到,为了保证用户输入的有效性,我们使用了自定义的 DocumentFilter 类。除了将输入字符转换为大写字母的 UpcaseFilter ,还需要对字段的字符长度进行限制。可以创建一个 LimitFilter 类来实现这一功能。

package sis.ui;
import javax.swing.text.*;
public class LimitFilter extends DocumentFilter {
    private int limit;
    public LimitFilter(int limit) {
        this.limit = limit;
    }
    public void insertString(
        DocumentFilter.FilterBypass bypass,
        int offset,
        String str,
        AttributeSet attrSet) throws BadLocationException {
        replace(bypass, offset, 0, str, attrSet);
    }
    public void replace(
        DocumentFilter.FilterBypass bypass,
        int offset,
        int length,
        String str,
        AttributeSet attrSet) throws BadLocationException {
        int newLength = 
            bypass.getDocument().getLength() - length + str.length();
        if (newLength > limit)
            throw new BadLocationException(
                "New characters exceeds max size of document", offset);
        bypass.replace(offset, length, str, attrSet);
    }
}

在这个 LimitFilter 类中,构造函数接受一个 limit 参数,表示字段允许的最大字符长度。 insertString 方法将插入操作委托给 replace 方法,而 replace 方法会检查插入或替换后的新长度是否超过限制,如果超过则抛出 BadLocationException

然而,一个 Document 对象只能设置一个过滤器。为了解决这个问题,可以创建一个 ChainableFilter 类,它可以管理一系列的过滤器,并依次调用它们。以下是相关的测试和使用流程:

graph TD;
    A[创建LimitFilter和UpcaseFilter] --> B[创建ChainableFilter并添加子过滤器];
    B --> C[获取文本字段的Document对象];
    C --> D[将ChainableFilter设置为Document的过滤器];
6. JFormattedTextField的使用

JFormattedTextField JTextField 的子类,可以为其附加格式化器,确保字段内容符合指定的格式,并且可以以适当的对象类型检索字段内容。

以课程的有效日期字段为例,用户必须以 mm/dd/yy 的格式输入日期。以下是相关的测试代码:

private void verifyEffectiveDate() {
    assertLabelText(EFFECTIVE_DATE_LABEL_NAME,
        EFFECTIVE_DATE_LABEL_TEXT);
    JFormattedTextField dateField =
        (JFormattedTextField)panel.getField(EFFECTIVE_DATE_FIELD_NAME);
    DateFormatter formatter = (DateFormatter)dateField.getFormatter();
    SimpleDateFormat format = (SimpleDateFormat)formatter.getFormat();
    assertEquals("MM/dd/yy", format.toPattern());
    assertEquals(Date.class, dateField.getValue().getClass());
}

CoursesPanel 中创建 JFormattedTextField 的代码如下:

JPanel createFieldsPanel() {
    GridBagLayout layout = new GridBagLayout();
    JPanel panel = new JPanel(layout);
    int columns = 20;
    addField(panel, layout, 0,
        DEPARTMENT_LABEL_NAME, DEPARTMENT_LABEL_TEXT,
        createField(DEPARTMENT_FIELD_NAME, columns));
    addField(panel, layout, 1,
        NUMBER_LABEL_NAME, NUMBER_LABEL_TEXT,
        createField(NUMBER_FIELD_NAME, columns));
    Format format = new SimpleDateFormat("MM/dd/yy");
    JFormattedTextField dateField = new JFormattedTextField(format);
    dateField.setValue(new Date());
    dateField.setColumns(columns);
    dateField.setName(EFFECTIVE_DATE_FIELD_NAME);
    addField(panel, layout, 2,
        EFFECTIVE_DATE_LABEL_NAME, EFFECTIVE_DATE_LABEL_TEXT,
        dateField);
    return panel;
}

在上述代码中,创建了一个 SimpleDateFormat 对象作为格式化器,并将其传递给 JFormattedTextField 的构造函数。同时,设置了初始日期值。

7. 分离视图与业务逻辑

为了使代码更加清晰和易于维护,需要将视图和业务逻辑分离。可以创建几个新的类来实现这一目标。

  • Field 类 :这是一个简单的数据类,用于描述创建 Swing 文本字段所需的信息,不包含任何 Swing 相关的知识。
  • FieldCatalog 类 :包含可用字段的集合,可以根据字段名称返回 Field 对象。
// FieldCatalog.java
package sis.ui;
import java.util.*;
import java.text.*;
public class FieldCatalog {
    public static final DateFormat DEFAULT_DATE_FORMAT =
        new SimpleDateFormat("MM/dd/yy");
    static final String DEPARTMENT_FIELD_NAME = "deptField";
    static final String DEPARTMENT_LABEL_TEXT = "Department";
    static final int DEPARTMENT_FIELD_LIMIT = 4;
    static final String NUMBER_FIELD_NAME = "numberField";
    static final String NUMBER_LABEL_TEXT = "Number";
    static final int NUMBER_FIELD_LIMIT = 3;
    static final String EFFECTIVE_DATE_FIELD_NAME = "effectiveDateField";
    static final String EFFECTIVE_DATE_LABEL_TEXT = "Effective Date";
    static final int DEFAULT_COLUMNS = 20;
    private Map<String,Field> fields;
    public FieldCatalog() {
        loadFields();
    }
    public int size() {
        return fields.size();
    }
    private void loadFields() {
        fields = new HashMap<String,Field>();
        Field fieldSpec = new Field(DEPARTMENT_FIELD_NAME);
        fieldSpec.setLabel(DEPARTMENT_LABEL_TEXT);
        fieldSpec.setLimit(DEPARTMENT_FIELD_LIMIT);
        fieldSpec.setColumns(DEFAULT_COLUMNS);
        fieldSpec.setUpcaseOnly();
        put(fieldSpec);
        fieldSpec = new Field(NUMBER_FIELD_NAME);
        fieldSpec.setLabel(NUMBER_LABEL_TEXT);
        fieldSpec.setLimit(NUMBER_FIELD_LIMIT);
        fieldSpec.setColumns(DEFAULT_COLUMNS);
        put(fieldSpec);
        fieldSpec = new Field(EFFECTIVE_DATE_FIELD_NAME);
        fieldSpec.setLabel(EFFECTIVE_DATE_LABEL_TEXT);
        fieldSpec.setFormat(DEFAULT_DATE_FORMAT);
        fieldSpec.setInitialValue(new Date());
        fieldSpec.setColumns(DEFAULT_COLUMNS);
        put(fieldSpec);
    }
    private void put(Field fieldSpec) {
        fields.put(fieldSpec.getName(), fieldSpec);
    }
    public Field get(String fieldName) {
        return fields.get(fieldName);
    }
}
  • TextFieldFactory 类 :负责根据 Field 对象创建 JTextField ,并添加各种约束,如格式、过滤器和长度限制。
// TextFieldFactory.java
package sis.ui;
import javax.swing.*;
import javax.swing.text.*;
public class TextFieldFactory {
    public static JTextField create(Field fieldSpec) {
        JTextField field = null;
        if (fieldSpec.getFormat() != null)
            field = createFormattedTextField(fieldSpec);
        else {
            field = new JTextField();
            if (fieldSpec.getInitialValue() != null)
                field.setText(fieldSpec.getInitialValue().toString());
        }
        if (fieldSpec.getLimit() > 0)
            attachLimitFilter(field, fieldSpec.getLimit());
        if (fieldSpec.isUpcaseOnly())
            attachUpcaseFilter(field);
        field.setColumns(fieldSpec.getColumns());
        field.setName(fieldSpec.getName());
        return field;
    }
    private static void attachLimitFilter(JTextField field, int limit) {
        attachFilter(field, new LimitFilter(limit));
    }
    private static void attachUpcaseFilter(JTextField field) {
        attachFilter(field, new UpcaseFilter());
    }
    private static void attachFilter(
        JTextField field, ChainableFilter filter) {
        AbstractDocument document = (AbstractDocument)field.getDocument();
        ChainableFilter existingFilter = 
            (ChainableFilter)document.getDocumentFilter();
        if (existingFilter == null)
            document.setDocumentFilter(filter);
        else
            existingFilter.setNext(filter);
    }
    private static JTextField createFormattedTextField(Field fieldSpec) {
        JFormattedTextField field = 
            new JFormattedTextField(fieldSpec.getFormat());
        field.setValue(fieldSpec.getInitialValue());
        return field;
    }
}
  • CoursesPanel 类 :只需要包含要渲染的字段名称列表,通过迭代该列表,从 FieldCatalog 获取 Field 对象,并将其传递给 TextFieldFactory 来创建 JTextField
// CoursesPanel.java
...
JPanel createFieldsPanel() {
    GridBagLayout layout = new GridBagLayout();
    JPanel panel = new JPanel(layout);
    int i = 0;
    FieldCatalog catalog = new FieldCatalog();
    for (String fieldName: getFieldNames()) {
        Field fieldSpec = catalog.get(fieldName);
        addField(panel, layout, i++,
            createLabel(fieldSpec),
            TextFieldFactory.create(fieldSpec));
    }
    return panel;
}
private String[] getFieldNames() {
    return new String[]
        { FieldCatalog.DEPARTMENT_FIELD_NAME,
          FieldCatalog.NUMBER_FIELD_NAME,
          FieldCatalog.EFFECTIVE_DATE_FIELD_NAME };
}
private void addField(
    JPanel panel, GridBagLayout layout, int row,
    JLabel label, JTextField field) {
    ...
    panel.add(label);
    panel.add(field);
}
...
8. 总结

通过本文的介绍,我们深入了解了 Java Swing 中的布局管理和界面优化技术。从 GridBagLayout SpringLayout 等布局管理器的使用,到界面美观性的提升,如使用滚动面板、边框、标题和图标,再到用户体验的优化,包括键盘支持、按钮助记符、必需字段的处理和字段编辑,以及自定义过滤器和 JFormattedTextField 的应用。最后,通过分离视图和业务逻辑,使代码更加清晰和易于维护。

在实际开发中,要根据具体的需求选择合适的布局管理器和界面优化技术。同时,注重代码的可维护性和可测试性,将业务逻辑和视图逻辑分离,这样可以提高开发效率,降低维护成本。希望这些技术能够帮助你构建出布局合理、美观且用户体验良好的 Java Swing 应用程序。

以下是整个开发流程的总结表格:
| 技术点 | 作用 | 示例代码 |
| ---- | ---- | ---- |
| GridBagLayout | 高度可配置的布局管理器,组件可跨越多行或多列 | JPanel createFieldsPanel() 方法 |
| SpringLayout | 通过弹簧约束定义布局,适用于 GUI 组合工具 | |
| JScrollPane | 解决 JList 显示不全的问题,提供滚动功能 | private void createLayout() 方法 |
| 边框(Borders) | 在面板边缘和组件之间创建缓冲区,增加美观性 | private void createLayout() 方法 |
| 添加标题 | 为窗口标题栏添加文本 | Sis 类和 SisTest 类相关代码 |
| 图标(Icons) | 为窗口添加图标 | ImageUtil 类和相关测试代码 |
| 键盘支持 | 允许用户使用键盘控制应用程序 | |
| 按钮助记符(Button Mnemonics) | 使用 Alt 键组合激活按钮 | CoursesPanelTest CoursesPanel 类相关代码 |
| 必需字段(Required Fields) | 确保用户输入必要信息 | Sis 类和相关测试代码 |
| 字段编辑(Field Edits) | 避免用户输入无效数据,使用自定义过滤器和 JFormattedTextField | UpcaseFilter LimitFilter 类和相关测试代码 |
| 分离视图与业务逻辑 | 提高代码可维护性和可测试性 | Field FieldCatalog TextFieldFactory CoursesPanel 类 |

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值