ZetCode

Java Swing 模型架构

最后修改于 2023 年 1 月 10 日

Swing 工程师创建了 Swing 工具包,实现了改进的 Model View Controller 设计模式。这使得可以有效地处理数据并在运行时使用可插拔的外观和感觉。

传统的 MVC 模式将一个应用程序分为三个部分:模型、视图和控制器。模型表示应用程序中的数据。视图是数据的可视化表示。最后,控制器处理并响应事件,通常是用户操作,并且可以对模型调用更改。其思想是通过引入中间组件:控制器,将数据访问和业务逻辑与数据呈现和用户交互分离。

Swing 工具包使用改进的 MVC 设计模式。它有一个用于视图和控制器的单一 UI 对象。这种改进的 MVC 有时被称为 可分离的模型架构

在 Swing 工具包中,每个组件都有其模型,即使是按钮等基本组件。Swing 工具包中有两种模型

状态模型处理组件的状态。例如,模型跟踪组件是否被选中或按下。数据模型处理它们所使用的数据。一个列表组件保留它正在显示的项目的列表。

对于 Swing 开发者来说,这意味着他们通常需要获取一个模型实例才能操作组件中的数据。但也有例外。为了方便起见,有一些方法可以在无需程序员访问模型的情况下返回数据。

public int getValue() { 

    return getModel().getValue(); 
}

一个例子是 JSlider 组件的 getValue() 方法。开发者不需要直接使用模型。相反,对模型的访问是在幕后完成的。在如此简单的情况下直接使用模型将是过度的。因此,Swing 提供了一些方便的方法,例如前一个方法。

为了查询模型的状态,我们有两种通知方式

轻量级通知使用 ChangeListener 类。我们只有一个事件 (ChangeEvent) 用于来自组件的所有通知。对于更复杂的组件,使用有状态通知。对于此类通知,我们有不同类型的事件。例如,JList 组件有 ListDataEventListSelectionEvent

如果我们没有为组件设置模型,则会创建一个默认模型。例如,按钮组件有一个 DefaultButtonModel 模型。

public JButton(String text, Icon icon) {
  // Create the model
  setModel(new DefaultButtonModel());

  // initialize
  init(text, icon);
}

查看 JButton.java 源文件,我们发现默认模型是在组件的构造过程中创建的。

ButtonModel

该模型用于各种按钮,如推送按钮、复选框、单选框和菜单项。以下示例说明了 JButton 的模型。我们只能管理按钮的状态,因为没有数据可以与推送按钮关联。

com/zetcode/ButtonModelEx.java
package com.zetcode;

import javax.swing.AbstractAction;
import javax.swing.DefaultButtonModel;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;

public class ButtonModelEx extends JFrame {

    private JButton okBtn;
    private JLabel enabledLbl;
    private JLabel pressedLbl;
    private JLabel armedLbl;
    private JCheckBox checkBox;

    public ButtonModelEx() {

        initUI();
    }

    private void initUI() {

        okBtn = new JButton("OK");
        okBtn.addChangeListener(new DisabledChangeListener());
        checkBox = new JCheckBox();
        checkBox.setAction(new CheckBoxAction());

        enabledLbl = new JLabel("Enabled: true");
        pressedLbl = new JLabel("Pressed: false");
        armedLbl = new JLabel("Armed: false");

        createLayout(okBtn, checkBox, enabledLbl, pressedLbl, armedLbl);

        setTitle("ButtonModel");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    private void createLayout(JComponent... arg) {

        var pane = getContentPane();
        var gl = new GroupLayout(pane);
        pane.setLayout(gl);


        gl.setAutoCreateContainerGaps(true);
        gl.setAutoCreateGaps(true);

        gl.setHorizontalGroup(gl.createParallelGroup()
                .addGroup(gl.createSequentialGroup()
                        .addComponent(arg[0])
                        .addGap(80)
                        .addComponent(arg[1]))
                .addGroup(gl.createParallelGroup()
                        .addComponent(arg[2])
                        .addComponent(arg[3])
                        .addComponent(arg[4]))
        );

        gl.setVerticalGroup(gl.createSequentialGroup()
                .addGroup(gl.createParallelGroup()
                        .addComponent(arg[0])
                        .addComponent(arg[1]))
                .addGap(40)
                .addGroup(gl.createSequentialGroup()
                        .addComponent(arg[2])
                        .addComponent(arg[3])
                        .addComponent(arg[4]))
        );

        pack();
    }

    private class DisabledChangeListener implements ChangeListener {

        @Override
        public void stateChanged(ChangeEvent e) {

            var model = (DefaultButtonModel) okBtn.getModel();

            if (model.isEnabled()) {
                enabledLbl.setText("Enabled: true");
            } else {
                enabledLbl.setText("Enabled: false");
            }

            if (model.isArmed()) {
                armedLbl.setText("Armed: true");
            } else {
                armedLbl.setText("Armed: false");
            }

            if (model.isPressed()) {
                pressedLbl.setText("Pressed: true");
            } else {
                pressedLbl.setText("Pressed: false");
            }
        }
    }

    private class CheckBoxAction extends AbstractAction {

        public CheckBoxAction() {
            super("Disabled");
        }

        @Override
        public void actionPerformed(ActionEvent e) {

            if (okBtn.isEnabled()) {
                okBtn.setEnabled(false);
            } else {
                okBtn.setEnabled(true);
            }
        }
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {

            var ex = new ButtonModelEx();
            ex.setVisible(true);
        });
    }
}

在我们的例子中,我们有一个按钮、一个复选框和三个标签。标签代表按钮的三个属性:按下、禁用或武装状态。

okbtn.addChangeListener(new DisabledChangeListener());

我们使用 ChangeListener 来监听按钮状态的变化。

var model = (DefaultButtonModel) okBtn.getModel();

在这里,我们获取默认的按钮模型。

if (model.isEnabled()) {
    enabledLbl.setText("Enabled: true");
} else {
    enabledLbl.setText("Enabled: false");
}

我们查询模型按钮是否已启用。标签会相应地更新。

if (okBtn.isEnabled()) {
    okBtn.setEnabled(false);
} else {
    okBtn.setEnabled(true);
}

复选框启用或禁用按钮。要启用确定按钮,我们调用 setEnabled() 方法。因此,我们更改了按钮的状态。模型在哪里?答案在于 AbstractButton.java 文件。

public void setEnabled(boolean b) {
    if (!b && model.isRollover()) {
        model.setRollover(false);
    } 
    super.setEnabled(b);
    model.setEnabled(b);
}

答案是 Swing 工具包在内部使用一个模型。setEnabled() 是程序员的另一个方便方法。

ButtonModel
图:ButtonModel

自定义 ButtonModel

在前面的示例中,我们使用了默认的按钮模型。在下面的代码示例中,我们将使用我们自己的按钮模型。

com/zetcode/CustomButtonModelEx.java
package com.zetcode;

import javax.swing.AbstractAction;
import javax.swing.DefaultButtonModel;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;

public class CustomButtonModelEx extends JFrame {

    private JButton okBtn;
    private JLabel enabledLbl;
    private JLabel pressedLbl;
    private JLabel armedLbl;
    private JCheckBox checkBox;

    public CustomButtonModelEx() {

        initUI();
    }

    private void initUI() {

        okBtn = new JButton("OK");
        checkBox = new JCheckBox();
        checkBox.setAction(new CheckBoxAction());

        enabledLbl = new JLabel("Enabled: true");
        pressedLbl = new JLabel("Pressed: false");
        armedLbl  = new JLabel("Armed: false");

        var model = new OkButtonModel();
        okBtn.setModel(model);

        createLayout(okBtn, checkBox, enabledLbl, pressedLbl, armedLbl);

        setTitle("Custom button model");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    private void createLayout(JComponent... arg) {

        var pane = getContentPane();
        var gl = new GroupLayout(pane);
        pane.setLayout(gl);

        gl.setAutoCreateContainerGaps(true);
        gl.setAutoCreateGaps(true);

        gl.setHorizontalGroup(gl.createParallelGroup()
                .addGroup(gl.createSequentialGroup()
                        .addComponent(arg[0])
                        .addGap(80)
                        .addComponent(arg[1]))
                .addGroup(gl.createParallelGroup()
                        .addComponent(arg[2])
                        .addComponent(arg[3])
                        .addComponent(arg[4]))
        );

        gl.setVerticalGroup(gl.createSequentialGroup()
                .addGroup(gl.createParallelGroup()
                        .addComponent(arg[0])
                        .addComponent(arg[1]))
                .addGap(40)
                .addGroup(gl.createSequentialGroup()
                        .addComponent(arg[2])
                        .addComponent(arg[3])
                        .addComponent(arg[4]))
        );

        pack();
    }

    private class OkButtonModel extends DefaultButtonModel {

        @Override
        public void setEnabled(boolean b) {
            if (b) {
                enabledLbl.setText("Enabled: true");
            } else {
                enabledLbl.setText("Enabled: false");
            }

            super.setEnabled(b);
        }

        @Override
        public void setArmed(boolean b) {
            if (b) {
                armedLbl.setText("Armed: true");
            } else {
                armedLbl.setText("Armed: false");
            }

            super.setArmed(b);
        }

        @Override
        public void setPressed(boolean b) {
            if (b) {
                pressedLbl.setText("Pressed: true");
            } else {
                pressedLbl.setText("Pressed: false");
            }

            super.setPressed(b);
        }
    }

    private class CheckBoxAction extends AbstractAction {

        public CheckBoxAction() {
            super("Disabled");
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (okBtn.isEnabled()) {
                okBtn.setEnabled(false);
            } else {
                okBtn.setEnabled(true);
            }
        }
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {

            var ex = new CustomButtonModelEx();
            ex.setVisible(true);
        });
    }
}

此示例与前一个示例执行相同的操作。区别在于我们不使用更改监听器,并且我们使用自定义按钮模型。

var model = new OkButtonModel();
okBtn.setModel(model);

我们为按钮设置自定义模型。

private class OkButtonModel extends DefaultButtonModel {
...
}

我们创建一个自定义按钮模型并重写必要的方法。

@Override
public void setEnabled(boolean b) {
    if (b) {
        enabledLbl.setText("Enabled: true");
    } else {
        enabledLbl.setText("Enabled: false");
    }

    super.setEnabled(b);
}

我们重写 setEnabled() 方法并在其中添加一些功能。我们也不要忘记调用父方法以继续处理。

JList 模型

几个组件有两个模型;JList 是其中之一。它具有以下模型:ListModelListSelectionModelListModel 处理数据,而 ListSelectionModel 处理列表的选择状态。以下示例使用这两个模型。

com/zetcode/ListModelsEx.java
package com.zetcode;

import javax.swing.DefaultListModel;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.ListSelectionModel;
import java.awt.EventQueue;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import static javax.swing.GroupLayout.Alignment.CENTER;

public class ListModelsEx extends JFrame {

    private DefaultListModel<String> model;
    private JList<String> myList;
    private JButton remAllBtn;
    private JButton addBtn;
    private JButton renBtn;
    private JButton delBtn;

    public ListModelsEx() {

        initUI();
    }

    private void createList() {

        model = new DefaultListModel<>();
        model.addElement("Amelie");
        model.addElement("Aguirre, der Zorn Gottes");
        model.addElement("Fargo");
        model.addElement("Exorcist");
        model.addElement("Schindler's myList");

        myList = new JList<>(model);
        myList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

        myList.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseClicked(MouseEvent e) {

                if (e.getClickCount() == 2) {

                    int index = myList.locationToIndex(e.getPoint());
                    var item = model.getElementAt(index);
                    var text = JOptionPane.showInputDialog("Rename item", item);

                    String newItem;

                    if (text != null) {
                        newItem = text.trim();
                    } else {
                        return;
                    }

                    if (!newItem.isEmpty()) {

                        model.remove(index);
                        model.add(index, newItem);

                        var selModel = myList.getSelectionModel();
                        selModel.setLeadSelectionIndex(index);
                    }
                }
            }
        });
    }

    private void createButtons() {

        remAllBtn = new JButton("Remove All");
        addBtn = new JButton("Add");
        renBtn = new JButton("Rename");
        delBtn = new JButton("Delete");

        addBtn.addActionListener(e -> {

            var text = JOptionPane.showInputDialog("Add a new item");
            String item;

            if (text != null) {
                item = text.trim();
            } else {
                return;
            }

            if (!item.isEmpty()) {

                model.addElement(item);
            }
        });

        delBtn.addActionListener(event -> {

            var selModel = myList.getSelectionModel();

            int index = selModel.getMinSelectionIndex();

            if (index >= 0) {
                model.remove(index);
            }
        });

        renBtn.addActionListener(e -> {

            var selModel = myList.getSelectionModel();
            int index = selModel.getMinSelectionIndex();

            if (index == -1) {
                return;
            }

            var item = model.getElementAt(index);
            var text = JOptionPane.showInputDialog("Rename item", item);
            String newItem;

            if (text != null) {
                newItem = text.trim();
            } else {
                return;
            }

            if (!newItem.isEmpty()) {

                model.remove(index);
                model.add(index, newItem);
            }
        });

        remAllBtn.addActionListener(e -> model.clear());
    }

    private void initUI() {

        createList();
        createButtons();

        var scrollPane = new JScrollPane(myList);
        createLayout(scrollPane, addBtn, renBtn, delBtn, remAllBtn);

        setTitle("JList models");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    private void createLayout(JComponent... arg) {

        var pane = getContentPane();
        var gl = new GroupLayout(pane);
        pane.setLayout(gl);

        gl.setAutoCreateContainerGaps(true);
        gl.setAutoCreateGaps(true);

        gl.setHorizontalGroup(gl.createSequentialGroup()
                .addComponent(arg[0])
                .addGroup(gl.createParallelGroup()
                        .addComponent(arg[1])
                        .addComponent(arg[2])
                        .addComponent(arg[3])
                        .addComponent(arg[4]))
        );

        gl.setVerticalGroup(gl.createParallelGroup(CENTER)
                .addComponent(arg[0])
                .addGroup(gl.createSequentialGroup()
                        .addComponent(arg[1])
                        .addComponent(arg[2])
                        .addComponent(arg[3])
                        .addComponent(arg[4]))
        );

        gl.linkSize(addBtn, renBtn, delBtn, remAllBtn);

        pack();
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {

            var ex = new ListModelsEx();
            ex.setVisible(true);
        });
    }
}

该示例显示了一个列表组件和四个按钮。这些按钮控制列表组件中的数据。该示例稍微大一些,因为我们在那里做了一些额外的检查。例如,我们不允许将空格输入到列表组件中。

model = new DefaultListModel<>();
model.addElement("Amelie");
model.addElement("Aguirre, der Zorn Gottes");
model.addElement("Fargo");
...

我们创建一个默认的列表模型并将元素添加到其中。

myList = new JList<>(model);
myList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

我们创建一个列表组件。构造函数的参数是我们创建的模型。我们将列表设置为单选模式。

if (text != null) {
    item = text.trim();
} else {
    return;
}

if (!item.isEmpty()) {

    model.addElement(item);
}

我们只添加不等于 null 且不为空的项,例如包含除空格之外的至少一个字符的项。将空格或空值添加到列表中没有意义。

var selModel = myList.getSelectionModel();

int index = selModel.getMinSelectionIndex();

if (index >= 0) {
    model.remove(index);
}

这是按下删除按钮时运行的代码。为了从列表中删除一个项目,它必须被选中 - 我们必须找出当前选中的项目。为此,我们调用 getSelectionModel() 方法。我们使用 getMinSelectionIndex() 获取选定的索引,并使用 remove() 方法删除该项目。

在本例中,我们使用了两个列表模型。我们调用了列表数据模型的 add()remove()clear() 方法来处理我们的数据。并且我们使用了列表选择模型来找出选定的项目。

List models
图:列表模型

Java Swing 文档模型

文档模型是数据与可视化表示分离的一个很好的例子。在 JTextPane 组件中,我们有一个 StyledDocument 用于设置文本数据的样式。

com/zetcode/DocumentModelEx.java
package com.zetcode;

import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.JToolBar;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import java.awt.BorderLayout;
import java.awt.EventQueue;

public class DocumentModelEx extends JFrame {

    private StyledDocument sdoc;
    private JTextPane textPane;

    public DocumentModelEx() {

        initUI();
    }

    private void initUI() {

        createToolbar();

        var panel =  new JPanel(new BorderLayout());
        panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));

        textPane = new JTextPane();
        sdoc = textPane.getStyledDocument();
        initStyles(textPane);

        panel.add(new JScrollPane(textPane));
        add(panel);
        pack();

        setTitle("Document Model");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    private void createToolbar() {

        var toolbar = new JToolBar();

        var bold = new ImageIcon("src/main/resources/bold.png");
        var italic = new ImageIcon("src/main/resources/italic.png");
        var strike = new ImageIcon("src/main/resources/strike.png");
        var underline = new ImageIcon("src/main/resources/underline.png");

        var boldBtn = new JButton(bold);
        var italBtn = new JButton(italic);
        var striBtn = new JButton(strike);
        var undeBtn = new JButton(underline);

        toolbar.add(boldBtn);
        toolbar.add(italBtn);
        toolbar.add(striBtn);
        toolbar.add(undeBtn);

        add(toolbar, BorderLayout.NORTH);

        boldBtn.addActionListener(e -> sdoc.setCharacterAttributes(
                textPane.getSelectionStart(),
                textPane.getSelectionEnd() - textPane.getSelectionStart(),
                textPane.getStyle("Bold"), false));

        italBtn.addActionListener(e -> sdoc.setCharacterAttributes(
                textPane.getSelectionStart(),
                textPane.getSelectionEnd() - textPane.getSelectionStart(),
                textPane.getStyle("Italic"), false));

        striBtn.addActionListener(e -> sdoc.setCharacterAttributes(
                textPane.getSelectionStart(),
                textPane.getSelectionEnd() - textPane.getSelectionStart(),
                textPane.getStyle("Strike"), false));

        undeBtn.addActionListener(e -> sdoc.setCharacterAttributes(
                textPane.getSelectionStart(),
                textPane.getSelectionEnd() - textPane.getSelectionStart(),
                textPane.getStyle("Underline"), false));
    }

    private void initStyles(JTextPane textPane) {

        var style = textPane.addStyle("Bold", null);
        StyleConstants.setBold(style, true);

        style = textPane.addStyle("Italic", null);
        StyleConstants.setItalic(style, true);

        style = textPane.addStyle("Underline", null);
        StyleConstants.setUnderline(style, true);

        style = textPane.addStyle("Strike", null);
        StyleConstants.setStrikeThrough(style, true);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {

            var ex = new DocumentModelEx();
            ex.setVisible(true);
        });
    }
}

该示例有一个文本窗格和一个工具栏。在工具栏中,我们有四个按钮可以更改文本的属性。

sdoc = textpane.getStyledDocument();

在这里,我们获取样式文档,它是文本窗格组件的模型。

var style = textpane.addStyle("Bold", null);
StyleConstants.setBold(style, true);

样式是一组文本属性,例如颜色和大小。在这里,我们为文本窗格组件注册一个粗体样式。注册的样式可以随时检索。

doc.setCharacterAttributes(textpane.getSelectionStart(), 
    textpane.getSelectionEnd() - textpane.getSelectionStart(),
    textpane.getStyle("Bold"), false);

在这里,我们更改文本的属性。参数是选择的偏移量和长度、样式和布尔值替换。偏移量是应用粗体文本的文本的开头。我们通过减去选择结束值和选择开始值来获取长度值。布尔值 false 意味着我们没有用新的样式替换旧的样式,而是将它们合并。这意味着如果文本带有下划线并且我们将其设置为粗体,则结果是带下划线的粗体文本。

Document model
图:文档模型

在本章中,我们提到了 Swing 模型。