4

I have been working on a music notation software that I want to use to train my music students on sight-reading.

I want to know whether I am taking the right approach or if there is a better way.

I have managed to make the clef, notes and staff objects using unicodes and fonts that support music notation using code like this:

public static void spaceStaff(Graphics g){
    Graphics2D g2 = (Graphics2D) g;
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    Font font = new Font("Bravura", Font.PLAIN, 32);
    g2.setFont(font);
    g2.drawString("\uD834\uDD1A", note.spacing-2, staffDistance);
    note.spacing = note.spacing + 16;
}

I then have a class called Surface that paints the notes onto a JPanel:

public class Surface extends JPanel {
@Override
public void paintComponent(Graphics g){
    super.paintComponent(g);
    staff.spaceStaff(g);
    clef.drawGclef(g);//not given in example code above
    note.drawCrotchet(g, note.B1);//not given in example code above
   }
}

I am using this code to start the application and display the music notes:

public class SightreadHelper extends JFrame {

public SightreadHelper(){
    initUI();
}

private void initUI() {

    JButton button = new JButton("add notes"); //explanation for this button below
    button.setSize(150, 75);
    button.setVisible(true);
    add( button );

    button.addActionListener( new ActionListener()
    {
        public void actionPerformed(ActionEvent e)
        {
            repaint();//this doesn't work as I expect: explanation below
        }
    });

    Surface srf = new Surface();
    add(new Surface());
    setSize(700, 1000);
    setLocationRelativeTo(null);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
    EventQueue.invokeLater(new Runnable() {
        @Override
        public void run() {
            SightreadHelper srh = new SightreadHelper();
            srh.setVisible(true);
        }
    });
  }

}

The button is supposed to add the notes at the end of the existing notes, but it doesn't. This image explains what happens after I click the button.

Am I using the right procedure for doing what I want to do? Thanks a lot for your help. I saw this question but it didnt help me: Repaint Applets in JAVA without losing previous contents

Community
  • 1
  • 1
Josiah
  • 118
  • 10

2 Answers2

5

in general your approach may work, but it is way to simple structured to work correctly, if you tend to push it to an application. You are lacking a data model here. Your software paints the notes directly without a model that represents the internal state.

You should have a look at Model View Controller abstraction pattern. and develop a datamodel first, that has nothing to do with rendering the notes.

Then you start develop a visualisation for the single elements e.g. the notes. You will compose the components in a panel depended on what the data model tells you and render them. Thats what you should do in the doPaintComponent Method.

When you hit the button add note, the following steps should happen:

  1. add a note to your model

  2. your model creates a ui event that results calling the repaint method and maybe even the layout method repainting your ui, ideally repainting only the parts that need to be repainted.

If you are not seeing something during rendering, make sure to first check the following things:

  1. is your rendering code called by the awt - thread?
  2. is your component visible?
  3. is the position you are trying to paint correct?

You should perhaps read one ore more tutorials on developing custom swing components like Oracle Tutorial Custom Swing component.

I know the answer is probably not really helping you in the short run, but I believe it is better to understand the concepts of UI development before trying to make your code somehow run.

Melv_80
  • 222
  • 1
  • 5
  • Thanks so much @Melv_80 for taking time to provide such a detailed answer.. I think this is what I am missing. I am sure that will work. I will get back to you when I get stuck. – Josiah Sep 05 '16 at 13:49
  • I am stuck. been trying almost all night. Can you do a very simple example for me? – Josiah Sep 06 '16 at 07:24
  • I have written two classes, that should visualize what i meant, i am going to post it in a new answer – Melv_80 Sep 06 '16 at 13:34
3

The class that visualizes a music sheet, it should be embedded in an application and not start a Frame on its on. Its only to demonstrate the MVC coding pattern. The visualization strictly paints, what the model (NoteSequence) is saying.

package com.musicsheet;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
public class MusicSheetComponent extends JPanel implements PropertyChangeListener {
  private NoteSequence noteSequence;
  public MusicSheetComponent(NoteSequence noteSequence) {
    this.noteSequence = noteSequence;
    this.noteSequence.addNoteSequenceChangedListener(this);
  }
  public static void main(String[] args) {

    SwingUtilities.invokeLater(() -> {
      JFrame f = new JFrame();
      NoteSequence noteSequence = new NoteSequence(); // the model
      f.setLayout(new BorderLayout()); // how should the screen be layouted
      f.add(new MusicSheetComponent(noteSequence), BorderLayout.CENTER); // the sheet component is the view, it renders whatever the model
                                                    // tells
      f.add(new JButton(new AbstractAction("Add note") {
        @Override
        public void actionPerformed(ActionEvent e) {
          noteSequence.addNote(new NoteSequence.Note((int)(Math.random()*5)));   // add a note on a random line
        }
      }), BorderLayout.SOUTH);
      f.setSize(320, 240);
      f.setVisible(true);
    });
  }

  @Override
  protected void paintComponent(Graphics g2d) {
    super.paintComponent(g2d);
    Graphics2D g = (Graphics2D) g2d; // graphics2d has more functions
    int w = getWidth();
    int h = getHeight();
    int lines = 5;
    int spacing = h / lines;
    paintSheetBackground(g, w, h, spacing);
    drawNotes(g, spacing);
  }

  private void paintSheetBackground(Graphics2D g, int w, int h, int spacing) {
    g.setColor(Color.white);
    g.fillRect(0, 0, w, h);

    g.setColor(Color.black);
    for (int i = 0; i < 5; i++) {
      final int y2 = i * spacing;
      g.drawLine(0, y2, w, y2);
    }
  }

  private void drawNotes(Graphics2D g, int heigtSpacing) {
    int xSpacing = 30;
    int x = 0;
    for (NoteSequence.Note note : noteSequence.getNotes()) {
      int y = (int) (note.getLine()*heigtSpacing);
      g.fillOval(x, y-3, 7, 6); // -3 because note should sit on line
      x+= xSpacing;
    }
  }

  @Override
  public void propertyChange(PropertyChangeEvent evt) {
    if (NoteSequence.PROP_NOTE_ADDED.equals(evt.getPropertyName()) || NoteSequence.PROP_NOTE_REMOVED.equals(evt.getPropertyName())) {
      repaint();
    }
  }
}

Next, the class that models the music sheet or note sequence.

package com.musicsheet;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * The model class, that holds all notes.
 */
public class NoteSequence {
  public static final String PROP_NOTE_ADDED = "noteAdded";
  public static final String PROP_NOTE_REMOVED = "noteRemoved";

  private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
  private final List<Note> notes = new ArrayList<>();

  public List<Note> getNotes() {
    return Collections.unmodifiableList(notes);
  }

  /**
   * is called by the UI or a button listener when a note should be added to this music sheet / notesequence
   * @param note
   */
  public void addNote(Note note) {
    this.notes.add(note);
    propertyChangeSupport.firePropertyChange(new PropertyChangeEvent(this, PROP_NOTE_ADDED, null, note));
  }

  public void removeNote(Note note) {
    this.notes.remove(note);
    propertyChangeSupport.firePropertyChange(new PropertyChangeEvent(this, PROP_NOTE_REMOVED, note, null));
  }

  void addNoteSequenceChangedListener(PropertyChangeListener listener) {
    propertyChangeSupport.addPropertyChangeListener(listener);
  }

  // not really needed atm
  void removeNoteSequenceChangedListener(PropertyChangeListener listener) {
    propertyChangeSupport.removePropertyChangeListener(listener);
  }

  /**
   * a single note.
   */
  public static class Note {
    private float line; // where does the note sit
    private float timestampSinceBeginning; // not used, but you have to know WHEN a note is played
    // ... more properties, e.g. name, or modifiers or whatever

    public Note(float line) { // this is certainly to easy, since notes can sit in between lines,
      // i did not try to think deeply into it, to define a better model
      this.line = line;
    }

    public float getLine() {
      return line;
    }

    public float getTimestampSinceBeginning() {
      return timestampSinceBeginning;
    }
  }
}
Melv_80
  • 222
  • 1
  • 5
  • Thanks so much. This puts me on the very right track. I appreciate for every minute you've taken to help me. – Josiah Sep 06 '16 at 18:44