wxPython和pycairo练习记录12

文章介绍了如何使用wxPython的Ticker控件创建滚动字幕效果,并讨论了在滚动文字衔接上遇到的问题。作者通过修改Ticker类,解决了文字空白期问题,实现了更平滑的滚动。此外,还探讨了使用双端队列优化显示性能的可能性,但考虑到特定场景下的效率和需求,最终选择了保留列表结构并优化处理方式。
摘要由CSDN通过智能技术生成

wxPython 实现滚动字幕效果

过年在家刷视频号直播时发现弹幕互动游戏,挺有意思的,刚好诠释了“反射”(一种基于字符串的事件驱动)的用法。想要自己也做一个弹幕游戏,于是就有了这个基本的需求,先让弹幕滚动显示出来,直播时可以当作小挂件。

滚动文字功能

先查 API 文档和 demo 看有没相应的控件和例子,发现 wxPython 有滚动文字控件 wx.lib.ticker.Ticker (Ticker翻译:滚动条;收报机;心脏;跑马灯;断续器)。

类签名:
wx.lib.ticker.Ticker(parent, id=-1, text="", fgcolor = wx.BLACK, bgcolor = wx.WHITE, start=True, ppf=2, fps=20, direction="rtl", pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.NO_BORDER, name="Ticker")

参数:	
parent (wx.Window) – the parent
id (integer) – an identifier for the control: a value of -1 is taken to mean a default
text (string) – text in the ticker
fgcolor (wx.Colour) – text/foreground color
bgcolor (wx.Colour) – background color
start (boolean) – if True, the ticker starts immediately
ppf (int) – pixels per frame
fps (int) – frames per second
direction – direction of ticking, ‘rtl’ or ‘ltr’
pos (wx.Point) – the control position. A value of (-1, -1) indicates a default position, chosen by either the windowing system or wxPython, depending on platform
name – the control name

看看效果:
在这里插入图片描述

源码:

# -*- coding: utf-8 -*-
import wx
from wx.lib.ticker import Ticker


class MyFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(MyFrame, self).__init__(*args, **kwargs)
        self.Center()

        self.ticker = Ticker(parent=self, id=-1, text="你好,世界!Hello World!😍 ",
                             fgcolor="#ff0000", bgcolor="#fff000", start=True,
                             ppf=2, fps=20, direction="rtl",
                             pos=wx.DefaultPosition, size=self.GetClientSize(), style=wx.NO_BORDER,
                             name="Ticker")
        self.ticker.SetFont(wx.Font(18, family=wx.SWISS, style=wx.NORMAL, weight=wx.BOLD, faceName=u"宋体"))
        self.ticker.Start()

        self.ticker.Bind(wx.EVT_RIGHT_UP, self.OnClose)
        self.ticker.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.ticker.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.ticker.Bind(wx.EVT_MOTION, self.OnMouseMove)

    def OnClose(self, event):
        self.Close()

    def OnLeftDown(self, evt):
        # CaptureMouse 把所有鼠标输入指向 self.ticker 控件窗口
        self.ticker.CaptureMouse()
        # ClientToScreen 把鼠标位置相对于程序窗口的坐标转换为相对于屏幕的坐标
        x, y = self.ClientToScreen(evt.GetPosition())
        originx, originy = self.GetPosition()
        dx = x - originx
        dy = y - originy
        self.delta = ((dx, dy))

    def OnLeftUp(self, evt):
        if self.ticker.HasCapture():
            self.ticker.ReleaseMouse()

    def OnMouseMove(self, evt):
        if evt.Dragging() and evt.LeftIsDown():
            x, y = self.ClientToScreen(evt.GetPosition())
            fp = (x - self.delta[0], y - self.delta[1])
            # Move 将窗口移动到指定坐标
            self.Move(fp)


class App(wx.App):
    def OnInit(self):
        frame = MyFrame(parent=None, size=(1075, 53), style=wx.NO_BORDER)
        frame.Show()
        return True

    def OnExit(self):
        return 0


if __name__ == "__main__":
    app = App()
    app.MainLoop()

滚动文字衔接问题

滚动循环,文字需要全部滚出才会重新滚入,有空白期。
在这里插入图片描述

原 Ticker 类中,滚动文字是用属性 self._text 来存储的,它是一个字符串。如果在字符串尾部附加其他新的字符串,并不能解决显示衔接问题,而且不便对指定字符串进行修改。那么直接继承 Ticker ,把 _text 改成 list,并修改相应方法。

本来是想像做动画一样,将 _text 复制一份,轮流滚动。但是因为后期还要动态添加字符串,并且不应该把所有文字都显示出来。

所以最后想到的解决办法是:另设一个列表 _show_text 用于存储要显示的字符串,_index 表示最后从 _text 添加到 _show_text 的字符串的_text 索引。 当 _show_text 头部节点滚出显示区时,从列表中去除,当尾节点尾部与显示区尾部相距超过设定间距时,则把 _text[_index+1] 附加到 _show_text。
在这里插入图片描述

效果,录像 GIF 帧率是 30 FPS:
在这里插入图片描述

源码:

# -*- coding: utf-8 -*-
# Author: SmileBasic
import wx
import wx.lib.ticker


class Ticker(wx.lib.ticker.Ticker):
    def __init__(self, *args, **kwargs):
        self._text = []  # 滚动字符串存储列表
        self._text_max = 500  # _text 最大可存储字符串数量
        self._show_text = []  # 显示字符串列表
        self._show_text_size = (-1, -1)
        self._gap = 0  # 滚动字符串之间间距
        self._gap_pixels = 0
        self._index = 0  # 最后进入显示区域的文字索引
        super(Ticker, self).__init__(*args, **kwargs)

    def SetText(self, text):
        """将字符串插入滚动字符串列表尾部"""
        if text.strip():
            if len(self._text) == self._text_max:
                self._text.pop(0)
                if self._index > 0:
                    self._index = self._index - 1
            self._text.append(text)
            if not self._show_text:
                self._show_text.append(text)
                self.Refresh()

    def SetGap(self, gap):
        """设置字符串显示间距"""
        self._gap = gap

    def UpdateExtent(self, dc):
        """更新字符串尺寸"""
        dc.SetFont(self.GetFont())
        self._gap_pixels = dc.GetTextExtent(self._gap * " ")[0]
        self._show_text_size = dc.GetTextExtent((self._gap * " ").join(self._show_text))
        if self._show_text:
            self._extent = dc.GetTextExtent(self._show_text[0])
        else:
            self._extent = (-1, -1)

    def OnTick(self, evt):
        """更新滚动文字坐标"""
        self._offset += self._ppf  # offset 偏移,ppf(pixels per frame) 每帧滚动的像素
        # 头节点滚出显示区
        if self._offset >= self.GetSize()[0] + self._extent[0]:
            self._offset = self._offset - self._extent[0] - self._gap_pixels
            self._show_text.pop(0)
            # 需要更新字符串尺寸
            self.UpdateExtent(wx.ClientDC(self))
        # 尾节点与显示区尾部相距超过指定间距
        if self._offset >= self._show_text_size[0] + self._gap_pixels and self._text:
            self._index = (self._index + 1) % len(self._text)
            self._show_text.append(self._text[self._index])
            self.UpdateExtent(wx.ClientDC(self))
        self.Refresh()

    def DrawText(self, dc):
        """绘制滚动文字"""
        dc.SetTextForeground(self.GetForegroundColour())
        dc.SetFont(self.GetFont())
        self.UpdateExtent(dc)
        if self._dir == "ltr":
            offx = self._offset - self._extent[0]
            fix_dir = -1
            fix_size = 1
        else:
            offx = self.GetSize()[0] - self._offset
            fix_dir = 1
            fix_size = 0
        offy = (self.GetSize()[1] - self._extent[1]) / 2
        for i, x in enumerate(self._show_text):
            dc.DrawText(x, int(offx), int(offy))
            if i < len(self._show_text) - 1:
                offx = offx + dc.GetTextExtent(self._show_text[i + fix_size])[0] * fix_dir + self._gap_pixels * fix_dir


class MyFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(MyFrame, self).__init__(*args, **kwargs)
        self.Center()

        self.ticker = Ticker(parent=self, id=-1, text="你好,世界!Hello World!😍",
                             fgcolor="#ff0000", bgcolor="#ffff00", start=True,
                             ppf=2, fps=20, direction="rtl",
                             pos=wx.DefaultPosition, size=self.GetClientSize(), style=wx.NO_BORDER | wx.TRANSPARENT_WINDOW,
                             name="Ticker")
        self.ticker.SetFont(wx.Font(18, family=wx.SWISS, style=wx.NORMAL, weight=wx.BOLD, faceName=u"宋体"))
        self.ticker.SetGap(5)
        self.ticker.SetText("CSDN")
        self.ticker.SetText("SmileBasic")
        self.ticker.Start()

        self.ticker.Bind(wx.EVT_RIGHT_UP, self.OnClose)
        self.ticker.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.ticker.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.ticker.Bind(wx.EVT_MOTION, self.OnMouseMove)

    def OnClose(self, event):
        self.Close()

    def OnLeftDown(self, evt):
        # CaptureMouse 把所有鼠标输入指向 self.ticker 控件窗口
        self.ticker.CaptureMouse()
        # ClientToScreen 把鼠标位置相对于程序窗口的坐标转换为相对于屏幕的坐标
        x, y = self.ClientToScreen(evt.GetPosition())
        originx, originy = self.GetPosition()
        dx = x - originx
        dy = y - originy
        self.delta = ((dx, dy))

    def OnLeftUp(self, evt):
        if self.ticker.HasCapture():
            self.ticker.ReleaseMouse()

    def OnMouseMove(self, evt):
        if evt.Dragging() and evt.LeftIsDown():
            x, y = self.ClientToScreen(evt.GetPosition())
            fp = (x - self.delta[0], y - self.delta[1])
            # Move 将窗口移动到指定坐标
            self.Move(fp)


class App(wx.App):
    def OnInit(self):
        frame = MyFrame(parent=None, size=(1075, 53), style=wx.NO_BORDER | wx.STAY_ON_TOP | wx.TRANSPARENT_WINDOW)
        frame.Show()
        return True

    def OnExit(self):
        return 0


if __name__ == "__main__":
    app = App()
    app.MainLoop()

修改

很明显,这样不断对列表 _show_text 头尾进行增删操作会造成资源浪费。另外,当 _text 存储超过数量时,也会陷入这种情况。

在某些场景下,我们需要频繁的对列表的头和尾进行操作,这时我们就可以使用双端队列collections.deque,这种数据结构被设计成能够在集合两端高效地增加和删除元素,在Python中,双端队列是以双向链表的方式实现的。来源:Python高性能计算之列表 https://zhuanlan.zhihu.com/p/342861939

但是使用索引读取时,列表时间复杂度是O(1),双端队列是O(n)。_text 需要频繁使用索引,不适合使用双端队列。

观察一下,_show_text 是 _text 中连续截取的元素,可以去掉,用头节点 索引 _index 和 显示数量 _num 代替。还有,字符串宽度一般是不变的,只在设置字体时会改变,可以在存储字符串的时候,把字符串尺寸也保存。
在这里插入图片描述

修改后源码:

# -*- coding: utf-8 -*-
# Author: SmileBasic
import wx
import wx.lib.ticker


class Ticker(wx.lib.ticker.Ticker):
    def __init__(self, *args, **kwargs):
        self._text = []  # 滚动字符串存储列表,格式[[字符串尺寸, 字符串], [(56, 22), "test"]]
        self._text_max = 500  # _text 最大可存储字符串数量
        self._gap = 0  # 滚动字符串之间间距
        self._gap_pixels = 0
        self._index = 0  # 显示字符串头节点
        self._num = 1  # 显示字符串数量
        self._dc = wx.MemoryDC()  # 用于获取文本尺寸
        super(Ticker, self).__init__(*args, **kwargs)

    def SetFont(self, font):
        """设置控件使用的字体"""
        wx.Control.SetFont(self, font)
        self._dc.SetFont(font)
        # 更新存储文本尺寸
        for text in self._text:
            text[0] = self._dc.GetTextExtent(text[1])

    def SetText(self, text):
        """将字符串插入滚动字符串列表尾部"""
        if text.strip():
            lens = len(self._text)
            if lens >= self._text_max:
                if self._index != 0:
                    self._text = self._text[self._index % lens:]
                    self._index = 0
                else:
                    self._text.pop(0)
            size = self._dc.GetTextExtent(text)
            self._text.append([size, text])
            self.Refresh()

    def SetGap(self, gap):
        """设置字符串显示间距"""
        self._gap = gap
        self._gap_pixels = self._dc.GetTextExtent(self._gap * " ")[0]

    def UpdateExtent(self, dc):
        """更新字符串尺寸"""
        pass

    def OnTick(self, evt):
        """更新滚动文字坐标"""
        if not self._text:
            return
        self._offset += self._ppf  # offset 偏移,ppf(pixels per frame) 每帧滚动的像素
        # 头节点滚出显示区
        w1 = self.GetSize()[0]
        w2 = self._text[self._index][0][0]
        if self._offset >= w1 + w2:
            self._offset = self._offset - w2 - self._gap_pixels
            self._index = (self._index + 1) % len(self._text)
            self._num -= 1
        # 尾节点与显示区尾部相距超过指定间距
        w3 = sum([self._text[i % len(self._text)][0][0] for i in range(self._index, self._index + self._num)])
        w3 = w3 + self._gap_pixels * self._num
        if self._offset >= w3:
            self._num += 1
        self.Refresh()

    def DrawText(self, dc):
        """绘制滚动文字"""
        if not self._text:
            return
        dc.SetTextForeground(self.GetForegroundColour())
        dc.SetFont(self.GetFont())
        if self._dir == "ltr":
            offx = self._offset - self._text[self._index][0][0]
            fix_dir = -1
            fix_size = 1
        else:
            offx = self.GetSize()[0] - self._offset
            fix_dir = 1
            fix_size = 0
        lens = len(self._text)
        for i in range(self._index, self._index + self._num):
            offy = (self.GetSize()[1] - self._text[i % lens][0][1]) / 2
            dc.DrawText(self._text[i % lens][1], int(offx), int(offy))
            if i < self._index + self._num - 1:
                offx = offx + self._text[(i + fix_size) % lens][0][0] * fix_dir + self._gap_pixels * fix_dir


class MyFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(MyFrame, self).__init__(*args, **kwargs)
        self.Center()

        self.ticker = Ticker(parent=self, id=-1, text="你好,世界!Hello World!😍",
                             fgcolor="#ff0000", bgcolor="#ffff00", start=True,
                             ppf=2, fps=20, direction="rtl",
                             pos=wx.DefaultPosition, size=self.GetClientSize(), style=wx.NO_BORDER | wx.TRANSPARENT_WINDOW,
                             name="Ticker")
        self.ticker.SetFont(wx.Font(18, family=wx.SWISS, style=wx.NORMAL, weight=wx.BOLD, faceName=u"宋体"))
        self.ticker.SetGap(5)
        self.ticker.SetText("CSDN")
        self.ticker.SetText("SmileBasic")
        self.ticker.Start()

        self.ticker.Bind(wx.EVT_RIGHT_UP, self.OnClose)
        self.ticker.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.ticker.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.ticker.Bind(wx.EVT_MOTION, self.OnMouseMove)

    def OnClose(self, event):
        self.Close()

    def OnLeftDown(self, evt):
        # CaptureMouse 把所有鼠标输入指向 self.ticker 控件窗口
        self.ticker.CaptureMouse()
        # ClientToScreen 把鼠标位置相对于程序窗口的坐标转换为相对于屏幕的坐标
        x, y = self.ClientToScreen(evt.GetPosition())
        originx, originy = self.GetPosition()
        dx = x - originx
        dy = y - originy
        self.delta = ((dx, dy))

    def OnLeftUp(self, evt):
        if self.ticker.HasCapture():
            self.ticker.ReleaseMouse()

    def OnMouseMove(self, evt):
        if evt.Dragging() and evt.LeftIsDown():
            x, y = self.ClientToScreen(evt.GetPosition())
            fp = (x - self.delta[0], y - self.delta[1])
            # Move 将窗口移动到指定坐标
            self.Move(fp)


class App(wx.App):
    def OnInit(self):
        frame = MyFrame(parent=None, size=(1075, 53), style=wx.NO_BORDER | wx.STAY_ON_TOP | wx.TRANSPARENT_WINDOW)
        frame.Show()
        return True

    def OnExit(self):
        return 0


if __name__ == "__main__":
    app = App()
    app.MainLoop()

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值