javaswingpiano

Is there a good way to make Piano Graphics in Java?


I searched on the internet if there is a proper way of making a piano in Java Swing. But either they had gaps between the black keys or they didn't explain how they've done it.

I tried using a JPanel with a null-layout and adding the white keys (Jpanels or Jbuttons) with a MouseListener first and then adding the black keys so they should be above the whites. The problem is that it isn't very elegant code and besides that, it doesn't work.

Does anyone know how to make a Piano in Java?

Here's my code:

package me.Trainer.Piano;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import javax.swing.JPanel;

import me.Trainer.Enums.Note;

public class PianoGraphics {

static volatile Note result = null;

public static JPanel getDrawnKeyboard() {

    JPanel panel = new JPanel() {
        private static final long serialVersionUID = 502433120279478947L;

        Dimension lastFrame;

        @Override
        protected void paintComponent(Graphics g) {

            super.paintComponent(g);

            int width = this.getWidth();
            int height = this.getHeight();

            if (lastFrame != this.getSize()) {
                this.removeAll();
                JPanel white = new JPanel() {
                    
                    private static final long serialVersionUID = 2350489085544800839L;

                    protected void paintComponent(Graphics g) {
                        super.paintComponent(g);
                        g.setColor(Color.LIGHT_GRAY);
                        g.drawRect(0, 0, this.getWidth(), this.getHeight());
                    };
                };
                white.setBackground(Color.WHITE);
                white.setSize(width / 52, height);
                for (int i = 0; i < 52; i++) {
                    Note note;
                    int oct = (int) i / 7;
                    switch(i % 7) {
                    case 0:
                        note = Note.values()[0 + (oct * 12)];
                        break;
                    case 1:
                        note = Note.values()[2 + (oct * 12)];
                        break;
                    case 2:
                        note = Note.values()[3 + (oct * 12)];
                        break;
                    case 3:
                        note = Note.values()[5 + (oct * 12)];
                        break;
                    case 4:
                        note = Note.values()[7 + (oct * 12)];
                        break;
                    case 5:
                        note = Note.values()[8 + (oct * 12)];
                        break;
                    case 6:
                        note = Note.values()[10 + (oct * 12)];
                        break;
                    default:
                        note = Note.C4;
                    }
                    white.setLocation(i * (width / 52), 0);
                    white.addMouseListener(new KeyboardMouseListener() {
                        
                        Note n = note;
                        
                        @Override
                        public void mouseReleased(MouseEvent e) {
                            white.setBackground(Color.WHITE);
                            result = null;
                        }
                        
                        @Override
                        public void mouseClicked(MouseEvent e) {
                            white.setBackground(Color.LIGHT_GRAY);
                            result = n;
                        }
                    });
                    this.add(white);
                }

                JPanel black = new JPanel() {

                    private static final long serialVersionUID = 8445848892107864631L;
                    
                    protected void paintComponent(Graphics g) {
                        
                        super.paintComponent(g);
                        g.setColor(Color.DARK_GRAY);
                        g.drawRect(0, 0, this.getWidth(), this.getHeight());
                        
                    };
                    
                };
                
                black.setBackground(Color.BLACK);
                black.setSize(width / 108, height / 3 * 2);
                
                for (int i = 0; i < 7; i++) {
                    Note note = Note.values()[1 + (i*12)];
                    JPanel b = black;
                    b.setLocation(i*12*8 + 7, 0);
                    b.addMouseListener(new KeyboardMouseListener() {
                        public void mouseClicked(MouseEvent e) {
                            b.setBackground(Color.DARK_GRAY);
                            result = note;
                        };
                        public void mouseReleased(MouseEvent e) {
                            b.setBackground(Color.BLACK);
                            result = null;
                            System.out.println(note.name());
                        };
                    });
                    this.add(b);
                    JPanel b1 = black;
                    Note note1 = Note.values()[1 + (i*12)];
                    b1.setLocation(i*12*8 + 21, 0);
                    b1.addMouseListener(new KeyboardMouseListener() {
                        public void mouseClicked(MouseEvent e) {
                            b1.setBackground(Color.DARK_GRAY);
                            result = note1;
                            System.out.println(note1.name());
                        };
                        public void mouseReleased(MouseEvent e) {
                            b1.setBackground(Color.BLACK);
                            result = null;
                        };
                    });
                    this.add(b1);
                    JPanel b2 = black;
                    Note note2 = Note.values()[1 + (i*12)];
                    b2.setLocation(i*12*8 + 30, 0);
                    b2.addMouseListener(new KeyboardMouseListener() {
                        public void mouseClicked(MouseEvent e) {
                            b2.setBackground(Color.DARK_GRAY);
                            result = note2;
                        };
                        public void mouseReleased(MouseEvent e) {
                            b2.setBackground(Color.BLACK);
                            result = null;
                        };
                    });
                    this.add(b2);
                    JPanel b3 = black;
                    Note note3 = Note.values()[1 + (i*12)];
                    b3.setLocation(i*12*8 + 45, 0);
                    b3.addMouseListener(new KeyboardMouseListener() {
                        public void mouseClicked(MouseEvent e) {
                            b3.setBackground(Color.DARK_GRAY);
                            result = note3;
                        };
                        public void mouseReleased(MouseEvent e) {
                            b3.setBackground(Color.BLACK);
                            result = null;
                        };
                    });
                    this.add(b3);
                    JPanel b4 = black;
                    Note note4 = Note.values()[1 + (i*12)];
                    b4.setLocation(i*12*8 + 53, 0);
                    b4.addMouseListener(new KeyboardMouseListener() {
                        public void mouseClicked(MouseEvent e) {
                            b4.setBackground(Color.DARK_GRAY);
                            result = note4;
                        };
                        public void mouseReleased(MouseEvent e) {
                            b4.setBackground(Color.BLACK);
                            result = null;
                        };
                    });
                    this.add(b4);
                }
            }
            
            lastFrame = this.getSize();

        }

    };

    panel.setLayout(null);
    
    return panel;

}

public static Note waitForNote() {
    while (result == null) {}
    Note note = result;
    result = null;
    return note;
}
}

class KeyboardMouseListener implements MouseListener {
    
    @Override
    public void mouseClicked(MouseEvent e) {}

    @Override
    public void mouseEntered(MouseEvent e) {}

    @Override
    public void mouseExited(MouseEvent e) {}

    @Override
    public void mousePressed(MouseEvent e) {}

    @Override
    public void mouseReleased(MouseEvent e) {}
}

And here's what I get: Nothing is clickable


Solution

  • You can use the Swing Shape interfaces, in particular java.awt.geom.Path2D to draw arbitrary shapes and also do click testing. I once wrote a Swing MIDI piano using this:

    I think it would be quite difficult to post the full program because it's entangled with some of my utility classes, and you presumably have your own design you want to build anyway. But here is the source of the graphical "Keyboard" component, which has no dependencies:

    import java.util.*;
    import java.util.List;
    import java.awt.*;
    import java.awt.geom.*;
    import javax.swing.*;
    
    public final class Keyboard extends JComponent {
        public static final float WHITE_KEY_ASPECT = (7f / 8f) / (5.7f);
        public static final float BLACK_KEY_HEIGHT = 3.5f / 6f;
        
        private char firstNote;
        private int whiteKeyCount;
        private int whiteKeyWidth;
        private int whiteKeyHeight;
        private List<KeyShape> keyShapes;
        
        private final Set<Integer> litKeys = new HashSet<>();
        
        
        public Keyboard() {
            setFirstNote('C');
            setWhiteKeyCount(7 * 7 + 1);
            setWhiteKeySize(Math.round(220 * WHITE_KEY_ASPECT), 220);
        }
        
        
        public void setFirstNote(char n) {
            if (n < 'A' || n > 'G') throw new IllegalArgumentException();
            this.firstNote = n;
            revalidate();
        }
        
        
        public void setWhiteKeyCount(int c) {
            if (c < 0) throw new IllegalArgumentException();
            this.whiteKeyCount = c;
            revalidate();
        }
        
        
        public void setWhiteKeySize(int width, int height) {
            if (width < 0) throw new IllegalArgumentException();
            if (height < 0) throw new IllegalArgumentException();
            this.whiteKeyWidth = width;
            this.whiteKeyHeight = height;
            revalidate();
        }
        
        
        private static class KeyShape {
            final Shape shape;
            final char color; // 'W' or 'B'
            
            KeyShape(Shape shape, char color) {
                this.shape = shape;
                this.color = color;
            }
        }
        
        
        @Override
        public void invalidate() {
            super.invalidate();
            keyShapes = null;
        }
        
        
        private List<KeyShape> getKeyShapes() {
            if (keyShapes == null) {
                keyShapes = generateKeyShapes();
            }
            return keyShapes;
        }
        
        
        private List<KeyShape> generateKeyShapes() {
            List<KeyShape> shapes = new ArrayList<>();
            
            int x = 0;
            char note = firstNote;
            for (int w = 0; w < whiteKeyCount; w++) {
                float cutLeft = 0, cutRight = 0;
                switch (note) {
                case 'C':
                    cutLeft  = 0 / 24f;
                    cutRight = 9 / 24f;
                    break;
                case 'D':
                    cutLeft  = 5 / 24f;
                    cutRight = 5 / 24f;
                    break;
                case 'E':
                    cutLeft  = 9 / 24f;
                    break;
                case 'F':
                    cutRight = 11 / 24f;
                    break;
                case 'G':
                    cutLeft  = 3 / 24f;
                    cutRight = 7 / 24f;
                    break;
                case 'A':
                    cutLeft  = 7 / 24f;
                    cutRight = 3 / 24f;
                    break;
                case 'B':
                    cutLeft  = 11 / 24f;
                    cutRight = 0 / 24f;
                    break;
                }
                if (w == 0)
                    cutLeft = 0;
                if (w == whiteKeyCount - 1)
                    cutRight = 0;
                
                shapes.add(new KeyShape(createWhiteKey(x, cutLeft, cutRight), 'W'));
                
                if (cutRight != 0) {
                    shapes.add(new KeyShape(createBlackKey(x + whiteKeyWidth - (whiteKeyWidth * cutRight)), 'B'));
                }
                
                x += whiteKeyWidth;
                if (++note == 'H') note = 'A';
            }
            
            return Collections.unmodifiableList(shapes);
        }
        
        
        private Shape createWhiteKey(float x, float cutLeft, float cutRight) {
            float width = whiteKeyWidth, height = whiteKeyHeight;
            Path2D.Float path = new Path2D.Float();
            path.moveTo(x + cutLeft * width, 0);
            path.lineTo(x + width - (width * cutRight), 0);
            if (cutRight != 0) {
                path.lineTo(x + width - (width * cutRight), height * BLACK_KEY_HEIGHT);
                path.lineTo(x + width, height * BLACK_KEY_HEIGHT);
            }
            final float bevel = 0.15f;
            path.lineTo(x + width, height - (width * bevel) - 1);
            if (bevel != 0) {
                path.quadTo(x + width, height, x + width * (1 - bevel), height - 1);
            }
            path.lineTo(x + width * bevel, height - 1);
            if (bevel != 0) {
                path.quadTo(x, height, x, height - (width * bevel) - 1);
            }
            if (cutLeft != 0) {
                path.lineTo(x, height * BLACK_KEY_HEIGHT);
                path.lineTo(x + width * cutLeft, height * BLACK_KEY_HEIGHT);
            }
            path.closePath();
            return path;
        }
        
        
        private Shape createBlackKey(float x) {
            return new Rectangle2D.Float(
                x, 0,
                whiteKeyWidth * 14f / 24,
                whiteKeyHeight * BLACK_KEY_HEIGHT
            );
        }
        
        
        @Override
        public void paintComponent(Graphics g1) {
            Graphics2D g = (Graphics2D)g1;
            Rectangle clipRect = g.getClipBounds();
            
            g.setColor(Color.BLACK);
            g.fill(clipRect);
            
            g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g.setStroke(new BasicStroke(1f));
            
            List<KeyShape> keyShapes = getKeyShapes();
            for (int i = 0; i < keyShapes.size(); i++) {
                KeyShape ks = keyShapes.get(i);
                Rectangle bounds = ks.shape.getBounds();
                if (!bounds.intersects(clipRect)) continue;
                
                g.setColor(isKeyLit(i)
                    ? (ks.color == 'W' ? new Color(0xFF5050) : new Color(0xDF3030))
                    : (ks.color == 'W' ? Color.WHITE : Color.BLACK)
                );
                g.fill(ks.shape);
                
                if (true) { // gradient
                    if (ks.color == 'W') {
                        g.setPaint(new LinearGradientPaint(
                            bounds.x, bounds.y, bounds.x, bounds.y + bounds.height,
                            new float[] { 0, 0.02f, 0.125f, 0.975f, 1 },
                            new Color[] {
                                new Color(0xA0000000, true),
                                new Color(0x30000000, true),
                                new Color(0x00000000, true),
                                new Color(0x00000000, true),
                                new Color(0x30000000, true),
                            }
                        ));
                        g.fill(ks.shape);
                    } else {
                        bounds.setRect(
                            bounds.getX() + bounds.getWidth() * 0.15f,
                            bounds.getY() + bounds.getHeight() * 0.03f,
                            bounds.getWidth() * 0.7f,
                            bounds.getHeight() * 0.97f
                        );
                        g.setPaint(new GradientPaint(
                            bounds.x, bounds.y, new Color(0x60FFFFFF, true),
                            bounds.x, bounds.y + bounds.height * 0.5f, new Color(0x00FFFFFF, true)
                        ));
                        g.fillRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4);
                        g.setPaint(new LinearGradientPaint(
                            bounds.x, bounds.y, bounds.x + bounds.width, bounds.y,
                            new float[] { 0, 0.2f, 0.8f, 1 },
                            new Color[] {
                                new Color(0x60FFFFFF, true),
                                new Color(0x00FFFFFF, true),
                                new Color(0x00FFFFFF, true),
                                new Color(0x60FFFFFF, true),
                            }
                        ));
                        g.fillRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4);
                    }
                }
                
                g.setColor(Color.BLACK);
                g.draw(ks.shape);
            }
        }
        
        
        @Override
        public Dimension getPreferredSize() {
            return new Dimension(
                whiteKeyCount * whiteKeyWidth,
                whiteKeyHeight
            );
        }
        
        
        public int getKeyAtPoint(Point2D p) {
            List<KeyShape> keyShapes = getKeyShapes();
            for (int i = 0; i < keyShapes.size(); i++) {
                if (keyShapes.get(i).shape.contains(p)) return i;
            }
            return -1;
        }
        
        
        public void setKeyLit(int index, boolean b) {
            if (index < 0 || index > getKeyShapes().size()) return;
            if (b) {
                litKeys.add(index);
            } else {
                litKeys.remove(index);
            }
            repaint(getKeyShapes().get(index).shape.getBounds());
        }
        
        
        public boolean isKeyLit(int index) {
            return litKeys.contains(index);
        }
        
        
        public void clearLitKeys() {
            litKeys.clear();
            repaint();
        }
        
        
    }
    

    I haven't looked at this code in years but here's the basic idea: The entire keyboard is one component. It generates a list of Shape objects for the keys, and uses the shapes both for painting the keys and click testing (add your MouseListener and MouseMotionListener which call getKeyAtPoint). There are two advantages to doing the keyboard as one component, rather than separate buttons. One is that you can do completely arbitrary shape boundaries, rather than just rectangles. The other is that you can drag/glide the mouse straight along the keyboard (which doesn't work with separate buttons).