Search code examples
javaswingpaintcomponentrepaintmousehover

Change color of letter on mouse hover only


I have a Jframe with two buttons: 'A' and 'B'. Clicking the button 'A' should display the capital letter A in the JPanel. On mouse hover only, any 'A' letter within the canvas should be displayed in red. When the mouse leaves, the text color should be back to black.

I've coded for this and it works only once. The letter 'A' changes to red but does not change back to black. Also, it does not work for multiple 'A's

Code for JFrame:

import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class DrawFrame extends JFrame{
    private final int WIDTH = 500;
    private final int HEIGHT = 300;

    private GUIModel model;

    private JButton number1;
    private JButton number2;

    private JPanel numberPanel;
    private DrawPanel graphicsPanel;

    public DrawFrame()
    {
        this.model = new GUIModel(" ");

        createSelectionPanel();
        createGraphicsPanel();

        this.setSize(WIDTH, HEIGHT);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);
    }

    private void createSelectionPanel()
    {
        numberPanel = new JPanel();

        ButtonListener listener = new ButtonListener();

        number1 = new JButton("A");
        number1.addActionListener(listener);

        number2 = new JButton("B");
        number2.addActionListener(listener);  

        numberPanel.setLayout(new GridLayout(2,2));
        numberPanel.add(number1);
        numberPanel.add(number2);

        this.add(numberPanel, BorderLayout.WEST);
    }

    private void createGraphicsPanel()
    {
        //instantiate drawing panel
        graphicsPanel = new DrawPanel(WIDTH, HEIGHT, model);
        //add drawing panel to right
        add(graphicsPanel);
    }

    private class ButtonListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent event) {
            model.setDisplayString(event.getActionCommand());
        }
    }

    //creates a drawing frame
    public static void main(String[] args)
    {
        DrawFrame draw = new DrawFrame();
    }   
}

Code for JPanel:

import javax.swing.JPanel;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.List;

public class DrawPanel extends JPanel{
    private static final long serialVersionUID = 3443814601865936618L;

    private Font font = new Font("Default", Font.BOLD, 30);

    private static final Color DEFAULT_TEXT_COLOR = Color.BLACK;
    private static final Color HOVER_TEXT_COLOR = Color.RED;
    private Color color = DEFAULT_TEXT_COLOR;

    private List<GUIModel> numberList = new ArrayList<GUIModel>();
    private GUIModel model;
    boolean mouseHover = false;

    public DrawPanel(int width, int height, GUIModel model){
        this.setPreferredSize(new Dimension(width, height));
        this.model = model;

        //set white background for drawing panel
        setBackground(Color.WHITE);

        //add mouse listeners
        MouseHandler mouseHandler = new MouseHandler();
        this.addMouseListener(mouseHandler);
        this.addMouseMotionListener(mouseHandler);
    }

    void checkForHover(MouseEvent event) {
        FontMetrics metrics = getFontMetrics(font);

        Graphics g = getGraphics();
        Rectangle textBounds = metrics.getStringBounds("A", g).getBounds();
        g.dispose();

        int index = 0;
        while (index < numberList.size()) {
            Double x = numberList.get(index).getCoordinate().getX();
            Double y = numberList.get(index).getCoordinate().getY();

            textBounds.translate(x.intValue(), y.intValue());

            if (textBounds.contains(event.getPoint())) {
                color = HOVER_TEXT_COLOR;
            }
            else {
                color = DEFAULT_TEXT_COLOR;
            }
            index++;
        }
        repaint(textBounds);
    }

    @Override
    public void paintComponent(Graphics g){
        super.paintComponent(g);
        g.setFont(font);
        g.setColor(color);

        int index = 0;
        while (index < numberList.size()) {
            Double x = numberList.get(index).getCoordinate().getX();
            Double y = numberList.get(index).getCoordinate().getY();
            String display = numberList.get(index).getDisplayString();
            g.drawString(display, x.intValue(), y.intValue());
            index++;
        }

        if (model.getCoordinate() != null) {
            Point p = model.getCoordinate();            
            g.drawString(model.getDisplayString(), p.x, p.y);
            GUIModel number = new GUIModel();
            number.setCoordinate(p);
            number.setDisplayString(model.getDisplayString());
            numberList.add(number);
        }
    }

    //class to handle all mouse events
    private class MouseHandler extends MouseAdapter implements MouseMotionListener
    {
        @Override
        public void mousePressed(MouseEvent event)
        {
           model.setCoordinate(event.getPoint());
        }

        @Override
        public void mouseReleased(MouseEvent event)
        {
            DrawPanel.this.repaint();
        }

        @Override
        public void mouseEntered(MouseEvent event) {
            checkForHover(event);
        }

        @Override
        public void mouseMoved(MouseEvent event) {
            checkForHover(event);
        }
    }
}

Code for GUIModel:

import java.awt.Point;

public class GUIModel {
    private String displayString;
    private Point coordinate;

    public GUIModel() {}

    public GUIModel(String displayString) {
        this.displayString = displayString;
    }
    public void setDisplayString(String displayString) {
        this.displayString = displayString;
    }

    public String getDisplayString() {
        return displayString;
    }

    public Point getCoordinate() {
        return coordinate;
    }

    public void setCoordinate(int x, int y) {
        this.coordinate = new Point(x, y);
    }

    public void setCoordinate(Point coordinate) {
        this.coordinate = coordinate;
    }   
}

Any help would be much appreciated. Thanks!


Solution

  • There's several misconceptions.

    • Graphics#drawString doesn't paint the text at the x/y position, so that the x/y is the top left corner of the String, but instead, the x/y position is the baseline of the font, this means that much of the text is draw above the y position, see Font Concepts for more details. This means that when you try and calculate the the Rectangle of the text, it's actually lower then where you painting it. Instead, you need to use y + ascent to get the text to position properly.
    • paintComponent can be called at any time for any number of reasons, many of which you don't control. To this end, paintComponent should only be used to paint the current state of the component and should never update or modify the state of the component. So adding a new GUIModel within the method is the wrong thing to do, instead, it should be added in the mouseClicked event of the MouseListener.
    • You're relying to much on the GUIModel variables. You should create a model only when you actually need it

    Conceptually, this example address most of the issues mentioned above

    import java.awt.BorderLayout;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.Font;
    import java.awt.FontMetrics;
    import java.awt.Graphics;
    import java.awt.GridLayout;
    import java.awt.Point;
    import java.awt.Rectangle;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.event.MouseAdapter;
    import java.awt.event.MouseEvent;
    import java.awt.event.MouseMotionListener;
    import java.util.ArrayList;
    import java.util.List;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    
    public class DrawFrame extends JFrame {
    
        private final int WIDTH = 500;
        private final int HEIGHT = 300;
    
    //  private GUIModel model;
        private JButton number1;
        private JButton number2;
    
        private JPanel numberPanel;
        private DrawPanel graphicsPanel;
    
        public DrawFrame() {
    //      this.model = new GUIModel(" ");
    
            createSelectionPanel();
            createGraphicsPanel();
    
            this.setSize(WIDTH, HEIGHT);
            this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            this.setVisible(true);
        }
    
        private void createSelectionPanel() {
            numberPanel = new JPanel();
    
            ButtonListener listener = new ButtonListener();
    
            number1 = new JButton("A");
            number1.addActionListener(listener);
    
            number2 = new JButton("B");
            number2.addActionListener(listener);
    
            numberPanel.setLayout(new GridLayout(2, 2));
            numberPanel.add(number1);
            numberPanel.add(number2);
    
            this.add(numberPanel, BorderLayout.WEST);
        }
    
        private void createGraphicsPanel() {
            //instantiate drawing panel
            graphicsPanel = new DrawPanel(WIDTH, HEIGHT);
            //add drawing panel to right
            add(graphicsPanel);
        }
    
        private class ButtonListener implements ActionListener {
    
            @Override
            public void actionPerformed(ActionEvent event) {
                graphicsPanel.setDisplayString(event.getActionCommand());
            }
        }
    
        //creates a drawing frame
        public static void main(String[] args) {
            DrawFrame draw = new DrawFrame();
        }
    
        public static class DrawPanel extends JPanel {
    
            private static final long serialVersionUID = 3443814601865936618L;
    
            private Font font = new Font("Default", Font.BOLD, 30);
    
            private static final Color DEFAULT_TEXT_COLOR = Color.BLACK;
            private static final Color HOVER_TEXT_COLOR = Color.RED;
            private Color color = DEFAULT_TEXT_COLOR;
    
            private List<GUIModel> numberList = new ArrayList<GUIModel>();
            boolean mouseHover = false;
    
            private String displayString;
            private GUIModel hoverModel;
    
            public DrawPanel(int width, int height) {
                this.setPreferredSize(new Dimension(width, height));
    
                //set white background for drawing panel
                setBackground(Color.WHITE);
    
                //add mouse listeners
                MouseHandler mouseHandler = new MouseHandler();
                this.addMouseListener(mouseHandler);
                this.addMouseMotionListener(mouseHandler);
            }
    
            protected Rectangle getBounds(GUIModel model) {
                FontMetrics metrics = getFontMetrics(font);
                Double x = model.getCoordinate().getX();
                Double y = model.getCoordinate().getY();
    
                Rectangle textBounds = new Rectangle(
                        x.intValue(),
                        y.intValue(),
                        metrics.stringWidth(model.getDisplayString()),
                        metrics.getHeight());
                return textBounds;
            }
    
            void checkForHover(MouseEvent event) {
    
                Rectangle textBounds = null;
                if (hoverModel != null) {
                    textBounds = getBounds(hoverModel);
                }
                hoverModel = null;
                if (textBounds != null) {
                    repaint(textBounds);
                }
                for (GUIModel model : numberList) {
    
                    textBounds = getBounds(model);
    
                    if (textBounds.contains(event.getPoint())) {
                        hoverModel = model;
                        repaint(textBounds);
                        break;
                    }
                }
            }
    
            @Override
            public void paintComponent(Graphics g) {
                super.paintComponent(g);
                g.setFont(font);
                g.setColor(DEFAULT_TEXT_COLOR);
    
                FontMetrics fm = g.getFontMetrics();
                for (GUIModel model : numberList) {
                    if (model != hoverModel) {
                        Double x = model.getCoordinate().getX();
                        Double y = model.getCoordinate().getY();
                        String display = model.getDisplayString();
                        g.drawString(display, x.intValue(), y.intValue() + fm.getAscent());
                    }
                }
                if (hoverModel != null) {
                    g.setColor(HOVER_TEXT_COLOR);
                    Double x = hoverModel.getCoordinate().getX();
                    Double y = hoverModel.getCoordinate().getY();
                    String display = hoverModel.getDisplayString();
                    g.drawString(display, x.intValue(), y.intValue() + fm.getAscent());
                }
    
    //          if (model.getCoordinate() != null) {
    //              Point p = model.getCoordinate();
    //              g.drawString(model.getDisplayString(), p.x, p.y);
    ////                GUIModel number = new GUIModel();
    ////                number.setCoordinate(p);
    ////                number.setDisplayString(model.getDisplayString());
    ////                numberList.add(number);
    //          }
            }
    
            public void setDisplayString(String text) {
                displayString = text;
            }
    
            //class to handle all mouse events
            private class MouseHandler extends MouseAdapter implements MouseMotionListener {
    
                @Override
                public void mouseClicked(MouseEvent e) {
                    GUIModel model = new GUIModel(displayString);
                    model.setCoordinate(e.getPoint());
                    numberList.add(model);
                    repaint();
                }
    
                @Override
                public void mouseMoved(MouseEvent event) {
                    checkForHover(event);
                }
            }
        }
    
        public static class GUIModel {
    
            private String displayString;
            private Point coordinate;
    
            public GUIModel() {
            }
    
            public GUIModel(String displayString) {
                this.displayString = displayString;
            }
    
            public void setDisplayString(String displayString) {
                this.displayString = displayString;
            }
    
            public String getDisplayString() {
                return displayString;
            }
    
            public Point getCoordinate() {
                return coordinate;
            }
    
            public void setCoordinate(int x, int y) {
                this.coordinate = new Point(x, y);
            }
    
            public void setCoordinate(Point coordinate) {
                this.coordinate = coordinate;
            }
        }
    }