Pygame实作GUI(02)Button

前言

按钮(Button)是GUI中最简单的一种控件,由一个矩形区域组成,有两个状态:作用中、非作用中。两种状态表示可以点击或无法点击。

按钮可以用图案表示,也可以使用文字,当然图案加文字也是可以的。因此大概知道一个Button 应该有一个代表底图的 surface 对象,以及一个显示文字的 Label 控件。

在 GUI 中除了 Button 控件,还有单选钮(RadioButton)、复选钮(checkbox),样式虽不同,但功能类似,也是在一个矩形区域,显示底图、文字,然后处理鼠标按下的事件(MOUSEBUTTONDOWN),所以为此设计一个基类(MyControl),然后其他的控件就由此衍生。

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 方法

使用 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 事件

判断此矩形区域是否有被鼠标点到,最基本的处理方式是判断是否有发生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

hide, disabled, enabled 方法

提供三个最基本的状态改变方法,这是最经常、普通的方法。

    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 控件

Button 控件继承自 MyControl 类。当用户点击按钮后,系统会因此而做出反应。pygame 是以事件来驱动,所以当 Button 被按到之后,就应该发送出一个事件,以便系统作反应。

创建 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 方法

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)

set_text 方法

文字虽然在创立时就应该设定,但有时会更改,所以提供此方法来更改文字。

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 写游戏-从入门到精通
作者:目光博客

你可能感兴趣的:(Python,Python,pygame,GUI,Button)