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

692 views Asked by At

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

2

There are 2 answers

5
Boann On BEST ANSWER

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).

0
camickr On

adding the white keys ... and then adding the black keys so they should be above the whites.

Actually Swing painting logic paints the last component added first. So your black keys will be painted first and the white painted on top. Normally this is not an issue since components don't overlap when using layout managers.

So, you need to add the black keys to the panel before adding the white keys.

However, this will not solve all the problems.

Swing painting is optimized assuming components don't overlap. Because your components do overlap you will also need to override the isOptimizedDrawingEnable() method to return false.

Here is a basic example (I found on the web a long time ago):

import java.awt.*;
import java.awt.event.*;
import javax.sound.midi.Instrument;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Synthesizer;
import javax.swing.*;

public class MidiPiano implements MouseListener {

    final int OCTAVES = 4; // change as desired

    private WhiteKey[] whites = new WhiteKey [7 * OCTAVES + 1];
    private BlackKey[] blacks = new BlackKey [5 * OCTAVES];

    MidiChannel channel;

    public MidiPiano () {

        try {
            Synthesizer synth = MidiSystem.getSynthesizer ();
            synth.open ();
            synth.loadAllInstruments (synth.getDefaultSoundbank ());
            Instrument [] insts = synth.getLoadedInstruments ();
            MidiChannel channels[] = synth.getChannels ();
            for (int i = 0; i < channels.length; i++) {
                if (channels [i] != null) {
                    channel = channels [i];
                    break;
                }
            }

            for (int i = 0; i < insts.length; i++) {
                if (insts [i].toString ()
                        .startsWith ("Instrument MidiPiano")) {
                    channel.programChange (i);
                    break;
                }
            }
        } catch (MidiUnavailableException ex) {
            ex.printStackTrace ();
        }
    }

    public void mousePressed (MouseEvent e) {
        Key key = (Key) e.getSource ();
        channel.noteOn (key.getNote (), 127);
    }

    public void mouseReleased (MouseEvent e) {
        Key key = (Key) e.getSource ();
        channel.noteOff (key.getNote ());
    }

    public void mouseClicked (MouseEvent e) { }
    public void mouseEntered (MouseEvent e) { }
    public void mouseExited (MouseEvent e) { }

    private void createAndShowGUI () {

        JPanel contentPane = new JPanel(null)
        {
            @Override
            public Dimension getPreferredSize()
            {
                int count = getComponentCount();
                Component last = getComponent(count - 1);
                Rectangle bounds = last.getBounds();
                int width = 10 + bounds.x + bounds.width;
                int height = 10 + bounds.y + bounds.height;

                return new Dimension(width, height);
            }

            @Override
            public boolean isOptimizedDrawingEnabled()
            {
                return false;
            }

        };

        for (int i = 0; i < blacks.length; i++) {
            blacks [i] = new BlackKey (i);
            contentPane.add (blacks [i]);
            blacks [i].addMouseListener (this);
        }

        for (int i = 0; i < whites.length; i++) {
            whites [i] = new WhiteKey (i);
            contentPane.add (whites [i]);
            whites [i].addMouseListener (this);
        }

        JFrame frame = new JFrame("Midi Piano");
        frame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
        //frame.add( contentPane );
        frame.add( new JScrollPane(contentPane) );
        frame.pack();
        frame.setLocationRelativeTo (null);
        frame.setVisible(true);
    }

    public static void main (String[] args) {
        SwingUtilities.invokeLater (new Runnable () {
            public void run () {
                new MidiPiano ().createAndShowGUI ();
            }
        });
    }
}

interface Key {
    // change WD to suit your screen
    int WD = 16;
    int HT = (WD * 9) / 2;
    // change baseNote for starting octave
    // multiples of 16 only
    int baseNote = 48;

    int getNote ();
}


class BlackKey extends JButton implements Key {

    final int note;

    public BlackKey (int pos) {
        note = baseNote + 1 + 2 * pos + (pos + 3) / 5 + pos / 5;
        int left = 10 + WD
                + ((WD * 3) / 2) * (pos + (pos / 5)
                + ((pos + 3) / 5));
        setBackground (Color.BLACK);
        setBounds (left, 10, WD, HT);
    }

    public int getNote () {
        return note;
    }
}
 
 
class WhiteKey  extends JButton implements Key {
    
    static int WWD = (WD * 3) / 2;
    static int WHT = (HT * 3) / 2;
    final int note;
    
    public WhiteKey (int pos) {
        
        note = baseNote + 2 * pos
                - (pos + 4) / 7
                - pos / 7;
        int left = 10 + WWD * pos;
        // I think metal looks better!
        //setBackground (Color.WHITE);
        setBounds (left, 10, WWD, WHT);
        
    }
    
    public int getNote () {
        return note;
    }
}