Creating our SmilieSelect class

We are going to pop up a small window that contains all available smileys as buttons. When the user clicks on one of these smiley buttons, the image will be inserted into their text_area.

Create a new Python file in your folder called smilieselect.py. In that file, begin with the following code:

import os
import tkinter as tk
import tkinter.ttk as ttk

class SmilieSelect(tk.Toplevel):
smilies_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
'smilies/'))

Our application needs to keep track of where these images are stored on the filesystem, since multiple classes will need to access them. We achieve this by setting a class variable on the SmilieSelect window called smilies_dir.

The smiley images will be stored in a folder named smilies, which will live in the folder holding the rest of our scripts. Go ahead and create this folder now:

def __init__(self, master, **kwargs):
super().__init__(**kwargs)
self.master = master
self.transient(master)
self.position_window()

Our __init__ method begins by setting the window to a transient of its master, then calls a method called position_window. This will allow us to place the smiley window relative to the chat window every time. Let's jump to this method now:

def position_window(self):
master_pos_x = self.master.winfo_x()
master_pos_y = self.master.winfo_y()

master_width = self.master.winfo_width()
master_height = self.master.winfo_height()

my_width = 100
my_height = 100

pos_x = (master_pos_x + (master_width // 2)) - (my_width // 2)
pos_y = (master_pos_y + (master_height // 2)) - (my_height // 2)

geometry = f"{my_width}x{my_height}+{pos_x}+{pos_y}"
self.geometry(geometry)

In order to place our smiley window in the center of the chat window, we need to know three things:

  • The chat window's position on the user's screen
  • The chat window's width and height
  • The smiley window's width and height

We can obtain the chat window's position by calling its winfo_x and winfo_y methods. Its width and height are accessed with winfo_width and winfo_height respectively. Finally, we define the smiley window's size as 100 x 100.

We can then use these numbers to calculate the x and y position to place the window at, which we store as pos_x and pos_y.

The geometry method can control not only the default size of a window, but its position as well. It does this when you pass its argument in the following format: width, x, height, +, x_coordinate, +,  and y_coordinate.

Or, in Python terms:

"{}x{}+{}+{}".format(width, height, x_coordinate, y_coordinate)

We can see this in action in the call to the geometry method of our position_window code, except using the formatted string syntax, which is available in Python version 3.6.

With this method written we can go back and finish off the __init__  method, which is where the bulk of our logic lies:

def __init__(self, master, **kwargs):
...
smilie_files = [file for file in os.listdir(self.smilies_dir) if file.endswith(".png")]

self.smilie_images = []

for file in smilie_files:
full_path = os.path.join(self.smilies_dir, file)
image = tk.PhotoImage(file=full_path)
self.smilie_images.append(image)

This class needs to hold a list of all available smiley images. This will take a few steps to accomplish.

We create a list of the filenames using the listdir method of the os module. This method simply performs a directory listing of the supplied folder. We provide this method our smilies_dir and filter out for files that don't have the .png extension.

In order for this code to function you will need some .png files inside your smilies directory. The ones I have used in my implementation are available on the Open Game Art website at https://opengameart.org/content/mikulka%E2%80%99s-smile. Due to permissive licensing, they are available on this book's GitHub repository as well.

Now that we have the filenames of our smileys, we need to turn them into PhotoImage objects, which Tkinter can utilize.

In order to do this, we loop over our list of smiley files and use the os.path.join method to join the file name to the smilies_dir location. This gives us the full path to our image files, which we can use to create a PhotoImage object. We then append this to our smilie_images list for later use.

With a full list of PhotoImage objects, we can now create Button widgets, which will allow the user to select that particular smiley for inclusion in their message.

The most logical way to display these buttons is in a grid. Since I will be using seven images in my implementation of this application, I will be using a 3 x 3 grid. If you have more smiley images available, feel free to adjust the numbers for a more suitable grid.

As the name suggests, the grid geometry manager makes creating grid layouts very easy, so we will be using this (instead of pack) to put our Button widgets into the window:

for index, file in enumerate(self.smilie_images):
row, col = divmod(index, 3)
button = ttk.Button(self, image=file,
command=lambda s=file: self.insert_smilie(s))
button.grid(row=row, column=col, sticky='nsew')

When adding our smilie buttons to the window, we loop over our list of PhotoImages using the enumerate function, which keeps track of the index within the list. We can use that index to figure out which cell to place the Button in via the divmod function. This function takes a number to be divided as the first argument, and a number to divide that number by as its second. We supply our index value as the to-be-divided number, and 3 (the number of rows and columns in our resultant grid) as the to-be-divided-by number. The divmod function returns a 2-tuple containing the amount of times the first number can be wholly divided by the second, and the remainder.

For example, since the number 4 can be divided by 3 once, with a remainder of 1, the result of divmod(4, 3) will be (1, 1). The result of divmod(6, 3) would be (2, 0) since the number 6 is divided by 3 twice with no remainder.

We can use this tuple to decide the row and column at which to place each Button.

A Button widget is created using the PhotoImage as the image argument to display the smiley. This Button is then put into the window with the grid geometry manager, passing the nsew string to the sticky argument, causing each button to expand as the window's grid expands.

In order to make the window's grid expand when the window changes size, we need to set the weight of each cell to the same value:

for i in range(3):
tk.Grid.columnconfigure(self, i, weight=1)
tk.Grid.rowconfigure(self, i, weight=1)

To adjust the weight of each cell, we need to use the columnconfigure and rowconfigure methods of the Grid class itself. Since we have 3 columns and 3 rows, we loop over the range of 3 and pass each value to this method, along with weight=1 to set each to the same weight.

With that, our __init__ method is finished. Now we need to write the method that each smilie button will call to add its image to the text_area of the chat window:

def insert_smilie(self, smilie):
self.master.add_smilie(smilie)
self.destroy()

This method is provided the PhotoImage of the clicked button, and simply passes this along to the add_smilie method of the master widget, which will be our ChatWindow.

That completes all of the functionality needed for our SmilieSelect class.

To test our SmilieSelect window, all we need to do now is add an if __name__ == "__main__" block:

if __name__ == '__main__':
w = tk.Tk()
s = SmilieSelect(w)
w.mainloop()

Give your smilieselect.py file a run and check out your grid of available smileys. Note that the buttons will not function, since the plain Tk widget we are using as its master has no add_smilie method:

Now we just need to link this window back to our ChatWindow to start sending smileys in our messages. Go back to your chatwindow.py file and create the add_smilie method:

def __init__(self, master, friend_name, friend_avatar, **kwargs):
...
self.text_area.smilies = []

def add_smilie(self, smilie):
smilie_index = self.text_area.index(self.text_area.image_create(tk.END, image=smilie))
self.text_area.smilies.append((smilie_index, smilie))

In order to link smileys back to our text_area, we will need to add an attribute to it named smilies. This will be a list of 2-tuples containing the index at which the image should be added and the PhotoImage instance that was used.

Of course, we also need a way to bring up the smiley window from within the ChatWindow:

from smilieselect import SmilieSelect

def __init__(self, master, friend_name, friend_avatar, **kwargs):
...
self.smilies_image = tk.PhotoImage(file="smilies/mikulka-smile-cool.png")
self.smilie_button = ttk.Button(self.bottom_frame,
image=self.smilies_image, command=self.smilie_chooser,
style="smilie.TButton")
...
self.bottom_frame.pack(side=tk.BOTTOM, fill=tk.X)
self.smilie_button.pack(side=tk.LEFT, pady=5)
self.text_area.pack(side=tk.LEFT, fill=tk.X, expand=1, pady=5)
self.send_button.pack(side=tk.RIGHT, pady=5)

def smilie_chooser(self, event=None):
SmilieSelect(self)

The SmilieSelect class is imported and a method named smilie_chooser is created, which simply instantiates the class.

In our __init__ method, we make a button that will spawn this window. The button will feature one of the smileys itself, so a PhotoImage with one of them is created and passed to the image argument of the Button. This Button is then packed on the left of the text_area.

Running this file will show us a message window that allows us to pick a smiley from the menu and have it appear in our message. However, when we send the message it will disappear:

To get the image to appear in our messages_area, we will have to adjust our send_message method:

def send_message(self, event=None):
message = self.text_area.get(1.0, tk.END)

if message.strip() or len(self.text_area.smilies):
message = "Me: " + message
self.messages_area.configure(state='normal')
self.messages_area.insert(tk.END, message)

The first change we need is to check for the presence of any smileys in our message, since there's no need to add images if none were selected. The logic for adding the text content of our message is unchanged.

If we do have some smileys in the message then we will have two pieces of information about each—the index at which they were inserted and the PhotoImage instance that represents them. Since we never know how many lines will already be in our messages_area, we can ignore the line number portion of the index and just get the character number from it.

To know where to place the image in our messages_area, we need to get the last line number from it. We can then join that to the character number from the smiley index and create the index at which we need to insert the image:

if len(self.text_area.smilies):
last_line_no = self.messages_area.index(tk.END)
last_line_no = str(last_line_no).split('.')[0]
last_line_no = str(int(last_line_no) - 2)

The line with our previous message in it is calculated by getting the index of the end of the Text widget and taking off 2 from the line number to accommodate for newline characters:

for index, file in self.text_area.smilies:
char_index = str(index).split('.')[1]
char_index = str(int(char_index) + 4)
smilile_index = last_line_no + '.' + char_index
self.messages_area.image_create(smilile_index, image=file)

self.text_area.smilies = []

Now that we have the correct line number, we can loop through any smileys added by the submitted message and find their character number. Note that we need to add 4 to this to take into account the Me: which has been added to the beginning of our message.

This character number is then joined to our calculated line number with a full stop character to create a Tkinter index, and we give this to the image_create method to add our smiley image.

Once we have added all of the chosen smileys to the messages_area, we need to clear the smilies attribute of our text_area, so that the smileys that were added in this message are not sent along with the next message as well:

  self.text_area.delete(1.0, tk.END)

return "break"

The method is finished off in the same way as before, by removing the contents of the text_area and returning the break string to prevent any default behaviors.

Give this file a run now and see your messages get sent along with any smileys you included!

That is where we will finish off this chapter. We have now laid out some components of a chat system that we are ready to connect to the internet. Soon, our application will send its messages to a server as well as update the window itself, and our friends will be able to see our messages and talk back, too!

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

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