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,每行一个方向,每列一个动画帧,这里把每行作为一个场景。
和 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)