Title: Draw and save more shapes with the scribble program in Python and tkinter
This new version of the program includes changes in three main areas: new shapes, deserialization, and fill colors.
New Shapes
It's pretty easy (and fun) to add new shapes to the original scribble program. For example, search the code for "oval," copy the code that draws ovals, and make whatever changes are appropriate for your new shape.
For a specific example, the star-drawing tool mostly requires changes when you click the tool and when you're drawing. Here's the code that executes when you click the tool.
case 'tool_star':
# Get the number of points the star should have.
self.num_star_points = simpledialog.askinteger(
'Star Points', '# Points:\t\t\t\t\t')
if self.num_star_points is None:
# Cancel.
self.select_tool('tool_arrow')
return
self.canvas.config(cursor='cross')
self.canvas.bind('<Button-1>', self.star_mouse_down)
self.canvas.bind('<B1-Motion>', self.star_mouse_move)
self.canvas.bind('<ButtonRelease-1>', self.star_mouse_up)
This code displays a dialog where you can enter the number of points that you want the star to have.
Note that the way the program currently works means you do not need to click the tool again to draw new stars after the first one. If you don't click the tool again, the dialog isn't displayed so any subsequent stars have the same number of points. You can always click the tool again to enter a new number of points.
Most of the rest of the new star-oriented code is in its mouse event handlers.
def polygon_mouse_down(self, event):
# Save the starting point.
self.start_point = (event.x, event.y)
def polygon_mouse_move(self, event):
# Ignore extraneous events.
if self.start_point is None: return
# If there is a current polygon, remove it.
if self.current_polygon is not None:
self.canvas.delete(self.current_polygon)
# Create a polygon.
self.end_point = (event.x, event.y)
width, dash, outline, fill = self.get_drawing_parameters()
self.draw_polygon(self.num_polygon_sides,
self.start_point, self.end_point,
width=width, dash=dash, outline=outline, fill=fill)
def draw_polygon(self, num_polygon_sides, start_point, end_point, width, dash, outline, fill):
cx = (start_point[0] + end_point[0]) / 2
cy = (start_point[1] + end_point[1]) / 2
rx = (start_point[0] - end_point[0]) / 2
ry = (start_point[1] - end_point[1]) / 2
points = []
theta = math.pi / 2 # Put the first vertex at the top.
dtheta = 2 * math.pi / self.num_polygon_sides
theta_limit = theta + 2 * math.pi - dtheta / 2
for i in range(self.num_polygon_sides):
points.append((
cx + rx * math.cos(theta),
cy + ry * math.sin(theta),
))
theta += dtheta
self.current_polygon = self.canvas.create_polygon(points,
width=width, dash=dash, outline=outline, fill=fill)
def polygon_mouse_up(self, event):
# Ignore extraneous events.
if self.start_point is None: return
# We are no longer creating an polygon.
self.start_point = None
self.end_point = None
self.current_polygon = None
The mouse down, mouse move, and mouse up functions are similar to those used to draw ovals. The big difference is that the polygon_mouse_move method calls draw_polygon to draw the current polygon. That method uses some simple trigonometry to draw the star.
Download the program to see how it handles the other new shapes (rectangle, polygon, and text), and to try adding new shapes of your own. (Perhaps free-form polygons, arrows, hearts, or whatever.)
Deserialization
The program's serialization code miraculously handles new shapes without any changes. Download the example to see how that works.
Unfortunately, you do need to change the deserialization code slightly to handle new shapes. Here's this example's deserialization method.
def deserialize_canvas(canvas, data):
# Delete any existing items.
canvas.delete(tk.ALL)
# Deserialize the JSON data.
items = json.loads(data)
# Recreate the items.
for item in items:
coords = item['coords']
options = item['options']
if item['type'] == 'line':
canvas.create_line(*coords, **options)
elif item['type'] == 'oval':
canvas.create_oval(*coords, **options)
elif item['type'] == 'rectangle':
canvas.create_rectangle(*coords, **options)
elif item['type'] == 'polygon': # Includes stars.
canvas.create_polygon(*coords, **options)
elif item['type'] == 'text':
canvas.create_text(*coords, **options)
This code now handles three new shape types: rectangle, polygon, and text. Note that stars are built as polygons so the polygon type handles them as well as regular polygons. If you add other polygons like irregularly shaped ones drawn by the user, they would also fit into this type.
Fill Colors
I decided to add a fill color button to the toolbar and that presented a few problems, mainly how to draw the fill color tool and how to let the user select a transparent fill color.
Drawing the Fill Color Tool
The program uses ImageTk.PhotoImage objects to represent the tool button images. Unfortunately that object has very limited drawing capabilities. Basically your only option is the put method, which fills a rectangular area with a color. (Perhaps I should have used a PIL image for drawing the tool and then converted it into a PhotoImage for display, but I'm lazy.)
The following set_fill_color method saves a new fill color and creates the tool's image.
def set_fill_color(self, color):
# Save the selected color.
self.fill_color = color
# Draw the selected color on the button.
x1 = 1
x2 = self.tool_wid - 1
y1 = 3
y2 = self.tool_hgt - 4
self.fill_button_image.put('black', to=(x1,y1,x2,y2))
if color == '': color = '#F0F0F0'
self.fill_button_image.put(color, to=(x1+1,y1+1,x2-1,y2-1))
After saving the color, this code fills part of the image with black. It then fills a slightly smaller rectangle with the fill color. The result is a one-pixel-wide rectangle filled with the fill color.
The color '' represents the transparent color. If the fill color is now transparent, the code fills the rectangle with the color #F0F0F0, which is the tool button's background color on my system. (I think drawing an X on top of it would be nice, but PhotoImage doesn't provide that capability. I also can't figure out how to make the program determine the button's default background color.)
Selecting Transparent Colors
The tkinter color chooser dialog has a big problem: it doesn't let you select transparent or translucent colors. To do that, you would need to build your own color selection dialog or let the user select a transparency value or something. Instead of doing that (see my earlier comment about being lazy), I just use a transparent color if you cancel the dialog. That means you can't keep the same color if you regret opening the dialog, but at least this approach is easy.
The following code displays the fill color chooser.
def choose_fill_color(self):
color = self.fill_color
if color == '': color = 'white'
color = colorchooser.askcolor(title='Fill Color',
initialcolor=color)
if color[1] is None:
self.set_fill_color('')
else:
self.set_fill_color(color[1])
The code first gets the current fill color. If that value is transparent, the code sets color to white. The code then displays the chooser, setting its initial color to color. (It has to treat the transparent color specially because the chooser cannot display that color. That's why the program uses white.)
The chooser returns a tuple with the following format.
((255, 128, 255), '#ff80ff')
If you cancel the dialog, it returns (None, None).
If the second return value is None, the code calls set_fill_color (which you saw earlier) passing it the transparent color. If the second parameter is not None, the code calls set_fill_color passing it the selected color.
The code that handles the outline color tool works similarly.
Summary
At this point, the program can draw an assortment of shapes with different outline and fill colors. Feel free to modify it to handle other shapes. (In fact, that's a good exercise.)
The only other thing I'm currently planning to add is document handling capabilities. For example, if you start a new drawing and then try to close the program, it should ask if you want to save your changes and it should let you decide to cancel so the program keeps running. Stay tuned...
Download the example to see all of the details.
|