我目前正在编写一个模板Java应用程序,不知何故,如果我想干净地遵循MVC模式,我不确定ActionListeners所属的位置。
示例是基于Swing的,但它不是关于框架,而是关于Java中MVC的基本概念,使用任何框架来创建GUI。
我开始使用一个包含JFrame和JButton的绝对简单的应用程序(用于处理框架因此关闭应用程序)。该帖子的代码落后。没有什么特别的,只是为了澄清我们在说什么。我没有从模型开始,因为这个问题太烦人了。
已经有不止一个类似的问题,如下:
MVC pattern with many ActionListeners
Java swing - Where should the ActionListener go?
但是他们之所以非常满意,因为我想知道两件事:
我希望这不是太笼统或模糊,我在这里问,但这让我想了一会儿。我总是使用我自己的方式,让ActionHandler了解Controller ,但这似乎不对,所以我最终想知道这是如何正确完成的。
亲切的问候,
杰森
控制器:
package controller;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import view.MainView;
public class MainController
{
MainView mainView = new MainView();
public MainController()
{
this.initViewActionListeners();
}
private void initViewActionListeners()
{
mainView.initButtons(new CloseListener());
}
public class CloseListener implements ActionListener
{
@Override
public void actionPerformed(ActionEvent e)
{
mainView.dispose();
}
}
}
视图:
package view;
import java.awt.Dimension;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class MainView extends JFrame
{
JButton button_close = new JButton();
JPanel panel_mainPanel = new JPanel();
private static final long serialVersionUID = 5791734712409634055L;
public MainView()
{
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
this.setSize(500, 500);
this.add(panel_mainPanel);
setVisible(true);
}
public void initButtons(ActionListener actionListener)
{
this.button_close = new JButton("Close");
this.button_close.setSize(new Dimension(100, 20));
this.button_close.addActionListener(actionListener);
this.panel_mainPanel.add(button_close);
}
}
答案 0 :(得分:16)
这对于Swing来说是一个非常难以回答的问题,因为Swing不是纯粹的MVC实现,视图和控制器是混合的。
从技术上讲,模型和控制器应该能够交互,控制器和视图应该能够交互,但视图和模型永远不应该交互,这显然不是Swing的工作方式,而是“#”。另一场辩论......
另一个问题是,你真的不想向任何人公开UI组件,控制器不应该关心某些动作是如何发生的,只是他们可以。
这表明视图应维护附加到UI控件的ActionListener
。然后,视图应警告控制器已发生某种操作。为此,您可以使用由视图管理的另一个ActionListener
,控制器订阅该文件。
更好的是,我会有一个专用的视图监听器,它描述了这个视图可能产生的动作,例如......
public interface MainViewListener {
public void didPerformClose(MainView mainView);
}
然后控制器将通过此侦听器订阅视图,并且当(在这种情况下)按下关闭按钮时,视图将调用didPerformClose
。
即使在这个例子中,我也很想制作一个"主视图"接口,描述了任何实现保证提供的属性(setter和getters)和动作(监听器/回调),然后你不关心这些动作是如何发生的,只有当它们发生时,你才应该这样做东西...
在您想要问自己的每个级别,更改另一个实例的任何元素(更改模型或控制器或视图)有多容易?如果您发现自己必须解耦代码,那么您就遇到了问题。通过接口进行通信,并尝试减少各层之间的耦合量以及每个层对其他层知道的数量,直到他们只是维护合同
<强>更新... 强>
让我们以此为例......
实际上有两个视图(折扣实际对话框),有凭据视图和登录视图,是的,它们是不同的,如您所见。
凭据视图负责收集要进行身份验证的详细信息,用户名和密码。它将向控制器提供信息,让它知道何时更改了这些凭证,因为控制器可能想要采取某些行动,例如启用&#34;登录&#34;按钮...
该视图还希望知道何时将要进行身份验证,因为它将要禁用它的字段,因此用户无法在身份验证发生时更新视图,同样,它需要知道身份验证何时失败或成功,因为它需要对这些可能性采取行动。
public interface CredentialsView {
public String getUserName();
public char[] getPassword();
public void willAuthenticate();
public void authenticationFailed();
public void authenticationSucceeded();
public void setCredentialsViewController(CredentialsViewController listener);
}
public interface CredentialsViewController {
public void credientialsDidChange(CredentialsView view);
}
CredentialsPane
是CredentialsView
的物理实现,它实现合同,但管理它自己的内部状态。如何管理合同对控制人来说是无关紧要的,它只关心合同的维护......
public class CredentialsPane extends JPanel implements CredentialsView {
private CredentialsViewController controller;
private JTextField userNameField;
private JPasswordField passwordField;
public CredentialsPane(CredentialsViewController controller) {
setCredentialsViewController(controller);
setLayout(new GridBagLayout());
userNameField = new JTextField(20);
passwordField = new JPasswordField(20);
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.insets = new Insets(2, 2, 2, 2);
gbc.anchor = GridBagConstraints.EAST;
add(new JLabel("Username: "), gbc);
gbc.gridy++;
add(new JLabel("Password: "), gbc);
gbc.gridx = 1;
gbc.gridy = 0;
gbc.anchor = GridBagConstraints.WEST;
gbc.fill = GridBagConstraints.HORIZONTAL;
add(userNameField, gbc);
gbc.gridy++;
add(passwordField, gbc);
DocumentListener listener = new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
getCredentialsViewController().credientialsDidChange(CredentialsPane.this);
}
@Override
public void removeUpdate(DocumentEvent e) {
getCredentialsViewController().credientialsDidChange(CredentialsPane.this);
}
@Override
public void changedUpdate(DocumentEvent e) {
getCredentialsViewController().credientialsDidChange(CredentialsPane.this);
}
};
userNameField.getDocument().addDocumentListener(listener);
passwordField.getDocument().addDocumentListener(listener);
}
@Override
public CredentialsViewController getCredentialsViewController() {
return controller;
}
@Override
public String getUserName() {
return userNameField.getText();
}
@Override
public char[] getPassword() {
return passwordField.getPassword();
}
@Override
public void willAuthenticate() {
userNameField.setEnabled(false);
passwordField.setEnabled(false);
}
@Override
public void authenticationFailed() {
userNameField.setEnabled(true);
passwordField.setEnabled(true);
userNameField.requestFocusInWindow();
userNameField.selectAll();
JOptionPane.showMessageDialog(this, "Authentication has failed", "Error", JOptionPane.ERROR_MESSAGE);
}
@Override
public void authenticationSucceeded() {
// Really don't care, but you might want to stop animation, for example...
}
public void setCredentialsViewController(CredentialsViewController controller){
this.controller = controller;
}
}
LoginView
负责管理CredentialsView
,但也用于通知LoginViewController
何时进行身份验证,或者用户是否通过某种方式取消了流程。
同样,LoginViewController
将告知视图何时进行身份验证以及身份验证失败或成功。
public interface LoginView {
public CredentialsView getCredentialsView();
public void willAuthenticate();
public void authenticationFailed();
public void authenticationSucceeded();
public void dismissView();
public LoginViewController getLoginViewController();
}
public interface LoginViewController {
public void authenticationWasRequested(LoginView view);
public void loginWasCancelled(LoginView view);
}
LoginPane
有点特殊,它充当LoginViewController
的视图,但它也充当CredentialsView
的控制器。这很重要,因为没有什么可以说视图不能成为一个控制器,但我会小心你如何实现这些事情,因为这样做可能并不总是有意义,但因为这两个在这种情况下,视图正在共同收集信息和管理事件。
因为LoginPane
需要根据CredentialsView
中的更改来更改自己的状态,所以允许LoginPane
充当控制器是有意义的在这种情况下,否则,您需要提供更多控制按钮状态的方法,但这会开始将UI逻辑渗透到控制器......
public static class LoginPane extends JPanel implements LoginView, CredentialsViewController {
private LoginViewController controller;
private CredentialsPane credientialsView;
private JButton btnAuthenticate;
private JButton btnCancel;
private boolean wasAuthenticated;
public LoginPane(LoginViewController controller) {
setLoginViewController(controller);
setLayout(new BorderLayout());
setBorder(new EmptyBorder(8, 8, 8, 8));
btnAuthenticate = new JButton("Login");
btnCancel = new JButton("Cancel");
JPanel buttons = new JPanel();
buttons.add(btnAuthenticate);
buttons.add(btnCancel);
add(buttons, BorderLayout.SOUTH);
credientialsView = new CredentialsPane(this);
add(credientialsView);
btnAuthenticate.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
getLoginViewController().authenticationWasRequested(LoginPane.this);
}
});
btnCancel.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
getLoginViewController().loginWasCancelled(LoginPane.this);
// I did think about calling dispose here,
// but's not really the the job of the cancel button to decide what should happen here...
}
});
validateCreientials();
}
public static boolean showLoginDialog(LoginViewController controller) {
final LoginPane pane = new LoginPane(controller);
JDialog dialog = new JDialog();
dialog.setTitle("Login");
dialog.setModal(true);
dialog.add(pane);
dialog.pack();
dialog.setLocationRelativeTo(null);
dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
dialog.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
pane.getLoginViewController().loginWasCancelled(pane);
}
});
dialog.setVisible(true);
return pane.wasAuthenticated();
}
public boolean wasAuthenticated() {
return wasAuthenticated;
}
public void validateCreientials() {
CredentialsView view = getCredentialsView();
String userName = view.getUserName();
char[] password = view.getPassword();
if ((userName != null && userName.trim().length() > 0) && (password != null && password.length > 0)) {
btnAuthenticate.setEnabled(true);
} else {
btnAuthenticate.setEnabled(false);
}
}
@Override
public void dismissView() {
SwingUtilities.windowForComponent(this).dispose();
}
@Override
public CredentialsView getCredentialsView() {
return credientialsView;
}
@Override
public void willAuthenticate() {
getCredentialsView().willAuthenticate();
btnAuthenticate.setEnabled(false);
}
@Override
public void authenticationFailed() {
getCredentialsView().authenticationFailed();
validateCreientials();
wasAuthenticated = false;
}
@Override
public void authenticationSucceeded() {
getCredentialsView().authenticationSucceeded();
validateCreientials();
wasAuthenticated = true;
}
public LoginViewController getLoginViewController() {
return controller;
}
public void setLoginViewController(LoginViewController controller) {
this.controller = controller;
}
@Override
public void credientialsDidChange(CredentialsView view) {
validateCreientials();
}
}
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.EmptyBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import sun.net.www.protocol.http.HttpURLConnection;
public class Test {
protected static final Random AUTHENTICATION_ORACLE = new Random();
public static void main(String[] args) {
new Test();
}
public interface CredentialsView {
public String getUserName();
public char[] getPassword();
public void willAuthenticate();
public void authenticationFailed();
public void authenticationSucceeded();
public CredentialsViewController getCredentialsViewController();
}
public interface CredentialsViewController {
public void credientialsDidChange(CredentialsView view);
}
public interface LoginView {
public CredentialsView getCredentialsView();
public void willAuthenticate();
public void authenticationFailed();
public void authenticationSucceeded();
public void dismissView();
public LoginViewController getLoginViewController();
}
public interface LoginViewController {
public void authenticationWasRequested(LoginView view);
public void loginWasCancelled(LoginView view);
}
public Test() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
LoginViewController controller = new LoginViewController() {
@Override
public void authenticationWasRequested(LoginView view) {
view.willAuthenticate();
LoginAuthenticator authenticator = new LoginAuthenticator(view);
authenticator.authenticate();
}
@Override
public void loginWasCancelled(LoginView view) {
view.dismissView();
}
};
if (LoginPane.showLoginDialog(controller)) {
System.out.println("You shell pass");
} else {
System.out.println("You shell not pass");
}
System.exit(0);
}
});
}
public class LoginAuthenticator {
private LoginView view;
public LoginAuthenticator(LoginView view) {
this.view = view;
}
public void authenticate() {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException ex) {
Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (AUTHENTICATION_ORACLE.nextBoolean()) {
view.authenticationSucceeded();
view.dismissView();
} else {
view.authenticationFailed();
}
}
});
}
});
t.start();
}
}
public static class LoginPane extends JPanel implements LoginView, CredentialsViewController {
private LoginViewController controller;
private CredentialsPane credientialsView;
private JButton btnAuthenticate;
private JButton btnCancel;
private boolean wasAuthenticated;
public LoginPane(LoginViewController controller) {
setLoginViewController(controller);
setLayout(new BorderLayout());
setBorder(new EmptyBorder(8, 8, 8, 8));
btnAuthenticate = new JButton("Login");
btnCancel = new JButton("Cancel");
JPanel buttons = new JPanel();
buttons.add(btnAuthenticate);
buttons.add(btnCancel);
add(buttons, BorderLayout.SOUTH);
credientialsView = new CredentialsPane(this);
add(credientialsView);
btnAuthenticate.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
getLoginViewController().authenticationWasRequested(LoginPane.this);
}
});
btnCancel.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
getLoginViewController().loginWasCancelled(LoginPane.this);
// I did think about calling dispose here,
// but's not really the the job of the cancel button to decide what should happen here...
}
});
validateCreientials();
}
public static boolean showLoginDialog(LoginViewController controller) {
final LoginPane pane = new LoginPane(controller);
JDialog dialog = new JDialog();
dialog.setTitle("Login");
dialog.setModal(true);
dialog.add(pane);
dialog.pack();
dialog.setLocationRelativeTo(null);
dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
dialog.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
pane.getLoginViewController().loginWasCancelled(pane);
}
});
dialog.setVisible(true);
return pane.wasAuthenticated();
}
public boolean wasAuthenticated() {
return wasAuthenticated;
}
public void validateCreientials() {
CredentialsView view = getCredentialsView();
String userName = view.getUserName();
char[] password = view.getPassword();
if ((userName != null && userName.trim().length() > 0) && (password != null && password.length > 0)) {
btnAuthenticate.setEnabled(true);
} else {
btnAuthenticate.setEnabled(false);
}
}
@Override
public void dismissView() {
SwingUtilities.windowForComponent(this).dispose();
}
@Override
public CredentialsView getCredentialsView() {
return credientialsView;
}
@Override
public void willAuthenticate() {
getCredentialsView().willAuthenticate();
btnAuthenticate.setEnabled(false);
}
@Override
public void authenticationFailed() {
getCredentialsView().authenticationFailed();
validateCreientials();
wasAuthenticated = false;
}
@Override
public void authenticationSucceeded() {
getCredentialsView().authenticationSucceeded();
validateCreientials();
wasAuthenticated = true;
}
public LoginViewController getLoginViewController() {
return controller;
}
public void setLoginViewController(LoginViewController controller) {
this.controller = controller;
}
@Override
public void credientialsDidChange(CredentialsView view) {
validateCreientials();
}
}
public static class CredentialsPane extends JPanel implements CredentialsView {
private CredentialsViewController controller;
private JTextField userNameField;
private JPasswordField passwordField;
public CredentialsPane(CredentialsViewController controller) {
setCredentialsViewController(controller);
setLayout(new GridBagLayout());
userNameField = new JTextField(20);
passwordField = new JPasswordField(20);
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.insets = new Insets(2, 2, 2, 2);
gbc.anchor = GridBagConstraints.EAST;
add(new JLabel("Username: "), gbc);
gbc.gridy++;
add(new JLabel("Password: "), gbc);
gbc.gridx = 1;
gbc.gridy = 0;
gbc.anchor = GridBagConstraints.WEST;
gbc.fill = GridBagConstraints.HORIZONTAL;
add(userNameField, gbc);
gbc.gridy++;
add(passwordField, gbc);
DocumentListener listener = new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
getCredentialsViewController().credientialsDidChange(CredentialsPane.this);
}
@Override
public void removeUpdate(DocumentEvent e) {
getCredentialsViewController().credientialsDidChange(CredentialsPane.this);
}
@Override
public void changedUpdate(DocumentEvent e) {
getCredentialsViewController().credientialsDidChange(CredentialsPane.this);
}
};
userNameField.getDocument().addDocumentListener(listener);
passwordField.getDocument().addDocumentListener(listener);
}
@Override
public CredentialsViewController getCredentialsViewController() {
return controller;
}
@Override
public String getUserName() {
return userNameField.getText();
}
@Override
public char[] getPassword() {
return passwordField.getPassword();
}
@Override
public void willAuthenticate() {
userNameField.setEnabled(false);
passwordField.setEnabled(false);
}
@Override
public void authenticationFailed() {
userNameField.setEnabled(true);
passwordField.setEnabled(true);
userNameField.requestFocusInWindow();
userNameField.selectAll();
JOptionPane.showMessageDialog(this, "Authentication has failed", "Error", JOptionPane.ERROR_MESSAGE);
}
@Override
public void authenticationSucceeded() {
// Really don't care, but you might want to stop animation, for example...
}
public void setCredentialsViewController(CredentialsViewController controller) {
this.controller = controller;
}
}
}
答案 1 :(得分:4)
它们与控件相关联,但它们不必是控件的直接部分。例如,请看下面发布的代码,我正在准备另一个问题,一个关于匿名内部类和耦合,这里我给所有按钮匿名内部动作(当然是ActionListeners),然后使用Actions来改变GUI状态。将向GUI(控件)的任何监听器通知此更改,然后可以采取相应的行动。
import java.awt.*;
import java.awt.event.*; java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;
import javax.swing.event.SwingPropertyChangeSupport;
public class AnonymousInnerEg2 {
private static void createAndShowUI() {
GuiModel2 model = new GuiModel2();
GuiPanel2 guiPanel = new GuiPanel2();
GuiControl2 guiControl = new GuiControl2();
guiControl.setGuiPanel(guiPanel);
guiControl.setGuiModel(model);
try {
guiControl.init();
} catch (GuiException2 e) {
e.printStackTrace();
System.exit(-1);
}
JFrame frame = new JFrame("AnonymousInnerEg");
frame.getContentPane().add(guiPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
createAndShowUI();
}
});
}
}
enum GuiState {
BASE("Base"), START("Start"), END("End");
private String name;
private GuiState(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class GuiModel2 {
public static final String STATE = "state";
private SwingPropertyChangeSupport support = new SwingPropertyChangeSupport(this);
private GuiState state = GuiState.BASE;
public GuiState getState() {
return state;
}
public void setState(GuiState state) {
GuiState oldValue = this.state;
GuiState newValue = state;
this.state = state;
support.firePropertyChange(STATE, oldValue, newValue);
}
public void addPropertyChangeListener(PropertyChangeListener l) {
support.addPropertyChangeListener(l);
}
public void removePropertyChangeListener(PropertyChangeListener l) {
support.removePropertyChangeListener(l);
}
}
@SuppressWarnings("serial")
class GuiPanel2 extends JPanel {
public static final String STATE = "state";
private String state = GuiState.BASE.getName();
private JLabel stateField = new JLabel("", SwingConstants.CENTER);
public GuiPanel2() {
JPanel btnPanel = new JPanel(new GridLayout(1, 0, 5, 0));
for (final GuiState guiState : GuiState.values()) {
btnPanel.add(new JButton(new AbstractAction(guiState.getName()) {
{
int mnemonic = (int) getValue(NAME).toString().charAt(0);
putValue(MNEMONIC_KEY, mnemonic);
}
@Override
public void actionPerformed(ActionEvent e) {
String name = getValue(NAME).toString();
setState(name);
}
}));
}
setLayout(new BorderLayout());
add(stateField, BorderLayout.PAGE_START);
add(btnPanel, BorderLayout.CENTER);
}
public String getState() {
return state;
}
public void setState(String state) {
String oldValue = this.state;
String newValue = state;
this.state = state;
firePropertyChange(STATE, oldValue, newValue);
}
public void setStateField(String name) {
stateField.setText(name);
}
}
class GuiControl2 {
private GuiPanel2 guiPanel;
private GuiModel2 model;
private boolean allOK = false;
public void setGuiPanel(GuiPanel2 guiPanel) {
this.guiPanel = guiPanel;
guiPanel.addPropertyChangeListener(GuiPanel2.STATE,
new GuiPanelStateListener());
}
public void init() throws GuiException2 {
if (model == null) {
throw new GuiException2("Model is null");
}
if (guiPanel == null) {
throw new GuiException2("GuiPanel is null");
}
allOK = true;
guiPanel.setStateField(model.getState().getName());
}
public void setGuiModel(GuiModel2 model) {
this.model = model;
model.addPropertyChangeListener(new ModelListener());
}
private class GuiPanelStateListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (!allOK) {
return;
}
if (GuiPanel2.STATE.equals(evt.getPropertyName())) {
String text = guiPanel.getState();
model.setState(GuiState.valueOf(text.toUpperCase()));
}
}
}
private class ModelListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (!allOK) {
return;
}
if (GuiModel2.STATE.equals(evt.getPropertyName())) {
GuiState state = (GuiState) evt.getNewValue();
guiPanel.setStateField(state.getName());
}
}
}
}
@SuppressWarnings("serial")
class GuiException2 extends Exception {
public GuiException2() {
super();
}
public GuiException2(String message) {
super(message);
}
}
请注意警告:我不是专业的程序员,甚至不是受过大学训练的程序员,所以请仅以我的观点为准。
答案 2 :(得分:0)
我正在学校学习Java。老师告诉我们,听众总是必须在 Controller 类中声明。我这样做的方法是实现一种方法,例如听众()的。 Inside是使用匿名类的所有侦听器声明。这就是我的老师希望它看到的方式,但坦率地说,我不确定他们是否完全正确。
答案 3 :(得分:0)
模型/视图/控制器 (MVC) 模式可以通过以下方式应用于 Swing。
现在,在 Swing 中,通常没有主控制器来“统治所有人”。每个 ActionListener
和 AbstractAction
都是自己的控制器,负责特定 JButton
或键盘键绑定的操作。
这是一个使用 MVC 编码的 Swing GUI 示例。
此 GUI 会在您左键单击图形 JPanel
时绘制不断扩大的圆圈。就像池塘里的涟漪。 GUI 足够简单,可以在 Stack Overflow 答案中进行解释,但也足够复杂,可以作为一个很好的 MVC 说明。
MVC 模式被称为 MVC 模式,因为您首先创建模型,然后是视图,最后是控制器。现在,这可以是一个迭代过程。通常,我发现在构建视图时需要在模型中添加一些内容。
从视图开始,或者更糟的是,从控制器开始,通常会导致无法测试或调试的混乱。当然,有时您需要视图来验证模型。但我不是一个优秀的开发人员,我可以编写几十行工作代码。
我写了一点,测试了很多,通常发现我给自己带来了问题。由于我写的很少,所以我一次只有一点点代码要调试。
该模型由两个类组成,RipplesModel
类和 Circle
类。 Circle
类是一个普通的 Java getter / setter 类,它包含中心 java.awt.Point
、int
半径和用于轮廓颜色的 java.awt.Color
。是的,我在模型中使用了 java.awt
类。这些 awt 类用于保存绘图信息。
RipplesModel
类是一个普通的 Java getter / setter 类,它包含 java.util.List
实例的 Circle
。绘图 List
将使用此 JPanel
来绘制 Circle
实例。
通常,您的 Swing 模型将由一个或多个普通的 Java getter/setter 类组成。
视图由 JFrame
和绘图 JPanel
组成。 JFrame
代码包含一个 WindowListener
,以便我可以停止动画 Thread
。 WindowListener
是控制器类之一。很简单,所以我把它做成了一个匿名类。
控制器代码可以驻留在其中一个视图类中。 MVC 模型没有规定代码所在的位置。 MVC 模型规定了代码的执行位置。
绘图 JPanel
在绘图 Circle
上绘制 JPanel
实例。时期。控制器负责更改 Circle
实例的半径并运行动画。
我使用 JFrame
。我扩展 JPanel
是因为我覆盖了 paintComponent
方法。只有当您想要覆盖一个或多个类方法时,才应该扩展 Swing 组件。
此 GUI 中有三个控制器类。我已经提到了停止动画 WindowListener
的匿名 Thread
类。
RipplesListener
类是一个 MouseListener
类,用于监听鼠标按下。侦听器创建一个 Circle
实例,您可以在其中单击鼠标左键。
Animation
Runnable
会增加每个 Circle
实例的半径,并要求重新绘制绘图 JPanel
。 JFrame
类包含一个 repaint
方法,该方法依次调用绘图 JPanel
repaint
方法。
控制器类不必知道视图是如何工作的。他们只需要知道 JFrame
(主视图)类有一个 repaint
方法。这有助于强制关注点分离,这是您使用 MVC 模式的主要原因之一。
Animation
Runnable
在单独的 Thread
中运行,因此 GUI 线程 Event Dispatch Thread 不会被阻塞。今天,我可能会使用 Swing Timer
,但是当我写这个的时候,我已经习惯了写我自己的动画 Runnable
。
这是完整的可运行代码。我把所有的类都放在了内部类中,这样我就可以将这段代码作为一个块发布。
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class Ripples implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Ripples());
}
private Animation animation;
private DrawingPanel drawingPanel;
private RipplesModel model;
public Ripples() {
model = new RipplesModel();
}
@Override
public void run() {
JFrame frame = new JFrame("Ripples");
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent event) {
stopAnimation();
frame.dispose();
System.exit(0);
}
});
drawingPanel = new DrawingPanel(model);
frame.add(drawingPanel, BorderLayout.CENTER);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
animation = new Animation(this, model);
new Thread(animation).start();
}
public void repaint() {
drawingPanel.repaint();
}
private void stopAnimation() {
if (animation != null) {
animation.setRunning(false);
}
}
public class DrawingPanel extends JPanel {
private static final long serialVersionUID = 1L;
private RipplesModel model;
public DrawingPanel(RipplesModel model) {
this.model = model;
setBackground(Color.BLACK);
setPreferredSize(new Dimension(500, 500));
addMouseListener(new RipplesListener(model));
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setStroke(new BasicStroke(5f));
List<Circle> circles = model.getCircles();
for (Circle circle : circles) {
Point p = circle.getCenter();
int radius = circle.getRadius();
g2.setColor(circle.getColor());
g2.drawOval(p.x - radius, p.y - radius,
2 * radius, 2 * radius);
}
}
}
public class RipplesListener extends MouseAdapter {
private Random random;
private RipplesModel model;
public RipplesListener(RipplesModel model) {
this.model = model;
this.random = new Random();
}
@Override
public void mousePressed(MouseEvent event) {
model.addCircle(new Circle(event.getPoint(),
createColor()));
}
private Color createColor() {
int r = random.nextInt(128) + 128;
int g = random.nextInt(128) + 128;
int b = random.nextInt(128) + 128;
return new Color(r, g, b);
}
}
public class Animation implements Runnable {
private volatile boolean running;
private Ripples frame;
private RipplesModel model;
public Animation(Ripples frame, RipplesModel model) {
this.frame = frame;
this.model = model;
this.running = true;
}
@Override
public void run() {
while (running) {
sleep(20L);
incrementRadius();
repaint();
}
}
private void incrementRadius() {
List<Circle> circles = model.getCircles();
for (Circle circle : circles) {
circle.incrementRadius();
}
}
private void sleep(long delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void repaint() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
frame.repaint();
}
});
}
public synchronized void setRunning(boolean running) {
this.running = running;
}
}
public class RipplesModel {
private List<Circle> circles;
public RipplesModel() {
this.circles = new ArrayList<>();
}
public void addCircle(Circle circle) {
this.circles.add(circle);
}
public List<Circle> getCircles() {
return circles;
}
}
public class Circle {
private int radius;
private final Color color;
private final Point center;
public Circle(Point center, Color color) {
this.center = center;
this.color = color;
this.radius = 10;
}
public void incrementRadius() {
radius = (++radius > 200) ? 10 : radius;
}
public Color getColor() {
return color;
}
public int getRadius() {
return radius;
}
public Point getCenter() {
return center;
}
}
}