【问题标题】:java swing mvc architecture - Q with MCVE examplejava swing mvc架构-Q与MCVE示例
【发布时间】:2015-12-04 13:26:09
【问题描述】:

鉴于多个问题excellent_informative_for_me - trashgod 的回答、thatthat 以及其他几个没有回答我的问题的问题,
一个关于 ActionListeners 的类应该如何设计位置
(以及整体 MVC 分离 - 下文将详细解释)。

目录

  1. 问题解释
  2. 我的示例的文件结构树 (4)
  3. 编译/清理源命令 (4)
  4. 来源

1。问题解释

我读过关于 MVC 的内容,并且我认为我了解其中的大部分内容,为了这个问题,让我们假设这是真的。不再赘述:

  1. 视图是根据控制器请求从模型生成的。
    在大多数实现中,View 可以访问 Model 实例。
  2. 控制器与用户交互,将更改传播到模型和视图。
  3. 在极端简化的情况下,模型是数据的容器。
    可以通过 View 观察到。

现在,我的困惑在于 ActionListeners - 应该注册哪个类 - 并且反过来还包含 - 按钮代码,或者实际上是大多数 View 元素的代码,它们实际上不仅仅是指示器,而是模型操纵器?

假设我们在视图中有两个项目 - 用于更改模型数据的按钮和一些仅用于更改视图外观的可视项目。让代码负责更改 View 类中的 View 外观似乎是合理的。我的问题与第一种情况有关。我有几个想法:

  • View 创建按钮,因此在 View 中创建 ActionListener 并同时注册回调是很自然的。 但这要求 View 有与模型相关的代码,打破了封装。 View 应该对底层的 Controller 或 Model 知之甚少,只能通过 Observer 与之交谈。
  • 我可以公开按钮等视图项,并从 Controller 将 ActionListener 附加到它们,但这又会破坏封装。
  • 我可以为每个按钮实现一些回调 - View 会询问控制器是否有任何代码应该注册为给定按钮名称的 ActionListener,但这似乎过于复杂,并且需要同步控制器和视图之间的名称。
  • 我可以假设,理智的 ;),TableFactory 中的按钮可能是公开的,允许将 ActionListener 注入任何代码。
  • 控制器可以替换整个视图项目(创建按钮并替换现有的)但这看起来很疯狂,因为它不是它的角色

2。我的例子的文件结构树(4)

.
└── test
    ├── controllers
    │   └── Controller.java
    ├── models
    │   └── Model.java
    ├── resources
    │   └── a.properties
    ├── Something.java
    └── views
        ├── TableFactory.java
        └── View.java

3。源代码的编译/清理命令 (4)

编译:

  • javac test/Something.java test/models/*.java test/controllers/*.java test/views/*.java

运行:

  • java test.Something

清理:

  • 找到 . -iname "*.class" -exec rm {} \;

4。来源

此代码还包含国际化存根,为此我提出了单独的问题,这些行已明确标记,不应对答案产生任何影响。

控制器 => Controller.java
package test.controllers;
import test.models.Model;
import test.views.View;
public class Controller {
// Stub - doing nothing for now.
}
模型 => 模型.java
package test.models;
import java.util.Observable;
public class Model extends Observable {
}
东西.java
package test;

import test.views.View;
import test.models.Model;
import test.controllers.Controller;

public class Something {

    Model m;
    View v;
    Controller c;

    Something() {
        initModel();
        initView();
        initController();
    }

    private void initModel() {
        m = new Model();
    }

    private void initView() {
        v = new View(m);
    }
    private void initController() {
        c = new Controller(m, v);
    }

    public static void main(String[] args) {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                Something it = new Something();
            }
        });
    }
}
查看 => 查看.java
package test.views;

import java.awt.*;              // layouts
import javax.swing.*;           // JPanel

import java.util.Observer;      // MVC => model
import java.util.Observable;    // MVC => model
import test.models.Model;       // MVC => model

import test.views.TableFactory;

public class View {

    private JFrame root;
    private Model model;
    public JPanel root_panel;

    public View(Model model){
        root = new JFrame("some tests");
        root.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        root_panel = new JPanel();

        root_panel.add(new TableFactory(new String[]{"a", "b", "c"}));

        this.model = model;
        this.model.addObserver(new ModelObserver());

        root.add(root_panel);
        root.pack();
        root.setLocationRelativeTo(null);
        root.setVisible(true);
    }
}

class ModelObserver implements Observer {

    @Override
    public void update(Observable o, Object arg) {
        System.out.print(arg.toString());
        System.out.print(o.toString());
    }
}
查看 => TableFactory.java
package test.views;

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;

public class TableFactory extends JPanel {

    private String[] cols;
    private String[] buttonNames;
    private Map<String, JButton> buttons;
    private JTable table;

    TableFactory(String[] cols){

        this.cols = cols;
        buttonNames = new String[]{"THIS", "ARE", "BUTTONS"};
        commonInit();
    }
    TableFactory(String[] cols, String[] buttons){

        this.cols = cols;
        this.buttonNames = buttons;
        commonInit();
    }

    private void commonInit(){

        this.buttons = makeButtonMap(buttonNames);
        DefaultTableModel model = new DefaultTableModel();
        this.table = new JTable(model);
        for (String col: this.cols)
            model.addColumn(col);

        setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));

        JPanel buttons_container = new JPanel(new GridLayout(1, 0));
        for (String name : buttonNames){
            buttons_container.add(buttons.get(name));
        }

        JScrollPane table_container = new JScrollPane(table);

        this.removeAll();
        this.add(buttons_container);
        this.add(table_container);
        this.repaint();
    }

    private Map<String, JButton> makeButtonMap(String[] cols){
        Map<String, JButton>  buttons = new HashMap<String, JButton>(cols.length);
        for (String name : cols){
            buttons.put(name, new JButton(name));
        }
        return buttons;
    }
}

编辑(回应下面的 cmets)


下一个信息来源here

经过深思熟虑后,我理解了 Olivier 的评论,后来气垫船充满了鳗鱼的详细信息……javax.swing.Action => setAction 是我要走的路。 Controller 访问 View 的 JPanel,获取对包含按钮或任何 JComponent 的映射的引用,并向其添加操作。 View 不知道控制器代码中的内容。当我使它正常工作时,我会更新这个答案,所以在这里绊倒的任何人都可能拥有它。

只有两件事让我担心(都非常罕见,但仍然如此):

  1. 由于我公开了 View 添加操作的方法,实际上我相信任何人都只能从控制器或视图添加它。但是如果模型可以访问视图 - 它也可以覆盖操作。
  2. 压倒一切。如果我设置了某个对象的动作,然后忘记并从不同的地方设置另一个动作,它就消失了,这可能会使调试变得困难。

【问题讨论】:

  • 如果你想完全解耦 Swing 作为视图,你应该在你的视图中创建你的ActionListeners。此外,您应该考虑在可能的情况下使用javax.swing.Action 而不是ActionListener
  • 视图可以将 ActionListeners 添加到 JButtons,但您可以让侦听器简单地调用 Control 方法,或更改视图“有界”属性的状态,该属性在更改时通知侦听器到视图,通过调用视图的属性更改支持的通知方法之一。或者去@OlivierGrégoire 的路线并使用AbstractActions,如果需要,这些可以通过控件“注入”到视图中。
  • Olivier - 我现在不太确定我是否想将它们解耦。也许从长远来看,努力不值得? @HovercraftFullOfEels - 我知道你提到了我的第一个子弹和第三个子弹?我不确定我是否遵循从“更改视图有界属性的状态”开始的确切句子含义 - 请您重新措辞,也许详细说明或只使用更简单的词?
  • 注意,你不会“暴露”按钮等等。您只公开允许添加侦听器或操作的公共方法。关于“绑定”属性,请查看JavaBean Properties Tutorial。请注意,Swing GUI 天生就支持这些,因为每个 Swing 组件都有自己的 SwingPropertyChangeSupport 字段。
  • @JustMe:你只听你想响应的属性。 For example.

标签: java swing model-view-controller


【解决方案1】:

虽然Model–View–Controller 模式是no panacea,但它 Swing 应用程序设计中的一个循环模式。正如here 所指出的,Swing 控制组件通常是视图包含层次结构的一部分:Swing 应用程序的控制器可能几乎不需要做任何事情,只需将这些组件连接到相关的侦听器; Action 的实例,“可用于从组件中分离功能和状态”,特别方便。在这个example 中,Reset 处理程序,一个简单的ActionListener,嵌套在控制器中,但它也可以作为Action 从模型中导出。正如here 建议的那样,您可能需要尝试不同的设计。引用了几种方法here

【讨论】:

    【解决方案2】:

    虽然@trashgod 的回答总结并在某种程度上扩展了 cmets 的讨论,但我正在发布基于该讨论的丑陋(但有效)解决方案(在 Controller 类中实现 Action)

    必须改变什么?

    1. TableFactory - 添加了public void setObjectAction(Action a, JButton b) 并允许公共访问Map&lt;String, JButton&gt; buttons

    2. Controller 类需要实现 AbstractAction 或继承自它的类,以及将此类的对象设置为 View 的深层嵌套 JComponent 的代码。我觉得那部分很丑,我不确定这种方法是否合理,或者有没有更好的解决方案(稍后会检查充满鳗鱼豆的气垫船)。

    3. 不需要额外的更改,但我稍微修改了 View.java 以稍微更好地显示这个问题的目的,或者从不同的角度来看(这使得 View.java 比 MCVE 更多,但恕我直言,更好地描述了目的)

    TableFactory.java

    package test.views;
    
    import java.awt.*;
    import java.awt.event.*;
    import java.util.*;
    import javax.swing.*;
    import javax.swing.table.DefaultTableModel;
    import javax.swing.Action;
    
    public class TableFactory extends JPanel {
    
        private String[] cols;
        private String[] buttonNames;
        public Map<String, JButton> buttons;
        private JTable table;
    
        TableFactory(String[] cols){
            this.cols = cols;
            buttonNames = new String[]{"some", "buttons"};
            commonInit();
        }
        TableFactory(String[] cols, String[] buttons){
    
            this.cols = cols;
            this.buttonNames = buttons;
            commonInit();
        }
    
        private void commonInit(){
    
            this.buttons = makeButtonMap(buttonNames);
    
            DefaultTableModel model = new DefaultTableModel();
            this.table = new JTable(model);
            for (String col: this.cols)
                model.addColumn(col);
    
            setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
    
            JPanel buttons_container = new JPanel(new GridLayout(1, 0));
            for (String name : buttonNames){
                buttons_container.add(buttons.get(name));
            }
    
            JScrollPane table_container = new JScrollPane(table);
    
            this.removeAll();
            this.add(buttons_container);
            this.add(table_container);
            this.repaint();
        }
    
        private Map<String, JButton> makeButtonMap(String[] cols){
            Map<String, JButton>  buttons = new HashMap<String, JButton>(cols.length);
            for (String name : cols){
                buttons.put(name, new JButton(name));
            }
            return buttons;
        }
    
        public void setObjectAction(Action a, JButton b){
            //it might be possible to set actions to something else than button, I imagine JComponent, but I havent figured out yet how
            b.setAction(a);
        }
    }
    

    View.java

    package test.views;
    
    import java.awt.*;              // layouts
    import javax.swing.*;           // JPanel
    
    import java.util.Observer;      // MVC => model
    import java.util.Observable;    // MVC => model
    import test.models.Model;       // MVC => model
    
    import test.views.TableFactory;
    
    public class View {
    
        private JFrame root;
        private Model model;
        public JPanel root_panel;
        public JPanel some_views[];
    
        public View(Model model){
            root = new JFrame("some tests");
            root.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            root_panel = new JPanel();
    
            some_views = new JPanel[] { 
                           new TableFactory(new String[]{"a", "b", "c"}),
                           new TableFactory(new String[]{"e", "e"}) };
            JTabbedPane tabs = new JTabbedPane();
            for (JPanel tab: some_views){
                String name = tab.getClass().getSimpleName();
                tabs.addTab(name, null, tab, name);
                //tab.setObjectAction(action, (JComponent) button); // can set internal 'decorative' View's action here, that are not interacting with Model
                // for example, add new (empty) row to JTable, as this does not modify Model (yet)
            }
            root_panel.add(tabs);
    
    
            this.model = model;
            this.model.addObserver(new ModelObserver());
    
            root.add(root_panel);
            root.pack();
            root.setLocationRelativeTo(null);
            root.setVisible(true);
        }
    }
    
    class ModelObserver implements Observer {
    
        @Override
        public void update(Observable o, Object arg) {
            System.out.print(arg.toString());
            System.out.print(o.toString());
        }
    }
    

    Controller.java

    package test.controllers;
    
    import test.models.Model;
    import test.views.View;
    
    
    import javax.swing.Action;
    import java.awt.event.*;
    import test.views.TableFactory;
    import javax.swing.*;
    
    public class Controller {
        private Model model;
        private View view;
        public Controller(Model model, View view){
            this.model = model;
            this.view = view;
    
            ((test.views.TableFactory)view.some_views[0]).setObjectAction(
                (Action) new ModelTouchingAction("move along, nothing here"),
                ((test.views.TableFactory)view.some_views[0]).buttons.get("FIRST") );
        }
    
        class ModelTouchingAction extends AbstractAction { 
            public ModelTouchingAction(String text) {
                super(text);
            }
            public void actionPerformed(ActionEvent e) {
                System.out.print("Invoked: " + e.toString());
            }
        }
    }
    

    【讨论】:

    • 扩展AbstractActionabstract,所以你扩展它。它让控件(例如按钮、菜单、弹出窗口和工具栏)以一致的方式共享功能。
    • @trashgod - 你对抽象的看法是正确的。关于 MCVE 如果您愿意指出您认为多余或不必要的内容,我将很乐意这样做。所有这些都已经被削减了。我将削减有关国际化的部分,并将它们复制到第二个问题,但仅此而已,我发现没有其他可删除的部分,因为这个问题主要关注架构,它也保持当前目录结构不变。跨度>
    • 您可以做的一件事是为整个程序创建一个文件。您仍然可以在该文件中有多个类,但只有一个是公共的,并且将包含 main 方法。为什么这很重要?这使得将整个内容全部复制并粘贴到我们的 IDE 中变得很容易,从而使我们能够更轻松地测试您的代码。
    • 同意,但这需要重新设计类权限,因为它们位于不同的包中,具有不同的访问权限。但是你说得很好,所以我会这样做。
    • 感谢您的关注;我同意@HovercraftFullOfEels 关于独立minimal reproducible example 的理由;请注意,控制器需要知道 Action,但不需要包含它; Swing 文本组件使用 package-private 访问权限使相关操作在内部可用。
    【解决方案3】:

    工作版本,压缩为单个 Something.java 文件。
    运行javac Something.java &amp;&amp; java Something

    import java.util.*;
    import java.util.Observer;
    import java.util.Observable;
    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;
    import javax.swing.table.DefaultTableModel;
    
    class Controller {
        private enum TYPE { RESET, ADD, DEL };
        private Model model;
        private View view;
        public Controller(Model model, View view){
            this.model = model;
            this.view = view;
            ((TableFactory) view.tf).setObjectAction(
                (Action) new ModelTouchingAction("reset*",TYPE.RESET),
                "BUTTON1" );
            ((TableFactory) view.tf).setObjectAction(
                (Action) new ModelTouchingAction("add*",TYPE.ADD),
                "BUTTON2" );
            ((TableFactory) view.tf).setObjectAction(
                (Action) new ModelTouchingAction("del*",TYPE.DEL),
                "BUTTON3" );
        }
    
        class ModelTouchingAction extends AbstractAction {
            private TYPE t;
            public ModelTouchingAction(String text, TYPE type) {
                super(text);
                this.t = type;
            }
            public void actionPerformed(ActionEvent e) {
                if(this.t == TYPE.ADD)
                    model.add();
                else if(this.t == TYPE.DEL)
                    model.del();
                else
                    model.reset();
            }
        }
    }
    
    class Model extends Observable {
        private ArrayList<String[]> data;
        private static int cnt = 0;
        Model(){ reset(); }
        public void reset(){
            data = new ArrayList<String[]>();
            data.add(new String[]{"cell a1", "cell a2", "cell a3"});
            data.add(new String[]{"cell b1", "cell b2", "cell b3"});
            info();
        }
        public void add(){
            cnt++;
            data.add(new String[]{String.valueOf(cnt++), 
                     String.valueOf(cnt++), String.valueOf(cnt++)});
            info();
        }
        public void del(){
            if (data.size()>0){
                data.remove(data.size() - 1);
                info();
            }
        }
        private void info(){ setChanged();  notifyObservers(); }
        public ArrayList<String[]> get(){ return data; }
    }
    
    public class Something {
    
        Model m;
        View v;
        Controller c;
        Something() {
            initModel();
            initView();
            initController();
        }
    
        private void initModel() {
            m = new Model();
        }
        private void initView() {
            v = new View(m);
        }
        private void initController() {
            c = new Controller(m, v);
        }
    
        public static void main(String[] args) {
            javax.swing.SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    Something it = new Something();
                }
            });
        }
    }
    
    class View {
    
        private JFrame root;
        private Model model;
        public JPanel root_panel;
        public TableFactory tf;
    
        public View(Model model){
            root = new JFrame("some tests");
            root.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            root_panel = new JPanel();
            tf = new TableFactory(new String[]{"col1", "col2", "col3"});
            root_panel.add(tf);
    
            this.model = model;
            this.model.addObserver(new ModelObserver(tf));
    
            root.add(root_panel);
            root.pack();
            root.setLocationRelativeTo(null);
            root.setVisible(true);
        }
    }
    
    class ModelObserver implements Observer {
        TableFactory tf;
        ModelObserver(TableFactory tf){ this.tf = tf; }
        @Override
        public void update(Observable o, Object arg) {
            if (null != o)
                this.tf.populate(((Model) o).get());
                // view reloads ALL from model, optimize it
                // check what to check to get CMD from Observable
            else
                System.out.print("\nobservable is null");
            if (null != arg)
                System.out.print(arg.toString());
            else
                System.out.print("\narg is null. No idea if it should be.");
        }
    }
    
    class TableFactory extends JPanel {
    
        private String[] cols;
        public String[] buttonNames;
        private Map<String, JButton> buttons;
        private JTable table;
    
        TableFactory(String[] cols){
    
            this.cols = cols;
            buttonNames = new String[]{"BUTTON1", "BUTTON2", "BUTTON3"};
            commonInit();
        }
        TableFactory(String[] cols, String[] buttons){
    
            this.cols = cols;
            this.buttonNames = buttons;
            commonInit();
        }
    
        private void commonInit(){
    
            this.buttons = makeButtonMap(buttonNames);
            DefaultTableModel tabModel = new DefaultTableModel();
            this.table = new JTable(tabModel);
            for (String col: this.cols)
                tabModel.addColumn(col);
    
            setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
    
            JPanel buttons_container = new JPanel(new GridLayout(1, 0));
            for (String name : buttonNames){
                buttons_container.add(buttons.get(name));
            }
    
            JScrollPane table_container = new JScrollPane(table);
    
            this.removeAll();
            this.add(buttons_container);
            this.add(table_container);
            this.repaint();
        }
        public void populate(ArrayList<String[]> data){
            ((DefaultTableModel) table.getModel()).setRowCount(0);
            for(String[] row:data) addRow(row); 
        }
        private void addRow(String[] row){
            ((DefaultTableModel) table.getModel()).addRow(row);
            // this is actually called only by populate, model does not have single 
            // row update here (and onUpdate ModelObserver cannot differentiate 
            // yet what method to call on Observable, TODO: check CMD? )
        }
        private void delRow(int rowID){
            System.out.print("\nJPanel should be deleting table row " + rowID);
        }
        public void setObjectAction(Action action, String buttonName){
            buttons.get(buttonName).setAction(action);
        }
        private Map<String, JButton> makeButtonMap(String[] cols){
            Map<String, JButton>  buttons = new HashMap<String, JButton>(cols.length);
            for (String name : cols){
                buttons.put(name, new JButton(name));
            }
            return buttons;
        }
    }
    

    【讨论】:

    • 感谢一个完整的例子;还可以考虑让TYPE 值实现一个接口来简化ModelTouchingAction
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-27
    • 1970-01-01
    • 2011-01-09
    • 2010-11-01
    • 1970-01-01
    • 2013-01-31
    相关资源
    最近更新 更多