Search code examples
javabuttonmousepress

Java - Create a button from a shape


I am learning Java currently and have been given the assignmnet of finidhing off a program to create the game Conways's life (we started with some code provided to us and must add features etc to this).

I am currently stuck on a menu option for the game. I want it to start off at the menu screen, wherein buttons appear at the top for "Start", "Random", "Load", Save". I have written code so that the program displays these buttons, through a fillRect option in my paint method.

My question is, how do I use the mousePressed method to recognise the cells selected so that I can get an action to occur when they are selected. I been looking at this for a while but can't seem to get this working.

Any suggestion would be a massive help. I have shared my code below. It's a work in progress but I would really like to get this working before continuing on with the other functionality.

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

public class ConwaysLife extends JFrame implements Runnable, MouseListener {
    
    // member data
    private BufferStrategy strategy;
    private Graphics offscreenBuffer;
    private boolean gameState[][] = new boolean[40][40];
    private boolean isGameInProgress = false;
    
    // constructor
    public ConwaysLife () {
        //Display the window, centred on the screen
         Dimension screensize = java.awt.Toolkit.getDefaultToolkit().getScreenSize();
         int x = screensize.width/2 - 400;
         int y = screensize.height/2 - 400;
         setBounds(x, y, 800, 800);
         setVisible(true);
         this.setTitle("Conway's game of life");
        
         // initialise double-buffering
         createBufferStrategy(2);
         strategy = getBufferStrategy();
         offscreenBuffer = strategy.getDrawGraphics();
        
         // register the Jframe itself to receive mouse events
         addMouseListener(this);
        
         // initialise the game state
         for (x=0;x<40;x++) {
         for (y=0;y<40;y++) {
             gameState[x][y]=false;
         }
         }
         
         // create and start our animation thread
         Thread t = new Thread(this);
         t.start();
    }
    
    // thread's entry point
    public void run() {
        while ( 1==1 ) {
        // 1: sleep for 1/5 sec
            try {
                Thread.sleep(200);
        } catch (InterruptedException e) { }
            
        // 2: animate game objects [nothing yet!]
        /*if (isGameInProgress == false) {
            this.repaint();
        }*/
            
            
            
            
            
        
        // 3: force an application repaint
        this.repaint();
    }
    }
    
    
    // mouse events which must be implemented for MouseListener
     public void mousePressed(MouseEvent e) {
         
         while (!isGameInProgress) {
             int x = e.getX()/20;
             int y = e.getY()/20;
             if(x >= 10 && x <= 80 && y >= 40 && y <= 65) {
                 isGameInProgress = !isGameInProgress;
                 this.repaint();
             }
             
         }
         
    
     // determine which cell of the gameState array was clicked on
         int x = e.getX()/20;
         int y = e.getY()/20;
         // toggle the state of the cell
         gameState[x][y] = !gameState[x][y];
         // request an extra repaint, to get immediate visual feedback
         this.repaint();
     }
     
     
     public void mouseReleased(MouseEvent e) { }
     public void mouseEntered(MouseEvent e) { }
     public void mouseExited(MouseEvent e) { }
     public void mouseClicked(MouseEvent e) { }
    //
     
    
     
    // application's paint method
    public void paint(Graphics g) {
        Font font = new Font("Veranda", Font.BOLD, 20);
            
        g = offscreenBuffer; // draw to off screen buffer
        
        // clear the canvas with a big black rectangle
        g.setColor(Color.BLACK);
        g.fillRect(0, 0, 800, 800);
        
        /*look to add a while game in progress loop here!!!*/
        // draw menu options
        if(!isGameInProgress) {
        g.setColor(Color.green); 
        g.fillRect(10, 40, 70, 25);
        g.fillRect(100, 40, 100, 25);
        g.fillRect(300, 40, 170, 25);
    
        g.setColor(Color.BLACK);
        g.setFont(font);
        g.drawString("Start", 15, 60);
        g.drawString("Random", 105, 60);
        g.drawString("Load", 305, 60);
        g.drawString("Save", 395, 60);
        }
            
        // redraw all game objects
        g.setColor(Color.WHITE);
             for (int x=0;x<40;x++) {
                 for (int y=0;y<40;y++) {
                     if (gameState[x][y]) {
                         g.fillRect(x*20, y*20, 20, 20);
                     }
                 }
             }
             
            // flip the buffers
            strategy.show();
    }
    
    // application entry point
    public static void main(String[] args) {
    ConwaysLife w = new ConwaysLife();
    }
}

Solution

  • You're not going to like the answer, but it's the "correct" way to approach the problem.

    What you need to understand is, Swing/AWT is using a "passive" rendering workflow and BufferStrategy is using a "active" rendering workflow, these are incompatible with each other.

    As a general rule, you should not be overriding paint of top level containers like JFrame, this is going to end in no end of issues. Instead, you should be starting with something like JPanel and overriding it's paintComponent method instead.

    Having said that, there's a "simpler" solution available to you. CardLayout. This will allow you to seperate the workflows of the menu from the game and resolve the issue between Swing/AWT and BufferStrategy

    For example...

    import java.awt.Canvas;
    import java.awt.CardLayout;
    import java.awt.Color;
    import java.awt.Component;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.FontMetrics;
    import java.awt.Graphics;
    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.image.BufferStrategy;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    
    public final class Main {
        public static void main(String[] args) {
            new Main();
        }
    
        public Main() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    JFrame frame = new JFrame();
                    frame.add(new MainPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        public class MainPane extends JPanel {
    
            enum View {
                MENU, GAME;
            }
    
            private CardLayout cardLayout = new CardLayout();
            private GamePane gamePane;
    
            public MainPane() {
                setLayout(cardLayout);
    
                gamePane = new GamePane();
    
                add(new MenuPane(new MenuPane.Observer() {
                    @Override
                    public void startNewGame() {
                        showGame();
                    }
    
                    @Override
                    public void randomGame() {
                    }
    
                    @Override
                    public void loadGame() {
                    }
    
                    @Override
                    public void saveGame() {
                    }
                }), View.MENU);
                add(gamePane, View.GAME);
            }
    
            protected void add(Component compent, View view) {
                add(compent, view.name());
            }
    
            protected void showGame() {
                show(View.GAME);
                gamePane.start();
            }
    
            protected void showMenu() {
                gamePane.stop();
                show(View.MENU);
            }
    
            protected void show(View view) {
                cardLayout.show(this, view.name());
            }
    
        }
    
        public class MenuPane extends JPanel {
    
            public interface Observer {
                public void startNewGame();
                public void randomGame();
                public void loadGame();
                public void saveGame();
            }
    
            private Observer observer;
    
            public MenuPane(Observer observer) {
                this.observer = observer;
    
                JButton startButton = new JButton("Start");
                JButton randomButton = new JButton("Random");
                JButton loadButton = new JButton("Load");
                JButton saveButton = new JButton("Save");
    
                setLayout(new GridBagLayout());
                GridBagConstraints gbc = new GridBagConstraints();
                gbc.ipadx = 10;
                gbc.ipady = 10;
                gbc.insets = new Insets(8, 8, 8, 8);
                gbc.weightx = GridBagConstraints.REMAINDER;
    
                add(startButton, gbc);
                add(randomButton, gbc);
                add(loadButton, gbc);
                add(saveButton, gbc);
    
                startButton.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        observer.startNewGame();
                    }
                });
                randomButton.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        observer.randomGame();
                    }
                });
                loadButton.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        observer.loadGame();
                    }
                });
                saveButton.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        observer.saveGame();
                    }
                });
            }
    
        }
    
        public class GamePane extends Canvas {
    
            private Thread thread;
            private volatile boolean isRunning = false;
    
            public GamePane() {
                setBackground(Color.BLACK);
            }
    
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(800, 800);
            }
    
            protected void start() {
                if (isRunning) {
                    return;
                }
                createBufferStrategy(3);
                isRunning = true;
                thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        mainLoop();
                    }
                });
                thread.start();
            }
    
            protected void stop() {
                if (!isRunning || thread == null) {
                    return;
                }
                isRunning = false;
                try {
                    thread.join();
                } catch (InterruptedException ex) {
                }
                thread = null;
            }
    
            protected void mainLoop() {
                try {
                    while (isRunning) {
                        render();
                        Thread.sleep(16);
                    }
                } catch (InterruptedException ex) {
                }
            }
    
            protected void render() {
                BufferStrategy strategy = getBufferStrategy();
                if (strategy == null) {
                    return;
                }
                // Render single frame
                do {
                    // The following loop ensures that the contents of the drawing buffer
                    // are consistent in case the underlying surface was recreated
                    do {
                        // Get a new graphics context every time through the loop
                        // to make sure the strategy is validated
                        Graphics graphics = strategy.getDrawGraphics();
    
                        FontMetrics fm = graphics.getFontMetrics();
                        String text = "All your game are belong to us";
                        int x = (getWidth() - fm.stringWidth(text)) / 2;
                        int y = (getHeight() - fm.getHeight()) / 2;
    
                        graphics.setColor(Color.WHITE);
                        graphics.drawString(text, x, y + fm.getAscent());
    
                        // Render to graphics
                        // ...
                        // Dispose the graphics
                        graphics.dispose();
    
                        // Repeat the rendering if the drawing buffer contents
                        // were restored
                    } while (strategy.contentsRestored());
    
                    // Display the buffer
                    strategy.show();
    
                    // Repeat the rendering if the drawing buffer was lost
                } while (strategy.contentsLost());
            }
        }
    }
    

    I would strongly recommend that you take the time to read through:

    A "fully" BufferStrategy based approach...

    Now, if you can't use Swing, "for reasons", you can still achieve a simular concept using "delegation".

    Basically this means "delegating" responsibility for performing some workflow to another. In this case, we want to delegate the rendering and the handling of the mouse events.

    This allows you to have a dedicated workflow for the menu and a dedicated workflow for the game, without having to try and mix a lot of state.

    Why do I keep on insisting on separating these two workflows? Simply, because it makes it MUCH easier to manage and reason about, but also because it supports the Single Responsibility Principle.

    The follow example makes use of Renderable interface to define the core functionality that end "render" delegate will need to implement, in this case, it's pretty simple, we want to tell the renderer to "render" it's content on each paint pass and we want to (optionally) delegate mouse clicked events (this could be done via a second interface or even just the MouseListener interface directly, but I've made it a requirement of the Renderable interface for demonstration purposes.

    The "basic" solution to your actual question is found through the use of Rectangle#contains(Point).

    This basically inspects each "button" Rectangle to determine if the supplied MouseEvent occurs within it's bounds, if it does, we take action.

    It is, however, a little more complicated then that, as we need to have the Rectangles built ahead of time, not difficult, but it's state which is actually reliant on the parent, as we need to know the area in which the renderer is been displayed, run the example, you'll see what I mean 😉

    import java.awt.Canvas;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.FontMetrics;
    import java.awt.Graphics2D;
    import java.awt.Rectangle;
    import java.awt.RenderingHints;
    import java.awt.event.MouseAdapter;
    import java.awt.event.MouseEvent;
    import java.awt.geom.RoundRectangle2D;
    import java.awt.image.BufferStrategy;
    import javax.swing.JFrame;
    
    public final class Main {
        public static void main(String[] args) {
            new Main();
        }
    
        public Main() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    MainPane mainPane = new MainPane();
                    JFrame frame = new JFrame();
                    frame.add(mainPane);
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
    
                    mainPane.start();
                }
            });
        }
    
        public interface Renderable {
            public void render(Graphics2D g2d, Dimension size);
    
            // We could just extend from MouseListener
            // but I don't need all those event handlers
            public void mouseClicked(MouseEvent e);
        }
    
        public class MainPane extends Canvas {
    
            private Thread thread;
            private volatile boolean isRunning = false;
    
            private Renderable currentRenderer;
    
            private MenuRenderer menuRenderer;
            private GameRenderer gameRenderer;
    
            public MainPane() {
                setBackground(Color.BLACK);
    
                gameRenderer = new GameRenderer();
                menuRenderer = new MenuRenderer(new MenuRenderer.Observer() {
                    @Override
                    public void startNewGame() {
                        showGame();
                    }
    
                    @Override
                    public void randomGame() {
                    }
    
                    @Override
                    public void loadGame() {
                    }
    
                    @Override
                    public void saveGame() {
                    }
                });
    
                showMenu();
    
                addMouseListener(new MouseAdapter() {
                    @Override
                    public void mouseClicked(MouseEvent e) {
                        if (currentRenderer == null) {
                            return;
                        }
                        currentRenderer.mouseClicked(e);
                    }
                });
            }
    
            protected void showMenu() {
                // This may need to tell the game renderer to stop
                // or pause
                currentRenderer = menuRenderer;
            }
    
            protected void showGame() {
                currentRenderer = gameRenderer;
            }
    
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(800, 800);
            }
    
            protected void start() {
                if (isRunning) {
                    return;
                }
                createBufferStrategy(3);
                isRunning = true;
                thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        mainLoop();
                    }
                });
                thread.start();
            }
    
            protected void stop() {
                if (!isRunning || thread == null) {
                    return;
                }
                isRunning = false;
                try {
                    thread.join();
                } catch (InterruptedException ex) {
                }
                thread = null;
            }
    
            protected void mainLoop() {
                try {
                    while (isRunning) {
                        render();
                        Thread.sleep(16);
                    }
                } catch (InterruptedException ex) {
                }
            }
    
            protected void render() {
                BufferStrategy strategy = getBufferStrategy();
                if (strategy == null && currentRenderer != null) {
                    return;
                }
                // Render single frame
                do {
                    // The following loop ensures that the contents of the drawing buffer
                    // are consistent in case the underlying surface was recreated
                    do {
                        // Get a new graphics context every time through the loop
                        // to make sure the strategy is validated
                        Graphics2D g2d = (Graphics2D) strategy.getDrawGraphics();
                        g2d.setBackground(Color.BLACK);
                        g2d.fillRect(0, 0, getWidth(), getHeight());
                        RenderingHints hints = new RenderingHints(
                                RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON
                        );
                        g2d.setRenderingHints(hints);
                        // Render to graphics
                        currentRenderer.render(g2d, getSize());
                        // Dispose the graphics
                        g2d.dispose();
                        // Repeat the rendering if the drawing buffer contents
                        // were restored
                    } while (strategy.contentsRestored());
    
                    // Display the buffer
                    strategy.show();
    
                    // Repeat the rendering if the drawing buffer was lost
                } while (strategy.contentsLost());
            }
        }
    
        public class GameRenderer implements Renderable {
            @Override
            public void render(Graphics2D g2d, Dimension size) {
                FontMetrics fm = g2d.getFontMetrics();
                String text = "All your game are belong to us";
                int x = (size.width - fm.stringWidth(text)) / 2;
                int y = (size.height - fm.getHeight()) / 2;
    
                g2d.setColor(Color.WHITE);
                g2d.drawString(text, x, y + fm.getAscent());
            }
    
            @Override
            public void mouseClicked(MouseEvent e) {
            }
        }
    
        public class MenuRenderer implements Renderable {
    
            public interface Observer {
                public void startNewGame();
                public void randomGame();
                public void loadGame();
                public void saveGame();
            }
    
            private Observer observer;
    
            private String[] menuOptions = new String[]{
                "New Game",
                "Random",
                "Load Game",
                "Save Game"
            };
    
            private Rectangle[] menuBounds;
    
            private int internalPadding = 20;
            private int horizontalGap = 16;
    
            public MenuRenderer(Observer observer) {
                this.observer = observer;
            }
    
            @Override
            public void render(Graphics2D g2d, Dimension size) {
                if (menuBounds == null) {
                    createMenus(g2d, size);
                }
                renderMenus(g2d);
            }
    
            protected void createMenus(Graphics2D g2d, Dimension size) {
                FontMetrics fm = g2d.getFontMetrics();
                int totalHeight = (((fm.getHeight() + internalPadding) + horizontalGap) * menuOptions.length) - horizontalGap;
                int buttonHeight = fm.getHeight() + internalPadding;
    
                menuBounds = new Rectangle[menuOptions.length];
    
                int buttonWidth = 0;
                for (int index = 0; index < menuOptions.length; index++) {
                    int width = fm.stringWidth(menuOptions[index]) + internalPadding;
                    buttonWidth = Math.max(width, buttonWidth);
                }
    
                int yPos = (size.height - totalHeight) / 2;
                for (int index = 0; index < menuOptions.length; index++) {
                    int xPos = (size.width - buttonWidth) / 2;
                    Rectangle menuRectangle = new Rectangle(xPos, yPos, buttonWidth, buttonHeight);
                    menuBounds[index] = menuRectangle;
                    yPos += buttonHeight + (horizontalGap / 2);
                }
            }
    
            protected void renderMenus(Graphics2D g2d) {
                for (int index = 0; index < menuOptions.length; index++) {
                    String text = menuOptions[index];
                    Rectangle bounds = menuBounds[index];
                    renderMenu(g2d, text, bounds);
                }
            }
    
            protected void renderMenu(Graphics2D g2d, String text, Rectangle bounds) {
                FontMetrics fm = g2d.getFontMetrics();
                int textWidth = fm.stringWidth(text);
    
                int textXPos = (bounds.x + (internalPadding / 2)) + ((bounds.width - internalPadding - textWidth) / 2);
                int textYPos = bounds.y + (internalPadding / 2);
    
                RoundRectangle2D buttonBackground = new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height, 20, 20);
    
                g2d.setColor(Color.BLUE.darker());
                g2d.fill(buttonBackground);
    
                g2d.setColor(Color.WHITE);
                g2d.drawString(text, textXPos, textYPos + fm.getAscent());
            }
    
            @Override
            public void mouseClicked(MouseEvent e) {
                if (menuBounds == null) {
                    return;
                }
                for (int index = 0; index < menuOptions.length; index++) {
                    if (menuBounds[index].contains(e.getPoint())) {
                        switch (index) {
                            case 0:
                                observer.startNewGame();
                                break;
                            case 2:
                                observer.randomGame();
                                break;
                            case 3:
                                observer.loadGame();
                                break;
                            case 4:
                                observer.saveGame();
                                break;
                        }
                    }
                }
            }
        }
    }