Pygame 教程(4):图像传输和绘制文本

本章,你将学会如何传输图像和绘制文本。

导航

上一章:绘制图形
下一章:监测游戏时间

加载图像

上一章学习了绘制图形,对于一些简单的应用程序来说,这或许够用,但是对于更大型的应用程序,需要用到更复杂的图形的时候,使用pygame.draw模块进行绘制就会变得烦琐,而且该模块渲染的质量较低,不符合大多数应用程序的要求。

所以,面对这种情况,通常的做法是,把复杂的图形编写在图片文件中,并使用 Pygame 加载它,最后绘制到屏幕上。

Pygame 使用 image模块进行图像传输的操作。最常用的函数就是用于加载图像文件的pygame.image.load函数。第一个参数filename可以是图像的文件名,也可以是一个 Python 类文件对象(file-like object,比如调用open函数返回的文件对象)。第二个可选参数name_hint可用于显式指定文件的类型,但在大多数情况下,Pygame 会自动识别文件类型,从数据中创建一个Surface对象。

Pygame 支持加载的图像文件类型如下(详见官方文档):

  • BMP
  • GIF (非动画,对于含动画的文件只使用第一帧画面)
  • JPEG
  • LBM(还有PBM,PGM,PPM)
  • PCX
  • PNG
  • PNM
  • SVG(有限支持,使用 Nano SVG
  • TGA(未压缩的)
  • TIFF
  • WEBP
  • XPM

导出图像

与加载文件相反,Pygame 也支持将Surface对象导出为图像文件。使用pygame.image.save函数即可。该函数的第一个参数surface指定要导出的Surface对象。第二个参数filenameload函数相同,可以是图像的文件名,也可以是一个 Python 类文件对象。第三个可选参数name_hint也同load函数。

与加载图像文件不同的是,Pygame 中支持导出的文件类型比较少,具体如下(详见官方文档):

  • BMP
  • JPEG
  • PNG
  • TGA

image模块中其他的函数可以阅读官方文档进行学习,再次不做赘述。

绘制文本

为了与用户交互,显示文本也是一项很重要的技术。作为一个 GUI(Graphical User Interface,图形用户界面)程序,使用print输出文本是不现实的,需要将文本绘制到屏幕上。

为了绘制文本,需要加载对应的字体。Pygame 使用pygame.font模块加载并渲染字体。

pygame.font.Font类用于从给定的文件名或 Python 文件对象中加载一个字体。如果对文件名传入None将会加载 Pygame 的默认字体 freesansbold.ttf(传入字符串'freesansbold.ttf'效果相同),你可以在 Python 的安装目录中的Lib/site-packages/pygame文件夹中找到这个字体文件。参数size指定字体的高度像素值,这个值在字体创建后将无法被改变。

Font类中定义的bolditalicunderline等属性可以为字体添加更多的格式,详见官方文档

如果始终使用文件名导入字体会比较麻烦,因此font模块中还提供了函数SysFont以从系统字体中导入文件(与Font类相反,该函数无法导入字体文件)。该函数依然返回Font对象。参数sizeFont类。可选参数bolditalic指定是否使用粗体或斜体。

Font类中还定义了一个重要的方法:render。它用于使用此字体渲染指定文本,并返回渲染的Surface对象。

render函数的完整定义是pygame.font.render(text, antialias, color, background=None) -> Surface。参数text指定渲染的文本。参数antialias指定是否使用抗锯齿技术。color指定渲染的文本颜色。可选参数background可以指定渲染文本的背景颜色,默认情况下,背景颜色是透明的。

实例:画板

依照惯例,我们将创建一个画板应用程序。

请新建一个文件,命名为drawing_board.py,并添加如下代码以创建初始窗口:

import sys

import pygame


class DrawingBoard:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((800, 800))
        pygame.display.set_caption('Drawing Board')
		
		self.screen.fill((255, 255, 255))

    def process_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

    def run(self):
        while True:
            self.process_events()
            pygame.display.update()


if __name__ == '__main__':
    app = DrawingBoard()
    app.run()

上一章相似,我们把程序代码封装到了一个类中。略微不同的是,我们把处理事件循环的代码封装到了process_events方法中,因为这个实例对事件的处理比较繁重,全部堆积在run方法中并不是一个好办法。

我们在构造函数中就将屏幕填充为白色。因为画板应用程序需要实时更新,如果每一次循环都填充屏幕,那么每一次循环过后用户绘制的内容都将被覆盖。

添加常量

类的外部添加以下声明常量的代码:

import sys

import pygame

COLORS = (
    (0, 0, 0),
    (255, 0, 0),
    (0, 255, 0),
    (0, 0, 255),
    (255, 255, 0),
    (255, 0, 255),
    (0, 255, 255),
    (255, 255, 255)
)  # 色板中的所有颜色

RADIUS = 15  # 色板中设置颜色的圆圈的半径
GAP = 15  # 色板中相邻的圆圈之间的空隙
CENTER_DIST = RADIUS * 2 + GAP  # 色板中相邻的圆圈的中心之间的距离
PALETTE_RECT = pygame.Rect(0, 0, 800, RADIUS * 2 + GAP * 2)  # 色板的矩形坐标
PALETTE_HEIGHT = PALETTE_RECT.height  # 色板的矩形的高度
LINE_WIDTH_MAX = 20  # 线条的最大粗度


class DrawingBoard:
# ...

在编写代码时,对常量的命名可以使用全部大写加下划线的形式,以便与变量区分开。而没有把常量声明为类实例的属性是因为它们与类的关系并不是非常密切。

限制坐标

当鼠标移动到色板上时,不应该继续画在色板上。所以需要限制鼠标的坐标。在代码中添加以下两个全局函数

def clamp(value, low, high):
    return low if value < low else (high if value > high else value)


def clamp_pos(pos):
    y = clamp(pos[1], PALETTE_HEIGHT, 800)
    return (pos[0], y)

函数clamp用于将指定的数限制在lowhigh之间。该函数使用了三目运算符,其格式为[statement_1] if [expression] else [statement_2],即当expressionTrue时,返回statement_1,反之,返回statement_2。为了方便,我们还定义了clamp_pos函数,用于将坐标的y值限制在COLORS_HEIGHT和800之间。

这两个函数也与类没有直接关系,所以声明为全局函数,而不是类方法。

定义属性

在类的构造函数中添加以下代码:

# ...
pygame.display.set_caption('Drawing Board')

self.color = (0, 0, 0)  # 画笔的颜色
self.width = 1  # 线条的粗细
self.last_pos = self.pos = (0, 0)  # 鼠标上一帧的位置以及当前位置
self.circles = []  # 色板中由圆圈的中心和对应颜色的元组组合而成。
self.font = pygame.font.SysFont('Times New Roman', 20)  # 字体
self.drawing = False  # 用户是否正在绘画

# ...

绘制色板

添加以下的两个类方法:

def draw_colors(self):
    for i in range(len(COLORS)):
        center = (i * CENTER_DIST + GAP + RADIUS, GAP + RADIUS)
        pygame.draw.circle(self.screen, COLORS[i], center, RADIUS)
        self.circles.append((center, COLORS[i]))

    pygame.draw.line(self.screen, (0, 0, 0), (0, PALETTE_HEIGHT), (800, PALETTE_HEIGHT))

def draw_text(self):
    surf = self.font.render(f'Line width: {self.width}', True, (0, 0, 0))
    rect = surf.get_rect()
    rect.right = PALETTE_RECT.right - GAP
    rect.centery = PALETTE_RECT.centery
    self.screen.blit(surf, rect)

draw_colors方法遍历所有的颜色,并绘制圆圈,为了使用户区分色板,我们还在下面绘制了一条线段。

draw_text方法显示当前的线条粗细。它使用render方法渲染文本,再绘制到屏幕上。为了调整文本的位置,我们做了一些工作:先把矩形的右侧移动到距离色板的右侧一个GAP的空隙,再使矩形的y中心对齐到色板矩形的y中心。这也是pygame.Rect的强大之处,多个属性可以使位置的处理更加灵活。

在构造函数的最后,调用这两个方法:

# ...
self.drawing = False  # 用户是否正在绘画

self.draw_colors()
self.draw_text()

# ...

更改线条粗细

本实例的设计为,当用户按下上箭头或下箭头时,更改线条的粗细。这里需要一些处理。因为如果直接地填充屏幕,用户之前所画的图形将消失,虽然可以通过截取子图像(使用pygame.Surface.subsurface方法,详见拓展)保留用户所画的图形的副本,但是会过于麻烦。本实例选择修改draw_text方法,在绘制文本之前填充一个白色的矩形覆盖原有文本,修改后的代码如下:

def draw_text(self):
    surf = self.font.render(f'Line width: {self.width}', True, (0, 0, 0))
    rect = surf.get_rect()
    rect.right = PALETTE_RECT.right - GAP
    rect.centery = PALETTE_RECT.centery
    fill_rect = pygame.Rect(rect.left - 5, rect.top - 5, rect.width + 10, rect.height + 10)
    pygame.draw.rect(self.screen, (255, 255, 255), fill_rect)
    self.screen.blit(surf, rect)

fill_rect是由rect向外延伸5个像素而成的,因为不同的文本渲染的宽度不同,多延伸5个像素可以确保完全地覆盖在原来的文本上。

处理鼠标事件

首先,添加以下全局函数到代码中:

def dist_between_points(point1, point2):
    return ((point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2) ** 0.5

该函数用于计算两点之间的距离,公式为 d = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 d=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2} d=(x1x2)2+(y1y2)2

然后,添加以下方法用于处理MOUSEBUTTONDOWN事件:

def on_mouse_down(self, pos):
    if PALETTE_RECT.collidepoint(pos):
        for center, color in self.circles:
            if dist_between_points(pos, center) < RADIUS:
                self.color = color
    else:
        self.last_pos = self.pos = pos
        self.drawing = True

pygame.Rect.collidepoint方法用于检测指定的点是否在此矩形对象内。在本实例中,如果该方法返回True,说明用户点击了色板,于是遍历所有的圆圈,判断鼠标是否点击到了圆圈,如果是,则改变画笔的颜色。为了检测一个点是否在圆内,我们判断鼠标与圆心的距离是否小于半径,是,则说明鼠标在圆圈内。如果collidepoint方法返回了False,说明用户在画画的区域按下了鼠标,所以将last_pospos属性都设置为当前鼠标的位置,并设置标志drawingTrue

再添加以下方法用于处理MOUSEMOTION事件:

def on_mouse_move(self, pos):
    if self.drawing:
        self.pos = clamp_pos(pos)
        pygame.draw.line(self.screen, self.color, self.last_pos, self.pos, self.width)
        self.last_pos = clamp_pos(pos)

该方法先将pos更新为当前鼠标的坐标,在poslast_pos之间画一条线段,再把last_pos更新为当前鼠标的坐标。

处理键盘事件

添加以下方法用于处理KEYDOWN事件:

def on_key_down(self, key):
    if self.drawing:
        return
    if key == pygame.K_UP:
        self.width += 1
        self.width = clamp(self.width, 1, LINE_WIDTH_MAX)
        self.draw_text()
    elif key == pygame.K_DOWN:
        self.width -= 1
        self.width = clamp(self.width, 1, LINE_WIDTH_MAX)
        self.draw_text()

return使用户只能在没有进行绘画时更改线条粗细。这里再次使用了clamp函数限制线条粗细。

调用事件处理方法

更改process_events方法,以调用事件处理的方法,代码如下:

def process_events(self):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
        elif event.type == pygame.MOUSEBUTTONDOWN:
            self.on_mouse_down(event.pos)
        elif event.type == pygame.MOUSEMOTION:
            self.on_mouse_move(event.pos)
        elif event.type == pygame.MOUSEBUTTONUP:
            self.drawing = False
        elif event.type == pygame.KEYDOWN:
            self.on_key_down(event.key)

注意,这里还添加了处理MOUSEBUTTONUP事件的代码。

完整代码

import sys

import pygame

COLORS = (
    (0, 0, 0),
    (255, 0, 0),
    (0, 255, 0),
    (0, 0, 255),
    (255, 255, 0),
    (255, 0, 255),
    (0, 255, 255),
    (255, 255, 255)
)  # 色板中的所有颜色

RADIUS = 15  # 色板中设置颜色的圆圈的半径
GAP = 15  # 色板中相邻的圆圈之间的空隙
CENTER_DIST = RADIUS * 2 + GAP  # 色板中相邻的圆圈的中心之间的距离
PALETTE_RECT = pygame.Rect(0, 0, 800, RADIUS * 2 + GAP * 2)  # 色板的矩形坐标
PALETTE_HEIGHT = PALETTE_RECT.height  # 色板的矩形的高度
LINE_WIDTH_MAX = 20  # 线条的最大粗度


def clamp(value, low, high):
    return low if value < low else (high if value > high else value)


def clamp_pos(pos):
    y = clamp(pos[1], PALETTE_HEIGHT, 800)
    return (pos[0], y)


def dist_between_points(point1, point2):
    return ((point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2) ** 0.5


class DrawingBoard:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((800, 800))
        self.screen.fill((255, 255, 255))
        pygame.display.set_caption('Drawing Board')

        self.color = (0, 0, 0)  # 画笔的颜色
        self.width = 1  # 线条的粗细
        self.last_pos = self.pos = (0, 0)  # 鼠标上一帧的位置以及当前位置
        self.circles = []  # 色板中由圆圈的中心和对应颜色的元组组合而成。
        self.font = pygame.font.SysFont('Times New Roman', 20)  # 字体
        self.drawing = False  # 用户是否正在绘画

        self.draw_colors()
        self.draw_text()

    def draw_colors(self):
        for i in range(len(COLORS)):
            center = (i * CENTER_DIST + GAP + RADIUS, GAP + RADIUS)
            pygame.draw.circle(self.screen, COLORS[i], center, RADIUS)
            self.circles.append((center, COLORS[i]))

        pygame.draw.line(self.screen, (0, 0, 0), (0, PALETTE_HEIGHT), (800, PALETTE_HEIGHT))

    def draw_text(self):
        surf = self.font.render(f'Line width: {self.width}', True, (0, 0, 0))
        rect = surf.get_rect()
        rect.right = PALETTE_RECT.right - GAP
        rect.centery = PALETTE_RECT.centery
        fill_rect = pygame.Rect(rect.left - 5, rect.top - 5, rect.width + 10, rect.height + 10)
        pygame.draw.rect(self.screen, (255, 255, 255), fill_rect)
        self.screen.blit(surf, rect)

    def on_mouse_down(self, pos):
        if PALETTE_RECT.collidepoint(pos):
            for center, color in self.circles:
                if dist_between_points(pos, center) < RADIUS:
                    self.color = color
        else:
            self.last_pos = self.pos = pos
            self.drawing = True
            
    def on_mouse_move(self, pos):
        if self.drawing:
            self.pos = clamp_pos(pos)
            pygame.draw.line(self.screen, self.color, self.last_pos, self.pos, self.width)
            self.last_pos = clamp_pos(pos)
            
    def on_key_down(self, key):
        if self.drawing:
            return
        if key == pygame.K_UP:
            self.width += 1
            self.width = clamp(self.width, 1, LINE_WIDTH_MAX)
            self.draw_text()
        elif key == pygame.K_DOWN:
            self.width -= 1
            self.width = clamp(self.width, 1, LINE_WIDTH_MAX)
            self.draw_text()

    def process_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == pygame.MOUSEBUTTONDOWN:
                self.on_mouse_down(event.pos)
            elif event.type == pygame.MOUSEMOTION:
                self.on_mouse_move(event.pos)
            elif event.type == pygame.MOUSEBUTTONUP:
                self.drawing = False
            elif event.type == pygame.KEYDOWN:
                self.on_key_down(event.key)

    def run(self):
        while True:
            self.process_events()
            pygame.display.update()


if __name__ == '__main__':
    app = DrawingBoard()
    app.run()

代码运行截图:

代码运行截图

结语

以上,就是本章的所有内容。在下一章,你将学习如何监测游戏时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值