Header Image - Randall Morgan

Tag Archives

2 Articles

Simple Graphics in Python – Part 3

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

Last time we left off discussing Color in John Zelle’s graphics.py library. If you didn’t catch part 1 and 2 of this series I recommend you read those parts first and then return here. You can find part 1 here: Simple Graphics in Python.

We’re almost done going over the library from a user standpoint. However, in the future I may discuss how the library works if there is enough interest. This time, we’ll be discussing window updating and animations. We’ll develop a few sample apps and have some fun. My intention here isn’t to develop full fledged apps but, rather give you a starting point for your own apps using the graphics.py library. So let’s get started.

Window Updates

The graphics.py library usually handles window updates for you anytime an object that has been drawn to the window changes. Under some circumstances it may be necessary to force a window update. For example, when using the library from some interactive shells. The window may be forced to update using the update() method on the GraphWin object. This will redraw all the items in the window.

The window auto update feature is great for simple graphics. However, as your scenes become more complex you may want to take charge and start updating the window when it bests fits your program’s schedule. This may become necessary when you are drawing many, many items to the window. You can improve efficiency by updating the window only after all items have been drawn. If you want to turn off the auto update window feature you can do so when you create the window by passing autoflush=False as in:

win = GraphWin("Window Title", 400, 400, autoflush=False)

This will disable the auto update feature and you’ll be responsible for calling win.update() when you desire to redraw all the objects in the window. Here’s an example:

"""
Prog:   ex-11_02.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library turning off the 
        window's auto update feature and 
        calling win.update() yourself.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

def main():
    win = GraphWin("Rectangles", 640, 480, autoflush=False)
    
    points = []
    for i in range(1):
        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)

    # First mouse click adds a polygon
    points[0] = win.getMouse()  
    points[-1] = points[0] 
    poly2 = Polygon(points)
    poly2.setOutline('white')
    poly2.draw(win) 

    # Second mouse click should show the new polygon
    win.getMouse()
    update()

    # Thrid mouse click should close the window.
    win.getMouse()
    win.close()

main()

When I read the docs and implemented this program I expected that updates would only occur when the update() method was called. When this didn’t work as expected I re-read the docs and when I still couldn’t understand what was happening, I emailed John. He was kind enough to respond and set me straight about my misunderstandings. After reading his response and once again, re-reading the docs, I realized I had read, but ignored one statement in the docs. This was the cause of misunderstanding. Here’s the line I skimmed over and missed the details:

Now changes to the objects in win will only be shown when the graphics system has some idle time or when the changes are forced by a call to update().

When I read this I walked away with the impression that updates only occur when update() was called if you passed autoflush=False. However, the statement clearly says that auto-updates still occur when the system has idle time. So update is only useful if you have a blocking operation that keeps the auto-update from running.

John pointed out that in the code above, the getMouse() method calls are blocking methods but that he coded them to call update() and force drawing on the window. So my calls to getMouse don’t actually work as commented in the code above. In fact, they force an update to the window.

So with that insight let’s see if we can write a sample that actually demonstrates the use of update. Create the example below:

"""
Prog:   ex-11_03.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library turning off the 
        window's auto update feature and 
        calling win.update() yourself.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

def main():
    win1 = GraphWin("Rectangles", 640, 480, autoflush=False)
    #win2 = GraphWin("Rectangles", 640, 480)
    
    points = []
    for i in range(1):
        # Build up initial polygon
        for p in range(0, randint(4, 20)):
            points.append(Point(randint(0,639), randint(0,479)))
        
        r = randint(80,255)
        g = randint(0,255)
        b = randint(50,255)

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

        # Loop to keep system busy
        # Since we are using a loop 
        # to delay the system you may need to increa
        max_iters = 999999999
        delay_frac = 33333333
        td = 0
        for j in range(0,max_iters):
            if j % delay_frac == 0:
                poly.undraw()
                poly = None
                r = randint(30,255)
                g = randint(30,255)
                b = randint(30,255)
                points.append(Point(randint(0,639), randint(0,479)))
                poly = Polygon(points)
                poly.setOutline(color_rgb(r, g, b))
                poly.draw(win1)
                print("Updated #", td)
                td += 1
                update() # We manually call update here, then delay again.
    
    
    # mouse click should show the new polygon
    # after busy loop completes
    win.getMouse()
    win.close()

main()

OK, with our new understanding of the autoflush=False option we’ll run the app above. You may need to adjust the value of max_iters and delay_frac as max_iters controls the total run-time of the delay loop and delay_frac controls the delay between updates during the loops.

Our program begins by creating a polygon and displaying it on the screen. Next, we enter a loop and stay in the loop for a very long time. This loop blocks the auto-update feature from updating the display. During the execution of the delay loop we check if we have made delay_frac (delay fraction) iterations since our last update. If so, the modulus expression will return 0 and the if statement will evaluate to true and we enter the if clause. Next, we erase and destroy the original polygon, and generate a new polygon using the points of last polygon with one new point added for good measure. Adding a point allows us to see the shape changed on update. The important thing to understand here is that the object isn’t being drawn to the window until we reach the update line at the bottom of the if clause.

However, if we were to forego the delay loop the auto-update feature would take over and draw the polygon when the system became idle or a method that itself (like the getMouse()) calls update() is called.

One last thing to know about the update() method is that it can take an integer parameter for the desired frame-rate. If you pass a desired frame-rate to update() it as in:

update(30)

It will update the window at this rate.

Animations

While it’s possible to use the graphics library for a GUI (Graphical User Interface), most NooBs will want to do something a bit more entertaining with it. I’m not going to tech game development here but I thought I would toss out a few example apps that I’ll intentionally leave unfinished so you, the reader, can have fun adding features and completing the demo apps.

There are a few ways to accomplish animation on a computer. The most often used is motion animation where an object is moved into it’s new location, then drawn, then erased and moved again. This cycle is known as the animation loop, or if you’re a gamer, the game loop.

Handball

Our first animation is a simple Pong-like game (remember these apps will be unfinished and incomplete) that simply draws a circle and a rectangle on the screen then moves them around the screen.

Screen Shot of the Handball App

We will use an OOP (Object Oriented Programming) approach for the Handball app.

#!/usr/bin/env python3

"""
Prog:   ex-12_01.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to do simple
        animation of a pendulum.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from math import *

width = 640
height = 480


class Paddle():
    def __init__(self, x, y, win):
        self.x = x
        self.y = y
        self.w = 10
        self.h = 50
        self.win = win
        self.rect = Rectangle(Point(self.x, self.y), Point(self.x+self.w, self.y+self.h))
        self.rect.setFill('white')
    
    def getX(self):
        return self.rect.p1.getX()

    def getY(self):
        return self.rect.p1.getY()

    def getW(self):
        return self.w

    def getH(self):
        return self.h
        
    def move(self, xspeed, yspeed):
        self.rect.move(xspeed, yspeed)

    def draw(self):
        self.rect.draw(self.win)

    def undraw(self):
        self.rect.undraw()

  
class Ball():
    def __init__(self, x, y, win):
        self.x = x
        self.y = y
        self.xspeed = 3
        self.yspeed = 1
        self.r = 10
        self.win = win
        self.cir = Circle(Point(self.x, self.y), self.r)
        self.cir.setFill('white')

    def set_speed(self, xspeed, yspeed):
        self.xspeed = xspeed
        self.yspeed = yspeed
    
    def move(self):
        self.cir.move(self.xspeed, self.yspeed)
        p1 = self.cir.getP1()
        self.x = p1.getX()
        self.y = p1.getY()
        
    def draw(self):
        self.cir.draw(self.win)

    def undraw(self):
        self.cir.undraw()

    def check_collision(self, pad):
        print("Xspeed: ", self.xspeed, "Yspeed: ", self.yspeed) 
        xbound = self.within_x_bounds(pad)
        ybound = self.within_y_bounds(pad)

        if xbound and ybound:
            if xbound:
                self.xspeed = -self.xspeed
            if ybound:
                self.yspeed = -self.yspeed
            self.move()
            return True

        return False
        

    def within_x_bounds(self, pad):
        if self.xspeed < 0:
            if (self.x < pad.getX() + pad.getW()) and (self.x > pad.getX()):
                return True
            else:
                return False
        else:
            if (self.x + self.r >= pad.getX()) and (self.x <= pad.getX() + pad.getW()):
                return True
            else:
                return False
    
    def within_y_bounds(self, pad):
        if self.y+self.r >= pad.getY() and self.y <= pad.getY()+pad.getH():
            return True
        else:
            return False
    
    def check_edges(self, width, height):
        p1 = self.cir.getP1()
        if p1.getX() < 0 or p1.getX()+self.r > width:
             self.xspeed = -self.xspeed;
        if p1.getY() < 0 or p1.getY()+self.r > height:
            self.yspeed = -self.yspeed;
  
      
def main():
    win = GraphWin("Handball", width, height)
    win.setBackground('black')

    xspeed = 0.1
    yspeed = 0.1

    # initial placement of paddle  
    pad = Paddle(10, (height/2)-25, win) 
    # Draw paddles.
    pad.draw()

    ball = Ball(width/2, height/2, win)
    ball.set_speed(xspeed, yspeed)
    ball.draw()
   
    while 1:
        # get imput if any
        k = win.checkKey()
        if k == 'a':
            if pad.getY() < 0:
                pad.move(0, 0) 
            else:
                pad.move(0, -20)
            print('Pad1: Move Up', pad.getX(), pad.getY())

        if k == 'z':
            if pad.getY() > height - 50:
                pad.move(0, 0)
            else:
                pad.move(0, 20)
            print('Pad1: Move down', pad.getX(), pad.getY())

        ball.check_edges(width, height)
        ball.move()

        if ball.check_collision(pad):
            print("Ball hit paddle")

    win.getMouse()
    win.close()


main()

This may not be the most efficient implementation however, it is only meant to provide you with some inspiration for creating your own apps by demonstrating what can be accomplished using the graphics.py library.

If you scan the code you quickly see that we have a Paddle calls, a Ball class, and a main function. Our Paddle object encapsulates properties (data) and methods (actions) our paddle can take. Our paddle needs to keep track of it’s position (x,y) and size (w,h). When we create a Paddle object from the class (a class is a blueprint for the object we want to create) we pass in these values along with the window we want the paddle to draw itself on. We save the window for future use as we will always draw the paddle to the same window. So saving it here simplifies our code and we no-longer have the need to pass the window each time we call draw() on the paddle.

When a paddle is instantiated (an object is created from the class), Python calls the __init__() method. In this method you place all the code that you need to run to set things up for use. So we create the rectangle that will represent our paddle on the screen. We also set the fill color on the paddle to white. Our paddle is now ready for use.

Often you’ll need to have access to the state of an object. Later, we’ll need to be able to determine if the ball hits our paddle so, we need access to the location and size of our paddle. Do enable this we provide accessor methods getX(), getY(), getW(), getH(). These return the paddles x, y, width, and height respectively.

Our paddle also needs to move up and down so we can hit the ball as it bounces across the court. So, well need a move() method. There may be times we want to move at different speeds. So will pass in the xspeed and yspeed for our paddle. You may be wondering why we need the xspeed. Truly we don’t. We could just hard code the xspeed in our class code. But that would restrict us to moving only in the Y plane. Yes, it’s true that the paddle in Pong moves only in the Y play (up and down). But think how much fun it would be to animate the paddle to shake when the ball hits it. Here, we would need access to the yspeed to accomplish this. Including it also opens the class up for reuse. For example, suppose you want to use the paddle in a falling object game. If we didn’t include the yspeed here, you wouldn’t be able to.

In almost all motion animations each object will need to complete the three tasks of the animation loop, Move, Draw, Erase, Repeat… The graphics library actually takes care of this for use in the move() method of the various shape objects. So we really don’t need to worry about it. But you do need to know it’s happening under the hood.

we’ll add a draw() method to our paddle. Here we only need to call draw on the rectangle that represents our paddle on on the display. We may also need an erase method at some point. So, we’ll include it here and again however, we’ll call it undraw() to stay consistent with the library methods. All we need to do in the undraw() method is to call it’s namesake on the rectangle that represents our paddle.

The Ball class is a bit more complicated. Mostly because we encapsulated the logic of what to do when the ball comes in contact with another object. For example, if the ball hits the edge of the screen or the paddle. The balls move method is a bit different than the paddle’s move method. This is because it is expected that once the ball is moving it will keep moving. Also, we don’t want to have to changed the balls direction ourselves. We want it to include this action when it hits an object so it bounces off on it’s own. So in the Ball class we provide an xspeed and yspeed and set default values for them. Our ball is represented by a circle on the screen. So we have to create a circle and save it. We laos set the fill color in the __init__() method.

We may need to change the ball’s speed so we include a set_speed() method. We will also need to know when the ball has hit the edge of the court. This is handled in the check_edges() method. Here you’ll need to pass in the courts with and height. It is assumed that the upper right corner of the court is (0,0) and all calculations make this assumption.

The check_collision() method is passed the paddle object to test for collision with the ball. The ball object includes two helper methods, within_x_bounds() and within_y_bounds() to check if the ball is within the bounds of the paddle object.

To make this a complete game you need to add scoring and allow the ball to reset and be re-served if it passes the paddle. You can also use this as the basic frame work for Pong by adding another paddle and additional input handling for another player. Just a hint if you try this, google keyboard input methods for python before you attempt this. As, the current approach wont report multiple key presses at once. Their are solutions but I’ll leave that as an exercise for the reader.

Simulations

Games and GUIs aren’t the only things that graphics can help with. Graphics are often used to convey information about some chemical or mathematical process. Let’s take a simple case, that of calculating pi. It is well known that PI can be estimated to surprising accuracy by randomly throwing darts at art board. OK, so it’s a bit more complex than that but, only a little. First, what we really need is a circle inside a square. The circle’s diameter must fit snugly inside the square. More precisely the diameter of the circle must equal the length of one side of the square.

The logic is simple: If the circle’s diameter is equal to the square’s length, than the area of the circle should be equal to: (area of the square / area of circle)*4. To learn more about this you can checkout this link: https://www.youtube.com/watch?v=M34TO71SKGk

We can draw circles, squares, and points (to represent darts) using the graphics.py library. So all we need to do is draw a circle inside a square and throw darts at it, then calculate the ratio of darts that landed in the circle to the total number of darts thrown. We will simply plot random points for our darts and keep track of how many we throw and where they landed. Let’s see how we might do this in python:


#!/usr/bin/env python3
"""
Prog:   ex-13.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to visualize the
        process of estimating pi by randomly
        throwing darts.

Lic:    This code is placed in the public domain.

"""
from graphics import *
from random import *
from math import *

width = 400
height = 400
center = width/2
r = width/2

# Find the deststance between two points
def dest(x1,y1, x2,y2):
    return sqrt((x1 - x2)**2 + (y1 - y2)**2)


def main():
    win = GraphWin("Pi Estimation", width, height)
    # Draw a square
    sq = Rectangle(Point(0,0), Point(width-1,height-1))
    sq.setOutline("blue")
    sq.draw(win)
    # Draw a circle fitting the square
    c = Circle(Point(center,center), r)
    c.setOutline("white")
    c.draw(win)

    darts_thrown = 0
    darts_in_circle = 0
    best = 0

    estimate = 0
    best_estimate = 0
    for i in range(1,100000):
        x = randint(0, 400)
        y = randint(0, 400)
        p = Point(x,y)
        darts_thrown += 1
                
        # Is are point in the circle?
        if(dest(center,center, x, y) < r):
            darts_in_circle += 1
            p.setFill(color_rgb(220,200, 120))
            p.draw(win)
        else:
            p.setFill(color_rgb(127, 200, 127))
            p.draw(win)
        
        if i % 3000 == 0:
            estimate = (darts_in_circle/darts_thrown)*4
            if abs(pi - estimate) < abs(pi - best_estimate):
                best_estimate = estimate
            
            print("Iteration: ", i, " Estimated PI: ", best_estimate)

    print("Done!")

    win.getMouse()
    win.close()


main()

If you run this code you should get a printed output of something like this:

Iteration: 3000 Estimated PI: 3.0893333333333333
Iteration: 6000 Estimated PI: 3.0893333333333333
Iteration: 9000 Estimated PI: 3.089777777777778
Iteration: 12000 Estimated PI: 3.0936666666666666
Iteration: 15000 Estimated PI: 3.1018666666666665
Iteration: 18000 Estimated PI: 3.110222222222222
Iteration: 21000 Estimated PI: 3.1125714285714285
Iteration: 24000 Estimated PI: 3.1161666666666665
Iteration: 27000 Estimated PI: 3.1161666666666665
Iteration: 30000 Estimated PI: 3.1161666666666665
Iteration: 33000 Estimated PI: 3.1161666666666665
Iteration: 36000 Estimated PI: 3.1172222222222223
Iteration: 39000 Estimated PI: 3.12174358974359
Iteration: 42000 Estimated PI: 3.12174358974359
Iteration: 45000 Estimated PI: 3.1226666666666665
Iteration: 48000 Estimated PI: 3.12475
Iteration: 51000 Estimated PI: 3.124941176470588
Iteration: 54000 Estimated PI: 3.124941176470588
Iteration: 57000 Estimated PI: 3.124941176470588
Iteration: 60000 Estimated PI: 3.124941176470588
Iteration: 63000 Estimated PI: 3.124941176470588
Iteration: 66000 Estimated PI: 3.1267878787878787
Iteration: 69000 Estimated PI: 3.1267878787878787
Iteration: 72000 Estimated PI: 3.1267878787878787
Iteration: 75000 Estimated PI: 3.1267878787878787
Iteration: 78000 Estimated PI: 3.1267878787878787
Iteration: 81000 Estimated PI: 3.1267878787878787
Iteration: 84000 Estimated PI: 3.1267878787878787
Iteration: 87000 Estimated PI: 3.1267878787878787
Iteration: 90000 Estimated PI: 3.1267878787878787
Iteration: 93000 Estimated PI: 3.1267878787878787
Iteration: 96000 Estimated PI: 3.1267878787878787
Iteration: 99000 Estimated PI: 3.1267878787878787
Done!

I ran this program several times and the best I did was 3.1419. Which is pretty good given the fact that our random number generator is actually a pseudo random number generator. I also believe that the math library in python may be rounding our calculations. Using a more precise math library would improve the estimate. However, this app is only meant to demonstrate the process. So, I’ll leave implementing a more precise version up to the reader.


Screen Shot of Pi Estimator App

Running the application longer with more dart throws will improve the estimation of PI.

Multiple Window

The graphics.py library allows you to have multiple windows. This can be handy for both GUIs and data visualization. You could for example display the plot of darts in a PI estimation program in one window while plotting the error on a graph in another window.

You might wonder why you would ever need more than one window. Well, how often do you use a drop down menu? The drop down menu is actually a small window with a list of items that is placed over the main window. Dialog boxes, popups, etc… are all windows. So being able to create additional windows comes in very handy for GUI applications. However, other types of applications can make use of multiple window. Take our PI estimating application above. We could use an additional window to plot the standard deviation of our a current estimate. Using multiple windows you can show many plots at the same time. This would allow the user to correlate the information in the various plots.

I’m going to show you a simple demo that is once again, an incomplete game. This game is a two player version of Battleship. It has several issues left for you to resolve. however, it does demonstrate the use of two windows being used in a single application. The code here is a bit longer than our other applications and I would say this code is in a pre-alpha state. It is only meant to ignite you imagination and give you a base from which to work to complete the game.

I’m sure I don’t have to explain how Battleship is played. However, if you need and explanation, google “battleship game” and you’ll find a wikipedia article on it. Let’s see some code:


#!/usr/bin/env python3
"""
Prog:   ex-14.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library and the use of
        multiple windows in a single app.

Lic:    This code is placed in the public domain.

"""
from graphics import *
from random import *
from math import *

width = 400
height = 400



class Board():

    def __init__(self, title, width, height):
        self.xsize = 10
        self.ysize = 10
        self.w = width
        self.h = height
        self.grid = []
        self.win = GraphWin(title, width, height)
        self.vessels = []


    # returns pixels per division
    def xdiv(self):
        xdiv = self.w / self.xsize
        return xdiv


    # returns pixels per division
    def ydiv(self):
        ydiv = self.h / self.ysize 
        return ydiv


    def rowcol_to_xy(self, r, c):
        y = int(r * self.ydiv())
        x = int(c * self.xdiv()) 
        return (x, y)


    def rowcol_to_point(self, r, c):
        p = self.rowcol_to_xy(r, c)
        return Point(p[0], p[1])


    def xy_to_rowcol(self, x, y):
        r = int(y / self.ydiv())
        c = int(x / self.xdiv())
        return (r, c)
    

    # return the coordinates in pixels for
    # the center of the cell at (row, col)
    def center_xy(self, r, c):
        # calc (x,y) position of upper left
        # corner of cell at (r,c)
        xy1 = self.rowcol_to_xy(r, c)
        # Calculate lower right corner
        xy2 = self.rowcol_to_xy(r+1, c+1)
        
        # find the middel of the cell
        dx = (xy2[0] - xy1[0]) - self.xdiv() / 2
        dy = (xy2[1] - xy1[1]) - self.ydiv() / 2
        cx = dx + xy1[0]
        cy = dy + xy1[1]

        return (cx, cy)


    def dist(self, x1, y1, x2, y2):
        return sqrt(((x2-x1)**2) + ((y2 - y1)**2))


    # draws the grid of cells on the board
    def draw(self):
        # Expects (0,0) to be located in the upper left
        xdiv = self.w / self.xsize
        ydiv = self.h / self.ysize
        for i in range(0, self.w, int(xdiv)):
            l = Line(Point(i, 0), Point(i, self.h))
            l.draw(self.win)

        for j in range(0, self.h, int(ydiv)):
            l = Line(Point(0, j), Point(self.w, j))
            l.draw(self.win)


    # place a vessel on the board
    def place(self, vessel):
        plX = vessel.row * self.ydiv()
        plY = vessel.col * self.xdiv()
        if vessel.rect == None:
            rowcol = self.rowcol_to_xy(vessel.row, vessel.col)
            x1 = rowcol[0]
            y1 = rowcol[1]
            if vessel.horz:
                x2 = x1 + (vessel.length * self.xdiv())
                y2 = y1 + self.ydiv()
            else:
                y2 = y1 + (vessel.length * self.ydiv())
                x2 = x1 + self.xdiv()
            vessel.rect = Rectangle(Point(x1, y1), Point(x2, y2))
        vessel.rect.setOutline(color_rgb(127,220,127))
        vessel.rect.draw(self.win)


    # tests to see if the vessels on this board
    # have been hit by the shot taken, and call
    # draw_hit() to mark the shot with a red X 
    # in the cell where it landed.
    def hit(self, loc):
        col = int(loc.getX() / (self.w / self.xsize))
        row = int(loc.getY() / (self.h / self.ysize))
        self.draw_hit(row, col)


    # draws the actual red X, called by hit()
    def draw_hit(self, row, col):
        xy1 = self.rowcol_to_xy(row, col)
        x1 = xy1[0]
        y1 = xy1[1]
        xy2 = self.rowcol_to_xy(row+1, col+1)
        x2 = xy2[0]
        y2 = xy2[1]
        
        p1 = Point(x1,y1)
        p2 = Point(x2, y2)
        p3 = Point(x1,y2)
        p4 = Point(x2, y1)

        l1 = Line(p1, p2)
        l2 = Line(p3, p4)
        l1.setOutline('red')
        l2.setOutline('red')
        l1.draw(self.win)
        l2.draw(self.win)


    # Use to mark the shooter's board
    # for shots taken. So the player may
    # know where they have already shot
    def mark(self, r, c):
        c = self.center_xy(r, c)
        print("Center of mark, x: " + str(c[0]) + ", y: " + str(c[1]))
        pc = Point(c[0], c[1])
        cir = Circle(pc, int(self.xdiv()/2))
        cir.setOutline(color_rgb(50, 50, 200))
        cir.draw(self.win)



# Simple vessel class
class Vessel():

    def __init__(self, name, row, col, length, place_horz):
        self.row = row
        self.col = col
        self.length = length
        self.horz = place_horz
        self.name = name
        self.hit_count = 0
        self.rect = None # created in board.place()

        if self.name == 'Carrier':
            self.makeCarrier()
            print("Row: " + str(self.row))
            print("Col: " + str(self.col))
        elif self.name == 'Battleship':
            self.makeBattleship()
            print("Row: " + str(self.row))
            print("Col: " + str(self.col))
        elif self.name == 'Cruiser':
            self.makeCruiser()
            print("Row: " + str(self.row))
            print("Col: " + str(self.col))
        elif self.name == 'Submarine':
            self.makeSubmarine()
            print("Row: " + str(self.row))
            print("Col: " + str(self.col))
        elif self.name == 'Destroyer':
            self.makeDestroyer()
            print("Row: " + str(self.row))
            print("Col: " + str(self.col))
        else:
            print('Illegal Vessel Type: "'+name+'" not defined')
            return None 


    def makeCarrier(self):
        if self.name != 'Carrier':
            return
        elif self.horz:
            self.col = randint(0, 4)
            self.row = randint(0, 9)
        else:
            self.col = randint(0, 9)
            self.row = randint(0, 4)


    def makeBattleship(self):
        if self.name != 'Battleship':
            return
        elif self.horz:
            self.col = randint(0,5)
            self.row = randint(0, 9)
        else:
            self.col = randint(0, 9)
            self.row = randint(0, 5)


    def makeCruiser(self):
        if self.name != 'Cruiser':
            return
        elif self.horz:
            self.col = randint(0,6)
            self.row = randint(0, 9)
        else:
            self.col = randint(0, 9)
            self.row = randint(0, 6)


    def makeSubmarine(self):
        if self.name != 'Submarine':
            return
        elif self.horz:
            self.col = randint(0,6)
            self.row = randint(0, 9)
        else:
            self.col = randint(0, 9)
            self.row = randint(0, 6)


    def makeDestroyer(self):
        if self.name != 'Destroyer':
            return
        elif self.horz:
            self.col = randint(0,7)
            self.row = randint(0, 9)
        else:
            self.col = randint(0, 9)
            self.row = randint(0, 7)


    def getName(self):
        return self.name


    def move(self, x, y):
        self.rect.move(x, y)


    def draw(self):
        self.rect.draw()   


    # Not Yet Implemented
    # Given a row, col value for
    # a shot, return true if the
    # vessel was hit by shot 
    def hit(self, r, c):
        return False
    


# Simple player class
class Player():

    def __init__(self, name, width, height):
        self.name = name
        self.board = Board(name, width, height)

        # Create fleet
        self.Carrier = Vessel('Carrier', randint(0,4), randint(0, 9), 5, True)
        self.Battleship = Vessel('Battleship', randint(0,5), randint(0,5), 4, False)
        self.Cruiser = Vessel('Cruiser', randint(0,4), randint(0,4), 3, True)
        self.Submarine = Vessel('Submarine', randint(1,4), randint(1,4), 3, True)
        self.Destroyer = Vessel('Destroyer', randint(1,4), randint(1,4), 2, True)


    def getName(self):
        return self.name


    def getMouse(self):
        return self.board.win.getMouse()


    # called when player should take turn
    def turn(self, board):
        loc = self.getMouse()
        board.hit(loc)
        rc = self.board.xy_to_rowcol(loc.getX(), loc.getY())
        self.board.mark(rc[0], rc[1])
        

    # Not Yet Implemented
    # Should test if the player's
    # entire fleet has been sunk,
    # if so, game over!
    def fleetSunk(self):
        return False
        pass


    def close(self):
        self.board.win.close()


    # Initialize fleet
    def draw(self):
        self.board.draw()
        self.board.place(self.Carrier)
        self.board.place(self.Battleship)
        self.board.place(self.Cruiser)
        self.board.place(self.Submarine)
        self.board.place(self.Destroyer)



def main():
    # Open game boards
    player1 = Player("Player 1", 400, 400)
    player2 = Player("Player 2", 400, 400)
    player1.draw()
    player2.draw()

    while ~player1.fleetSunk() and ~player2.fleetSunk():
        player1.turn(player2.board)
        player2.turn(player1.board)

    player1.getMouse()
    player1.close()
    player2.close()


main()
   

Looking over this code we can see it is really rather simple. In main() we create two players and call draw() on them. Next, we enter a while loop. This loop will loop forever as I left the fleetSunk() method unimplemented. It is hard coded to return false. I’ve left implementing this method up to the reader.

Within the loop we call turn on each player passing in the opponent’s game board. Each game board is responsible for calculating it’s own size, and completing all drawing operations on it’s grid.

The player.turn() method takes the opponent’s game board as a parameter and and after getting the mouse click location, passes that location to the opponent’s board.hit() method. Next, we convert the pixel (x,y) values to (row, column) values and pass those to our own board’s mark() method to draw a blue circle to indicate where we’ve taken shots. You could leave this set out or toggle it to make the game more challenging.

The board’s hit method is not implemented in this code and is also left as an exercise for the reader. However, it should take the row, column values passed in and determine if any of the vessels on it’s board have been hit. If so, it should mark that vessel as damaged and increment the vessel.hit_count. This should be done by calling the vessel’s hit() method. The vessel is sunk if the hit_count matches the vessel’s length.

The player’s fleetSunk() method should simply test if all the player’s vessels have been sunk and return true if they have.

I’m leaving the completion of this up to the readers. You’ll most likely want to add some type of scoring. You might even change the X draw for hit’s and the circle drawn as a marker, to an image of an explosion and a slash in the water respectively. You might also make the shooter’s board indicate whether the shot was a hit or a miss. You should have all the tools you need to implement these features. If you take a little time and analyze each class and each of it’s methods, you should have little trouble.

There is one issue that this code has I didn’t have time to correct. That is that the vessels are drawn at random locations and therefor often overlap each other. This isn’t good, as one shot can damage two vessels. This might be allowed if this were Angry Birds (two bird, one shot…). However, it’s Battleship! SO you’ll need to implement some method for ensuring that all vessels are placed in such a manner that they wont overlap. You can find one such solution here: https://stackoverflow.com/questions/3265986. This isn’t the only solution but it’s one that isn’t too hard to implement. Do note that one issue with this method is that everything is placed around a focal point that will never be occupied by a vessel. Effectively ensuring that the center call of the board will always be empty. This could be dealt with by shrinking the field for the purpose of the placement calculation and then randomly shifting it up or down one row.

Good luck! If you have questions of comments I’d enjoy hearing from you.

Simple Graphics in Python

by SysOps 0 Comments
This entry is part 1 of 3 in the series Simple Graphic in Python

For the beginning Python programmer, getting started with command line applications is the first step. At some point however, even a NooB will get bored with the terminal window and desire something more. Yet, many of the graphics and gui libraries for Python can be overwhelming for a novice. Prof. John Zelle saw the need for a simple graphics library for his students and went searching. He didn’t find anything he felt was suitable for the truly novice python student. So, he created a new graphics library to fill this niche’. John’s library was used in his book “Python Programming An Introduction to Computer Science”. Which has seen three editions so far.

John’s library is great for teaching and keeping students interested. It’s simple, lacks complex features, and is straight-forward to use. John has provided great documentation. However, many new pythonistas will desire examples.

In searching for examples I came across a short series on youtube.com.   That series, though well done and well received , was also incomplete. There had been sever posts requesting updates but those have yet to surface. For that reason I decided to do a series of articles showing some sample code and just what can be accomplished with John’s great little library. So here goes:

As a software developer you must get used to reading documentation. You can find the documentation for John’s library at : http://mcsp.wartburg.edu/zelle/python/. The best thing you can do for yourself is read this documentation. It is short, only 8 pages. Then come back here and follow along with the examples.

Before we can start drawing on a window, we need to get our environment set up. First, we need John’s graphics library. Second, we need to place it in the same folder as our code because it is not part of the python standard libraries. You can download the graphics.py file from John’s site here: http://mcsp.wartburg.edu/zelle/python/graphics.py. Once you have the library place it in the same folder as your project file. It is important that you place the “graphics.py” file in the same folder as your code file. Otherwise python may not be able to locate it. Also note that you will tkinter (python-tk) installed on your machine to use John’s library.


While Completing this series of blog posts, I had a minor issue. The issue turned out to be that I misunderstood the way autoflush=false worked, we’ll cover that much later in this series. I wrote John and he set me straight on the inner workings of the update() method, (also covered later). In his reply he informed me that his library can now be installed using pip. To install with pip run this command:

pip3 install –user http://bit.ly/csc161graphics

You can find more info here: http://www.pas.rochester.edu/~rsarkis/csc161/python/pip-graphics.html

Now create a folder for your projects. We will call this folder “graphics-zelle” and it will be placed in a “projects” folder.  Finally, we will create a sub-folder for each exercise we do. Create a file named ex-01.py and add the code below.

Ok, now let’s write some code!

Opening a window

To open a window that we can draw on requires two simple steps. First, import the graphics library file and second, instantiate a window object and assign it to a variable so we can access it later.

The first step is simple:

from graphics import *

Once we have imported the library we can instantiate the window by calling the GraphWin function and passing in the window title and it’s width and height. We’ll do this in function we’ll call main. Lastly, we need to call the main function to execute the program.


# Define a main function and instantiate the window object.
def main():
    window = GraphWin("Exercise-01", 640, 480)

main() # Call the main function 

Putting this all together we get:

"""
Prog:   ex-01.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to open a window
        on the desktop.

Lic:    This code is placed in the public domain.

"""

# Creating a gui window requires only two steps
# Step 1: import the graphics.py library into 
# your local folder.

# Step 2: Instantiate a GraphWin object and assign
# it to a variable, passing the window title, and
# size parameters.

# Demo
from graphics import *

def main():
    win = GraphWin("Window Title", 640, 480)

    
main()

Now run ex-01.py. If you’re quick you’ll see a window pop open and immediately close. What’s going on here?

What’s going on is the program is doing exactly what was asked of it. It creates a window and then exits. So how do we stop this from happening? The easiest solution for the moment is get the program to wait for some input, until we close the window. We can add one line of code to ex-01.py to ask it to wait for a mouse click before exiting.


"""
Prog:  ex-01.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to open a window
        on the desktop.

Lic:    This code is placed in the public domain.

"""

# Creating a gui window requires only two steps
# Step 1: import the graphics.py library into 
# your local folder.

# Step 2: Instantiate a GraphWin object and assign
# it to a variable, passing the window title, and
# size parameters.

# Demo
from graphics import *

def main():
    win = GraphWin("Window Title", 640, 480)
    win.getMouse()

main()

Now run the program. You should see the window open and stay open until you click the close button on the widow’s frame.  Clicking the window frame close button causes python to ask the OS and interpreter to clean up the window we created. This is a very poor practice. If we want the window destroyed, we should explicitly close it. We can do this by calling the close method on the window object. Modify your code as show below:


"""
Prog:   ex-01.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to open a window
        on the desktop.

Lic:    This code is placed in the public domain.

"""

# Creating a gui window requires only two steps
# Step 1: import the graphics.py library into 
# your local folder.

# Step 2: Instantiate a GraphWin object and assign
# it to a variable, passing the window title, and
# size parameters.

# Demo
from graphics import *

def main():
    win = GraphWin("Window Title", 640, 480)
    win.getMouse()
    win.close()    

main()

This is much better. Now we can close the window simply by clicking on it. Still not a perfect solution but it at least allows the GraphWin object to clean itself up.

Drawing Points

When I first started programming PC’s back in the 1980s, I learned that the first step to drawing anything on the screen was to draw a simple point. In a Compuserve chat with Andre’ LaMonthe (then a hot shot game developer), he told me if you can draw a pixel, you can draw anything! Take that to heart. It’s the most basic object you can put on the screen. A simple point of light and everything else is simply built up out of many of these points of light. So, that’s our first task is to draw a point. 

Create a file called ex-02.py and in it add the code below:

"""
Prog:   ex-02.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw a point 
        in the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *

def main():
    win = GraphWin("Exercise-02, Points", 640, 480)
    # Our first point
    p = Point(320, 200)
    win.getMouse()
    win.close()
    
main()

Now if you run this code you may not see a point. It depends on you’re desktop’s default background color. On my desktop I have a darkula theme and my windows have a dark background. The default color in the graphics library is black and so in my window the point is very difficult to see.

To solve this we need to change the color of the point to something more easily seen. Before we can do that, we need to talk about how computer monitors display color.

Each pixel (picture element) on the screen is actually made up of fields. There is a field for each color channel, Red, Green, and Blue. From these three colors we can make almost any color. You may be saying wait? how can that be? These are not the primary colors I learned about in school. We’ll the colors you were taught were the primary colors are indeed one set of primary colors called the Sink or Subtractive primary colors.  When you add these colors (such as mixing paint or crayons) the color get darker. This is because the colors are reflecting light and adding another color means you’ll be reflecting more light off the surface. However, there is another set of primary colors known as the Source or Additive colors. These colors come from light sources so adding more colors means they get brighter. These are the primary colors used in computer monitors and light emitting devices.For the more curious readers checkout: https://stackoverflow.com/questions/6531536/why-rgb-and-not-ryb and http://en.wikipedia.org/wiki/Additive_color and http://en.wikipedia.org/wiki/Subtractive_color.

The graphics library we’re using has a special function for setting the values of these color channels. The color_rgb() method returns a color object that can be used in many of the library’s drawing methods. We simply need to pass in the values for the r, g, and b color channels. The values for these channels must be integers and be between a value of 0 and 255 inclusive. If we set a channel’s value to 0 it will turn that channel off. For example, if we pass (0,255,127) we are telling the system to add 0 red to the color, full green to the color, and half the available blue to the color. We can generate black by passing (0,0,0) and telling the system to add 0 red, 0 green and 0 blue to the color. We can also generate white by passing (255, 255, 255) to the system. And we can generate a gray scale of 256 levels from black to white by passing equal amounts of each color ex: (68, 68, 68).


"""
Prog:   ex-02.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw a point 
        in the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *

def main():
    win = GraphWin("Exercise-02, Points", 640, 480)
    win.setBackground(color_rgb(0,0,0))

    # Our first point
    p = Point(320, 200) 
    p.setFill(color_rgb(255,255,255))
    p.draw(win)
                    
    win.getMouse()
    win.close()
    
main()

Looking at the code we can see we call setBackground() on the window object, passing it a color_rgb object set to produce a black color. This simply sets our windows background to black as you would expect. Next, we create a Point() passing in the x and y coordinates of where it should be drawn. In most computer graphics systems the upper left corner of the screen will be (0,0). The x coordinate grows larger as we travel across the screen from left to right, and the y coordinate grows larger as we travel from the top down. This is a Cartesian coordinate system using only the fourth quadrant and taking the absolute value of the y axis. If that sounds confusing, see: https://www.mathsisfun.com/data/cartesian-coordinates.html or simply google Cartesian Coordinate System.

Next, we see that call the setFill() method on our point and pass in a color_rgb object with three equal values for red, green, and blue. So this will set the point’s color to white. Lastly, we call the draw method on the point so that it is displayed on the screen.

Run the code and you should see a small white dot in the middle of a window with a black background. It’s really that simple.

Now that we can draw a point in the window, let’s have some fun! First, let’s put what we’ve learned about setting colors and plotting points to work. We’ll create a little program to fill our screen with random points of random colors. Enter the code below:


"""
Prog:   ex-02.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw a point 
        in the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

def main():
    width = 640
    height = 480

    win = GraphWin("Exercise-02, Points", width, height)
    win.setBackground(color_rgb(0,0,0))

    # Draw 10,000 points in the window
    # Each of a random color and at a
    # random position.
    for i in range(10000):
        # Get random position
        x = randint(0, width)
        y = randint(0, height)
        p = Point(x, y)
    
        # Get random color
        r = randint(0, 255)
        g = randint(0, 255)
        b = randint(0, 255)
        p.setFill(color_rgb(r, g, b))
        p.draw(win)
                    
    win.getMouse()
    win.close()
    

main()

Inspecting the code above the first thing we see is that we’ve imported the random library’s randint() method. We’ll use this method to generate random integer values for our point’s location and it’s color values.

Next, we see we created variables width and height to hold the width and height of the window. We then pass these variables to the GraphWin() method. Then we create a simple loop that iterates from 0 to 9,9999, for a total of 10,000 values. Within this loop we generate a point with random x and y coordinates and then generate a random color and set that color for the point’s color. Then we simply iterate and do it all again until we’ve filled the window with 10,000 random points of random colors.

Drawing Lines With Points

Ok, that was fun. But, we want to be able to draw more than just points. So, let’s try drawing a line. Now, we’ll use one of the most basic line drawing algorithms available. It’s the Bresenham line algorithm. This was the first line drawing algorithm I learned and perhaps the simplest. So it’s a great starting point for us. If you want to learn more about this algorithm check out Wikipedia’s article here: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm.

But first, we'll start by drawing a simple horizontal line as this and the purely vertical lines are the simplest case.

We'll add a horzLine function to our code. To draw a horizontal line all we need to do is draw a series of points starting a (x1,y) and continuing to (x2,y). Notice the y coordinate remains constant for a horizontal line

Create a new file called ex-03.py and add the following code:


"""
Prog:   ex-03.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw horizontal
        lines at random positions and colors
        in the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

def horzLine(x1, y1, x2, color, win): 
    for x in range(x1, x2):
        p = Point(x, y1)
        p.setFill(color)
        p.draw(win)


def main():

    width = 640
    height = 480

    win = GraphWin("Exercise-03, Lines", width, height)
    win.setBackground(color_rgb(0,0,0))

    # Draw 10,000 points in the window
    # each with a random color and at a
    # random position/
    for i in range(1000):
        # Get random position
        x1 = randint(0, width)
        x2 = randint(0, width)
        y = randint(0, height)
            
        # Set random color
        r = randint(0, 255)
        g = randint(0, 255)
        b = randint(0, 255)

        # Draw our line using generated coordinates and color
        horzLine(x1, y, x2, color_rgb(r, g, b), win)
                      
    win.getMouse()
    win.close()
    
main()

As you can see I’ve added a function to create a horizontal line given the start and end points on the x axis and the y location on the y axis.  All we have to do is walk along the path drawing points until we reach the end of the line. Now it’s your turn. Create a function for drawing a vertical line and call it from the existing loop.

Hopefully, you were able to knock that code out quickly. If not, here’s a hint. All that is needed is to loop over the y coordinate just as we looped over the x coordinate in the horzLine function. Then in the main function’s loop we generate a single x coordinate and a y1 and y2 coordinate for our new line.  Here’s my solution:

"""
Prog:   ex-03.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw lines 
        at random positions and colors
        in the window.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

def vertLine(x, y1, y2, color, win):
    for y in range(y1, y2):
        p = Point(x,y)
        p.setFill(color)
        p.draw(win)

def horzLine(x1, y1, x2, color, win):  
    for x in range(x1, x2):
        p = Point(x, y1)
        p.setFill(color)
        p.draw(win)

def main():

    width = 640
    height = 480

    win = GraphWin("Exercise-03, Lines", width, height)
    win.setBackground(color_rgb(0,0,0))

    # Draw 10,000 points in the window
    # each with a random color and at a
    # random position/
    for i in range(1000):
        # Get random position
        x = randint(0, width)
        y1 = randint(0, height)
        y2 = randint(0, height)
        
            
        # Set random color
        r = randint(0, 255)
        g = randint(0, 255)
        b = randint(0, 255)

        # Draw our line using generated coordinates and color
        vertLine(x, y1, y2, color_rgb(r, g, b), win)
                    
    win.getMouse()
    win.close()
    
main()

If you run this code you’ll see random vertical lines drawn at random locations and in random colors.  I think that was pretty straight forward.

Now what about lines that are not horizontal or vertical? How do we draw them? Well we will use a version of Bresenham line algorithm to handle this. This algorithm is designed to handle lines at arbitrary angles.  Therefore it can draw any line in our 2d window. The basic idea of the algorithm is to compute a step and direction such that the point positions fall on integer (x,y) coordinates. This is important as some of the pixels would typically fall partially on two separate (x,y) coordinates. That’s a problem because we can’t draw a partial point. So the algorithm calculates how far we should move in one direction before we take a step in the other direction, so that all our points end up on integer coordinates. For example, how far we should move horizontally before moving vertically. You can find a simple discussion of the algorithm here: https://www.tutorialspoint.com/computer_graphics/line_generation_algorithm.htm and a more in depth discussion at: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm. For those who prefer videos, watch this youtube video for more details: https://www.youtube.com/watch?v=zytBpLlSHms. If you want some truly awesome and detailed text on the subject of computer graphics check out the Graphic Gen series. While they may be older books the information in them is still very relevant.

Ok, so let’s see how we can implement a simple version of this:


"""
Prog:   ex-03.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw lines 
        at random positions and colors
        in the window using points.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from random import randint

# Bresenham's line drawing algorithm
# to handle lines of any orientation
def line(x1, y1, x2, y2, color, win):
    # Calculate dx, sx
    dx = x2 -x1
    if dx < 0:
        sx = -1
    else:
        sx = 1
    # calculate dy, sy
    dy = y2 - y1
    if dy < 0:
        sy = -1
    else: 
        sy = 1
  
    if abs(dx) > abs(dy):
        slope = dy/dx
        pitch = y1 - slope * x1
        while x1 != x2:
            y = slope * x1 + pitch
            p = Point(x1,y)
            p.setFill(color) 
            p.draw(win)
            x1 += sx
    else:
        slope = dx/dy
        pitch = x1 - slope * y1
        while y1 != y2:
            x = slope = slope * y1 + pitch
            p = Point(x,y1) 
            p.setFill(color) 
            p.draw(win)
            y1 += sy  


def vertLine(x, y1, y2, color, win):
    for y in range(y1, y2):
        p = Point(x,y)
        p.setFill(color)
        p.draw(win)


def horzLine(x1, y1, x2, color, win):  
    for x in range(x1, x2):
        p = Point(x, y1)
        p.setFill(color)
        p.draw(win)


def main():

    width = 640
    height = 480
    color = None

    win = GraphWin("Exercise-03, Lines", width, height)
    win.setBackground(color_rgb(0,0,0))

    # Draw 10,000 points in the window
    # each with a random color and at a
    # random position/
    for i in range(100):
        # Get random position
        x1 = randint(0, width)
        x2 = randint(0, width)
        y1 = randint(0, height)
        y2 = randint(0, height)
            
        # Set random color
        r = randint(0, 255)
        g = randint(0, 255)
        b = randint(0, 255)

        # Draw our line using generated coordinates and color
        line(x1, y1, x2, y2, color_rgb(r, g, b), win)
    
    color = color_rgb(255,255,228)

    line(width/2, 0, 0, height/2, color, win)
    line(0, height/2, width/2, height, color, win)
    line(width/2, height, width, height/2, color, win)
    line(width, height/2, width/2, 0, color, win)
                    
    win.getMouse()
    win.close()
    
main()

Now John new that lines would be a desired feature of his library so he included a line drawing function. So why did we draw lines using points then? Well, so you would have an idea of just what the graphics library is doing for you.

Drawing Lines with the Library

Using the Line method of the graphics library is pretty simple. First, you create a Line object and assign it to a variable.

p1 = Point(0,0)
p2 = Point(640,480)
myLine = Line(p1, p2)
myLine.setFill(color_rgb(0,255,255))
myLine.draw(win)

Using line is really no different than using point. There is one aspect we didn’t touch on. That is The width of a line or point. You can set the width of a line or point with:

p1.setWidth(5)
myLine.setWidth(5)

The width value is given in terms of pixels.

Try using the library Line method in our ex-03.py random line program.

Calculating the Distance Between two Points

OK, create a new file called ex-04.py. In this section we are going to calculate the distance between two points. Now if you’ve had geometry in school you may recall that we can calculate this distance with the formula sqrt((x2 – x1)^2 + (y2 – y1)^2). So let’s create a new function called dist() that given two points will return the distance between them.

We don’t really need the graphics library to develop this function however, we can use it to draw the a line between the two points. So I’ve included it in the code in ex-04.py. We’ve developed this function as it is missing (IMHO) from the library (perhaps John left it out so his student had to code it manually?). However, it is easy to develop. enter the code below in to the ex-04.py file and run it. You should she a line from the upper left corner of the screen to the lower right corner. The window size here has been hard coded to (400 x 400). This makes it easy to calculate the length of the line using a calculator so we can confirm our function is working correctly.

#!/usr/bin/env python3

"""
Prog:   ex-04.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to find the 
        distance between two points in the 
        window.

Lic:    This code is placed in the public domain.

"""
from graphics import *
from math import *

# Given (x1,y1) and (x2,y2)
# determine the distance 
# between the two points.
def dist(x1, y1, x2, y2):
    return sqrt(((x2-x1)**2) + ((y2 - y1)**2))

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

    x1 = 0
    y1 = 0

    x2 = 400
    y2 = 400

    p1 = Point(x1, y1)
    p2 = Point(x2, y2)
    myLine = Line(p1, p2)
    myLine.setFill(color_rgb(188, 128, 176))
    myLine.draw(win)

    d = dist(x1, y1, x2, y2)
    
    print("Distance is: " + str(d))

    win.getMouse()
    win.close()

main()

If you run this code, you should see the length print in the terminal window, not the gui. The length should be 565.685…  You can check that with a calculator.

Ok, that brings up another issue. We’ve drawn points and line but what about text? We’ll get to the soon. I promise. But first let’s touch on circles.

Drawing Circles

We’ll begin by plotting our own circles and then touch on the Circle commands in the graphics library. Go ahead and create a new file ex-05.py and leave it empty for now.

Bresenham who we discussed earlier when talking about line drawing actually has an algorithm for circles. There on many circle drawing algorithms each with it’s own advantages and trade offs. We’ll use one of the simpler solutions for circle drawing. We’ll use plot the x,y coordinates of each point along the circumference using sin and cos from the python math library. Note that this is perhaps the poorest performing circle algorithm but it is simple to implement and easy to understand for most middle and high school math students.

The formula for each point on our circles circumference is: x = cos(i)*r + cx; y = sin(i)*r + cy; Where x and y are point coordinates, i is the angle in radians, cx and cy are the circle’s center coordinate, and r is the circle’s radius.  What we want to do is walk through 360 degrees or 2Pi radians. We could convert the degrees to radians but in practice, it works best to simply plot a minimum of 58 points. Why 58? Well 360 degrees / 6.28 ~= 58. So by plotting 58 points we walk all the way around our circle. We’ll see shortly that it’s not quite as easy as this but it’s a start.

Open your ex-05.py file and enter the code show below:


#!/usr/bin/env python3

"""
Prog:   ex-04.py

Auth:   R. Morgan

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

Lic:    This code is placed in the public domain.

"""

from graphics import *
from math import *

# Given the center x, center y
# and radius, draw a circle 
# using points.
def circle(cx, cy, r, color, win):
    # loop over 360 degree arc plotting pixels
    for i in range(0,58):
        x = cos(i)*r + cx;
        y = sin(i)*r + 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))

    circle(200, 200, 50, color_rgb(255, 128, 200), win)

    win.getMouse()
    win.close()

main()

Now if you run this code you’ll see our circle has many gaps in it’s circumference. This is because our point resolution isn’t high enough for the diameter of the circle we are drawing. So, we have two options:

  1. Shrink the size of our circle until the points meet.
  2. Draw many more points on the circumference of the circle.

Option 1 doesn’t work very well as we may need a larger circle. Option 2 reduces performance but allows us to grow the circle larger without producing gaps. So, we will take this route for now. Simply dump up the number of points produce in the loop from 58 to say 360. Now try the code.

That actually looks pretty good right? Ok, next try increasing the radius from 50 to 100 or 150 in the call to circle(). Oops! With a larger circle our missing points problem comes right back. Using this method to draw circles requires a balance between speed, size, and resolution. We can increase the circle size and resolution at the cost of speed, or reduce the resolution and increase speed at the cost of circle diameter. You’ll find much of engineering, not just Computer Science, requires constant trade-offs. Getting these right for every circumstance is as much an art as mathematics.

Above I only gave you two options for dealing with the gaps in the circles circumference. There are other methods that can be used. One such method is to use line segments to connect the points along the circumference. With this method you would simply place all your points in a list, sort them radially, and then use that list to draw line segments between them. At the end you’ll need to connect the last point to the first with another line segment. This sounds like a great exercise for the reader so I’ll leave it to you.

Drawing Circles with the Circle Method

OK, so you’ve seen that drawing lines and circles isn’t as straight forward as one might think. So let’s see how well the graphics library does on circles. We’ll add a few lines of code and compare out circles.  Edit ex-05.py to read as follows:


#!/usr/bin/env python3

"""
Prog:   ex-04.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw circles.

Lic:    This code is placed in the public domain.

"""

from graphics import *
from math import *

# Given the center x, center y
# and radius, draw a circle 
# using points.
def circle(cx, cy, r, color, win):  
    for i in range(0,360):
        x = cos(i)*r + cx;
        y = sin(i)*r + 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))

    circle(200, 200, 150, color_rgb(255, 128, 200), win)

    # Draw circle from library
    p1 = Point(200,200)
    c = Circle(p1, 200)
    c.setOutline(color_rgb(220, 128, 164))
    c.draw(win)

    win.getMouse()
    win.close()

main()

Here you can see that using the Circle method from the library follows the same pattern as the Point and Line method. First, we instantiate an object and assign it to a variable, passing coordinates as a Point object. Then we call setOutline to passing a color_rgb object to set the outline color. We could also call setFill and pass a color_rgb object but that would hide the circle we drew ourselves. So if you decide to try setFill with Circle, make the calls to draw the Circle object before calling our own circle function. 

Now if you run this code, you’ll see that the library draws a much nicer circle than we do. This tutorial is more about using and appreciating the graphics library than about all the graphics algorithms. So I’ll leave it to the reader to explore the web in search of better 2D circle drawing algorithms.

Drawing Text in the Window

OK, as promised, we will now cover drawing text. Drawing text is much more complex than circles or lines so we wont try this ourselves. However, I encourage you to give it a try and see what you can accomplish. Playing around with code and ideas will give you a deeper understanding of programming in general, and besides, its a lot of fun!

The Text() method of the graphics.py library instantiates a text object that is centered around it’s anchor point. The anchor point is an (x,y) coordinate you pass to the method to position it in the window. You also need to pass the text string to be displayed.   You can also changed the text on the Text object by calling setText and retreieve the text contained in the object by calling getText. You might think that setting the text color would require a call to setFill or setOutline. But, you would be wrong. The text object requires you call setTextColor and pass a color_rgb object. Other notable text methods include setFace which sets the font family. setAnchor to set the text position (remember the text will be centered around this position. setSize which takes an integer font-point size (not to be confused with Points) between 5 and 36.  setStyle allows you to pass in strings of “bold”, “italic”, “italic bold” and “normal”. These do eactly what you expect.

So let’s see some Text in action. Create a file named ex-06.py and add the code shown below:


#!/usr/bin/env python3

"""
Prog:   ex-04.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to draw text in
        the window.

Lic:    This code is placed in the public domain.

"""

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

width = 640
height = 480

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

    messages = {"John Zelle", "Allen Turning",
                "Tommy Flowers", "Max Newman",
                "Gordon Wlchman", "John Atanasoff",
                "Tony Sale", "William Tutte",
                "Konrad Zuse", "Howard Aiken",
                "Charles Babbage", "Ada Lovelace",
                "Jack Kilby", "Robert Noyce",
                "Grace Hopper", "Ted Hoff",
                "Doug Engelbart", "Paul Otlet"
                "George Stibitz", "Clifford Berry"}
    fonts = ["helvetica","courier", "times roman", "arial"]
    styles = ["normal", "bold", "italic", "bold italic"]

    for msg in messages:
        # randomly pick a display point
        x = randint(100, width-100)
        y = randint(20, height-20)
        # randomly pick a text color
        r = randint(0, 255)
        g = randint(0, 255)
        b = randint(0, 255)

        p = Point(x, y)
        t = Text(p, msg)
        t.setTextColor(color_rgb(r, g, b))

        size = randint(10, 20)
        t.setSize(size)
        # Randomly set the font face and style
        s = randint(0,3)
        f = randint(0, 3)
        style = styles[s]
        print("style: " + style)
        t.setStyle(style)
        font = fonts[f]
        t.setFace(font)

        # draw the text to the window
        t.draw(win)
     
    win.getMouse()
    win.close()

main()

Run the code and you should see a bunch of names randomly placed on the screen at random locations, with random styles, sizes, colors, and font families.  This is just a simple demo. BTW, if these names mean nothing to you I recommend that if you’re serious about computer science you look these people up and read some of their history and any papers they have written.

OK, we can now place text on the screen. But how can we get text from the user? For this, graphics.py contains an object called Entry. Entry objects are little text boxes you enter text into. The Entry object is just an extension of the Text object we just saw. However, it is designed to allow the programmer to retrieve text from the user. Where the Text object is designed to display text to the user. The Entry object has the same set of methods as the Text object including all the font and style methods. It centers itself on the anchor as does the Text object. The only real difference is that the user can type into it.

So, how do we use the Entry to get text from the user? It’s really very simple. Set up the Entry object the same way you do Text and then call the getText() method to read the text out. Note that reading the text is nondestructive. If you need to clear the Entry after reading text you’ll need to call setText() on the object with an empty string. 

I’ve put together a simple example but before you can use it, you need to create a file called button.py and place it in the same folder as your other project code. This file provides a button widget built up out of  Rectangle and Text objects. If you have johns version you can use it. However, modified my version show below has added features. So create a button.py file and place the button code below in it.


"""
Prog:   button.py

Auth:   John Zelle

Mods:   Randall Morgan

Desc:   This is a modified version of John Zelle's
        Button class. Randall Morgan added additional
        features to allow the button outline, background,
        font face, and text style to be change. Also added
        is a method to force a redraw on the button.

Lic:    This code released under GPL to remain 
        compliant with John's original license.

"""

from graphics import *

class Button:

    """A button is a labeled rectangle in a window.
    It is activated or deactivated with the activate()
    and deactivate() methods. The clicked(p) method
    returns true if the button is active and p is inside it."""

    def __init__(self, win, center, width, height, label):
        """ Creates a rectangular button, eg:
        qb = Button(myWin, centerPoint, width, height, 'Quit') """ 

        self.win = win
        w,h = width/2.0, height/2.0
        x,y = center.getX(), center.getY()
        self.xmax, self.xmin = x+w, x-w
        self.ymax, self.ymin = y+h, y-h
        p1 = Point(self.xmin, self.ymin)
        p2 = Point(self.xmax, self.ymax)

        self.text_color = color_rgb(0,0,0)
        self.text_family = "helvetica"
        self.text_style = "bold"
        self.fill = color_rgb(200,200,225) # fill for btn background
        self.border_color = color_rgb(255,255,255)
        
        self.rect = Rectangle(p1,p2)
        self.rect.setOutline(self.border_color)
        self.rect.setFill(self.fill) 
        self.rect.setOutline(self.border_color)
        self.rect.draw(win)

        self.label = Text(center, label)
        self.label.setTextColor(self.text_color)
        self.label.setFace(self.text_family)
        self.label.setStyle(self.text_style)
        self.label.draw(win)
        self.deactivate()
        
    def clicked(self, p):
        "Returns true if button active and p is inside"
        return (self.active and
                self.xmin <= p.getX() <= self.xmax and
                self.ymin <= p.getY() <= self.ymax)

    def getLabel(self):
        "Returns the label string of this button."
        return self.label.getText()

    def activate(self):
        "Sets this button to 'active'."
        self.rect.setFill(self.fill)
        self.rect.setWidth(2)
        self.active = True

    def deactivate(self):
        "Sets this button to 'inactive'."
        self.label.setFill(self.text_color)
        self.rect.setWidth(1)
        self.active = False

    def setFill(self, color):
        "Sets the fill color of the button"
        self.fill = color
        self.rect.setFill(self.fill)
        self.rect.setOutline(self.border_color)

    def setTextColor(self, color):
        self.text_color = color
        self.label.setTextColor(color)

    def setTextFace(family):
        self.text_family = family
        self.label.setTextFace(family)

    def setTextSyle(style):
        self.text_style = style
        self.label.setTextSyle(self.text_style)

    def setBorderColor(self, color):
        self.border_color = color
        self.rect.setOutline(self.border_color)

    def draw(self, win):
        self.rect.undraw()
        self.label.undraw()
        self.rect.draw(self.win)
        self.label.draw(self.win)

I recommend you read through the button code. It’s pretty straight forward and you may find modifications you yourself would like to make.

Time to get on with our text input demo. We will set aside any discussion of the button code and concentrate on the text Entry object’s use.Create a file named ex-07.py and add the code shown next to it. Then run the code.


#!/usr/bin/env python3

"""
Prog:   ex-07.py

Auth:   R. Morgan

Desc:   Demonstrate how to use John Zelle's
        graphics.py library to get text entered 
        by the user into an Entry object.

Lic:    This code is placed in the public domain.

"""
from graphics import *
from button import *
from random import randint
from math import *

width = 400
height = 400

words = ["Tom Collins", "John Smith", "Alan Turing", "Mike Jones"]

def show_list(offset, words, win):
    y = 40
    for word in words:
        p = Point(offset, y)
        tbox = Text(p, word)
        tbox.setTextColor(color_rgb(255,255,255))
        tbox.draw(win)
        print("show: " + word)
        y += 20

def main():
    global words

    win = GraphWin("Word Sort", width, height)
    win.setBackground(color_rgb(0,0,0))

    p = Point(100, 40)
    box = Entry(p, 20)
    box.setText("Enter a word to sort")
    box.draw(win)

    p1 = Point(100, 80)
                #win, center, width, height, label
    btn = Button(win, p1, 100, 30, "Add to list")
    btn.setFill(color_rgb(200,200,225))
    btn.setBorderColor(color_rgb(255,255,255))
    btn.setTextColor(color_rgb(0,0,0))
    btn.activate()
    btn.draw(win)
   
    show_list(300, words, win)

    while True:
        pos = win.getMouse()
        if btn.clicked(pos):
            # get the text from the 
            # Entry and put it in the list.
            # then redraw the list
            text = box.getText()
            box.setText("")
            words.append(text)
            show_list(300, words, win)

    # Close app    
    win.close()

main()

The ex-07.py app opens a window and displays a Entry fields and a few names in a list on the right. Click in the Entry field and delete the current text. Next, type a name and click the button. The name will be added to the list.

The important thing to realize here is that the call to getText() on the Entry object simply returns immediately with what ever text is in the text field. This means we need to monitor some action, such as clicking a button, to indicate we need to read the text from the Entry object. The infinite while loop handles this for us. Once the loop is entered, the system will remain in the loop until the application is closed. So the win.close() method will never be called. This will cause an error message to be printed on exit. A better approach would have been to add an Exit button so the user could click it to exit cleanly. This would only require another if statement to handle the click on the Exit button. 

Alright, I think this will do for this post. I’ll continue this post soon and add more drawing functions and more information on the library. Their might even be a simple game soon.

Hope you enjoyed the post. If you find it helpful leave a comment and let me know.


Newsletter Powered By : XYZScripts.com