本章,你将学会如何传输图像和绘制文本。
导航
加载图像
上一章学习了绘制图形,对于一些简单的应用程序来说,这或许够用,但是对于更大型的应用程序,需要用到更复杂的图形的时候,使用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
对象。第二个参数filename
与load
函数相同,可以是图像的文件名,也可以是一个 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
类中定义的bold
,italic
,underline
等属性可以为字体添加更多的格式,详见官方文档。
如果始终使用文件名导入字体会比较麻烦,因此font
模块中还提供了函数SysFont
以从系统字体中导入文件(与Font
类相反,该函数无法导入字体文件)。该函数依然返回Font
对象。参数size
同Font
类。可选参数bold
和italic
指定是否使用粗体或斜体。
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
用于将指定的数限制在low
和high
之间。该函数使用了三目运算符,其格式为[statement_1] if [expression] else [statement_2]
,即当expression
为True
时,返回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=(x1−x2)2+(y1−y2)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_pos
和pos
属性都设置为当前鼠标的位置,并设置标志drawing
为True
。
再添加以下方法用于处理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
更新为当前鼠标的坐标,在pos
和last_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()
代码运行截图:
结语
以上,就是本章的所有内容。在下一章,你将学习如何监测游戏时间。