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
组件有 ListDataEvent
和 ListSelectionEvent
。
如果我们没有为组件设置模型,则会创建一个默认模型。例如,按钮组件有一个 DefaultButtonModel
模型。
public JButton(String text, Icon icon) { // Create the model setModel(new DefaultButtonModel()); // initialize init(text, icon); }
查看 JButton.java 源文件,我们发现默认模型是在组件的构造过程中创建的。
ButtonModel
该模型用于各种按钮,如推送按钮、复选框、单选框和菜单项。以下示例说明了 JButton
的模型。我们只能管理按钮的状态,因为没有数据可以与推送按钮关联。
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
在前面的示例中,我们使用了默认的按钮模型。在下面的代码示例中,我们将使用我们自己的按钮模型。
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
是其中之一。它具有以下模型:ListModel
和 ListSelectionModel
。ListModel
处理数据,而 ListSelectionModel
处理列表的选择状态。以下示例使用这两个模型。
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()
方法来处理我们的数据。并且我们使用了列表选择模型来找出选定的项目。

Java Swing 文档模型
文档模型是数据与可视化表示分离的一个很好的例子。在 JTextPane
组件中,我们有一个 StyledDocument
用于设置文本数据的样式。
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 意味着我们没有用新的样式替换旧的样式,而是将它们合并。这意味着如果文本带有下划线并且我们将其设置为粗体,则结果是带下划线的粗体文本。

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