Sam Geisler
Sam Geisler

Reputation: 15

Python Tkinter - Draw Shape to Screen Without Creating New Instance

I'm tying to create a basic program for drawing to the screen by creating ovals every frame when the mouse is clicked. However, as the program runs for a bit, it starts becoming very choppy and the circles stop forming cohesive lines, due to the code not running fast enough to process precise mouse movement.

Here is my code -

import tkinter as tk

DRAW_HEIGHT = 560
DRAW_WIDTH = 560
PALETTE_HEIGHT = 40

def draw_palette(canvas):
    canvas.create_rectangle(0, 0, DRAW_WIDTH, PALETTE_HEIGHT, fill = 'light grey', width= 0)
    canvas.create_rectangle(DRAW_WIDTH/8, PALETTE_HEIGHT/5, 3*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill = 'dark grey', width = 1)
    canvas.create_rectangle(5*DRAW_WIDTH/8, PALETTE_HEIGHT/5, 7*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill = 'dark grey',width = 1)
    canvas.create_text(DRAW_WIDTH/4, PALETTE_HEIGHT/2, text = 'clear screen') #non-functional
    
class Brush():
    def __init__(self,stroke_size,stroke_color):
        self.size = stroke_size
        self.color = stroke_color
        self.mode = 'draw'
        self.pos = (0,0)
        self.clicked = False
        
    def render(self,canvas):
        if self.clicked:
            canvas.create_oval( self.pos.x-self.size/2, self.pos.y-self.size/2,
                                self.pos.x+self.size/2, self.pos.y+self.size/2,
                                width = 0, fill = self.color )
            
    def mouse_moved(self,event):
        self.pos = event
    
    def mouse_clicked(self,throwaway):
        self.clicked = True
    
    def mouse_released(self,throwaway):
        self.clicked = False


#set up root window and canvas
root = tk.Tk()
root.geometry('{}x{}'.format(DRAW_WIDTH,DRAW_HEIGHT+PALETTE_HEIGHT))
c = tk.Canvas(root, width = DRAW_WIDTH, height = DRAW_HEIGHT + PALETTE_HEIGHT, bg = 'white')
c.pack()

b = Brush(40,'black')

#bind actions to functions
c.bind("<Button-1>",b.mouse_clicked)
c.bind("<ButtonRelease-1>",b.mouse_released)
c.bind("<Motion>",b.mouse_moved)

#main loop
while 1:
    b.render(c)
    draw_palette(c)
    root.update()

I suppose I'm just asking if there's any way I can speed this up, but specifically I'm wondering if I can draw the shapes to the screen without using create_shape() every time.

For example,

oval = c.create_oval()

while 1:
    canvas.draw(oval)

I know you can do something similar with canvas.move(), but I couldn't find anything that fit my situation.

Upvotes: 0

Views: 1284

Answers (1)

furas
furas

Reputation: 142641

I don't understand why you created loop while 1 and run render() and draw_palette() hundreds of times even if you don't need it.

I draw new circle in mouse_moved() and use root.mainloop() and it runs much better and create smoother line. Probably if I would draw line from previous place to current place or many ovals with some step then I would get even better line

EDIT: I changed little to draw first oval in mouse_click() - so I can see first oval even if I only click and don't move.

import tkinter as tk

# --- constanst ---

DRAW_HEIGHT = 560
DRAW_WIDTH = 560
PALETTE_HEIGHT = 40

# --- classes ---

class Brush():
    
    def __init__(self,stroke_size,stroke_color):
        self.size = stroke_size
        self.color = stroke_color
        self.mode = 'draw'
        self.pos = (0,0)
        self.clicked = False
        
    def draw(self):
        s = self.size/2
        c.create_oval(
            self.pos.x-s, self.pos.y-s,
            self.pos.x+s, self.pos.y+s,
            width=0, fill=self.color
        )
        
    def mouse_moved(self, event):
        if self.clicked:
            self.pos = event
            self.draw()

    def mouse_clicked(self, event):
        self.clicked = True
        self.pos = event
        self.draw()

    def mouse_released(self, event):
        self.clicked = False

# --- functions ---

def draw_palette(canvas):
    canvas.create_rectangle(0, 0, DRAW_WIDTH, PALETTE_HEIGHT, fill='light grey', width=0)
    canvas.create_rectangle(DRAW_WIDTH/8, PALETTE_HEIGHT/5, 3*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill='dark grey', width=1)
    canvas.create_rectangle(5*DRAW_WIDTH/8, PALETTE_HEIGHT/5, 7*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill='dark grey', width=1)
    canvas.create_text(DRAW_WIDTH/4, PALETTE_HEIGHT/2, text='clear screen') #non-functional

# --- main ---

#set up root window and canvas
root = tk.Tk()
root.geometry('{}x{}'.format(DRAW_WIDTH, DRAW_HEIGHT+PALETTE_HEIGHT))

c = tk.Canvas(root, width=DRAW_WIDTH, height=DRAW_HEIGHT+PALETTE_HEIGHT, bg='white')
c.pack()

b = Brush(40, 'black')

#bind actions to functions
c.bind("<Button-1>", b.mouse_clicked)
c.bind("<ButtonRelease-1>", b.mouse_released)
c.bind("<Motion>", b.mouse_moved)

draw_palette(c)

root.mainloop()

EDIT:

I added function which adds ovals if distance between previous and current position is too big and there is gap. Now line is smooth even if mouse moves fast.

import tkinter as tk

# --- constanst ---

DRAW_HEIGHT = 560
DRAW_WIDTH = 560
PALETTE_HEIGHT = 40

# --- classes ---

class Brush():
    
    def __init__(self,stroke_size,stroke_color):
        self.size = stroke_size
        self.color = stroke_color
        self.mode = 'draw'
        self.pos = None
        self.prev = None
        self.clicked = False
        
    def draw_oval(self, x, y):
        r = self.size/2 # radius
        c.create_oval(x-r, y-r, x+r, y+r, width=0, fill=self.color)
        
    def draw(self):
        if self.pos:
            self.draw_oval(self.pos.x, self.pos.y)
            
        if self.prev:
            # calculate distance between ovals
            dx = self.pos.x - self.prev.x
            dy = self.pos.y - self.prev.y
            
            max_diff = max(abs(dx), abs(dy))
            
            # add ovals if distance bigger then some size of oval (tested with //4, //8, //6, //5)
            if max_diff > (self.size//6):
                
                # how many ovals to add
                parts = max_diff//(self.size//6)
                
                # distance between ovals
                step_x = dx/parts
                step_y = dy/parts

                # add ovals except first which is already on canvas 
                for i in range(1, parts):
                    x = self.pos.x - i*step_x
                    y = self.pos.y - i*step_y
                    self.draw_oval(x, y)
                        
    def mouse_moved(self, event):
        if self.clicked:
            self.prev = self.pos
            self.pos = event
            self.draw()

    def mouse_clicked(self, event):
        self.clicked = True
        self.prev = None
        self.pos = event
        self.draw()

    def mouse_released(self, event):
        self.clicked = False
        self.prev = None
        self.pos = None

# --- functions ---

def draw_palette(canvas):
    canvas.create_rectangle(0, 0, DRAW_WIDTH, PALETTE_HEIGHT, fill='light grey', width=0)
    canvas.create_rectangle(DRAW_WIDTH/8, PALETTE_HEIGHT/5, 3*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill='dark grey', width=1)
    canvas.create_rectangle(5*DRAW_WIDTH/8, PALETTE_HEIGHT/5, 7*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill='dark grey', width=1)
    canvas.create_text(DRAW_WIDTH/4, PALETTE_HEIGHT/2, text='clear screen') #non-functional

# --- main ---

#set up root window and canvas
root = tk.Tk()
root.geometry('{}x{}'.format(DRAW_WIDTH, DRAW_HEIGHT+PALETTE_HEIGHT))

c = tk.Canvas(root, width=DRAW_WIDTH, height=DRAW_HEIGHT+PALETTE_HEIGHT, bg='white')
c.pack()

b = Brush(40, 'black')

#bind actions to functions
c.bind("<Button-1>", b.mouse_clicked)
c.bind("<ButtonRelease-1>", b.mouse_released)
c.bind("<Motion>", b.mouse_moved)

draw_palette(c)

root.mainloop()

Upvotes: 1

Related Questions