Simple Graphics in Python – Part 2

by SysOps
This entry is part 2 of 3 in the series Simple Graphic in Python

We left off the last time discussing how to use the Text and Entry objects in John Zelle’s graphics.py library from his book and course Python Programming an Introduction to Computer Science. If you missed the first installment check it out here: http://www.randallmorgan.me/blog/simple-graphics-in-python/.

This time we will cover a few topics I skipped over such as drawing Rectangles and Ovals. As we did in the first part, we’ll draw these using points or lines first, and then introduce the library methods. This gives you the opportunity to appreciate what the library does for you and gives you the insight needed if you desire to change a method or extend the library by mucking around in it. From what I’ve seen of John on https://www.youtube.com/results?search_query=John+Zelle and at talks, I suspect he would invite you to muck around and make it your own.

Drawing Rectangles

So let’s start with rectangles. A rectangle is simply a four sided object with right angles. A square is a special rectangle in which all sides are of equal length. However, not all rectangles are not squares. Most are not in fact. So we saw in part one how to draw lines using points, our most basic visual object. So we will skip to drawing rectangles using lines and forego the lower level drawing with points.

Notice that in all of computer science, and indeed in all forms of engineering, complex objects are built up from simpler objects. We started with points and turned them into lines and circles. Now we’ll turn lines into rectangles and polygons and circles into ovals.

Our draw_rect function will take five parameters. First, we need the x and y positions of the upper left corner of the rectangle. Next, we need the width and height. Finally, we need the window to draw the rectangle into. Given these parameters we can draw rectangles of all sizes.

Take a look at the code below. Create a finle called ex-08_01.py and enter the following code:

"""
Prog:   ex-08_01.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library for drawing
        rectangles to the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

def draw_rect(x, y, w, h, win):
    # Build points for the rect's corners
    p1 = Point(x,y)
    p2 = Point(x+w, y)
    p3 = Point(x+w, y+h)
    p4 = Point(x, y+h)
  
    # draw the lines to connect the points
    lines = []
    lines.append(Line(p1, p2))
    lines.append(Line(p2, p3))
    lines.append(Line(p3, p4))
    lines.append(Line(p4, p1))

    # set the line color
    # and draw
    for ln in lines:
        r = randint(0, 255)
        g = randint(0, 255)
        b = randint(0, 255)
        ln.setFill(color_rgb(r, g, b))
        ln.draw(win)
    
    
    
def main():
    win = GraphWin("Rectangles", 400, 400)

    for i in range(0,50):
        x = randint(0, 400)
        y = randint(0, 400)
        w = randint(0,400)
        h = randint(0,400)
        draw_rect(x, y, w, h, win)

    win.getMouse()
    win.close()

main()

In the main() function we create a loop to draw 50 rectangles using our draw_rect() function. We generate the random position and size and call draw_rect() with them also passing along the window we want them drawn on.

In our draw_rect function() we first generate the points that form the four corners of the rectangle. Then we generate a list of lines passing the points as needed to draw the rectangle, being sure to close the rectangle by connecting the first and last points with the final line. In the loop we set the fill color and draw each line. It’s all very straight forward. Looking back at the line function in the first part of this article, you can see how we might draw rectangles using points if we didn’t have a line function.

Ok, the graphics.py library has it’s own Rectangle() method. I’ll demonstrate using it next.

"""
Prog:   ex-08_02.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library for drawing
        rectangles to the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

def main():
    win = GraphWin("Rectangles", 640, 480)
    
    for i in range(1000):
        p1 = Point(randint(0,639), randint(0,479))
        p2 = Point(randint(0,639), randint(0,479))

        r = randint(0,255)
        g = randint(0,255)
        b = randint(0,255)

        rect = Rectangle(p1,p2)
        rect.setOutline(color_rgb(r, g, b))
        rect.draw(win)

    win.getMouse()
    win.close()

main()

Here you can see that the library method takes two points that define the upper left and lower right corners of the rectangle. Our code above then set’s the Outline color to a randomly generated color and draws the rectangle. Nothing special going on here.

There really isn’t a need to include a method for squares unless you need to draw a lot of them. If you do, then it may be worth including a method just for squares. Such a method might look like this:

def square(x, y, side_length, win):
    p1 = Point(x, y)
    p2 = Point(x+side_length, y+side_length)

    r1 = Rectangle(p1, p2)
    r1.draw(win)

If you wanted to keep the same API as the graphics.py library, all of our functions could just return the composed type to the caller so the draw and other methods could be called on it. Then the caller would be in control of calling the draw method where and when it needed to. I changed the API on our functions mainly to differentiate them from the library methods.

Drawing Ovals

OK, next up is Ovals. Just a squares are a special case of rectangles, circles are a special case of ovals. So why didn’t the library call it’s circle method ovals and allow us to simply pass like parameters as we do to get a square from the Rectangle() method? I suspect this inconsistency was mostly for convenience.

Recall how we calculated the (x, y) positions on the circumference of the circle the using cos() and sin() functions? We used a loop that produced a value between 0 and 360 and feed that to the cos and sin functions for the x and y positions. However, because we needed a circle greater than a unit in radius, we had to scale the (x, y) values by the desired radius.

Now ask yourself what happens if the scale factor for x and y are not identical? Would we still get a circle? No, we would get a circle that was squished or expanded in one direction. The would result in an oval being drawn. See the following code in ex-09_01.py:

#!/usr/bin/env python3

"""
Prog:   ex-09_01.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw ovals 
        using points.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from math import *
from random import randint

# Given the center x, center y
# and radius, draw a circle 
# using points.
def oval(cx, cy, rx, ry, color, win):
    
    for i in range(0, 360):
        x = cos(i)*rx + cx;
        y = sin(i)*ry + cy

        p = Point(x,y)
        p.setFill(color)
        p.draw(win)


def main():
    win = GraphWin("Circles", 400, 400)
    win.setBackground(color_rgb(0,0,0))

    for i in range(0, 20):
        rx = randint(10, 200)
        ry = randint(10, 200)

        r = randint(0,255)
        g = randint(0,255)
        b = randint(0,255) 
        oval(200, 200, rx, ry, color_rgb(r, g, b), win)
   
    win.getMouse()
    win.close()


main()

Run the code above. You should see some attractive ovals… Compare the code here to the circle code we used in the first part of the series.

OK, now let’s see how the library does ovals. Run the code below:

"""
Prog:   ex-09_02.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library for drawing
        ovals to the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

def main():
    win = GraphWin("Rectangles", 640, 480)
    
    for i in range(0,50):
        p1 = Point(randint(0,639), randint(0,479))
        p2 = Point(randint(0,639), randint(0,479))

        r = randint(0,255)
        g = randint(0,255)
        b = randint(0,255)

        rect = Oval(p1,p2)
        rect.setOutline(color_rgb(r, g, b))
        rect.draw(win)

    win.getMouse()
    win.close()

main()

Here again we see that the oval method contains the same API calls as the Circle method. The library’s Oval() method takes different parameters than our function. It take a bounding box, a rectangle made of two points that enclose the desired oval. However, it produces an oval none the less.

Polygons

Alright, we’ve covered points, lines, rectangles, squares, circles and ovals, not to mention text. What else can we do with the library? Well, we could build up a collection of points and connect them with lines to build polygons. However, we wont write the code ourselves for this. John’s library already contains a Polygon method and if you’ve made it this far, you can infer how to develop such a function. So I’ll just demonstrate how to use the method from the graphics.py library:


"""
Prog:   ex-10.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library for drawing
        polygons to the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

def main():
    win = GraphWin("Rectangles", 640, 480)
    
    for i in range(10):
        points = []
        for p in range(0, randint(4, 20)):
            points.append(Point(randint(0,639), randint(0,479)))
        
        r = randint(0,255)
        g = randint(0,255)
        b = randint(0,255)

        poly = Polygon(points)
        poly.setOutline(color_rgb(r, g, b))
        poly.draw(win)

    win.getMouse()
    win.close()

main()

Loading Images

The graphics.py library contains a method for loading images. However, the formats support depend on your system and it’s setup. Most systems should handle working with PPM and GIF formats. Some may even handle PNG and TIFF formats. Installing PIL (Photo Imaging Library) on your system may get you additional formats. John’s graphics.py library uses the TK or more formally Tkinter library under the hood. graphics.py is really just a lite weight wrapper to make using Tk easier. So if you can get Tk to support a new image format, you can probably get the graphics.py library to support it.

All images support the generic methods of move(), draw(), undraw(), and clone(). In addition there is the Image() method for creating an Image object. You need to pass it an anchor point and a filename. The constructed image will be centered at the given anchor point. You may also call this method passing the width and height along with the filename.

You can call the getAnchor() to get the Point where the image is anchored. Calling getWidth() and getHeight() do exactly as you would expect and return the width and height of the image respectively.

A fun and functional method is the getPixel method that let’s us retrieve the pixel in the image at location (x, y). This method returns the color as a list of (r,g,b) color values. Note that the pixel location is relative to the image it’s self and must be inside the image.

The getPixel method can help us do some pretty cool things when paired with the setPixel() method. This method takes a color_rgb() value.

Lastly, the library can save images to a file. The format saved is determined by the filename extension given. You can perform the save operation by calling the save() method.

Now let us see how simple the graphics.py library make loading an image. Create a new file and enter the program below.


"""
Prog:   ex-10.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library for images into
        the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *

def main():
    win = GraphWin("Image Loader", 640, 480)
    win.setBackground(color_rgb(0,0,0))

    p1 = Point(320, 240) # Point of center of image
    img = Image(p1, 'images/PixelCar.gif')
    img.draw(win)

    win.getMouse()
    win.close()

main() # Call main()

Now before we can run this program we need and image. You can down load the one I used here: http://www.randallmorgan.me/wp-content/uploads/2018/12/PixelCar.gif

Download this file and save it. I placed my copy in a sub-directory of my program’s directory names images. So the path to the image in the code reflects this. If you place the image in the same directory as your code, you’ll need to remove the ‘image/’ part of the filename and just pass ‘PixelCar.gif’.

Once you have the image and have modified the path in the code to point to your image, it is time to run the application. You should see the car image in the middle of the screen.

Let’s take a short detour and talk about color next.

Color

Throughout this series we’ve been using the color_rgb() method to generate colors. However, the library is based on the TKinter library and it supports named colors. Specifically, X11 named colors. We can easily pass these color names to any method that takes a color_rgb() value. I tend to use the color_rgb() method as it gives more control over the color. However, it can be very convenient to simply pass the color name for things like background colors or well defined colors such as white. You can learn more about X11 Color names here: http://cng.seas.rochester.edu/CNG/docs/x11color.html

Rather than writing an app to demonstrate the use of color names, just take the the image app above (ex-11_01.py) and change the color in the win.setBackground() method to ‘white’. Once you made the change run the program. Go ahead and try some of the other color names you find at the link above.

OK, we’ve covered a lot of material in this issue. In my next installment I’ll cover how to force a window update and animation.

Keep Coding!

Series Navigation<< Simple Graphics in PythonSimple Graphics in Python – Part 3 >>