Henry
Henry

Reputation: 3944

Stop text from overlapping in Tkinter canvas

I am creating a game in the tkinter canvas which involves generating text (1 or 2 digit numbers) and I've gotten that to work, but I can't work out how to display them so they don't overlap. At the moment I have this:

import tkinter as tk
from tkinter import font
import random
BOX_SIZE = 300
class Game(tk.Tk):
    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)
        self.config(bg = "white")
        self.numBox = tk.Canvas(self, height = BOX_SIZE, width = BOX_SIZE , bg = "white", highlightthickness = 0)
        self.numBox.pack(expand = True)
        self.score = 0
        self.numberSpawn()
    def placeNumber(self, value):
        validSpawn = False
        attempts = 0
        maxAttempt = False
        while not validSpawn and not maxAttempt:
            attempts += 1
            if attempts > 20:
                maxAttempt = True
                attempts = 0
            size = random.choice([24,36,48,72])
            coord = [random.randint(40,BOX_SIZE - 40) for x in range(2)]
            self.numBox.update()
            pxSize = tk.font.Font(size = size, family = "Times New Roman").measure(value)
            if len(str(value)) == 1:
                secondCoords = [coord[0] + pxSize *2.5 , coord[1] + pxSize]
            else:
                secondCoords = [x + pxSize for x in coord]
            if not self.numBox.find_overlapping(*coord, *secondCoords):
                validSpawn = True
        if not maxAttempt:
            newTxt = self.numBox.create_text(*coord, font = ("Times New Roman",size), text = value)
    def numberSpawn(self):
        self.maxNum = random.randint(3,19)
        self.placeNumber(self.maxNum)
        for i in range(random.randint(4, 16)):
            num = random.randint(0, self.maxNum-1)
            self.placeNumber(num)
        
app = Game()
app.mainloop()

value is the number to be displayed, BOX_SIZE is the dimensions of the canvas. I tried using this to stop the text overlapping and this to find the pixel size of the text before creating it. Despite this, the text still overlaps like this:
Overlapping numbers
I'm not sure how to fix this, or why it doesn't work as it is. Any help is appreciated.

Upvotes: 0

Views: 883

Answers (2)

Bryan Oakley
Bryan Oakley

Reputation: 386332

I think the problem is that:

  1. you are giving up too soon, and
  2. when you hit your limit of attempts, you add the text whether it overlaps or not

You should bump the number of attempts up considerably (maybe a few hundred), and then if the number exceeds the maximum then you shouldn't draw the text.

I think a better strategy might be to first draw the text item, then use the bbox of the method to compute the actual amount of space taken up by the item. Then, use that to find overlapping items. The just-created item will always overlap, but if the number of overlapping is greater than 1, pick new random coordinates.

For example, something like this perhaps:

def placeNumber(self, value):
    size = random.choice([24,36,48,72])
    coord = [random.randint(40,BOX_SIZE - 40) for x in range(2)]
    newTxt = self.numBox.create_text(*coord, font = ("Times New Roman",size), text = value)

    for i in range(1000):  # 1000 is the maximum number of tries to make
        bbox = self.numBox.bbox(newTxt)
        overlapping = self.numBox.find_overlapping(*bbox)
        if len(overlapping) == 1:
            return

        # compute new coordinate
        coord = [random.randint(40,BOX_SIZE - 40) for x in range(2)]
        self.numBox.coords(newTxt, *coord)

    # delete the text since we couldn't find a space for it.
    self.numBox.delete(newTxt)

Either algorithm will be slow when there isn't much free space. When I created a 1000x1000 canvas with 100 numbers, it laid them out with zero overlaps in under a second.

Upvotes: 1

Joel Toutloff
Joel Toutloff

Reputation: 484

Here is a solution for you:

import tkinter as tk
from tkinter import font
import random

def checkOverlap(R1, R2):
    if (R1[0]>=R2[2]) or (R1[2]<=R2[0]) or (R1[3]<=R2[1]) or (R1[1]>=R2[3]):
         return False
    else:
          return True

def go():
    validSpawn = False
    while not validSpawn:
        value = random.randint(1,99)
        size = random.choice([24,36,48,72])
        coord = [random.randint(40,500 - 40) for x in range(2)]
        new_number = canvas.create_text(*coord, font = ("Times New Roman",size),text=value)
        new_box = canvas.bbox(new_number)
        canvas.itemconfigure(new_number, state='hidden')
        validSpawn = True
        for i in canvas.items:
            this_box = canvas.bbox(i)
            if checkOverlap(this_box, new_box):
                validSpawn = False
                break
    canvas.itemconfigure(new_number, state='normal')
    canvas.items.append(new_number)

root = tk.Tk()

canvas = tk.Canvas(root, width = 500, height = 500, bg='white')
canvas.items = []
canvas.pack()
btn = tk.Button(root, text="Go", command=go)
btn.pack()

root.mainloop()

Instead of having it try to figure out how big the item was going to be, I just had it draw it, take its measurements, hide it and then look for overlaps and either delete it or show it based on the results. You will need to add in your maximum tries in there or it does start to get slower the more numbers there are on the screen. It should not draw a frame in the middle of a function so the user will never see it there while it is taking the measurement.

I also had it keep an array of all the number that are saved to the screen so I can loop through them and run my own overlapping function. That's just how I like to do it, you can go back to using find_overlapping and it should still work.

Upvotes: 1

Related Questions