Creating broader visual elements

Next, let's lay out the broader visual elements of our program. For the sake of modularity, we divide the program into four broad visual sections, as shown in the following diagram:

Let's define a method called init_gui(), which is called from within the __init__ method as follows (see code 3.03.py):

def init_gui(self):
self.create_top_bar()
self.create_left_drum_loader()
self.create_right_button_matrix()
self.create_play_bar()

We then proceed to define all four of these methods (3.03.py). The code is not discussed here, as we have done similar coding in previous chapters.

We begin with the Top Bar section. The Top Bar is simple. It has a few labels, three Spinboxes, and an Entry widget. We will not reproduce the entire code here (see code 3.03.py) as we have already seen examples of creating Labels and Entry widgets several times in the previous chapters. For Spinbox, the options are specified as follows:

Spinbox(frame, from_=1, to=MAX_BPU, width=5,command=self.on_bpu_changed).grid(row=0, column=7)

We set the class-level properties accordingly:

 self.beats_per_minute = INITIAL_BEATS_PER_MINUTE
self.current_pattern_index = 0

Since we will allow multiple patterns to be designed, we need to keep track of the currently showing or active pattern. The self.current_pattern_index property keeps track of the currently active pattern.

Next, let's code the create_left_drum_loader() method. This again is pretty self-explanatory. We create a loop (see code 3.03.py):

for i in range (MAX_NUMBER_OF_DRUM_SAMPLES):
# create compound button here
# create entry widgets here and keep reference to each entry widget in
#a list for future update of values

Before we proceed to code the create_right_button_matrix() method, let's finish coding the create_play_bar() method, as it is the simpler of the two. All it contains is two Buttons, a Checkbutton, a Spinbox, and an image. We have coded similar widgets earlier in the book, and so I will leave it for you to explore on your own (see code 3.03.py).

Next, let's code the create_right_button_matrix() method. This is the most complex of all.

The right button matrix comprises a two-dimensional array of rows and columns. The number of rows in the matrix equals the constant, MAX_NUMBER_OF_DRUM_SAMPLES, and the number of columns represents the number of beat units per cycle and is calculated by multiplying the number of units and the number of beats per unit.

The code that creates the button matrix looks like this (see code 3.03.py):

self.buttons = [[None for x in range(self.find_number_of_columns())] for x in range(MAX_NUMBER_OF_DRUM_SAMPLES)]
for row in range(MAX_NUMBER_OF_DRUM_SAMPLES):
for col in range(self.find_number_of_columns()):
self.buttons[row][col] = Button(right_frame,
command=self.on_button_clicked(row, col))
self.buttons[row][col].grid(row=row, column=col)
self.display_button_color(row, col)

The associated code for the find_number_of_columns() method is as follows:

    def find_number_of_columns(self):
return int(self.number_of_units_widget.get()) *
int(self.bpu_widget.get())

We have already created the button matrix, but we want the buttons to be colored in two alternating shades. Therefore, we define two constants:

COLOR_1 = 'grey55'
COLOR_2 = 'khaki'

This can be any hexadecimal color code or any color from Tkinter's predefined list of colors. We also require a third color to represent the button in a pressed state.

The constant BUTTON_CLICKED_COLOR = 'green' takes care of that.

We then define two methods:

def display_button_color(self, row, col):
original_color = COLOR_1 if ((col//self.bpu)%2) else COLOR_2
button_color = BUTTON_CLICKED_COLOR if
self.get_button_value(row, col) else original_color
self.buttons[row][col].config(background=button_color)

def display_all_button_colors(self):
number_of_columns = self.find_number_of_columns()
for r in range(MAX_NUMBER_OF_DRUM_SAMPLES):
for c in range(number_of_columns):
self.display_button_color(r, c)

The idea is simple. A button is to be colored green if the value of the button is found to be True in our data structure, or else the button is to be shaded in patterns of COLOR_1 and COLOR_2 for each alternating unit of beats.

This alternating color is obtained by using this mathematical formula:

original_color = COLOR_1 if (col//bpu)%2) else COLOR_2

Remember that we had created a two-dimensional Boolean list called is_button_clicked_list as a dictionary item in our original data structure to hold this value.

We change the color of the button to BUTTON_CLICKED_COLOR if that value is found to be True. Accordingly, we define a getter method to get the value of the button:

def get_button_value(self, row, col):
return
self.all_patterns[self.current_pattern.get()]
['is_button_clicked_list'][row][col]

Now each button is attached to the command callback named on_button_clicked, which is coded as follows (see code 3.03.py):

def on_button_clicked(self, row, col):
def event_handler():
self.process_button_clicked(row, col)
return event_handler

Notice something fancy with this piece of code? This method defines a function within the function. It does not return a value as is typical of functions. Instead, it returns a function that can be executed at a later stage. These are called higher-order functions or, more precisely, function closures.

Why did we need to do this? We had to do this because each button is identified by its unique row and column-based indexes. The row values and column values are only available when the loop runs at the time of creating the buttons. The row and col variables are lost after that. We, therefore, need some way to keep these variables alive if we have to identify which button was clicked later on.

These callback functions come to our rescue as they encapsulate the row and column values in the function that they return at the time of creation.

Functions are first-class objects in Python. This means that you can pass a function to another function as a parameter and you can return a function from another function. In short, you can treat a function as any other object.
You can bind a method object to a particular context, as we did in the previous code, by nested scoping of a method within a method. Higher-order functions like these are a common way of associating functions with widgets in GUI programming.

You can find more information about function closures at https://en.wikipedia.org/wiki/Closure_(computer_programming).

We then define a method called process_button_clicked:

def process_button_clicked(self, row, col):
self.set_button_value(row, col, not self.get_button_value(row, col))
self.display_button_color(row, col)

def set_button_value(self, row, col, bool_value):
self.all_patterns[self.current_pattern.get()][
'is_button_clicked_list'][row][col] = bool_value

The key section in the preceding code is the line that sets the button value opposite to its current value using the not operator. Once the value is toggled, the method calls the display_button_color method to recolor the buttons.

Finally, let's complete this iteration by defining some dummy methods for now and attach them as command callbacks to the respective widgets:

on_pattern_changed()
on_number_of_units_changed()
on_bpu_changed()
on_open_file_button_clicked()
on_button_clicked()
on_play_button_clicked()
on_stop_button_clicked()
on_loop_button_toggled()
on_beats_per_minute_changed()

That completes the iteration. Now if you run the program (see code 3.03.py), it should display all the broad visual elements:

The buttons matrix should be colored in two alternating shades, and pressing the buttons should toggle its color between green and its previous color.

All other widgets remain non-functional at this stage as we have attached them to non-functional command callbacks. We will soon make them functional but, before we do that, let's do something to make all our future coding simple, clean, and elegant.

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

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