Python Turtle 在容器中写入值

Python Turtle Write Value in Containing Box

我希望能够通过子类化 turtle.Turtle 来创建一些显示值的海龟。

这些海龟应该将它们的值显示为以它们自己的形状为中心的文本。我还希望能够准确定位海龟,因此 setting/determining 它们相对于给定字体大小的宽度和高度很重要。

这是我目前的尝试:

我认为这个答案是相关的: 但它很旧,它在文本周围绘制的边界框使用 python 3.8 时不在正确的位置。

import turtle

FONT_SIZE = 32

class Tile(turtle.Turtle):
    def __init__(self):
        super().__init__(shape="square")
        self.penup()
    
    def show_value(self, val):
        self.write(val, font=("Arial", FONT_SIZE, "bold"), align="center")


screen = turtle.Screen()
vals = [5, 7, 8, 2]
for i in range(len(vals)):
    tile = Tile()
    tile_size = (FONT_SIZE / 20)
    tile.shapesize(tile_size)
    tile.fillcolor("red" if i % 2 == 0 else "blue")
    tile.setx(i * FONT_SIZE)
    tile.show_value(vals[i])
turtle.done()

It would be very helpful to have Turtle Objects containing text such as integer values, which can be used to display a variety of puzzles and games, and can have their own click handlers attached.

这就是问题所在,其他答案中建议的使用 stamp() 的(两个)原因是行不通的。首先,您不能点击隐藏的海龟:

from turtle import *

def doit(x, y):
    print("Just do it!")

yertle = Turtle()
# comment out the following line if you want `onlick()` to work
yertle.hideturtle()
yertle.shape('square')
yertle.stamp()
yertle.onclick(doit)

done()

图章 不是可点击的实体。其次,你甚至不能点击这个或另一个乌龟留下的墨水后面的乌龟:

from turtle import *

def doit(x, y):
    print("Just do it!")

yertle = Turtle()
yertle.shape('square')
yertle.fillcolor('white')
yertle.onclick(doit)

myrtle = Turtle()
myrtle.shape('turtle')
myrtle.penup()
myrtle.sety(-16)
# comment out the following line if you want `onlick()` to work
myrtle.write('X', align='center', font=('Courier', 32, 'bold'))
myrtle.goto(100, 100)  # move myrtle out of the way of clicking

done()

如果您点击字母 'X',除非您设法点击字母之外的方块部分,否则什么也不会发生。我的信念是,尽管我们将 'X' 视为 dead ink 而不是我们的 live turtle,但在 tkinter 级别它们都是相似的,可能两者都能够接收事件,所以一个会掩盖另一个的点击。

那么我们该怎么做呢?我要使用的方法是制作一个 tile 带有图像的海龟,其中图像是通过写入位图生成的:

tileset.py

from turtle import Screen, Turtle, Shape
from PIL import Image, ImageDraw, ImageFont, ImageTk

DEFAULT_FONT_FILE = "/Library/Fonts/Courier New Bold.ttf"  # adjust for your system
DEFAULT_POINT_SIZE = 32
DEFAULT_OUTLINE_SIZE = 1
DEFAULT_OUTLINE_COLOR = 'black'
DEFAULT_BACKGROUND_COLOR = 'white'

class Tile(Turtle):
    def __init__(self, shape, size):
        super().__init__(shape)
        self.penup()

        self.size = size

    def tile_size(self):
        return self.size

class TileSet():

    def __init__(self, font_file=DEFAULT_FONT_FILE, point_size=DEFAULT_POINT_SIZE, background_color=DEFAULT_BACKGROUND_COLOR, outline_size=DEFAULT_OUTLINE_SIZE, outline_color=DEFAULT_OUTLINE_COLOR):
        self.font = ImageFont.truetype(font_file, point_size)
        self.image = Image.new("RGB", (point_size, point_size))
        self.draw = ImageDraw.Draw(self.image)

        self.background_color = background_color
        self.outline_size = outline_size
        self.outline_color = outline_color

    def register_image(self, string):
        width, height = self.draw.textsize(string, font=self.font)
        image = Image.new("RGB", (width + self.outline_size*2, height + self.outline_size*2), self.background_color)
        draw = ImageDraw.Draw(image)
        tile_size = (width + self.outline_size, height + self.outline_size)
        draw.rectangle([(0, 0), tile_size], outline=self.outline_color)
        draw.text((0, 0), string, font=self.font, fill="#000000")
        photo_image = ImageTk.PhotoImage(image)
        shape = Shape("image", photo_image)
        Screen()._shapes[string] = shape  # underpinning, not published API

        return tile_size

    def make_tile(self, string):
        tile_size = self.register_image(string)
        return Tile(string, tile_size)

除了它的图像,Tile 实例与 Turtle 实例的唯一区别是一个额外的方法 tile_size() return 它的 widthheight 因为普通海龟在图像的情况下不能这样做。瓷砖的笔在开始时是 up,而不是 down

我借鉴了几个 SO 问题和答案:

  • Dump characters (glyphs) from TrueType font (TTF) into bitmaps

当我在做的时候,这个答案已经更新为更加独立于系统:

为了演示我的 tile sets 是如何工作的,下面是使用它们实现的 well-know 15 puzzle。它创建了两个图块集,一个具有白色背景,一个具有红色(粉红色)背景:

from tileset import TileSet
from turtle import Screen
from functools import partial
from random import shuffle

SIZE = 4
OFFSETS = [(-1, 0), (0, -1), (1, 0), (0, 1)]

def slide(tile, row, col, x, y):
    tile.onclick(None)  # disable handler inside handler

    for dy, dx in OFFSETS:
        try:
            if row + dy >= 0 <= col + dx and matrix[row + dy][col + dx] == None:
                matrix[row][col] = None
                row, col = row + dy, col + dx
                matrix[row][col] = tile
                width, height = tile.tile_size()
                x, y = tile.position()
                tile.setposition(x + dx * width, y - dy * height)
                break
        except IndexError:
            pass

    tile.onclick(partial(slide, tile, row, col))

screen = Screen()

matrix = [[None for _ in range(SIZE)] for _ in range(SIZE)]

white_tiles = TileSet(background_color='white')
red_tiles = TileSet(background_color='pink')

tiles = []
parity = True

for number in range(1, SIZE * SIZE):
    string = str(number).rjust(2)
    tiles.append(white_tiles.make_tile(string) if parity else red_tiles.make_tile(string))
    parity = not parity

    if number % SIZE == 0:
        parity = not parity

shuffle(tiles)

width, height = tiles[0].tile_size()
offset_width, offset_height = width * 1.5, height * 1.5

for row in range(SIZE):
    for col in range(SIZE):
        if row == SIZE - 1 == col:
            break

        tile = tiles.pop(0)
        width, height = tile.tile_size()
        tile.goto(col * width - offset_width, offset_height - row * height)
        tile.onclick(partial(slide, tile, row, col))
        matrix[row][col] = tile

screen.mainloop()

如果您单击空白 space 旁边的数字块,它将移入空白 space,否则什么也不会发生。此代码不保证可以解决难题——由于随机洗牌,一半将无法解决。这只是一个演示,它的细节以及磁贴本身都留给您了。