wxPython和pycairo练习记录1
学习,分为学和习,学完了不练习,让大脑觉得不重要,会忘记。所以,我决定练习并记录 wxPython 和 pycairo 的代码编写,如果你也想试试请自行配置环境。
wxPython官网:https://www.wxpython.org/
pycairo官网:https://cairographics.org/pycairo/
本机环境:
WIN10
Python(3.8.7)
wxPython(4.1.1 msw (phoenix) wxWidgets 3.1.5) pycairo(1.21.0)
因为没有相关完整项目经验,所以不能把全局都设计好,先从基本的迭代。把可能经常需要用到的,几乎不会变的公共部分先写出来,主要参考zetcode的wxPython教程https://zetcode.com/wxpython/ 。
1.1 导入模块
把练习代码放到一个模块,测试不同的练习只改动这里的模块名。
import wx
import wx.lib.wxcairo
import cairo
import example # 用来编写练习代码的模块
1.2 设置全局变量
感觉很多概念都是用来避免命名冲突的,局部变量、全局变量、闭包、内置变量、类属性、对象属性,当然作用还有便于权限控制,降低耦合。
说到属性,很多教程解释类属性和对象属性的区别,就是讲在哪声明,讲在哪存储,讲怎么调用,看了也还是不理解。后来终于找到一个能看懂的说法,类属性就是跟类绑定的属性,对象属性就是跟对象绑定的属性。
非重点的内容可以先当作常识,不然很快就会沿着各种分支偏离目标,例如为什么要避免使用全局变量?
class cv:
"""
常量,用类属性避免使用全局变量
"""
# 刷新定时器 id
TIMER_ID = 1
# 刷新间隔时间 毫秒, FPS (frames per second) 就是 1000 // SPEED
SPEED = 10
# 面板尺寸
BOARD_WIDTH = 800
BOARD_HEIGHT = 600
1.3 主面板编写
数据的更新和图像的重新绘制分开处理,如响应坦克更新位置操作,只更新位置数据,在重新绘制的时候,读取新的位置数据并绘制坦克。
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()
1.4 完整代码
用一个定时器刷新数据和图像,初始公共部分仅绘制黑色背景。
# -*- coding: utf-8 -*-
import wx
import wx.lib.wxcairo
import cairo
import example # 用来编写练习代码的模块
class cv:
"""
常量,用类属性避免使用全局变量
"""
# 刷新定时器 id
TIMER_ID = 1
# 刷新间隔时间 毫秒, 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):
# 处理更新事件,定时更新变量,刷新重绘
for so in self.sceneObjects:
so.Update()
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
class MyFrame(wx.Frame):
def __init__(self, *args, **kwargs):
super(MyFrame, self).__init__(*args, **kwargs)
Board(parent=self)
self.Center() # 窗口在屏幕居中
def main():
app = wx.App()
frame = MyFrame(parent=None, title="cairotest", size=(cv.BOARD_WIDTH, cv.BOARD_HEIGHT))
frame.Show()
app.MainLoop()
if __name__ == "__main__":
main()
1.5 拆分公共部分
上面的文件,作为单独的文件运行没有问题,但是我们还要进一步分离出变化和不变化的部分。
期望的结果是,只需要在example中写练习用的代码,其他基础部分都不变。而example中需要继承cv和Board,那么需要把cv和Board单独分离到一个文件作为模板,另外还需要一个文件写主要执行流程。
# -*- coding: utf-8 -*-
# main.py
import wx
from example import cv, Board
class MyFrame(wx.Frame):
def __init__(self, *args, **kwargs):
super(MyFrame, self).__init__(*args, **kwargs)
Board(parent=self)
self.Center() # 窗口在屏幕居中
def main():
app = wx.App()
frame = MyFrame(parent=None, title="cairotest", size=(cv.BOARD_WIDTH, cv.BOARD_HEIGHT))
frame.Show()
app.MainLoop()
if __name__ == "__main__":
main()
# -*- coding: utf-8 -*-
# board.py
import wx
import wx.lib.wxcairo
class cv:
"""
常量,用类属性避免使用全局变量
"""
# 刷新定时器 id
TIMER_ID = 1
# 刷新间隔时间 毫秒, 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):
# 处理更新事件,定时更新变量,刷新重绘
for so in self.sceneObjects:
so.Update()
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
# -*- coding: utf-8 -*-
# example.py
# 这个例子我修改了窗口尺寸和添加了smilebasic文字显示
import wx
import wx.lib.wxcairo
import cairo
import board
class cv(board.cv):
"""
常量,用类属性避免使用全局变量
"""
# 面板尺寸
BOARD_WIDTH = 600
BOARD_HEIGHT = 400
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)
1.6 效果