按钮(Button)是GUI中最简单的一种控件,由一个矩形区域组成,有两个状态:作用中、非作用中。两种状态表示可以点击或无法点击。
按钮可以用图案表示,也可以使用文字,当然图案加文字也是可以的。因此大概知道一个Button 应该有一个代表底图的 surface 对象,以及一个显示文字的 Label 控件。
在 GUI 中除了 Button 控件,还有单选钮(RadioButton)、复选钮(checkbox),样式虽不同,但功能类似,也是在一个矩形区域,显示底图、文字,然后处理鼠标按下的事件(MOUSEBUTTONDOWN),所以为此设计一个基类(MyControl),然后其他的控件就由此衍生。
先看一下 MyControl 类的建构函式:
obj = MyControl(rect, img_file, img_cx, text, font_info)
rect 是一个元组(tuple)其值为(left, top, width, height),代表矩形区域所在的位置与大小。img_file 是一张图,里头可切成 img_cx 张小图,例如下图,此时 img_cx 为 2:
基本上 Button 只有两种状态,disabled 及 enabled。但 RadioButton 就有三种状态:disabled, enabled, selected。例如:
MyControl中有个变量 status 用于纪录目前是哪种状态,以便显示相对应的图。最后的两个参数 text 及 font_info 就如同 Label 控件,就不多说。
建构函式的代码如下:
class MyControl(object):
def __init__(self, rect, img_file, img_cx, text, font_info):
self.status = 0
self.rect = rect
self.img_cx = img_cx
self.text = text
self.font_info = font_info
# 设定底图,每一种 status 一张。
if img_file is None:
self.__img = None
self.img_width = 0
else:
self.__img = pygame.image.load(img_file)
self.__image = []
img_rect = self.__img.get_rect()
width = int(img_rect.width / img_cx)
x = 0
for i in range(self.img_cx):
self.__image.append(self.__img.subsurface((x, 0), (width, img_rect.height)))
x += width
self.img_width = width
# 设定 Lable 对象
if text == "":
self.label = None
else:
self.label = Label(rect.left, rect.top, text, font_info)
使用 render 方法将 MyControl 控件显示在屏幕上:
my_control.render(screen)
render 代码如下:
def render(self, surface):
if self.status >= 0:
if self.__img is not None:
if self.status < self.img_cx:
surface.blit(self.__image[self.status], (self.rect.left, self.rect.top))
if self.label is not None:
self.label.render(surface)
显示时,先判断 status 的值,若值小于0,代表隐藏,就不显示。显示时,必须判断是否有底图?若有,则显示相对应的状态。不要让状态(status)大于图像张数,否则无法显示图案。
判断此矩形区域是否有被鼠标点到,最基本的处理方式是判断是否有发生MOUSEBUTTONDOWN事件,然后发生此事件的坐标值是否落在此区域内?若是,则代表被按到。
def check_click(self, event):
if event.type == MOUSEBUTTONDOWN:
return self.is_over(event.pos)
def is_over(self, point):
if self.status <= 0:
bflag = False # disabled
else:
bflag = self.rect.collidepoint(point)
return bflag
矩形区域因用 pygame.Rect() 建立,所以有collidepoint 方法可以用来判断是否包含某点坐标。如果所用的 rect 没有此方法,那就得自己判断。
point_x, point_y = point
in_x = point_x >= rect.left and point_x < rect.left + rect.width/2
in_y = point_y >= rect.top and point_y < rect.top + rect.height/2
return in_x and in_y
提供三个最基本的状态改变方法,这是最经常、普通的方法。
def hide(self):
self.status = -1
def disabled(self):
self.status = 0
def enabled(self):
self.status = 1
MyControl 控件最主要就是负责显示及判断是否被按到,其余复杂的工作就由其衍生类自行处理。
完整代码如下:
class MyControl(object):
def __init__(self, rect, img_file, img_cx, text, font_info):
self.status = 0
self.rect = rect
self.img_cx = img_cx
self.text = text
self.font_info = font_info
# 设定底图,每一种 status 一张。
if img_file is None:
self.__img = None
self.img_width = 0
else:
self.__img = pygame.image.load(img_file)
self.__image = []
img_rect = self.__img.get_rect()
width = int(img_rect.width / img_cx)
x = 0
for i in range(self.img_cx):
self.__image.append(self.__img.subsurface((x, 0), (width, img_rect.height)))
x += width
self.img_width = width
# 设定 Lable 对象
if text == "":
self.label = None
else:
self.label = Label(rect.left, rect.top, text, font_info)
def render(self, surface):
if self.status >= 0:
if self.__img is not None:
if self.status < self.img_cx:
surface.blit(self.__image[self.status], (self.rect.left, self.rect.top))
if self.label is not None:
self.label.render(surface)
def is_over(self, point):
if self.status <= 0:
bflag = False # disabled
else:
bflag = self.rect.collidepoint(point)
return bflag
def check_click(self, event):
if event.type == MOUSEBUTTONDOWN:
return self.is_over(event.pos)
def hide(self):
self.status = -1
def disabled(self):
self.status = 0
def enabled(self):
self.status = 1
Button 控件继承自 MyControl 类。当用户点击按钮后,系统会因此而做出反应。pygame 是以事件来驱动,所以当 Button 被按到之后,就应该发送出一个事件,以便系统作反应。
rect = pygame.Rect(410, 258, 48, 48)
img_file = "images/btn_48_back.png"
btn_1 = Button("Back", EVENT_BACK, rect, img_file, 2, None,None)
最简单图像按钮,没有文字。按钮名称为 “Back”,它会发送 EVENT_BACK 事件,在游戏主循环中,要作出相对应的处理。
update 方法用来处理特定的事件,如果侦测到此按钮被点击,就发送被设定的 EVENT,然后附加一个 data JSON 数据。”from_ui” 代表按钮名称,”status” 则是目前的状态。由于 pygame 能自定义的 EVENT,从 pygame.USEREVENT 开始到 pygame.NUMEVENTS(不含此事件),只有八个,所以有时必须用附加的 JSON 数据来做区分。
def update(self, event):
if self.check_click(event):
data = {
"from_ui": self.name, "status":self.status}
ev = pygame.event.Event(self.event_id, data)
pygame.event.post(ev)
文字虽然在创立时就应该设定,但有时会更改,所以提供此方法来更改文字。
def set_text(self, text):
self.label.set_text(text)
直接用 MyControl 中的 Label 控件来设定文字。
完整代码如下:
class Button(MyControl):
def __init__(self, btn_name, event_id, rect, img_file, img_cx, text="", font_info=None):
MyControl.__init__(self, rect, img_file, img_cx, text, font_info)
self.event_id = event_id
self.name = btn_name
# 调整文字的位置为居中
if self.label is not None:
x = rect.left + int(rect.width / 2)
y = rect.top + int(rect.height / 2)
self.label.set_pos(x, y, 1, 1)
self.status = 1
def set_text(self, text):
self.label.set_text(text)
def update(self, event):
if self.check_click(event):
data = {
"from_ui": self.name, "status":self.status}
ev = pygame.event.Event(self.event_id, data)
pygame.event.post(ev)
将所有的 Button 控件用一个 buttons 字典来管理,例如:
buttons = {
}
buttons[“play”] = Button("play", EVENT_MUSIC_PLAY, pygame.Rect(x+dx*3, y, w, h), "images/btn_48_play.png", 4)
buttons[“stop”] = Button("stop", EVENT_MUSIC_STOP, pygame.Rect(x+dx*4, y, w, h), "images/btn_48_stop.png")
显示所有按钮的方式:
for button in buttons.values():
button.render(screen)
While True:
for event in pygame.event.get():
if event.type == QUIT:
exit()
if event.type == EVENT_MUSIC_PLAY:
# 作某事
elif event.type == EVENT_MUSIC_STOP:
# 作某事
elif event.type == MOUSEBUTTONDOWN:
for btn in buttons.values():
btn.update(event)
for btn in buttons.values():
btn.render(screen)
pygame.display.update()
clock.tick(FPS)
参考:
用 Python 和 Pygame 写游戏-从入门到精通
作者:目光博客