GUI not working after rewriting to MVC
Asked Answered
T

2

134

I'm practicing MVC style programming. I have a Mastermind game in a single file, working fine (maybe apart of the fact that "Check" button is invisible at start).

http://paste.pocoo.org/show/226726/

But when I've rewritten it to model, view, controller files - and when I click on empty Pin (that should be updated, and repainted with new color) - noting happens. Can anybody see any problems here ? I've tried placing repaint() in different places, but it simply does not work at all :/

Main :

public class Main { 
    public static void main(String[] args){
        Model model = new Model();
        View view = new View("Mastermind", 400, 590, model);
        Controller controller = new Controller(model, view); 
        view.setVisible(true);
    }
}

Model :

import java.util.Random;

public class Model{
    static final int
    LINE = 5,
    SCORE = 10, OPTIONS = 20;
    Pin pins[][] = new Pin[21][LINE];
    int combination[] = new int[LINE];
    int curPin = 0;
    int turn = 1;
    Random generator = new Random();
    int repaintPin;
    boolean pinsRepaint=false;
    int pinsToRepaint;
    boolean isUpdate = true, isPlaying = true, isRowFull = false;
    static final int HIT_X[] = {270,290,310,290,310}, HIT_Y[] = {506,496,496,516,516};

    public Model(){

        for ( int i=0; i < SCORE; i++ ){
            for ( int j = 0; j < LINE; j++ ){
                pins[i][j] = new Pin(20,0);
                pins[i][j].setPosition(j*50+30,510-i*50);
                pins[i+SCORE][j] = new Pin(8,0);
                pins[i+SCORE][j].setPosition(HIT_X[j],HIT_Y[j]-i*50);
            }
        }
        for ( int i=0; i < LINE; i++ ){
            pins[OPTIONS][i] = new Pin( 20, i+2 );
            pins[OPTIONS][i].setPosition( 370,i * 50 + 56);
        }

    }

    void fillHole(int color) {
        pins[turn-1][curPin].setColor(color+1);
        pinsRepaint = true;
        pinsToRepaint = turn;
        curPin = (curPin+1) % LINE;
        if (curPin == 0){
            isRowFull = true;
        }
        pinsRepaint = false;
        pinsToRepaint = 0;
    }

    void check() {
        int junkPins[] = new int[LINE], junkCode[] = new int[LINE];
        int pinCount = 0, pico = 0;

        for ( int i = 0; i < LINE; i++ ) {
            junkPins[i] = pins[turn-1][i].getColor();
            junkCode[i] = combination[i];
        }
        for ( int i = 0; i < LINE; i++ ){
            if (junkPins[i]==junkCode[i]) {
                pins[turn+SCORE][pinCount].setColor(1);
                pinCount++;
                pico++;
                junkPins[i] = 98;
                junkCode[i] = 99;
            }
        }
        for ( int i = 0; i < LINE; i++ ){
            for ( int j = 0; j < LINE; j++ )
                if (junkPins[i]==junkCode[j]) {
                    pins[turn+SCORE][pinCount].setColor(2);
                    pinCount++;
                    junkPins[i] = 98;
                    junkCode[j] = 99;
                    j = LINE;
            }
        }
        pinsRepaint = true;
        pinsToRepaint = turn + SCORE;
        pinsRepaint = false;
        pinsToRepaint=0;

        if ( pico == LINE ){
            isPlaying = false;
        }
        else if ( turn >= 10 ){
                isPlaying = false;
        }
        else{
            curPin = 0;
            isRowFull = false;
            turn++;
        }
    }

    void combination() {
        for ( int i = 0; i < LINE; i++ ){
          combination[i] = generator.nextInt(6) + 1;
        }
    }
}

class Pin{
    private int color, X, Y, radius;

    public Pin(){
        X = 0; Y = 0; radius = 0; color = 0;
    }

    public Pin( int r,int c ){
        X = 0; Y = 0; radius = r; color = c;
    }

    public int getX(){
        return X;
    }

    public int getY(){
        return Y;
    }

    public int getRadius(){
        return radius;
    }

    public void setRadius(int r){
        radius = r;
    }

    public void setPosition( int x,int y ){
        this.X = x ;
        this.Y = y ;
    }
    public void setColor( int c ){
        color = c;
    }
    public int getColor() {
        return color;
    }
}

View:

import java.awt.*;
import javax.swing.*;

public class View extends Frame{  
    Model model;
    JButton checkAnswer;
    private JPanel button;
    private static final Color COLORS[] = {Color.black, Color.white, Color.red, Color.yellow, Color.green, Color.blue, new Color(7, 254, 250)};

    public View(String name, int w, int h, Model m){
        model = m;
        setTitle( name );
        setSize( w,h );
        setResizable( false );
        this.setLayout(new BorderLayout());

        button = new JPanel();
        button.setSize( new Dimension(400, 100));
        button.setVisible(true);
        checkAnswer = new JButton("Check");
        checkAnswer.setSize( new Dimension(200, 30));
        button.add( checkAnswer );
        this.add( button, BorderLayout.SOUTH);
        button.setVisible(true);
    }

    @Override
    public void paint( Graphics g ) {
        g.setColor( new Color(238, 238, 238));
        g.fillRect( 0,0,400,590);

        for ( int i=0; i < model.pins.length; i++ ) {
            paintPins(model.pins[i][0],g);
            paintPins(model.pins[i][1],g);
            paintPins(model.pins[i][2],g);
            paintPins(model.pins[i][3],g);
            paintPins(model.pins[i][4],g);
        }
    }

    @Override
    public void update( Graphics g ) {
        if ( model.isUpdate ) {
            paint(g);
        }
        else {
            model.isUpdate = true;
            paintPins(model.pins[model.repaintPin-1][0],g);
            paintPins(model.pins[model.repaintPin-1][1],g);
            paintPins(model.pins[model.repaintPin-1][2],g);
            paintPins(model.pins[model.repaintPin-1][3],g);
            paintPins(model.pins[model.repaintPin-1][4],g);
        }
    }

    void repaintPins( int pin ) {
        model.repaintPin = pin;
        model.isUpdate = false;
        repaint();
    }

    public void paintPins(Pin p, Graphics g ){
        int X = p.getX();
        int Y = p.getY();
        int color = p.getColor();
        int radius = p.getRadius();
        int x = X-radius;
        int y = Y-radius;

        if (color > 0){
            g.setColor( COLORS[color]);
            g.fillOval( x,y,2*radius,2*radius );
        }
        else{
            g.setColor( new Color(238, 238, 238) );
            g.drawOval( x,y,2*radius-1,2*radius-1 );
        }
        g.setColor( Color.black );
        g.drawOval( x,y,2*radius,2*radius );
    }
}

Controller:

import java.awt.*;
import java.awt.event.*;

public class Controller implements MouseListener, ActionListener { 
    private Model model;
    private View view;

    public Controller(Model m, View v){ 
        model = m;
        view = v;

        view.addWindowListener( new WindowAdapter(){
            public void windowClosing(WindowEvent e){
            System.exit(0);
        } });
        view.addMouseListener(this);
        view.checkAnswer.addActionListener(this);
        model.combination();
    }

    public void actionPerformed( ActionEvent e ) {
        if(e.getSource() == view.checkAnswer){
            if(model.isRowFull){
                model.check();
            }
        }
    }

    public void mousePressed(MouseEvent e) {
        Point mouse = new Point();

        mouse = e.getPoint();
        if (model.isPlaying){
            if (mouse.x > 350) {
                int button = 1 + (int)((mouse.y - 32) / 50);
                if ((button >= 1) && (button <= 5)){
                    model.fillHole(button);
                    if(model.pinsRepaint){
                        view.repaintPins( model.pinsToRepaint );
                    }
                }
            }
        }
    }

    public void mouseClicked(MouseEvent e) {}
    public void mouseReleased(MouseEvent e){}
    public void mouseEntered(MouseEvent e) {}
    public void mouseExited(MouseEvent e)  {}
}
Thibodeaux answered 18/6, 2010 at 0:55 Comment(4)
Both the old and new code have problems related to mixing AWT and Swing components. See also stackoverflow.com/questions/2687871Lapsus
so the problem with no update may be caused by that ?Thibodeaux
Yes. java.sun.com/products/jfc/tsc/articles/mixingLapsus
I've added and example that may guide your redesign.Lapsus
L
160

As you've discovered, the Model–View–Controller pattern is no panacea, but it offers some advantages. Rooted in MVC, the Swing separable model architecture is discussed in A Swing Architecture Overview. Based on this outline, the following example shows an MVC implementation of a much simpler game that illustrates similar principles. Note that the Model manages a single Piece, chosen at random. In response to a user's selection, the View invokes the check() method, while listening for a response from the Model via update(). The View then updates itself using information obtained from the Model. Similarly, the Controller may reset() the Model. In particular, there is no drawing in the Model and no game logic in the View. This somewhat more complex game was designed to illustrate the same concepts.

Addendum: I've modified the original example to show how MVC allows one to enhance the View without changing the nature of the Model.

Addendum: As @akf observes, MVC hinges on the observer pattern. Your Model needs a way to notify the View of changes. Several approaches are widely used:

Addendum: Some common questions about Swing controllers are addressed here and here.

screen capture

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Observable;
import java.util.Observer;
import java.util.Random;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

/**
 * @see https://mcmap.net/q/167526/-gui-not-working-after-rewriting-to-mvc/230513
 * @see http://stackoverflow.com/questions/5274962
 */
public class MVCGame {

    public static void main(String[] args) {
        EventQueue.invokeLater(new MVCGame()::display);
    }

    public void display() {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(new MainPanel());
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }
}

final class MainPanel extends JPanel {

    public MainPanel() {
        super(new BorderLayout());
        Model model = new Model();
        View view = new View(model);
        Control control = new Control(model, view);
        JLabel label = new JLabel("Guess what color!", JLabel.CENTER);
        this.add(label, BorderLayout.NORTH);
        this.add(view, BorderLayout.CENTER);
        this.add(control.createPanel(), BorderLayout.SOUTH);
    }
}

/**
 * Control panel
 */
final class Control {

    private final Model model;
    private final View view;
    private final JButton reset = new JButton("Reset");

    public Control(Model model, View view) {
        this.model = model;
        this.view = view;
    }

    public JPanel createPanel() {
        JPanel panel = new JPanel();
        panel.add(reset);
        reset.addActionListener(new ButtonHandler());
        return panel;
    }

    private class ButtonHandler implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            String cmd = e.getActionCommand();
            if ("Reset".equals(cmd)) {
                model.reset();
            }
        }
    }
}

/**
 * View
 */
final class View extends JPanel {

    private static final String PROMPT = "Click a button.";
    private final Model model;
    private final ColorIcon icon = new ColorIcon(80, Color.gray);
    private final JLabel label = new JLabel(PROMPT, icon, JLabel.CENTER);

    public View(Model model) {
        super(new BorderLayout());
        this.model = model;
        label.setVerticalTextPosition(JLabel.BOTTOM);
        label.setHorizontalTextPosition(JLabel.CENTER);
        this.add(label, BorderLayout.CENTER);
        this.add(genButtonPanel(), BorderLayout.SOUTH);
        model.addObserver(new ModelObserver());
    }

    private JPanel genButtonPanel() {
        JPanel panel = new JPanel();
        for (Piece p : Piece.values()) {
            PieceButton pb = new PieceButton(p);
            pb.addActionListener(new ButtonHandler());
            panel.add(pb);
        }
        return panel;
    }

    private class ModelObserver implements Observer {

        @Override
        public void update(Observable o, Object arg) {
            if (arg == null) {
                label.setText(PROMPT);
                icon.color = Color.gray;
            } else {
                if ((Boolean) arg) {
                    label.setText("Win!");
                } else {
                    label.setText("Keep trying.");
                }
            }
        }
    }

    private class ButtonHandler implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            PieceButton pb = (PieceButton) e.getSource();
            icon.color = pb.piece.color;
            label.repaint();
            model.check(pb.piece);
        }
    }

    private final static class PieceButton extends JButton {

        private Piece piece;

        public PieceButton(Piece piece) {
            this.piece = piece;
            this.setIcon(new ColorIcon(16, piece.color));
        }
    }

    private static class ColorIcon implements Icon {

        private final int size;
        private Color color;

        public ColorIcon(int size, Color color) {
            this.size = size;
            this.color = color;
        }

        @Override
        public void paintIcon(Component c, Graphics g, int x, int y) {
            Graphics2D g2d = (Graphics2D) g;
            g2d.setRenderingHint(
                RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
            g2d.setColor(color);
            g2d.fillOval(x, y, size, size);
        }

        @Override
        public int getIconWidth() {
            return size;
        }

        @Override
        public int getIconHeight() {
            return size;
        }
    }
}

/**
 * Model
 */
class Model extends Observable {

    private static final Random rnd = new Random();
    private static final Piece[] pieces = Piece.values();
    private Piece hidden = init();

    private Piece init() {
        return pieces[rnd.nextInt(pieces.length)];
    }

    public void reset() {
        hidden = init();
        setChanged();
        notifyObservers();
    }

    public void check(Piece guess) {
        setChanged();
        notifyObservers(guess.equals(hidden));
    }
}

enum Piece {

    Red(Color.red), Green(Color.green), Blue(Color.blue);
    public Color color;

    private Piece(Color color) {
        this.color = color;
    }
}
Lapsus answered 18/6, 2010 at 20:50 Comment(14)
@trevor_nise: I've updated the example above. You may find it useful to compare the revisions.Lapsus
For anyone that's curious Fowler put up the following article in 2006: martinfowler.com/eaaDev/SeparatedPresentation.htmlCabochon
See also Java SE Application Design With MVC.Lapsus
What is the reason for these classes extending JFrame ?Cabochon
Great answer, but it seems a little weird to me a Controller inheriting JPanel and being added to the main panel. Isn't the controller supposed to be something logical and thus not visible? What am I missing?Manslayer
@miguelcobain: Good observation; I wanted to illustrate how the controller may alter both the view and model via a separate implementation of the pattern in which the button combines a view and model. Control overrides no methods of JPanel, so a static factory might be better.Lapsus
controller shouldn't need to call view.reset explicitly: calling model.reset will notify the view, or not? (Guilty of not running the example :-)Murat
@kleopatra: Good eye! Careless re-factoring on my part in r8. I've updated the example accordingly and reverted to a single listing for easier running.Lapsus
+1 Before this, I hadn't seen a fully working example of a Swing app using MVC. Kudos. It has inspired me to rewrite the same app in an alternate MVP architecture. For those interested in comparing/contrasting, see: https://mcmap.net/q/168972/-model-view-presenter-passive-view-bootstraping-who-displays-the-view-initiallyPsychrometer
@Lapsus Thanks for this thorough example (you linked me here from a recent question of mine). I've understood quite some things a lot better. However, I remain confused about why the Control class needs a reference to view. To me it seems Control only contains the button to reset the model; it doesn't use the view reference at all. Couldn't this be integrated into the View, since it contains UI elements?Bittencourt
@MHaaZ: Possibly; more here and here.Lapsus
@Lapsus Thanks for the great example. A couple of questions: I can't see why the View class is specially called a View (as in MVC). Is it because it's the only one observing the model? The JLabel "Click a button." and the Control class are views too, all nested into the MainPanel which is a View too I believe (assuming the MVC model supports nested views).Crookback
@Lapsus Another question is about the grouping of components into Views. Why is the View class composed of an icon, a label which displays the move outcome messages and a button panel, while the ControlPanel is composed of only a Reset button? Shouldn't the button panel belong to ControlPanel as well, making up a true 'control panel' with controls? If on the other hand ControlPanel is a controller, why does it handle the Reset button only and not the choosing of a color button?Crookback
As noted here, "not every interaction needs to pass through your application's controller." As note here, the example illustrates how a controller may listen to the view, as shown in this diagram.Lapsus
M
20

When looking through Swing, one way that the designers consistently employ updating of View components in its MVC implementation is through Observer/Observable callbacks. An example can be seen in the AbstractTableModel, which has a variety of fireTable*Changed/Updated/etc methods that will alert all of its TableModelListener observers of mods to the model.

One option you have is to add a listener type to your Model class, and then notify your registered observers of any mods to the state of your model. Your View should be a listener, and it should repaint itself upon receipt of an update.

EDIT: +1 to trashgod. consider this an alternate wording to his explanation.

Malayopolynesian answered 20/6, 2010 at 2:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.