Title: Draw on a scrolled image in Python
My post Scroll a scaled image in Python shows how you can display an image at different scales and scroll it. This example shows how you can interact with the scaled image, in this case by drawing on it. In the picture on the right, you can see that I've drawn white circles showing places where the Mandelbrot set contains smaller copies of itself.
After you draw on the image, scrolling the drawing is easy. Any lines that you draw in the Canvas widget are contained inside that widget, so when the program scrolls the Canvas the lines scroll, too.
The trickier part occurs when you change the image's scale because you also need to scale the drawn lines.
|
Note that the Canvas widget's idea of a line is really a polyline: a sequence of points connected by line segments. You can use techniques similar to those shown here to draw other shapes like single lines, rectangles, ellipses, and more.
|
Drawing
You can draw on the Canvas widget the same way you always draw: catch mouse down, move, and up events and add points to a list that determines how the line is drawn.
The example executes the following statements as part of its startup code.
# Watch for mouse events.
self.canvas.bind('', self.mouse_down)
self.canvas.bind('', self.mouse_move)
self.canvas.bind('', self.mouse_up)
# No drawing yet.
self.lines = [] # A list of lists of points for the lines.
self.new_line = None # The ID of the line we are drawing.
self.new_points = None # The points for the line we are drawing.
self.current_scale = 1.0 # The current scale.
The first three statements bind the Canvas widget's mouse down, move, and up events. The other four statements initialize a few variables to indicate that we have not yet drawn any lines and that no line drawing is in progress.
When you press the left mouse button down to start a new line, the following code executes.
def mouse_down(self, event):
'''Begin drawing.'''
self.new_points = [(event.x, event.y), (event.x, event.y)]
self.lines.append(self.new_points)
self.new_line = self.canvas.create_line(self.new_points, fill='white',
width=3)
This code sets self.new_points to a new list holding two copies of the mouse's current position. We only want one, but the create_line method gets annoyed if you don't give it at least two points.
Next, the code adds the new list to self.lines. It then calls the Canvas widget's create_line method to make the initial line and saves the new line's ID in self.new_line. The new line is white and three pixels wide so it stands out on the Mandelbrot set, but you can change that if you like.
When you move the mouse with the left mouse button pressed, the following code executes.
def mouse_move(self, event):
'''Continue drawing.'''
if self.new_line is None: return
self.new_points.append((event.x, event.y))
self.canvas.coords(self.new_line, self.new_points)
If there is no new line in progress, this method simply returns.
Otherwise, the code adds the mouse's current position to self.new_points and then calls the Canvas widget's coords method to update the coordinates used by the new line.
Finally, when you release the left mouse button, the following code executes.
def mouse_up(self, event):
'''Finish drawing.'''
self.new_points = None
self.new_line = None
This code simply sets the new_points and new_line lists to None to indicate that we're done drawing.
Scaling Drawings
As I mentioned earlier, when you rescale the image, you also need to rescale any lines that you have drawn. Here's the code that resizes the current image with the new code highlighted in blue.
def mnu_scale(self):
'''Show the image at the desired scale.'''
# Display the scale in the Scale menu.
new_scale = self.scale_var.get()
scale_str = int(new_scale * 100)
self.menu_bar.entryconfig(2, label=f'Scale ({scale_str}%)')
# Display the image at the desired scale.
self.show_image()
# Rescale the drawn lines.
scale_by = new_scale / self.current_scale
# With nested list comprehensions.
self.lines = [
[(scale_by * x, scale_by * y) for (x, y) in line]
for line in self.lines
]
# With a list comprehension and a loop.
# new_lines = []
# for line in self.lines:
# new_line = [(scale_by * x, scale_by * y) for (x, y) in line]
# new_lines.append(new_line)
# self.lines = new_lines
self.current_scale = new_scale
# Draw the rescaled lines.
for line in self.lines:
self.canvas.create_line(line, fill='white', width=3)
The code updates the Scale menu's caption and calls show_image more or less as before. It then divides the new scale factor by the current scale factor. that gives the amount by which any current drawings must be scaled.
Next, the code uses a list comprehension to scale the points in all of the drawings by the scale_by value. If you don't like the nested list comprehension, you can use a loop and a simpler comprehension. (If you don't like comprehensions at all, you can use nested loops.)
The code then updates self.current_scale to the new scale value. It finishes by looping through the lines and recreating them on the Canvas.
Loading Images
The last change is in the following code.
def mnu_open_image(self):
'''Open an image file.'''
# Delete any previous line points.
self.lines = []
# Load the image.
self.pil_image = self.get_image_file('Load Image')
# Display the image at the desired scale.
self.show_image()
When you open a new image file, the code first sets self.lines to a new list, essentially discarding any previous drawing. It then loads the new image as before.
Conclusion
Adding drawing to the example isn't too hard. You track mouse events as you normally do when drawing. The only real trick is in scaling the drawing points any time you scale the image.
Download the example to experiment with it and to see additional details.
One change you might like to make is to scale the line thickness when you scale the image.
|