在Java中有没有制作钢琴图形的好方法?

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

我在网上查了下JavaSwing有没有合适的钢琴制作方法。但是要么他们在黑键之间有间隙,要么他们没有解释他们是如何做到的。

我尝试使用具有空布局的 JPanel 并首先使用 MouseListener 添加白键(Jpanels 或 Jbuttons),然后添加黑键,这样它们应该在白键上方。问题是它不是非常优雅的代码,除此之外,它不起作用。

有谁知道如何在 Java 中制作钢琴?

这是我的代码:

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) {}
}

这是我得到的: Nothing is clickable

您可以使用 Swing Shape 界面,特别是 java.awt.geom.Path2D 来绘制任意形状并进行点击测试。我曾经用这个写过 Swing MIDI 钢琴:

我认为 post 完整的程序会非常困难,因为它与我的一些实用程序纠缠在一起 类,并且您大概有自己的设计想要构建。但这里是图形“键盘”组件的源代码,它没有依赖关系:

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();
    }
    
    
}

我已经很多年没看过这段代码了,但基本思想如下:整个键盘是一个组件。它为键生成一个 Shape 对象列表,并使用形状绘制键 点击测试(添加你的 MouseListenerMouseMotionListener 调用 getKeyAtPoint)。将键盘作为一个组件而不是单独的按钮有两个优点。一是你可以做完全任意的形状边界,而不仅仅是矩形。另一个是您可以 drag/glide 鼠标直接沿着键盘移动(这不适用于单独的按钮)。

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

实际上,Swing 绘制逻辑首先绘制最后添加的组件。所以你的黑键将首先被绘制,白色被绘制在最上面。通常这不是问题,因为在使用布局管理器时组件不会重叠。

因此,您需要先将黑键添加到面板,然后再添加白键。

但是,这并不能解决所有问题。

Swing 绘画在假设组件不重叠的情况下进行了优化。因为您的组件确实重叠,您还需要将 isOptimizedDrawingEnable() 方法覆盖为 return false.

这是一个基本的例子(我很久以前在网上找到的):

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;
    }
}