Three people have just fallen past that window
When writing GUI code, we often want to group widgets. This typically happens when we want these widgets to appear together in our GUI and/or when these widgets, as a group, have a role in the program. We can think of this as creating our own widget. We will take our game screen example from the previous section as an example. The four directional buttons could be grouped together into a single “controls” widget that can be dealt with separately. To do this, we will write a class that inherits from Frame
and contains our directional buttons.
import tkinter as tk
class Controls(tk.Frame):
"""Widget containing four directional buttons."""
BUTTON_WIDTH = 10
def __init__(self, parent):
"""Set up the four directional buttons in the frame.
Parameters:
parent (Tk): Window in which this widget is to be placed.
"""
super().__init__(parent)
upBtn = tk.Button(self, text="UP", width=self.BUTTON_WIDTH,
command=self.push_up)
upBtn.pack(side=tk.TOP)
leftBtn = tk.Button(self, text="LEFT", width=self.BUTTON_WIDTH,
command=self.push_left)
leftBtn.pack(side=tk.LEFT)
downBtn = tk.Button(self, text="DOWN", width=self.BUTTON_WIDTH,
command=self.push_down)
downBtn.pack(side=tk.LEFT)
rightBtn = tk.Button(self, text="RIGHT", width=self.BUTTON_WIDTH,
command=self.push_right)
rightBtn.pack(side=tk.LEFT)
def push_up(self):
print("UP")
def push_down(self):
print("DOWN")
def push_left(self):
print("LEFT")
def push_right(self):
print("RIGHT")
class GameApp(object):
"""Basic game window design."""
def __init__(self, master):
"""Initialise the game window layout
with four directional buttons widget and a screen.
Parameters:
master (Tk): Main window for application.
"""
master.title("Buttons are good")
controls = Controls(master)
controls.pack(side=tk.LEFT)
screen = tk.Label(master, text="screen", bg="light blue",
width=38, height=16)
screen.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
root = tk.Tk()
app = GameApp(root)
root.mainloop()
The first class we wrote is our Controls
class, to represent the directional buttons widget. This class inherits from Frame
, so the __init__
method calls Super().__init__
with the argument parent
. Conventionally parent
is the name used for the widget that will contain this one, i.e. its parent. In this case, the Controls
widget will be contained inside the root Tk object. We might consider another application where the Controls
widget is packed inside a different container widget, such as another Frame
.
The __init__
method then creates our four buttons. Again, as we are not expecting to update or get information from our buttons we do not need to store them in class variables. We want to pack the buttons into the Controls
instance (recall that Controls
is just a specialisation of a Frame
, so we can pack widgets into it). To do this, we set the parent widget of the buttons as self
, the Controls
object.
The Controls
class contains four more methods, which are used as the callbacks for the buttons. By writing these methods in the Controls
class, we make it clear that they will only be used by the four buttons contained in this widget. It also gives these methods access to any information in the Controls
object, which might be necessary if the buttons needed to do more complex tasks.
In the GameApp
class we now only need to create two widgets, our Controls
widget and the Label
widget. (Where we are using the Label
to represent the game’s screen.)
We can now save our code as game_screen_classes.py
and have a look.
Writing the code this way, where we group sets of related widgets into classes, makes the code look very simple, and it is. It is easy code to read, debug and modify, which is why this method is preferred.
As well as Frame
, tkInter includes other blank container widgets to arrange widgets inside. The Toplevel
widget represents a new blank window. This can be useful for creating dialog boxes within an application, by creating a class which inherits from Toplevel
.
We have already seen the Label
, Button
and Entry
widgets and now we consider the Canvas
widget. The Canvas
widget represents a space for drawing objects on the screen, such as lines, ovals and polygons. The following example shows the use of some of the drawing methods available.
import tkinter as tk
class CanvasApp(object):
def __init__(self, master):
master.title("Canvas")
self._canvas = tk.Canvas(master, bg="white", width=500, height=500)
self._canvas.pack(side=tk.TOP, expand=True, fill=tk.BOTH)
frame = tk.Frame(master)
drawBtn = tk.Button(frame, text="Draw", command=self.draw)
drawBtn.pack(side=tk.LEFT)
dltBtn = tk.Button(frame, text="Delete", command=self.delete)
dltBtn.pack(side=tk.LEFT)
frame.pack(side=tk.TOP)
def draw(self):
# Example 1
self._canvas.create_line([(0, 0), (150, 50), (200, 200)])
# Example 2
self._canvas.create_polygon([(300, 50), (330, 80), (300, 140), (270, 80)])
# Example 3
self._canvas.create_oval(250, 200, 300, 300, outline="red", width=5)
# Example 4
self._canvas.create_rectangle(350, 350, 431, 400, fill="blue")
# Example 5
centre = (100, 400)
radius = 50
self._canvas.create_oval(centre[0]-radius, centre[1]-radius,
centre[0]+radius, centre[1]+radius)
self._canvas.create_rectangle(centre[0]-radius, centre[1]-radius,
centre[0]+radius, centre[1]+radius)
def delete(self):
self._canvas.delete(tk.ALL)
root = tk.Tk()
app = CanvasApp(root)
root.mainloop()
This code is available as canvas.py
. When we run the script, we see a blank white screen with two buttons. When we click on the “Draw” button, we see this result:
First, the CanvasApp
creates and packs a Canvas
with a white background, and a width and height of 500 pixels. Each of the drawing methods of the Canvas
use a coordinate system in pixels, where (0, 0)
is the top-left, the positive x
direction is to the right, and the positive y
direction is down the screen. Note that this is different from the standard Cartesian coordinate system where the positive y
direction is up. The coordinates of the bottom-right corner of the canvas are the same as the width and height of the canvas.
Note, we did not create a separate widget for the two control buttons, Draw
and Delete
. As there are only two buttons, this example did not call for the creation of a new widget.
The first object drawn on the canvas is a line. The create_line
method takes a sequence of x and y coordinates, and draws a straight line between each pair of coordinates. The example above shows a line drawn from (0, 0)
in the top-left corner, through (150, 50)
, ending at (200, 200)
. create_line
can also be used to draw a curve or freehand line, by drawing straight lines with many coordinates placed very close together. This does not make an exact curve, but it will appear very close to a curve when viewed on a monitor.
The second object drawn here is a polygon. Similar to drawing a line, it takes a sequence of coordinates. The polygon is then drawn with these coordinates as the vertices, and then the polygon is filled in (coloured black by default). This example shows a kite shape, drawn in the order of the coordinates specified. Note that unlike the create_line
method, create_polygon
will join the last pair of coordinates back to the first (which is necessary to draw a closed polygon).
Example 3 above draws a red oval. Here, we specify four coordinates, representing the leftmost x
-coordinate, the uppermost y
-coordinate, the rightmost x
-coordinate, and the lowermost y
-coordinate. Another way to visualise the drawing of the oval is to imagine a rectangle drawn around it, such as the square and circle arrangement in the above image. When drawing the oval, we specify the top-left and bottom-right corners of the surrounding rectangle, and the oval is drawn inside. Here, we also make use of two optional arguments, outline
and width
. Each of the Canvas
“create” methods can accept optional arguments to specify other details, such as colour and line width. outline
and width
are used to specify the colour and width of the oval’s outline.
Example 4 draws a blue rectangle. To do this, we specify the coordinates of the top-left and bottom-right corners of the rectangle. Here we have also used another optional argument, fill
, to specify the internal colour of the rectangle. By default, ovals and rectangles are transparent. Notice though that the rectangle still has a black border, but it can be hard to spot, because it is only one pixel wide. To set the entire rectangle to the same colour, we would either set the outline
to the same colour, or set the outline width
to 0 to remove the outline entirely.
In the final example, we draw a circle by specifying a centre and radius, then using simple arithmetic to create an oval at the desired coordinates. We also draw a square at the same coordinates. If we were writing an application that involved drawing a lot of circles, we might consider writing a method that takes a pair of coordinates and a radius, and draws a circle centred at those coordinates. This would make the drawing part of the application much easier to write.
The Canvas
has a delete
method, which can be used to remove drawings from the screen. Clicking on the “Delete” button will call self._canvas.delete(tk.ALL)
, which deletes all the drawings. Clicking on “Draw” will bring them back. Note that clicking on “Draw” multiple times will actually draw a complete set of new objects on the canvas each time, but because they all overlap, we only see one set of drawings.
We will continue with the game example and replace the screen label with a Canvas
. In doing this, we will need to think about the logic of the application. What will be the initial setup of the screen area? What information about the game will need to be stored in the program? How will user interactions change the internal state, and how do we show this change in the screen?
In this simple example, the game involves a circle shown on the screen, initially in the centre. Pressing the four buttons will move the circle around the screen, drawing lines behind it to form a path. To do this, the application will need to keep track of where the circle is, and all the coordinates of where it has been. When updating a figure or drawing on a Canvas
widget, it can often be easiest to clear the entire canvas, then redraw everything from scratch.
Now that we have investigated the functionality required for the canvas, we realise that each of the buttons needs to take a number of steps to complete its task. Because this all seems to be the work of the Canvas
, we will customise Canvas
to have extra methods and attributes to fit our situation. That is, we will write a subclass of Canvas
which has this functionality.
class Screen(tk.Canvas):
"""A customised Canvas that can move a circle and draw a path."""
SIZE = 230 # The size of the screen.
RADIUS = 5 # The radius of the circle
MOVE = 10 # The amount to move on each step
def __init__(self, parent):
"""Create and initialise a screen.
Parameters:
parent (Tk): Window in which this screen is to be placed.
"""
super().__init__(parent, bg="light blue", width=self.SIZE,
height=self.SIZE)
# Start in the centre, without any points in the path.
self._x, self._y = (self.SIZE / 2, self.SIZE / 2)
self._path = [(self._x, self._y)]
self._redraw()
def _redraw(self):
"""Redraw the game screen after a move."""
self.delete(tk.ALL)
coords = (self._x - self.RADIUS,
self._y - self.RADIUS,
self._x + self.RADIUS,
self._y + self.RADIUS)
self.create_oval(coords, fill="black", width=0)
if len(self._path) > 1:
self.create_line(self._path)
def _move(self, dx, dy):
"""Move the circle by a given amount.
Parameters:
dx (int): Amount to move in the x-coordinate.
dy (int): Amount to move in the y-coordinate.
"""
self._x += dx
self._y += dy
self._path.append((self._x, self._y))
self._redraw()
def move_up(self):
"""Move the circle up."""
self._move(0, -self.MOVE)
def move_down(self):
"""Move the circle down."""
self._move(0, self.MOVE)
def move_left(self):
"""Move the circle left."""
self._move(-self.MOVE, 0)
def move_right(self):
"""Move the circle right."""
self._move(self.MOVE, 0)
This Screen
will store the current coordinates of the circle, and a list of coordinates it has been to. The private method _redraw
will delete and redraw the circle and path on the screen, which is done at the beginning and after every movement. The methods move_up
, move_down
, move_left
and move_right
will perform the actions required by the four buttons. Note that, as Screen
inherits from Canvas
, it can make method calls such as self.delete(tk.ALL)
and self.create_oval
to draw on the canvas. We can now modify the Controls
and GameApp
classes to use a Screen
:
class Controls(tk.Frame):
"""Widget containing four directional buttons."""
BUTTON_WIDTH = 10
def __init__(self, parent):
"""Set up the four directional buttons in the frame.
Parameters:
parent (Tk): Window in which this widget is to be placed.
screen (Screen): Screen which has the movement methods.
"""
super().__init__(parent)
upBtn = tk.Button(self, text="UP", width=self.BUTTON_WIDTH,
command=screen.move_up)
upBtn.pack(side=tk.TOP)
leftBtn = tk.Button(self, text="LEFT", width=self.BUTTON_WIDTH,
command=screen.move_left)
leftBtn.pack(side=tk.LEFT)
downBtn = tk.Button(self, text="DOWN", width=self.BUTTON_WIDTH,
command=screen.move_down)
downBtn.pack(side=tk.LEFT)
rightBtn = tk.Button(self, text="RIGHT", width=self.BUTTON_WIDTH,
command=screen.move_right)
rightBtn.pack(side=tk.LEFT)
class GameApp(object):
"""Basic game window design."""
def __init__(self, master):
"""Initialise the game window layout with
four directional buttons and a screen.
Parameters:
master (Tk): Main window for application.
"""
master.title("Game")
screen = Screen(master)
controls = Controls(master, screen)
controls.pack(side=tk.LEFT)
screen.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
The updated Controls
class now also requires the Screen
object, and it uses this to access the four methods needed as Button
callbacks. The GameApp
is also modified to create a Screen
instead of a Label
. This code is available to download as game_canvas.py
. When we interact with this program, we see this result:
Controls
and Screen
The constructor of the Controls
class above requires an object called screen
, and it uses this to access move_up
, move_left
, move_down
and move_right
, which should be methods that perform the relevant actions. For this program, there is no problem, but what if we wanted to modify the program? There are several potential issues to watch out for.
If we modified (or replaced) the Screen
and renamed the four movement methods, we would need to modify the code in Controls
. Worse, if the methods were moved into separate classes, or made into top-level functions, we could no longer pass in a single object to Controls
which can access all the methods. This is still not a major issue in an application this small, but in a larger application, many other classes could be dependent on the Screen
, which would cause problems when we tried to change the Screen
. We could also imagine a situation where multiple Controls
widgets were needed, each requiring different method names.
To fix these issues, we can modify the Controls
class to require the four callback functions directly; then the internals of Controls
would still work, even if the methods were renamed or moved. We then have this result:
class Controls(tk.Frame):
BUTTON_WIDTH = 10
def __init__(self, parent, up, down, left right):
super().__init__(parent)
upBtn = tk.Button(self, text="UP", width=self.BUTTON_WIDTH,
command=up)
upBtn.pack(side=tk.TOP)
leftBtn = tk.Button(self, text="LEFT", width=self.BUTTON_WIDTH,
command=left)
leftBtn.pack(side=tk.LEFT)
downBtn = tk.Button(self, text="DOWN", width=self.BUTTON_WIDTH,
command=down)
downBtn.pack(side=tk.LEFT)
rightBtn = Button(self, text="RIGHT", width=self.BUTTON_WIDTH,
command=right)
rightBtn.pack(side=tk.LEFT)
class GameApp(object):
def __init__(self, master):
master.title("Game")
screen = Screen(master)
controls = Controls(master, screen.move_up, screen.move_down,
screen.move_left, screen.move_right)
controls.pack(side=tk.LEFT)
screen.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
The GameApp
is also modified to access the respective methods of Screen
. In a sense, we have now completely separated the Controls
and Screen
classes, and the interaction between them is defined entirely by the GameApp
class. In a larger application, moving all of this control to the one class will make it much easier to treat all the other classes as distinct from each other.
The implementation for this code is provided in game_canvas_sep_controls.py
.
The program we wrote above feels difficult to use. In particular, the use of the four buttons is unintuitive. We would like the user to interact directly with the canvas using mouse gestures. tkInter
provides a means of doing this using events. An event is a trigger which occurs when the user makes an action with the mouse or keyboard. For example, when the user makes a left click, tkInter
will trigger a <Button-1>
event, and when the user drags the mouse with a left click, tkInter
will trigger a <B1-Motion>
event. To use these types of event, we must bind
the event to a callback function, similar to a callback function for a Button
. Widgets have a method bind which takes an event name (as a string) and a callback function. The function must take one argument, typically called event
, which contains information on the trigger that occurred. For example, if the mouse was clicked or moved, the event
would contain the coordinates of the cursor.
As an example, we will modify the game screen example so that clicking and dragging will move the circle. We should again consider the application logic: what should happen when the mouse is pressed or released? What should happen when the pressed mouse is dragged slightly? If the mouse is pressed, we should look to see if we have clicked on the circle, and if so mark it as being “selected”. When the mouse is dragged, we should see if the circle has been selected, and if so move it accordingly. When the mouse is released, then we mark the circle as “deselected”.
import tkinter as tk
import math
class Screen(tk.Canvas):
""Customised Canvas that can move a circle and draw a path."""
SIZE = 230 # The size of the screen.
RADIUS = 5 # The radius of the circle
def __init__(self, parent):
"""Create and initialise a screen.
Parameters:
parent (Tk): Window in which this screen is to be placed.
"""
super().__init__(parent, bg="light blue", width=self.SIZE,
height=self.SIZE)
# Start in the centre, without any points in the path.
self._x, self._y = (self.SIZE / 2, self.SIZE / 2)
self._path = [(self._x, self._y)]
self._circle_select = False # Is the circle selected.
self.bind("<Button-1>", self._click_event)
self.bind("<B1-Motion>", self._move_event)
self._redraw()
def _redraw(self):
"""Redraw the game screen after a move."""
self.delete(tk.ALL)
coords = (self._x - self.RADIUS,
self._y - self.RADIUS,
self._x + self.RADIUS,
self._y + self.RADIUS)
self.create_oval(coords, fill="black", width=0)
if len(self._path) > 1:
self.create_line(self._path)
def _move(self, dx, dy):
"""Move the circle by a given amount.
Parameters:
dx (int): Amount to move in the x-coordinate.
dy (int): Amount to move in the y-coordinate.
"""
self._x += dx
self._y += dy
self._path.append((self._x, self._y))
self._redraw()
def _click_event(self, event):
"""Sets whether the circle is selected.
Parameters:
event (tk.Event): Selection event with mouse coordinates.
"""
dist_to_circle = math.hypot(event.x - self._x, event.y - self._y)
self._circle_select = (dist_to_circle < self.RADIUS)
def _move_event(self, event):
"""Calculates the distance to move the circle so that it moves
With the mouse.
Parameters:
event (tk.Event): Drag event with the new mouse coordinates.
"""
if self._circle_select:
dx = event.x - self._x
dy = event.y - self._y
self._move(dx, dy)
class GameApp(object):
"""Basic game to move a circle around a screen with the mouse."""
def __init__(self, master):
"""Initialise the game screen.
Parameters:
master (Tk): Main window for application.
"""
screen = Screen(master)
screen.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
root = tk.Tk()
app = GameApp(root)
root.mainloop()
We have removed the buttons as they are no longer needed, we will now have full control over the circle with the mouse. The first big change to notice is the addition of three new lines in the Screen.__init__
method. The first line is simply a boolean value to tell if the circle is selected. The next two lines are calling the bind
method. bind
takes a string as the first argument for what type of event we are interested in. The second argument is the method to call when that event occurs. The two events we are interested in is Button-1
(If the left mouse button is clicked) and B1-Motion
(If the mouse is moved while the left mouse button is held down).
Next, we have removed the four move methods of Screen
and replaced them with two new methods. These methods are the methods called by our two bound events. Notice how each takes an argument event
. This argument is the event. It is a class of Python and contains information about the event that occurred. We can use event
to access the x, y location of the mouse when the event occurred by accessing the class variable. The _click_event
method sets self._circle_select
to a boolean value representing whether or not the mouse is inside the circle. The _move_event
method calculates the distance between where the circle is and where the mouse is so that we can move the circle to the mouse’s location.
We can now save our code as game.py
and have a look at our new game.
We can now move the circle any way that we wish at any time just by simply moving the mouse.
Here is a table of some of the events that can be used. (Sourced from http://www.python-course.eu/tkinter_events_binds.php)
Events | Description |
---|---|
<Button-1> |
A mouse button is pressed over the widget. Button 1 is the leftmost button, button 2 is the middle button (where available), and button 3 the rightmost button. When a mouse button is pressed down over a widget, Tkinter will automatically “grab” the mouse pointer, and mouse events will then be sent to the current widget as long as the mouse button is held down. The current position of the mouse pointer (relative to the widget) is provided in the x and y members of the event object passed to the callback. |
<B1-Motion> |
The mouse is moved, with mouse button 1 being held down (use B2 for the middle button, B3 for the right button). The current position of the mouse pointer is provided in the x and y members of the event object passed to the callback. |
<Return> |
The user pressed the Enter key. Virtually all keys on the keyboard can be bound to. For an ordinary 102-key PC-style keyboard, the special keys are Cancel (the Break key), BackSpace, Tab, Return(the Enter key), Shift_L (any Shift key), Control_L (any Control key), Alt_L (any Alt key), Pause, Caps_Lock, Escape, Prior (Page Up), Next (Page Down), End, Home, Left, Up, Right, Down, Print, Insert, Delete, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, Num_Lock, and Scroll_Lock. |
<Key> |
The user pressed any key. The key is provided in the char member of the event object passed to the callback (this is an empty string for special keys). |
a |
The user typed an “a”. Most printable characters can be used as is. The exceptions are space (<space> ) and less than (<less> ). Note that 1 is a keyboard binding, while <1> is a button binding. |
<Configure> |
The widget changed size (or location, on some platforms). The new size is provided in the width and height attributes of the event object passed to the callback. |
Some of the attributes of the event
class
| Attribute | Description |
| —————— | ———————————————————— |
| x
, y
| The current mouse position, in pixels. |
| x_root
, y_root
| The current mouse position relative to the upper left corner of the screen, in pixels. |
| char
| The character code (keyboard events only), as a string. |
| width
, height
| The new size of the widget, in pixels (Configure events only). |
| type
| The event type. |
In this example, we will create a simple text editor application. This text editor will be able to open, edit, save and close files. We will also keep track of whether the text file has been edited without saving, and prompt the user to save before exiting.
We will introduce a new widget, Text
, to represent the text editing area. This widget has three methods which will be useful in this application: text.get(1.0, tk.END)
will retrieve the text which has been entered into the widget, text.delete(1.0, tk.END)
will remove all the text from the widget, and text.insert(tk.END, string)
will insert a string
of text into the widget. There are many other types of widgets not described here, to represent other GUI elements, such as lists of elements, scroll bars, check boxes, and radio buttons. Each has its own set of methods which are useful in manipulating the information that type of widget stores.
To open and save files, we will use the tkInter filedialog
module. This module is imported using the other type of import
that is performed as follows: from module import submodule_or_class
This comes with two methods, askopenfilename
for choosing a file to open, and asksaveasfilename
for choosing a file to save to. These functions will open a dialog box prompting the user to choose a file, and then return the file name. The appearance of the dialog boxes is determined by the operating system (that is, it is a native dialog box), so the user will already be familiar with using the dialog without requiring effort from the programmer.
We will also need to display short dialog message boxes. When the user tries to abandon a file without saving changes, we will prompt them to save. We will add this functionality to an “Exit” menu item, as well as when the user closes the window using the “X” button on the top of the window. We will also create a simple “About” dialog box, giving information about the text editor when the user asks for it. These dialog boxes will be modal; this means that the user will not be able to continue using the application until they respond to the dialog. We will use tkInter’s messagebox
module. This comes with several functions for showing different types of dialog such as errors, warnings, “Yes/No” or “Retry/Cancel” questions. They can be customised to show different titles, messages, icons and buttons, and will return a value based on the user’s button choice.
We will also introduce the Menu
widget, for adding the native menus on the top of the window. To make a set of menus, we create a Menu
object to represent the menu bar. We then add more Menu
objects to represent each of the drop-down menus. To add individual menu items, we give them a label and assign a callback command
, just as we create callbacks for buttons.
We will now write the text editor application. As well as constructing the GUI, we will need to store the filename of the document being edited. We will also store a boolean flag indicating whether or not the file has been edited without saving. When we attempt to close the file or open a new one, we will check to see if the user wishes to save their work.
import tkinter as tk
from tkinter import filedialog
from tkinter import messagebox
class TextEditor(object) :
"""Simple text editing application."""
def __init__(self, master) :
""" Create the screen for the text editor
Parameters:
master (Tk): Window in which this application is to be displayed.
"""
self._master = master
master.title("Text Editor")
self._filename = ''
self._is_edited = False
self._text = tk.Text(master)
self._text.pack(side=tk.TOP, expand=True, fill=tk.BOTH)
self._text.bind("<Key>", self._set_edited)
# Create the menu.
menubar = tk.Menu(master)
master.config(menu=menubar)
filemenu = tk.Menu(menubar)
menubar.add_cascade(label="File", menu=filemenu)
filemenu.add_command(label="New", command=self.new)
filemenu.add_command(label="Open", command=self.open_file)
filemenu.add_command(label="Save", command=self.save)
filemenu.add_command(label="Save As...", command=self.save_as)
filemenu.add_command(label="Exit", command=self.close)
helpmenu = tk.Menu(menubar)
menubar.add_cascade(label="Help", menu=helpmenu)
helpmenu.add_command(label="About", command=self.about)
master.protocol("WM_DELETE_WINDOW", self.close)
def new(self) :
"""Create a new text file."""
if self._can_close() :
# Forget about the currently open file.
self._text.delete(1.0, tk.END)
self._filename = ''
self._master.title("Text Editor")
self._is_edited = False
def open_file(self) :
"""Open a text file."""
if not self._can_close() :
return
self._filename = filedialog.askopenfilename()
if self._filename :
f = open(self._filename, "r")
text = f.read()
f.close()
self._text.delete(1.0, tk.END)
self._text.insert(tk.END, text)
self._master.title("Text Editor: {0}".format(self._filename))
self._is_edited = False
def save(self) :
""" Save the contents of the text in memory to the file."""
if not self._filename :
self._filename = filedialog.asksaveasfilename()
self._perform_save()
def save_as(self) :
""" Allow saving the contents of the text in memory to a new file."""
filename = filedialog.asksaveasfilename()
if filename :
self._filename = filename
self._perform_save()
def close(self) :
"""Exit the text editor application."""
if self._can_close() :
self._master.destroy()
def about(self) :
"""Generate an 'About' dialog."""
messagebox.showinfo(title="Text Editor", message="A simple text editor")
def _set_edited(self, event) :
"""Record that the text file has been edited.
Parameters:
event (Tk.Event): Record that text has been edited if any event
occurs in the text.
"""
self._is_edited = True
def _perform_save(self) :
"""Store the contents in memory into a file."""
if self._filename:
self._master.title("Text Editor: {0}".format(self._filename))
f = open(self._filename, "w")
text = self._text.get(1.0, tk.END)[:-1]
f.write(text)
f.close()
self._is_edited = False
def _can_close(self) :
""" Ask the user if they want to save the changed text to a file.
Returns:
(bool) True if it is safe to close the file;
False if the user wants to continue editing.
"""
can_close = True
if self._is_edited :
reply = messagebox.askquestion(type=messagebox.YESNOCANCEL,
title="File not saved!",
message="Would you like to save this file?")
if reply == messagebox.YES :
self.save() # can_close is already True.
# elif reply == messagebox.NO :
# can_close is already True.
elif reply == messagebox.CANCEL :
can_close = False
# else file is not edited, so can close.
return can_close
if __name__ == "__main__" :
root = tk.Tk()
TextEditor(root)
root.mainloop()
First, we will look at the __init__
method. We create and pack a Text
, and bind the "<Key>"
event to a method which will set the _is_edited
flag to True whenever a key is pressed. The statement menubar = tk.Menu(master)
will create a menu bar for the master
window, and in the line below, we configure the master
to display this menu bar. To create menus, tk.Menu(menubar)
will create an empty menu list, and menubar.add_cascade
will insert it onto the menu bar with the given text label. To add a menu item, we use the method add_command
, and pass in a text label to display, and a callback function, just as with Button
objects. If, for example, we wanted to create a sub-menu to the “File” menu, we would create tk.Menu(filemenu)
and call filemenu.add_cascade
; menu items and further sub-menus can then be added into this menu. The statement master.protocol("WM_DELETE_WINDOW", self.close)
is similar to a binding; it allows us to set an action to perform when a particular event occurs. In this case, "WM_DELETE_WINDOW"
is the event represented by closing the window (using the “X” icon), and we set the self.close
method as a callback. Note that the same callback is also assigned to the “File -> Exit” menu item, so either of these actions will do the same thing.
In the new
, open
and close
methods, we need to check if the user has edited the file and wishes to save. To do this, we use the _can_close
helper method to ask this question, save if necessary, and return False
if the user wishes to continue editing the file. Since the save
and save_as
functionality is similar (they both save the text to a file), we abstract this to the _perform_save
method.
This program is available to download as text_editor.py
. When we run the application, we now see this: