Canvas, Drawing Graphics Part 1 / tkinter 18

by Alex

Download lesson code from GitLab: https://gitlab.com/PythonRu/tkinter-uroki

The previous tutorials focused on the standard Tkinter widget. However, the Canvas widget has been left out of the spotlight. The reason is that it provides a lot of graphical possibilities and deserves a separate consideration. Canvas is a rectangular area in which not only text or geometric shapes such as lines, rectangles or ovals can be displayed, but also other Tkinter widgets. All nested objects are called Canvas elements, and each has its own identifier with which they can be manipulated even before they are displayed. Let’s look at methods of the Canvas class using real-world examples, which will help familiarize you with common patterns that will help you create applications in the future.

Understanding the coordinate system

To draw graphical elements on a canvas, you need to denote their position using a coordinate system. Since Canvas is a two-dimensional area, points will be denoted by horizontal and vertical axis coordinates – the traditional x and y, respectively. Using the example of a simple application, it is easy to depict exactly how to position these points in relation to the base of the coordinate system, which is in the upper left corner of the canvas area. The following program contains an empty canvas as well as a marker that shows the position of the cursor on it. You can move the cursor and see what position it is in. This clearly shows how the x and y coordinates change depending on the position of the cursor:

import tkinter as tk

class App(tk.Tk):
   def __init__(self):
       super().__init__()
        self.title("Basic canvas")

        self.canvas = tk.Canvas(self, bg="white")
        self.label = tk.Label(self)
        self.canvas.bind("", self.mouse_motion)

        self.canvas.pack()
        self.label.pack()

   def mouse_motion(self, event):
        x, y = event.x, event.y
        text = "cursor position: ({}, {})".format(x, y)
        self.label.config(text=text)

if __name__ == "__main__":
    app = App()
    app.mainloop()

How the coordinate system works

A Canvas instance is created similar to any other Tkinter widget. It receives the parent container as well as all the settings in the form of keywords:

def __init__(self):
   # ...
    self.canvas = tk.Canvas(self, bg="white")
    self.label = tk.Label(self)
    self.canvas.bind("", self.mouse_motion)

The following screenshot shows a point made up of perpendicular projections of two axes:

  • The x coordinate corresponds to the distance on the horizontal axis and increases as you move from left to right;
  • The y coordinate corresponds to the distance on the vertical axis and increases as you move from bottom to top;
Canvas, Drawing Graphics Part 1 / tkinter 18

You may notice that these coordinates correspond exactly to the x and y attributes of the event instance that was passed to the handler:

def mouse_motion(self, event):
    x, y = event.x, event.y
    text = "cursor position: ({}, {})".format(x, y)
    self.label.config(text=text)
That’s because attributes are calculated relative to the widget the event is attached to – in this case it’s a <Motion> sequence.

Canvas Square is also capable of displaying elements with negative values of their coordinates. Depending on the size of the element, it may be partially visible at the left or top border of the canvas. Similarly, if you place an element so that its coordinates lie outside the canvas, part of it will be visible at the right and bottom edges.

Drawing lines and arrows

One of the basic things you can do on the canvas is to draw segments from one point to another. Although there are other ways to draw polygons, the create_line method of the Canvas class offers enough options to understand the basics of displaying elements. In this example, let’s create an application that allows you to draw lines by clicking on the canvas. Each of these will be displayed after two clicks: the first will indicate the beginning of the line and the second will indicate the end of the line. It will also be possible to set certain appearance elements, such as thickness and color:

Canvas, Drawing Graphics Part 1 / tkinter 18

The App class will be responsible for creating an empty canvas and handling mouse clicks. Line information will come from the LineForm class. This approach of separating the component into a separate class will allow to abstract the details of its implementation and focus on working with the Canvas widget. To put it simply, we bypass the LineForm implementation in the following code:

import tkinter as tk

class LineForm(tk.LabelFrame):
   # ...

class App(tk.Tk):
   def __init__(self):
       super().__init__()
        self.title("Basic canvas")
        self.line_start = None
        self.form = LineForm(self)
        self.canvas = tk.Canvas(self, bg="white")
        self.canvas.bind("", self.draw)

        self.form.pack(side=tk.LEFT, padx=10, pady=10)
        self.canvas.pack(side=tk.LEFT)

   def draw(self, event):
        x, y = event.x, event.y
       if not self.line_start:
            self.line_start = (x, y)
       else:
            x_origin, y_origin = self.line_start
            self.line_start = None
            line = (x_origin, y_origin, x, y)
            arrow = self.form.get_arrow()
            color = self.form.get_color()
            width = self.form.get_width()
            self.canvas.create_line(*line, arrow=arrow,
                                    fill=color, width=width)

if __name__ == "__main__":
    app = App()
    app.mainloop()

The entire code can be found in the separate file lesson_18/drawing.py.

How to draw lines in Tkinter

Since we want to handle mouse clicks on the canvas, we associate the draw() method with this event type. We also define a line_start field to keep track of the start position of each line:

def __init__(self):
   # ...
    self.line_start = None
    self.form = LineForm(self)
    self.canvas = tk.Canvas(self, bg="white")
    self.canvas.bind("", self.draw)

The draw() method contains the basic logic of the application. The first click serves to determine the start for each line and does not draw anything. It gets the coordinates from the event object, which is passed to the handler:

def draw(self, event):
    x, y = event.x, event.y
   if not self.line_start:
        self.line_start = (x, y)
   else:
       # ...

If line_start already has a value, we get it and pass the coordinates of the current event to draw the line:

def draw(self, event):
    x, y = event.x, event.y
   if not self.line_start:
       # ...
   else:
        x_origin, y_origin = self.line_start
        self.line_start = None
        line = (x_origin, y_origin, x, y)
        self.canvas.create_line(*line)
        text = "The line is drawn from ({}, {}) to ({}, {})".format(*line)

The canvas.create_line() method takes four arguments, where the first two are the horizontal and vertical coordinates of the start of the line and the second two are its endpoint.

How to get text to appear on canvas

In some cases it is necessary to display text on canvas. There is no need to use an extra widget for this, like the Label. The Canvas class includes a create_text method to display a string that can be manipulated just like any other item on the canvas. It is possible to use the same formatting parameters, which will allow you to set the text style: color, size and font family. In this example, we’ll merge the Entry widget with the content of the canvas text element. And if the former has a standard style, the text on the canvas can be styled:

Canvas, Drawing Graphics Part 1 / tkinter 18

The default text element will display with canvas.create_text() and additional parameters to add the Consolas font family and blue color. The dynamic behavior of the text item is implemented with StringVar. By tracking this Tkinter variable, you can change the content of the element:

import tkinter as tk

class App(tk.tk):
   def __init__(self):
       super().__init__()
        self.title("Canvas text elements")
        self.geometry("300x100")

        self.var = tk.StringVar()
        self.entry = tk.Entry(self, textvariable=self.var)
        self.canvas = tk.Canvas(self, bg="white")

        self.entry.pack(pady=5)
        self.canvas.pack()
        self.update()

        w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
        options = {"font": "courier", "fill": "blue",
                   " activefill": "red"}
        self.text_id = self.canvas.create_text((w / 2, h / 2), **options)
        self.var.trace("w", self.write_text)

   def write_text(self, *args):
        self.canvas.itemconfig(self.text_id, text=self.var.get())

if __name__ == "__main__":
    app = App()
    app.mainloop()

You can familiarize yourself with this program by entering any text in the input box, which will automatically update it on the canvas.

How text output on the canvas works

First, an Entry instance is created with the StringVar variable and the Canvas widget:

    self.var = tk.StringVar()
    self.entry = tk.Entry(self, textvariable=self.var)
    self.canvas = tk.Canvas(self, bg="white")

The widget is then placed using geometry manager Pack method calls. It is important to note that update() has to be called in the root window, thanks to which Tkinter will have to handle all changes, in this case rendering the widgets before the __init__ method will continue execution:

    self.entry.pack(pady=5)
    self.canvas.pack()
    self.update()

This is done because the next step will calculate the web dimensions, and until the geometry manager places the widget, it won’t have any real height and width values. After that you can safely get the dimensions of the web. Since the text needs to be aligned to the center of the web, it is sufficient to divide the width and length values in half. These coordinates will determine the position of the element, and together with the style parameters they must be passed to the create_text() method. The argument-keyword text is a standard parameter, but you can skip it because it will be set dynamically when you change the value of StringVar:

    w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
options = { "font": "courier", "fill": "blue",
                 " activefill": "red" }
    self.text_id = self.canvas.create_text((w/2, h/2), **options)
    self.var.trace("w", self.write_text)

The identifier that create_text() returns will be stored in the text_id field. It will be used in the write_text() method to reference the element. And this method will be called due to the mechanism for tracking the write operation in the var instance. To update the text parameter in the write_text() handler, the canvas.itemconfig() method is called with the item ID as the first argument and the setting as the second argument. In this program, we use the field_id field stored when the App instance was created and the contents of the StringVar using the get() method: The write_text() method is defined so that it can get a variable number of arguments, although they are not needed because the trace() method of Tkinter variables passes them to the callback function. The canvas.create_text() method has many other parameters for changing the appearance of canvas elements.

Placement of text in the upper left corner

The anchor parameter allows you to control the position of the element relative to the coordinates passed as the first argument to canvas.create_text(). The default value is tk.CENTER, which means the text will be centered in those coordinates. If you want to place it in the upper left corner, however, just pass (0, 0) and set tk.NW to anchor, which aligns it to the northwest position of the rectangular area where the text is located:

   # ...
    options = { "font": "courier", "fill": "blue",
                    "activefill": "red", "anchor": tk.NW }
    self.text_id = self.canvas.create_text((0, 0), **options)

This code will provide that result:

Canvas, Drawing Graphics Part 1 / tkinter 18

Line Break

By default, the content of the text item will be rendered as a single line. The parameter width, on the other hand, allows you to set the maximum width of the line. If it is larger, the content will be moved to the new line:

   # ...
    options = { "font": "courier", "fill": "blue",
                    "activefill": "red", "width": 70 }
    self.text_id = self.canvas.create_text((w/2, h/2), **options)

Now if you write Hello World, some of the text will go beyond the specified width and will be moved to a new line:

Canvas, Drawing Graphics Part 1 / tkinter 18

Related Posts

LEAVE A COMMENT