Reputation: 3944
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:
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
Reputation: 386332
I think the problem is that:
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
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