Java Swing 模型架构
Swing 工程师创建了 Swing 工具箱,实现了修改后的 MVC 设计模式。 这样可以有效地处理数据,并在运行时使用可插入的外观。
传统的 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
源文件,我们发现默认模型是在构建组件时创建的。
按钮模型
该模型用于各种按钮,例如按钮,复选框,单选框和菜单项。 以下示例说明了JButton
的模型。 因为没有数据可以与按钮关联,所以我们只能管理按钮的状态。
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
在前面的示例中,我们使用了默认按钮模型。 在下面的代码示例中,我们将使用我们自己的按钮模型。
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
是其中之一。 它具有以下模型:ListModel
和ListSelectionModel
。 ListModel
处理数据,ListSelectionModel
处理列表的选择状态。 以下示例使用两种模型。
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()
方法来处理我们的数据。 并且我们使用了一个列表选择模型,以便找出所选项目。
图:列表模型
文件模型
文档模型是从视觉表示中分离数据的一个很好的例子。 在JTextPane
组件中,我们有一个StyledDocument
用于设置文本数据的样式。
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
表示我们不会用新样式替换旧样式,而是将它们合并。 这意味着如果文本带有下划线,并且我们将其设为粗体,则结果为带下划线的粗体文本。
图:文档模型
在本章中,我们提到了 Swing 模型。