Java 和 GUI - 根据 MVC 模式,ActionListener 属于哪里?

Posted

技术标签:

【中文标题】Java 和 GUI - 根据 MVC 模式,ActionListener 属于哪里?【英文标题】:Java and GUI - Where do ActionListeners belong according to MVC pattern? 【发布时间】:2014-12-18 12:24:33 【问题描述】:

我目前正在编写一个模板 Java 应用程序,但不知何故,如果我想完全遵循 MVC 模式,我不确定 ActionListener 属于哪里。

该示例是基于 Swing 的,但它不是关于框架,而是关于 Java 中 MVC 的基本概念,使用任何框架来创建 GUI。

我从一个包含一个 JFrame 和一个 JButton 的绝对简单的应用程序开始(处理框架,因此关闭应用程序)。这篇文章后面的代码。没什么特别的,只是为了澄清我们在说什么。我还没有从模型开始,因为这个问题让我很烦恼。

已经有不止一个类似的问题,例如:MVC pattern with many ActionListenersJava swing - Where should the ActionListener go?

但没有一个是真正令人满意的,因为我想知道两件事:

将所有 ActionListener 放在一个单独的包中是否合理? 我想这样做是为了 View 和 Controller 的可读性,特别是。如果有很多听众 如果侦听器不是 Controller 内的子类,我将如何从 ActionListener 内执行 Controller 函数? (后续问题)

我希望我在这里问的不是太笼统或模糊,但这让我想了一会儿。我总是用一种我自己的方式,让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);
    

【问题讨论】:

它们属于视图层。不应在视图之外定义任何动作侦听器。 这很奇怪,但据我了解,动作监听器应该调用 Controller 函数,因为视图不应该像 Service 类那样具有任何“功能”。 @jaySon 但是控制器也应该没有 UI 逻辑 ;) 【参考方案1】:

对于 Swing,这是一个很难回答的问题,因为 Swing 不是纯 MVC 实现,视图和控制器是混合的。

从技术上讲,模型和控制器应该能够交互,控制器和视图应该能够交互,但是视图和模型永远不应该交互,这显然不是 Swing 的工作原理,但这是另一个争论......

另一个问题是,您真的不想将 UI 组件暴露给任何人,控制器不应该关心某些操作是如何发生的,只要它们可以。

这表明附加到 UI 控件的 ActionListeners 应该由视图维护。然后视图应该提醒控制器发生了某种动作。为此,您可以使用另一个由控制器订阅的视图管理的ActionListener

更好的是,我会有一个专门的视图监听器,它描述了这个视图可能产生的动作,例如......

public interface MainViewListener 
    public void didPerformClose(MainView mainView);

然后,控制器将通过此侦听器订阅视图,并且当(在这种情况下)按下关闭按钮时,视图将调用 didPerformClose

即使在这个例子中,我也很想创建一个“主视图”界面,它描述了任何实现都保证提供的属性(setter 和 getter)和操作(listeners/callbacks),然后你不关心这些动作是如何发生的,只是当它们发生时,你应该做某事......

在您想问自己的每个级别,为另一个实例更改任何元素(更改模型或控制器或视图)有多容易?如果你发现自己不得不解耦代码,那么你就有问题了。通过接口进行通信,并尝试减少层之间的耦合量以及每个层对其他层的了解程度,以达到他们只是维护合约的程度

更新...

我们以这个为例……

实际上有两个视图(不包括实际对话框),凭据视图和登录视图,是的,您将看到它们是不同的。

凭据视图

凭证视图负责收集要验证的详细信息、用户名和密码。它将向控制器提供信息,让其知道这些凭据何时已更改,因为控制器可能想要采取一些措施,例如启用“登录”按钮...

视图还想知道何时进行身份验证,因为它想禁用它的字段,因此用户无法在身份验证期间更新视图,同样,它需要知道当身份验证失败或成功时,它需要针对这些可能性采取措施。

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);


凭据窗格

CredentialsPaneCredentialsView 的物理实现,它实现合约,但管理它自己的内部状态。合约如何管理与控制器无关,它只关心合约是否被维护......

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;
        

    


【讨论】:

我非常感谢您的明确回答,但我只得到了您所说的一半,因为您提到的界面似乎只是意味着将我的 ActionListener 问题转移到界面上,不是吗?在哪里实现接口?谁应该能够使用它?如果我在这里看起来很笨,我很抱歉,但是你的MainViewListener 界面,尤其是。该功能对我来说似乎不清楚,因为我不明白关闭操作会如何表现。 从我的角度来看,您可以将ActionListener 附加到视图中的按钮。那么问题是如何将其传达给控制器,因为控制器不应该关心“关闭”动作是如何触发的,只是它是,所以我会使用某种通信接口来描述可能的动作视图可能会创建以及控制器应该响应的视图。然后,它使您不必在它们之间串连ActionListeners、ChangeListeners、PropertyChangeListeners。你只是说,干草控制器我做了“这个”...... 这是一个多层次的概念。视图将实现一个允许控制器管理它的接口(描述视图与控制器的契约)。控制器将向视图注册一个接口(侦听器),以便在视图中发生某些事件时通知它。视图将管理它自己的内部事件管理(例如ActionListener)并通过控制器/视图侦听器接口告诉控制器发生了一些事情,这将控制器和视图彼此分离,这就是重点。 .. 嗯,好的。说白了,视图包含 actionListeners 并实现了接口,因此控制器可以使用接口函数,直到这里。但是这个“向视图注册一个接口(监听器)”在 Java 中是如何工作的呢?我从来没有做过这样的事情,所以如果你能为我提供一些例子,我会很高兴。谢谢。 当然,我做到了。可能我在脑海中描绘了一幅错误的画面,但我只能直截了当地创建一个侦听器(无论如何可能是什么侦听器?)由什么调用的控制器端?我认为视图不应该知道控制器......或者我完全错了,视图实际上正在调用一个接口方法,这意味着视图是活动的而控制器是被动的?对不起,它仍然让我感到困惑。 :-/【参考方案2】:

它们与控件相关联,但它们不必是控件的直接部分。例如,请参阅下面发布的代码,我正在准备另一个问题,一个关于匿名内部类和耦合的问题,这里我给所有按钮匿名内部操作(当然是 ActionListeners),然后使用操作来更改GUI 状态。 GUI(控件)的任何侦听器都会收到此更改的通知,然后可以采取相应的行动。

import java.awt.*;
import java.awt.event.*; 
import 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);
   

但请注意:我不是专业编码员,甚至不是受过大学培训的编码员,因此请仅将其视为我的意见。

【讨论】:

仅供参考:MVC 的 Swing 实现更像 (VC)M,其中控制器和视图是同一个类。假设这主要是因为外观和感觉......就个人而言,我更喜欢它,因为你得到了一个独立的工作单元,但它可以争辩说它降低了组件的灵活性和可扩展性......但无论如何... +1 @MadProgrammer:感谢您精明的 cmets 和出色的回答。我总是从你的帖子中学到很多东西。我的主要问题是如何在大型复杂程序中处理 MVC,主要是如何连接监听器,如何扩大规模。我还没有解决这个问题。 这似乎是一个常见问题。我认为Actions 是一个开始,但他们往往专注于视图方面。一种方法可能是描述视图的用途并围绕它设计一个interface。视图不能包含其他 MVC 并且一个模型可以将信息提供给另一个模型,这可能会影响它的关联控制器等等,这是没有理由的。再一次,核心问题是 Swing 不是纯粹的 MVC,所以这就像试图将大象挤进 mini 的靴子,当然,这可能是可能的,但不会很漂亮 @MadProgrammer:是的,我同意。 MVC 不必是全部或全部,它可以有更精细的粒度,mini-MVC 的所有添加到更大的整体。感谢您的见解! 我为 OP 做的例子有一个视图充当它的子视图的控制器,在这种情况下是有意义的,但可能并非总是如此......【参考方案3】:

简介

模型/视图/控制器 (MVC) 模式可以通过以下方式应用于 Swing。

    视图从模型中读取信息。 视图可能不会更新模型。 控制器将更新模型并重新绘制/重新验证视图。

现在,在 Swing 中,通常没有主控制器来“统辖”它们。每个ActionListenerAbstractAction 都是它自己的控制器,负责特定JButton 或键盘键绑定的操作。

这是一个使用 MVC 编码的 Swing GUI 示例。

此 GUI 会在您左键单击绘图 JPanel 的位置绘制不断扩大的圆圈。就像池塘里的涟漪。 GUI 很简单,可以在 Stack Overflow 答案中进行解释,但也很复杂,可以作为一个很好的 MVC 插图。

说明

MVC 模式之所以称为 MVC 模式,是因为您首先创建模型,然后是视图,最后是控制器。现在,这可以是一个迭代过程。通常,当我构建视图时,我发现模型中需要一些东西。

从视图开始,或者更糟糕的是,从控制器开始,通常会导致无法测试或调试的混乱局面。当然,有时您需要视图来验证模型。但我不是一个优秀的开发人员,我可以编写几十行工作代码。

我写了一点,测试了很多,但通常会发现我给自己带来了问题。由于我写的很少,一次只能调试一点代码。

型号

该模型由两个类组成,RipplesModel 类和Circle 类。 Circle 类是一个普通的 Java getter / setter 类,它包含中心 java.awt.Pointint 半径和用于轮廓颜色的 java.awt.Color。是的,我在模型中使用java.awt 类。这些 awt 类用于保存绘图信息。

RipplesModel 类是一个普通的 Java getter / setter 类,它包含 java.util.ListCircle 实例。这个List 将被JPanel 用于绘制Circle 实例。

通常,您的 Swing 模型将包含一个或多个普通 Java getter / setter 类。

查看

视图由JFrame 和绘图JPanel 组成。 JFrame 代码包含 WindowListener,以便我可以停止动画 ThreadWindowListener 是控制器类之一。很简单,所以我把它做成了一个匿名类。

控制器代码可以驻留在其中一个视图类中。 MVC 模型并没有规定代码所在的位置。 MVC 模型规定了代码的执行位置。

绘图JPanel 在绘图JPanel 上绘制Circle 实例。时期。控制器负责更改Circle 实例的半径并运行动画。

我使用JFrame。我扩展了JPanel,因为我覆盖了paintComponent 方法。唯一应该扩展 Swing 组件的时候是当您想要覆盖一个或多个类方法时。

控制器

此 GUI 中有三个控制器类。我已经提到了停止动画Thread 的匿名WindowListener 类。

RipplesListener 类是一个 MouseListener,它监听鼠标按下。侦听器创建一个Circle 实例,您可以在其中单击鼠标左键。

AnimationRunnable 增加每个Circle 实例的半径,并要求重新绘制绘图JPanelJFrame 类包含一个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;
        

    


【讨论】:

【参考方案4】:

我目前正在学校学习 Java。老师告诉我们,监听器总是必须在 Controller 类中声明。我这样做的方式是实现一个方法,例如监听器()。里面都是使用匿名类的监听器声明。这就是我的老师希望看到的方式,但坦率地说,我不确定他们是否完全正确。

【讨论】:

以上是关于Java 和 GUI - 根据 MVC 模式,ActionListener 属于哪里?的主要内容,如果未能解决你的问题,请参考以下文章

java swing vs mvc:这种模式真的可能吗?

Java:具有后台线程的 GUI 应用程序

浅谈Java Web经典三层架构和MVC框架模式

Java Web应用程序的MVC应用[重复]

了解 MVC 模式

框架模式:MVC、MVP、MVVM、MVPVM