2

I'm trying to create a rectangle with text on top, but the text shows below the rectangle.

How can I make the text go to the front layer?

My code so far:

from turtle import Turtle


class Rectangle(Turtle):
    def __init__(self, x, y, width, height, color='white'):
        super().__init__()
        self.penup()
        self.goto(x, y)
        self.color(color)
        self.shape('square')
        self.shapesize(stretch_wid=(height / 20), stretch_len=(width / 20))

class Writer(Turtle):
    def __init__(self, x, y):
        super().__init__()
        self.penup()
        self.goto(x, y)
        self.hideturtle()

    def writetext(self, text, font="Arial", size=8, textType="normal", color="black", x=None, y=None):
        if x is None:
            x = self.xcor()
        if y is None:
            y = self.ycor()
        self.goto(x, y)
        self.color(color)
        self.write(text, move=False, align="center", font=(font, size, textType))

class Button(Rectangle):
    def __init__(self, position, width, height, text, onclick, color="gray"):
        position = list(position)
        super().__init__(position[0], position[1], width, height, color)
        writer = Writer(position[0], position[1])
        writer.writetext(text, color="white")
        self.goto(position[0], position[1])
        self.color(color)
        self.onclick(onclick)


def start_game(_arg1=None, _arg2=None):  # requires arguments since Python turtle module passes two in when calling it using onclick
    print('game started')

Button((0, 0), 50, 20, 'click me to start the game', start_game)

I've been searching on google for over half an hour and couldn't find anything

  • How do you create instances of these classes? – mkrieger1 Feb 05 '22 at 15:22
  • I just write `Button((0, 0), 50, 20, 'click me', start_game)` and then when the button is clicked it should execute start_game, which it does, but the problem is that the text that I write appears on the back layer and not on the front layer. (`start_game` isn't defined, it's just an example) – Joshua Kordek Feb 05 '22 at 15:43

2 Answers2

2

I believe the design has some fundamental flaws that arise from subclassing Turtle.

The issue is that the Button drawing is happening outside of the constructor where the text is written. The drawing function is called automatically by the turtle library. Regardless of whether you're hooking into Turtle's classes or not, a constructor isn't typically the ideal place to draw things for this reason. I don't see a clean way to make a Button out of two separate turtles with subclassing.

A quick (but poor) fix is to override the internal method that turtle calls to update the object so your button can inject the text after the super() call draws the rectangle (you could also try to hook into drawing with _drawturtle):

class Button(Rectangle):
    # ....

    def _update(self):
        super()._update()
        Writer(*self.position()).writetext("foobar", color="white")

But since the leading _ indicates a private, internal method, this is circumventing the turtle API -- not a good idea.

A second try would be to disable the internal update loop with turtle.tracer(0) and take control of the update loop manually, but then that seems to defeat the purpose of subclassing Turtle, which is that you want things to "just work" automatically.

There's a deeper problem here, though, which is that once you get your text on top of the button as shown above in the workaround, the text blocks clicks from being detected by the rectangle underneath it.

After playing around with it quite a bit, the best alternatives I came up with were to create an image with the text pre-rendered in, or else use a global click listener and use the coordinates to determine if the click occurred on the button. The following code isn't reusable, but it's a proof of concept that you could abstract into a function or class by parameterizing everything:

import turtle
from turtle import Turtle


def handle_btn_click(x, y):
    if (x > -btn_size and y > -btn_size / 2 and 
            x < btn_size and y < btn_size / 2):
        print("game started")
        turtle.tracer(1)
        turtle.clearscreen()
        turtle.bye()

turtle.tracer(0)
r = Turtle(shape="square")
btn_size = 80
r.shapesize(btn_size // 20, btn_size // 10)
r.color("black")
turtle.update()
t = Turtle()
t.color("white")
t.ht()
text = "click me to start the game"
t.write(text, move=False, align="center", font=("Arial", 8, "normal"))
turtle.Screen().onclick(handle_btn_click)
turtle.mainloop()

As a final note, if you're only adding a single button, I suggest keeping it simple and imperative. A class structure is often overkill for most turtle apps, and the large parameter lists are a pain to deal with for only a single instance.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
  • What does `turtle.bye()` do? – Joshua Kordek Feb 06 '22 at 01:20
  • That exits the window as [the docs indicate](https://docs.python.org/3/library/turtle.html#turtle.bye). You can also comment it out, then run the program again and observe the result. The cleanup after `print()` isn't necessarily what you want to do, but it's likely you'll want to re-enable `tracer` and wipe the screen clear so you can begin drawing your game, so I included those as suggestions. – ggorlen Feb 06 '22 at 01:22
  • I'll test this once I'm on my Raspberry Pi, thanks. :) – Joshua Kordek Feb 06 '22 at 01:28
0

The text shows below the button seems to happen when you call any methods of the Button class after you wrote the text. Try the following code:

class Button(Rectangle):
    def __init__(self, position, width, height, text, onclick, color="gray"):
        #...
        writer.writetext(text, color="red")
        self.goto(position[0], position[1])
        self.color(color)
        self.onclick(onclick)
        writer.writetext("abc", color="blue")

If you set turtle.tracer with a bit of delay, you can actually see the grey button being created first, then the text in red on the grey button, then the grey button is brought back up by the three self. methods, then the text "abc" in blue written on top of everything.

Still, as mentioned in ggorlen's answer, the text will block the button from being clicked. Please refer to ggorlen's answer for the alternative solution.

wcfung14
  • 1
  • 1