Building the score maker

Let us now build the score maker. This will display whatever is played on the piano in music notation. For the sake of program modularity, we will build the program in a separate file named score_maker.py.

We start by defining a class ScoreMaker. Since we will be showing just two octaves of notes, we will define a constant NOTES listing all the notes (7.06/score_maker.py):

class ScoreMaker:

NOTES = ['C1','D1', 'E1', 'F1', 'G1','A1', 'B1', 'C2','D2', 'E2', 'F2', 'G2','A2', 'B2']

The __init__ method of this class takes the container as an argument. This is the container on which this class will draw the score (7.06/score_maker.py):

def __init__(self, container):
self.canvas = Canvas(container, width=500, height = 110)
self.canvas.grid(row=0, column = 1)
container.update_idletasks()
self.canvas_width = self.canvas.winfo_width()
self.treble_clef_image = PhotoImage(file='../pictures/treble-clef.gif')
self.sharp_image = PhotoImage(file='../pictures/sharp.gif')

Note the use of update_idletasks() on the container frame. Calling this method here is necessary because we created a canvas in the previous line of code, which requires a redrawing of widgets. However, the redraw will only take place after the next run of the event loop. But we want to know the canvas width immediately after it was created. An explicit call to update_idletasks immediately carries out all pending events including geometry management. This ensures that we get the correct width of the canvas in the very next step. If you comment out the update_idletasks line and try to print the width of the canvas, it will print 1 even though we have explicitly set it to 500.

We also initialize two .gif images that we will use to draw the score. The treble_clef image will be used to draw the treble clef to the left of the score, while the sharp_image  will draw a sharp (#) symbol prior to any sharp note (notes on the black keys).

Tkinter uses the concept of event loop to handle all events. Here's an excellent article that explains the concept in depth http://wiki.tcl.tk/1527.  update_idletask is an example of the method available on all widgets. Visit http://effbot.org/tkinterbook/widget.htm to see a list of methods that are available to be called on all widgets.

Our first task is to draw five equally spaced lines on the canvas. We accordingly define a new method to do that:

 def _draw_five_lines(self):
w = self.canvas_width
self.canvas.create_line(0,40,w,40, fill="#555")
self.canvas.create_line(0,50,w,50, fill="#555")
self.canvas.create_line(0,60,w,60, fill="#555")
self.canvas.create_line(0,70,w,70, fill="#555")
self.canvas.create_line(0,80,w,80, fill="#555")

This creates five parallel lines each 10 pixels apart. The underscore in the method name is an indication that this is to be treated as a private method of the class. While Python does not enforce method privacy, this tells the users not to use this method directly in their program.

Let's then build a method that actually calls the previous method and adds a treble clef to the left, thereby creating an empty staff on which we can draw notes:

 def _create_treble_staff(self):
self._draw_five_lines()
self.canvas.create_image(10, 20, image=self.treble_clef_image, anchor=NW)

At the outset, we need to differentiate between drawing a chord and drawing notes of the scale. Since all the notes of a chord are played together, the notes of a chord are drawn at a single x location. In contrast, the notes in a scale are drawn at regular x offsets, as shown here:

Since we need to offset the x value for scales at regular intervals, we use the count method from the itertools module to provide an ever-increasing value of x:

import itertools 
self.x_counter = itertools.count(start=50, step=30)

Now every subsequent call to x = next(self.x_counter) increments x by 30.

Now here's the code that draws the actual note on the canvas:

 def _draw_single_note(self, note, is_in_chord=False):
is_sharp = "#" in note
note = note.replace("#","")
radius = 9
if is_in_chord:
x = 75
else:
x = next(self.x_counter)
i = self.NOTES.index(note)
y = 85-(5*i)
self.canvas.create_oval(x,y,x+radius, y+ radius, fill="#555")
if is_sharp:
self.canvas.create_image(x-10,y, image=self.sharp_image, anchor=NW)
if note=="C1":
self.canvas.create_line(x-5,90,x+15, 90, fill="#555")
elif note=="G2":
self.canvas.create_line(x-5,35,x+15, 35, fill="#555")
elif note=="A2":
self.canvas.create_line(x-5,35,x+15, 35, fill="#555")
elif note=="B2":
self.canvas.create_line(x-5,35,x+15, 35, fill="#555")
self.canvas.create_line(x-5,25,x+15, 25, fill="#555")

The description of the preceding code is as follows:

  • The method accepts a note name, for example, C1 or D2#, and draws an oval at an appropriate place.
  • We need to get the x, y values for drawing an oval.  
  • We first calculate the x value. If the note is part of a chord, we fix the x value at 75 px, whereas if the note is part of a scale, the x value is incremented by 30 pixels from the previous x value by calling next on the itertool counter method.

Next, we calculate the y value. The code to do this is as follows:

i = self.NOTES.index(note)
y = 85-(5*i)

Basically, the y offset is calculated based on the index of the note in the list and each subsequent note is offset by 5 pixels.  The number 85 is found by trial and error.

Now that we have the x and y value, we simply draw the oval of given radius:

self.canvas.create_oval(x,y,x+radius, y+ radius, fill="#555")

If the note is a sharp note, that is, if it contains the character #,  it draws the # image 10 pixels left of the oval for the note.

The notes C1, G2, A2, and B2 are drawn outside the five lines. So in addition to oval we need to draw a small line crossing horizontally through them. This is what the last 11 lines of if…else statements achieve.

Finally, we have the draw_notes method and draw_chord method that given a list of notes draw out the notes and chords, respectively. These are the only two methods that do not have an underscore before their names. This means we expose the interface of our program only using these two methods:

def draw_notes(self, notes):
self._clean_score_sheet()
self._create_treble_staff()
for note in notes:
self._draw_single_note(note)

def draw_chord(self, chord):
self._clean_score_sheet()
self._create_treble_staff()
for note in chord:
self._draw_single_note(note, is_in_chord=True)

Now that we have our ScoreMaker ready, we simply import it into 7.07/view.py:

from score_maker import ScoreMaker

We modify build_score_sheet_frame to instantiate the ScoreMaker:

self.score_maker = ScoreMaker(self.score_sheet_frame) 

We then modify find_scale to add this line (7.07/view.py):

self.score_maker.draw_notes(self.keys_to_highlight)

We similarly modify find_chord  and on_progression_button_clicked to add this line (7.07/view.py):

self.score_maker.draw_chord(self.keys_to_highlight)

That brings us to the end of this project. If you now run 7.07/view.py, you should see a functional score maker and a functional Piano Tutor.

However, let's end this chapter with a brief discussion on window responsiveness.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.93.141