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 模型。