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

问题描述

我在互联网上搜索了用Java Swing制作钢琴的正确方法。但是他们要么在黑键之间有缝隙,要么没有解释他们是如何做到的。

我尝试使用具有null布局的JPanel并先将白色按键(Jpanels或Jbuttons)与MouseListener一起添加,然后再添加黑色按键,使它们应位于白色上方。问题在于它不是很优雅的代码,除此之外,它也不起作用。

有人知道如何用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,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,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钢琴:

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

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,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,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,new Color(0x00000000,}
                    ));
                    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,new Color(0x60FFFFFF,bounds.y + bounds.height * 0.5f,new Color(0x00FFFFFF,true)
                    ));
                    g.fillRoundRect(bounds.x,bounds.width,bounds.height,4,4);
                    g.setPaint(new LinearGradientPaint(
                        bounds.x,bounds.x + bounds.width,0.2f,0.8f,new Color[] {
                            new Color(0x60FFFFFF,}
                    ));
                    g.fillRoundRect(bounds.x,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)。将键盘作为一个组件而不是单独的按钮具有两个优点。一种是您可以做完全任意的形状边界,而不仅仅是矩形。另一种是您可以沿键盘笔直拖动/滑动鼠标(这不适用于单独的按钮)。

,

添加白色键...然后添加黑色键,使它们应位于白色上方。

实际上,Swing绘画逻辑会绘画最先添加的最后一个组件。因此,您的黑色按键将首先被绘制,白色被顶部绘制。通常这不是问题,因为使用布局管理器时组件不会重叠。

因此,您需要在面板上添加黑键,然后再添加白键。

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

在不重叠的情况下优化了摇摆绘画。因为您的组件确实重叠,所以您还需要重写isOptimizedDrawingEnable()方法以返回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,WWD,WHT);
        
    }
    
    public int getNote () {
        return note;
    }
}