wxPython和pycairo练习记录2

wxPython和pycairo练习记录2

因为要实现图像的显示和交互,今天添加两个基础的图像容器类。

2.1 前置知识

成员属性访问权限

来源:https://blog.csdn.net/weiguang102/article/details/117020112

说明 public(默认) private protected
同一个类中访问
在子类中访问 ×
在类的外部访问 × ×

python中访问权限属性声明方式

权限 格式(_为英文下划线) 示例
public variable age = 18
private __variable __money = 0
protected _variable _hobby = [“sing”]

2.2 Sprite

Sprite 主要源于 zetcode 高级教程。

Sprite 的属性包括坐标、尺寸、图像、边框矩形和销毁状态。它并没有什么产生实际操作的方法,仅仅是属性和属性修改查询方法的集合。

class Sprite:
    def __init__(self, x, y, path):
        self._x = x
        self._y = y
        self._destroyed = False # 销毁状态

        # 从文件路径加载图像
        try:
            self._surface = cairo.ImageSurface.create_from_png(path)
        except IOError as e:
            print(e.message)

        # 获取图像尺寸
        self._width = self._surface.get_width()
        self._height = self._surface.get_height()

    def GetX(self):
        return self._x

    def SetX(self, x):
        self._x = x

    def GetY(self):
        return self._y

    def SetY(self, y):
        self._y = y

    def GetSurface(self):
        return self._surface

    def SetSurface(self, surface):
        self._surface = surface
        # 修改图像后,更新尺寸
        self._width = surface.get_width()
        self._height = surface.get_height()

    def GetRect(self):
        # 获取边框矩形对象,可用于碰撞检测等
        return wx.Rect(self._x, self._y, self._width, self._height)

    def IsDestroyed(self):
        return self._destroyed

    def Destroy(self):
        self._destroyed = True

2.3 MovieClip

参考 flash 的 MovieClip https://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/MovieClip.html

MovieClip 继承自 Sprite ,因为仅仅是参考 flash ,这里仅仅保留一些需要的。根据这张坦克游戏图片素材,坦克宽高48,每行一个方向,每列一个动画帧,这里把每行作为一个场景。
tank_T1_0.png

和 Sprite 一样,构造参数需要初始绘制坐标,文件路径(每行一个场景的图片),还需要每帧图像显示的初始区域矩形(后面的帧只需要改变显示坐标)。

class MovieClip(Sprite):
    def __init__(self, x, y, path, rect, fps):
        super(MovieClip, self).__init__(x, y, path)

        self._scenes = [] # 保存所有场景,每个场景保存当前场景所有帧
        self._frames = [] # 保存当前场景所有帧
        self._currentScene = 0 # 当前场景索引
        self._totalScenes = 0 # 总场景数
        self._currentFrame = 0 # 当前帧索引
        self._totalFrames = 0 # 当前场景总帧数
        self._isPlaying = False # 播放状态
        self._fps = 1000 // fps  # 动画可能需要不同于主程序的FPS,这里换算成定时器的时间单位

        self.LoadFrames(rect)

2.4 完整代码

# -*- coding: utf-8 -*-
# display.py

import wx
import cairo

class Sprite:
    def __init__(self, x, y, path):
        self._x = x
        self._y = y
        self._destroyed = False # 销毁状态

        # 从文件路径加载图像
        try:
            self._surface = cairo.ImageSurface.create_from_png(path)
        except IOError as e:
            print(e.message)

        # 获取图像尺寸
        self._width = self._surface.get_width()
        self._height = self._surface.get_height()

    def GetX(self):
        return self._x

    def SetX(self, x):
        self._x = x

    def GetY(self):
        return self._y

    def SetY(self, y):
        self._y = y

    def GetSurface(self):
        return self._surface

    def SetSurface(self, surface):
        self._surface = surface
        # 修改图像后,更新尺寸
        self._width = surface.get_width()
        self._height = surface.get_height()

    def GetRect(self):
        # 获取边框矩形对象,可用于碰撞检测等
        return wx.Rect(self._x, self._y, self._width, self._height)

    def IsDestroyed(self):
        return self._destroyed

    def Destroy(self):
        self._destroyed = True


class MovieClip(Sprite):
    def __init__(self, x, y, path, rect, fps):
        super(MovieClip, self).__init__(x, y, path)

        self._scenes = [] # 保存所有场景,每个场景保存当前场景所有帧
        self._frames = [] # 保存当前场景所有帧
        self._currentScene = 0 # 当前场景索引
        self._totalScenes = 0 # 总场景数
        self._currentFrame = 0 # 当前帧索引
        self._totalFrames = 0 # 当前场景总帧数
        self._isPlaying = False # 播放状态
        self._fps = 1000 // fps  # 动画可能需要不同于主程序的FPS,这里换算成定时器的时间单位

        self.LoadFrames(rect)

    def LoadFrames(self, rect):
        # 按显示区域载入场景和帧
        x, y, width, height = rect

        # 每行表示一个场景scene,scene中每列表示一个frame
        try:
            for j in range(self._height // height):
                frames = []
                for i in range(self._width // width):
                    frame = self._surface.create_for_rectangle(x + width * i, y + height * j, width, height)
                    frames.append(frame)
                self._scenes.append(frames)
        except Exception as e:
            print(str(e))

        # 更新初始化变量
        self._surface = self._scenes[self._currentScene][self._currentFrame]
        self._totalScenes = len(self._scenes)
        self._totalFrames = len(self._scenes[self._currentScene])
        # create_for_rectangle 得到的 surface 获取到的宽高为0,这里直接使用矩形宽高
        self._width = width
        self._height = height

    def Play(self):
        self._isPlaying = True

    def Stop(self):
        self._isPlaying = False

    def PrevScene(self):
        self._currentScene -= 1
        self._isPlaying = False

    def NextScene(self):
        self._currentScene += 1
        self._isPlaying = False

    def PrevFrame(self):
        self._currentFrame -= 1
        self._isPlaying = False

    def NextFrame(self):
        self._currentFrame += 1
        self._isPlaying = False

    def GotoAndPlay(self, frame, scene):
        self._currentScene = scene
        self._currentFrame = frame
        self._isPlaying = True

    def GotoAndStop(self, frame, scene):
        self._currentScene = scene
        self._currentFrame = frame
        self._isPlaying = False

    def Update(self, times, speed):
        # 交给主程序执行,用于刷新 MovieClip 对象数据,主程序需要增加 times 用于记录主程序刷新次数
        # 主程序刷新速度要比 MovieClip 对象刷新快
        print("刷新次数:", times, "当前帧索引:", self._currentFrame)
        if times % (self._fps // speed) == 0 and self._isPlaying:
            self._currentFrame += 1

    def __setattr__(self, name, value):
        self.__dict__[name] = value
        # 更新 _currentFrame 和 _currentScene 时,同时更新相应变量
        if self.__dict__.get("_scenes"):
            if self.__dict__.get("_frames") and name == "_currentFrame":
                self._surface = self._frames[self._currentFrame % self._totalFrames]
            elif name == "_currentScene":
                self._frames = self._scenes[self._currentScene % self._totalScenes]
                self._totalFrames = len(self._frames)

# -*- coding: utf-8 -*-
# board.py
# 增加刷新次数记次 times

import wx
import wx.lib.wxcairo


class cv:
    """
    常量,用类属性避免使用全局变量
    """
    # 刷新定时器 id
    TIMER_ID = 1
    # 刷新次数
    times = 0
    # 刷新间隔时间 毫秒, FPS (frames per second) 就是 1000 // SPEED
    SPEED = 10
    # 面板尺寸
    BOARD_WIDTH = 800
    BOARD_HEIGHT = 600


class Board(wx.Panel):
    def __init__(self, parent):
        super(Board, self).__init__(parent=parent, style=wx.WANTS_CHARS) # wx.WANTS_CHARS 可接收键盘事件
        self.SetDoubleBuffered(True) # 双缓冲,防止闪烁
        self.InitVariables()
        self.BindEvent()

    def InitVariables(self):
        # 初始化变量
        self.InitSceneObjects()
        # 设置定时器
        self.timer = wx.Timer(owner=self, id=cv.TIMER_ID)
        self.timer.Start(cv.SPEED)

    def InitSceneObjects(self):
        # 初始化场景中对象变量,如坦克实例、计分栏初始分数等
        self.sceneObjects = []

    def BindEvent(self):
        # 绑定事件
        self.Bind(wx.EVT_TIMER, self.OnTimer, id=cv.TIMER_ID)
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        self.Bind(wx.EVT_KEY_UP, self.OnKeyUp)

    def OnTimer(self, e):
        # 处理更新事件,定时更新变量,刷新重绘
        cv.times += 1

        for so in self.sceneObjects:
            so.Update(cv.times, cv.SPEED)

        self.CheckStrategies()
        self.Refresh() # 重绘,执行OnPaint

    def CheckStrategies(self):
        # 碰撞检测等
        pass

    def OnPaint(self, e):
        # 处理重绘事件
        dc = wx.PaintDC(window=self) # device context,设备上下文,相当于画布或虚拟的屏幕,
        ctx = wx.lib.wxcairo.ContextFromDC(dc) # 获取 cairo.Context 对象,同上

        self.DrawBackground(ctx)
        self.DrawSceneObjects(ctx)

    def DrawBackground(self, ctx):
        # 填充黑色背景
        ctx.set_source_rgb(0, 0, 0)
        ctx.paint()

    def DrawSceneObjects(self, ctx):
        # 绘制要显示的对象,如坦克、计分栏等
        pass

    def OnKeyDown(self, e):
        # 处理键盘按下事件
        pass

    def OnKeyUp(self, e):
        # 处理键盘弹起事件
        pass

2.5 效果

这里写了一个可控制方向的坦克,不同方向播放不同场景动画。并不完善,比如方向控制,边界检测,但基本达到要求。
在这里插入图片描述

# -*- coding: utf-8 -*-
# example2.py

import wx
import wx.lib.wxcairo
import cairo
import board
from display import MovieClip


class cv(board.cv):
    """
    常量,用类属性避免使用全局变量
    """
    # 面板尺寸
    BOARD_WIDTH = 600
    BOARD_HEIGHT = 400


class Tank(MovieClip):
    def __init__(self, *args, **kwargs):
        super(Tank, self).__init__(*args, **kwargs)
        self._speed = 10 # 移动速度
        self._dx = 0 # x 轴方向
        self._dy = 0 # y 轴方向

    def Up(self):
        self._dy = -1
        self._dx = 0
        self.GotoAndPlay(0, 0)

    def Down(self):
        self._dy = 1
        self._dx = 0
        self.GotoAndPlay(0, 1)

    def Left(self):
        self._dx = -1
        self._dy = 0
        self.GotoAndPlay(0, 2)

    def Right(self):
        self._dx = 1
        self._dy = 0
        self.GotoAndPlay(0, 3)


class Player(Tank):
    def __init__(self, x, y, path="tank_T1_0.png", rect=(0, 0, 48, 48), fps=100):
        super(Player, self).__init__(x, y, path, rect, fps)

    def OnKeyDown(self, e):
        key = e.GetKeyCode()

        if key == wx.WXK_UP:
            self.Up()
        if key == wx.WXK_DOWN:
            self.Down()
        if key == wx.WXK_LEFT:
            self.Left()
        if key == wx.WXK_RIGHT:
            self.Right()

        self._x += self._dx * self._speed
        self._y += self._dy * self._speed

    def OnKeyUp(self, e):
        key = e.GetKeyCode()

        if key == wx.WXK_UP or key == wx.WXK_DOWN:
            self._dy = 0
        if key == wx.WXK_LEFT or key == wx.WXK_RIGHT:
            self._dx = 0
        if self._dx == 0 and self._dy == 0:
            self.Stop()


class Board(board.Board):
    def DrawBackground(self, ctx):
        super().DrawBackground(ctx)

        text = "SmileBasic"
        ctx.set_font_size(40)
        _, _, w, h, _, _ = ctx.text_extents(text)
        x = (cv.BOARD_WIDTH - w) // 2
        y = (cv.BOARD_HEIGHT - h) // 2

        # 文字是以首个字左下角坐标定位,而矩形是以左上角定位,y轴相差文字的高度,不需要考虑线条宽度。
        # 另外PaintDC是不含标题栏的。
        ctx.rectangle(x - 10, y - 10 - h, w + 20, h + 20)
        ctx.set_source_rgb(1, 0, 0)
        ctx.stroke()

        ctx.move_to(x, y)
        ctx.set_source_rgb(1, 1, 1)
        ctx.show_text(text)

    def InitSceneObjects(self):
        super().InitSceneObjects()
        self.player = Player(50, 50) # 实例化一个玩家坦克
        self.sceneObjects.append(self.player)

    def DrawSceneObjects(self, ctx):
        ctx.set_source_surface(self.player.GetSurface(), self.player.GetX(), self.player.GetY())
        ctx.paint()

    def OnKeyDown(self, e):
        self.player.OnKeyDown(e)
        self.Refresh()

    def OnKeyUp(self, e):
        self.player.OnKeyUp(e)

猜你喜欢

转载自blog.csdn.net/SmileBasic/article/details/126330785