Header Image - Randall Morgan

Monthly Archives

4 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 – Part 2

by SysOps 0 Comments
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!

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.


Laser Communications With Arduino Developed MPA Workshop Series

by SysOps 0 Comments

I’ve been giving a series of workshops for several months now. These workshops have been focused on understanding the basics of the underlying computer hardware. But much of that material can get rather technical and so we needed some hardware projects that didn’t involve building logic gates from transistors or designing CPU hardware in Logisim or Digital. We needed a fun project to break up the more technical material we’ve been covering. So, I discussed some ideas with the host and we came up with a simple Laser Communications System using the Arduino Nano. The project is simple enough to complete in  an evening. Exciting enough to gain interest. After all, what geek wouldn’t want to play with a laser? So I spend a bit of time developing this simple project and thought I’d share it here.

Before we begin, we’ll need a few supplies.  Tools, components and a computing device capable of running the Arduino IDE. Any laptop, table, or desktop machine should do. Some larger cellphones may even work. Check and see if you can install the Arduino IDE on your phone first, if you are intending to use your phone.

Since this is a communications system, you’ll need two systems so you have a transmitter and receiver. Each system actually functions as both so two way communications is possible. But you still need something to communicate with. The parts list is for a single unit. So, if you are building this by yourself you’ll need to build two. So let’s see what we’ll need:

  • A Solder-less Breadboard

An Arduino board with a Usb connector and headers. We used the Nano

  • Assorted jumper wires.
  • A 2n2222P NPN Transistor. T0-92 case preferred.
  • Two 39K or 47K through-hole, axial-lead 1/4 watt resistors.
  • A DTOL 10 X Mini Laser Dot Diode Module Head WL Red 650nm, 5v, 5mW. Can be found on Amazon and eBay. Part number: DTOL-SHUS1517
  • A Small Signal Photo Transistor in T-1 5mm Case. Must be sensitive to lights at 650nm. I used some clear 400nm to 1000nm Photo transistors from Amazon. They worked well.
  • A 220 or 330 ohm 1/4 watt through-hole, axial-lead resistor.

So, first let’s look at the schematic for the Laser. Here we can see a simple NPN2N2222P transistor being used in switching mode. The base of the transistor is connected to the Arduino board’s Digital Pin 2 (D2) through a 39K ohm resistor. The resistor limits the current to the base and ensures proper operation of the transistor. The current applied to the base of the transistor is multiplied by the transistor and the multiple is passed through the collector – emitter circuit. A typical 2N2222P should have a gain (multiplying effect) or around 200 or more. So what ever current we place on the base will be multiplied by ~200 and that amount of current will pass through the collector – emitter circuit. This all actually over simplified but it gives a basic understanding of the circuit. By using the transistor we can control much more current than the Arduino board’s output pins are capa

Laser Control Circuit

ble of supplying. If we were to connect the Laser to the Arduino directly we could cause damage to the Arduino. Since it’s possible that the transistor could pass more current than our Laser diode is capable of using, we could also cause damage to the laser if we did not limit the current flow through the laser with R2 a 330 Ohm resistor. The value of this resistor may be as low as 100 ohms or as high as 400 ohms. The lower the rating the more current will flow and the brighter the laser. However, the risk of damaging the laser or the surrounding circuit also increases as this number is lowered. I recommend you stick with values between 180 ohms and 330 ohms.

 

Let’s see how this sub-circuit looks on the breadboard. Note that we don’t yet have R4 connected to pin D2 of the Nano. We need the Nano for power at this time but we don’t want to connect our control circuit until we know it works as expected.

Wire up the board as shown. I recommend you use the same color wires if possible. Always use RED for positive power and BLACK for 0-Volts or ground. Make sure you use the 5-Volt pin on the Arduino board and not the 3.3-volt pin.

Laser Control Circuit

When placing the Laser on the board the long leg (Anode) should be placed so it connects to R2 the 330 ohm resistor. The short leg (on the side with the flat spot on the rim) is called the Cathode. The cathode should connect to the collector of the transistor.

When placing the transistor the pins out are as shown for most 2N2222 transistors. If your’s has a part number that begins with a “P” then the collector and emitter may be switched. But most will be exactly as shown. This should put the flat side of the transistor facing you if you wire the board as I did.

Transistor Pin Out

Once you have the transistor placed and the board wired as show, double check everything. If there is someone around who can double check your work for you, that is always helpful. Be particularly cautious of the connections to the Arduino board.

Now, first we will be a manual test of our sub-circuit to ensure it works as expected. Apply a jumper wire from the open (non connected) end of R4 to ground. Now plug the Arduino board into a USB port on your computer. The Laser should be off at this point.

Next, move the jumper wire to make a connection from the open end of R4 to the positive rail (5-volts). Now the Laser should turn on. If this works as expected you’re ready to write some test code. If not, then you need to double check everything. Make sure you have +5 volts to the positive (RED rail of the breadboard and 0-volts on the blue or black rail). Also, check that your Laser is placed with the long led connected to R2 and the other end of R2 is connected to +5 volts. Next, make sure your Transistor is placed properly and try turning it around if the part number begins with a “P”.  Have someone else check the circuit for you. A new set of eyes often sees more than the tired set that’s been looking at the same thing for hours.

Above showing R4 jumper to GND. Laser should be off. Arduino must be powered.

Above showing R4 jumper to Vcc +5 Volts. Laser should be on. Arduino must be powered.

 

Test The Laser Driver

OK, now that we have the laser working manually, it’s time to write some code. If you don’t already have Arduino IDE installed, go on over to www.arduino.cc go to the Software menu and select download. Follow their instructions for installing the IDE and getting your Arduino board up and running with the Blink example provided in the IDE. Once you have that done, or if you already have your IDE installed, move on to the next step.

Our first step will be to connect the open end of R4 to the D2 pin on the Arduino board. Next, open a new sketch in the Arduino IDE and enter the following code exactly as shown below:

Listing #1


/*
   LASER Blink
   BY: Randall Morgan

   Turns the LASER on for one second, then off for one second, repeatedly.

   We use D2 to drive the base of the switching transistor.
   D3 will be used later to read the Phototransistor.
   
   This example code is in the public domain.
   www.randallmorgan.me
   www.eastern-montana-tech.com

*/

int LASER_OUT = 2; // Laser control pin
int DELAY_MS = 1000; // 2000 ms (2 Seconds) cycle period

// the setup function runs once when you press reset or power the board
void setup() {
   // initialize digital pin 2 as an output.
   pinMode(LASER_OUT, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
   // Loop turning LASER on and off
   digitalWrite(LASER_OUT, HIGH); // turn the LED on (HIGH is the voltage level)
   delay(DELAY_MS); // wait for a second
   digitalWrite(LASER_OUT, LOW); // turn the LED off by making the voltage LOW
   delay(DELAY_MS); // wait for a second
}

Load and run this code by pressing the right facing triangle in the IDE. You should see the Laser Diode turning on and off. This should continue until you kill the power to the board.

 

Receiving Light Signals

OK, so now we have a way to send light pulses. What we need now is a way to receive light pulses. To do this we will use the Photo-transistor.  This is a transistor who’s base is exposed as a light sensitive element under the plastic package. Actually, all transistors are sensitive to light but a Photo-transistor is manufactured to take advantage of this phenomenon where other types of transistors are designed to minimize this phenomenon. Photo-transistors aren’t the only light sensitive devices we could use. Photo resistors are slower but work very well for sensing ambient light. Photo diodes work almost like Photo-transistors and can switch very fast for high speed communications.

In our project we chose the photo transistor as they are easy to work with and readily available. There is a three legged photo-transistor. The third leg is an exposed base. Basically, you can use the exposed base to adjust the sensitivity of the device. That’s a real nice feature but it complicates the design. So we chose not to use this type of photo transistor. If you purchased one of these it may work fine with the base left floating (unconnected). Sp give it a try before going out and purchasing the two legged type.

Complete Schematic

Looking at the schematic we can see that the photo transistor has it’s emitter tied to ground. The collector is connected to Vcc +5 volts through a 39K ohm resistor. This arrangement is known as a PULL-UP configuration. When the photo transistor is in the dark (or at least not receiving light on it’s base element), it is turned off and no current can flow through the transistor. This allows R1 to pull the input pin D3 on the Arduino to 5 volts. This creates a logic hi on the input. When the transistor receives light it turns on and pulls the collector down to near ground. This applies a logic lo or zero on pin D3. In this way we can sense the incoming light pulses.   Please ignore the dashed line. Fritzing kept replacing it after I removed or rerouted it. Very frustrating…

Photo Transistor Circuit Added

 

Now lets write some code to test this circuit. If you’re wondering why I’m not doing a manual test first, it’s because the circuit is very simple and about the only two things that may go wrong are that perhaps you put the photo transistor in backwards or you connect it to the wrong pin on the Arduino. If you have a multi-meter you can manually test the circuit by measuring the voltage on the collector of the photo-transistor. Cut a short piece of soda straw and place it over the photo-transistor. Then place the Laser Diode in the other end. SO that the laser shines on the end of the photo transistor. The voltage should drop on the collector to somewhere around 0.1-0.2 volts of less. If it does, you’re good to go. If it doesn’t, try turning the transistor around.

We’re going to write a little test code before we go all out and start sending messages to Mars and beyond. We use a little program to flash the laser on and off, one second in each state. During the middle of the state we’ll read the photo transistor on the digital input pin 3.

Now we need a way to communicate back to user just what the value read from the input is. We could setup an led and turn it on if the input is one or, we could just use the serial port to send the value back to the serial monitor in the Arduino IDE. I think this second approach is best and requires no additional hardware. So let’s see what this looks like:



/*
  LightReader
  By: Randall Morgan

  This test circuit simply toggles the laser connected
  to digital pin D3 on and off at a rate of 1/2 Hertz.
  After toggling the laser's state it then reads the
  input from the photo transistor on digital pin 2.

  To run this program you'll need an Arduino board
  with a laser diode connected via an npn switching
  transistor to digital pin 3.

  In addition you'll need a photo transistor connected
  to a resistor in a pull-down arrangment and the
  junction of the pull-up resistor and the photo
  transistor collector connected to digital pin 2 on
  the Arduino board.

  See the article at: http://www.randallmorgan.me/blog/
  Search for Arduino Laser Communications

  This code is placed in the public domain
*/

int LASER_OUT = 5; // Laser control pin
int PHOTOTRANS_IN = 3; // Photo transitor sense pin
int DELAY_MS = 1000; // 2000 ms (2 Seconds) cycle period
int val = 0; // input value
char buf[100];

// the setup function runs once when you press reset or power the board
void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LASER_OUT, OUTPUT);
  pinMode(PHOTOTRANS_IN, INPUT);

  // Set up serial port
  Serial.begin(9600);
  // Wait for hardware serial to connect
  while(!Serial) {
    // wiating for connection....
  }
  Serial.print("Connected\n");
}

// the loop function runs over and over again forever
void loop() {
  // Turn on laser and read photo transistor
  digitalWrite(LASER_OUT, HIGH); // turn the LED on (HIGH is the voltage level)
  delay(DELAY_MS/2); // wait for a second
  val = digitalRead(PHOTOTRANS_IN); // read the photo transistor
  sprintf(buf, " Val: %d \n", val);
  Serial.print(buf);
  delay(DELAY_MS/2);

  // turn off laser and read photo transistor
  digitalWrite(LASER_OUT, LOW); // turn the LED off by making the voltage LOW
  delay(DELAY_MS/2);
  val = digitalRead(PHOTOTRANS_IN);
  sprintf(buf, " Val: %d \n", val);
  Serial.print(buf);
  delay(DELAY_MS/2);
}


Reviewing the code we can see two interesting additions. First, we call Serial.begin() in the setup() function and pass it the baud rate we want to use. Since the default baud rate for the serial monitor in the IDE is 9600, we use that. Next we wait for the serial library to get a connection to the host computer. The while loop performs this wait for us and exits when Serial returns true. This occurs when the serial library has a connection to the host.

In the function loop() we added calls to SerialWrite() and pass in a buffer containing a string of characters to send to the host. We cheated a little and initialized a character array of 100 characters. Ideally, you’d want to safe guard against buffer overflow here. If you don’t know what that is, do a little googling and read up on it.

Before we can send the value read from the digital input pin to the host we must convert it to a string representation. What we read is actually a number not a character. So, we pass the buffer and the value to the C function sprintf() for formatting and conversion. You can find good information on many C and C++ functions and libraries that can be used with Arduino googling the “Standard C/C++ libraries”.

OK, so now we can control our laser and sense when a laser is on or off when pointed at out photo transistor. The only piece we are lacking now is the code to allow us to communicate with another unit. To accomplish this, we only need to setup a software serial port. Software serial ports work similar to hardware serial ports except they are just routines running to read and send bits on the I/O pins.

The serial library requires the use of interrupts. An interrupt is a signal to the processor to stop what it is doing and service another routine. Since the SoftwareSerial routine needs to be called regualrly so it doesn’t miss a bit, it requires inputs that are interrupt capable. Both D2 and D3 are interrupt capable pins but they are not the only ones on the Arduino. So if you used other pins, you’ll need to move your control signals to pins with interrupt support. The SoftwareSerial library is shipped with Arduino IDE so there is no need to download and install another library.

So, let’s talk about what we need to accomplish with our code. First, we need to send data on the laser. We can do this by setting the laser control pin as the software serial libraries transmit pin. In order to receive data we simple need to set the photo transistor’s sense pin as the software serial libraries receive pin. Then we simple use the hardware serial port to send anything received on the software serial port to the host. Anything we receive from the host on the hardware port can be sent on by the software serial port. So let’s see how we can implement this:


/*
   LightReader
   By: Randall Morgan

   This test circuit simply toggles the laser connected
   to digital pin D3 on and off at a rate of 1/2 Hertz. 
   After toggling the laser's state it then reads the 
   input from the photo transistor on digital pin 2.

   To run this program you'll need an Arduino board
   with a laser  diode connected via an npn switching 
   transistor to digital pin 3.

   In addition you'll need a photo transistor connected 
   to a resistor in a pull-down arrangment and the 
   junction of the pull-up resistor and the photo 
   transistor collector connected to digital pin 2 on
   the Arduino board.

   See the article at: http://www.randallmorgan.me/blog/
   Search for Arduino Laser Communications 

   This code is placed in the public domain
*/

// Tell the IDE we want to use 
// the SoftwareSerial library
#include 

int LASER_OUT = 2;      // Laser control pin
int PHOTOTRANS_IN = 3;  // Photo transitor sense pin
int DELAY_MS = 1000;    // 2000 ms (2 Seconds) cycle period
int val = 0;            // input value
char buf[100];


SoftwareSerial laserSerial(PHOTOTRANS_IN, LASER_OUT);

// the setup function runs once when you press reset or power the board
void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LASER_OUT, OUTPUT);
  pinMode(PHOTOTRANS_IN, INPUT);  

  // Set up serial port
  Serial.begin(9600);
  // Wait for hardware serial to connect
  while(!Serial) {
    // wiating for connection....
  }

  Serial.print("Connected\n");

  // Setup Software Serial
  laserSerial.begin(57600);

  // Now we start sending data to 
  // allow the users to line up the
  // lasers and photo transistors.
  // Once the user hits a key, we 
  // stop and begin transceiving 
  // data on the serial ports.
  while(!Serial.available()){ 
    laserSerial.write("Light is beautiful!...");  
  }
  
}


// the loop function runs over and over again forever
void loop() {
  // If the photo transistor has received 
  // and data, send it to the host
  if(laserSerial.available()) {
    Serial.write(laserSerial.read());
  }

  if(Serial.available()) {
    laserSerial.write(Serial.read());
  }
}


Now enter the code above into the Arduino IDE and upload it to your Arduino board. The system should begin sending the phrase “Light is beautiful!…” immediately. Use this phrase data to line up your laser with the second unit’s photo transistor. Once the alignment is complete, hit a key on the keyboard and the transmission should stop. The system is now in transceiver mode. Anything you type into the serial monitor will be sent to the second unit. Anything the second unit sends will be sent to the serial monitor for display.

Congratulations, you now have completed the project. But this is only the beginning. There are many improvements to both the code and the circuit that can be made. Take the circuit presented here and make changes to it. Share those changes with others and let’s see what you come up with.

Until next time, Happy Hacking!

Newsletter Powered By : XYZScripts.com