wxPython和pycairo练习记录13

实现更美观的滚动字幕效果

之前虽然通过设置窗口样式去掉了标题栏,但是背景看起来还是有些碍眼。通过 SetTransparent 只能调整整体透明度,文字和背景都透明了。希望实现的效果是背景透明,而文字不透明。

透明背景的文字窗体

wxPython demo 中有两个实现形状窗口的例子,ShapedWindow 和 AdvancedSplash,其实都是通过 SetShape(wx.Region) 实现的。region 翻译为区域,不太理解,还有另一个签名 SetShape(wx.GraphicsPath),所以实际是设置像 PS 里的路径?

没有找到怎么用文字来设置形状,所以把文字先转为图像。通过 wx.GraphicsContext 创建透明背景文字 Bitmap,把图像设置为 Frame 的 shape。显示文字会有明显锯齿,且当字体设置较小时,文字周围会有黑边。

在 OnPaint 中绘制一个文字颜色的同窗口大小的矩形,文字黑边消失。后绘制的矩形是显示在文字上层,文字图片变成蒙版了?

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

源码:

# -*- coding: utf-8 -*-
import wx


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

        w, h = self.GetSize()
        self.bg = wx.Bitmap.FromRGBA(w, h)
        dc = wx.MemoryDC(self.bg)
        gc = wx.GraphicsContext.Create(dc)

        font = wx.Font(32, family=wx.SWISS, style=wx.NORMAL, weight=wx.BOLD, faceName=u"宋体")
        gc.SetFont(font, "#ff0000")
        gc.DrawText(u"你好,世界!Hello World!😍", 0, 0)  # emoji 不能显示

        region = wx.Region(self.bg, wx.TransparentColour)
        self.SetShape(region)

        self.Bind(wx.EVT_PAINT,self.onPaint)

        self.SetTransparent(150)

    def onPaint(self, evt):
        dc= wx.PaintDC(self)
        dc.DrawBitmap(self.bg, 0, 0, True)
        dc.SetBrush(wx.Brush(wx.RED))
        dc.DrawRectangle(0, 0, *self.GetSize())


app = wx.App()
frame = MyFrame(parent=None,
                size=(1075, 53),
                style=wx.FRAME_SHAPED | wx.NO_BORDER | wx.STAY_ON_TOP | wx.TRANSPARENT_WINDOW)
frame.Show()
app.MainLoop()

还有一种方法,直接通过 wx.ScreenDC 在电脑屏幕上任意位置绘制文字,不需要交互的话应该是可以的。脱离了程序窗口,如果和鼠标、键盘交互,要怎么处理呢?

锯齿问题待解决,使用 SetAntialiasMode 方法设置没有效果。抗锯齿参考帖子 https://discuss.wxpython.org/t/anti-aliased-text-with-wx-graphicscontext/25021/27

wx.GraphicsContext wraps GDI+, which is a system API, so it is still dependent on the system settings for AA. You could try using wx.lib.graphics.GraphicsContext instead, it uses Cairo on all the platforms instead of the native APIs.
来源:https://discuss.wxpython.org/t/anti-aliased-text-with-wx-graphicscontext/25021/4

大略就是自带的 GDI 是调用系统 API,需要使用优化的第三方库。后续再尝试。

整合滚动字幕

向歌词工具进发?下面是某音乐播放器的歌词显示效果。SetShape 方法传递一个空的 wx.Region 就能使原本的矩形窗口显示出来,可以用来实现歌词的鼠标移入移出效果。文字描边怎么实现的?还是先继续整合滚动弹幕吧。
在这里插入图片描述

wx.lib.ticker.Ticker 类是继承自 wx.Control , 而 SetShape 方法是继承自 wx.NonOwnedWindow,不能作用于 Ticker。直接继承 wx.Frame 重写 Ticker 控件?好像并不需要做很大改动,直接把基类改一下就能用。

还有些待完善:1.多线程用法;2.清空形状图像(如果用 wx.GCDC 的 Clear 也会显示矩形窗体);3.文字窗体用鼠标点击时,文字镂空范围是选择不到的,需要歌词那样的效果。

在这里插入图片描述

源码:

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


class Ticker(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)

        self.Center()
        self._bg = wx.Bitmap.FromRGBA(*self.GetSize())
        self._gc = wx.GraphicsContext.Create(wx.MemoryDC(self._bg))
        self._fgcolor = kwargs["fgcolor"]
        self._gc.SetFont(self.GetFont(), self._fgcolor)
        # 需要一个初始形状,不然会显示原矩形窗体
        self._gc.DrawText("SmileBasic", 0, 0)
        region = wx.Region(self._bg, wx.TransparentColour)
        self.SetShape(region)

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

    def SetFont(self, font):
        """设置控件使用的字体"""
        wx.Frame.SetFont(self, font)
        self._dc.SetFont(font)
        self._gc.SetFont(font, self._fgcolor)
        self.SetGap(self._gap)
        # 更新存储文本尺寸
        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 OnPaint(self, evt):
        """PAINT事件处理"""
        dc = wx.BufferedPaintDC(self)
        brush = wx.Brush(self.GetBackgroundColour())
        dc.SetBackground(brush)
        dc.Clear()
        dc.DrawBitmap(self._bg, 0, 0, True)
        dc.SetBrush(wx.Brush(self._fgcolor))
        dc.DrawRectangle(0, 0, *self.GetSize())
        self.DrawText(dc)

    def DrawText(self, dc):
        """绘制滚动文字"""
        if not self._text or self._offset <= 0:
            return
        # 笨办法重置 bitmap
        self._bg = wx.Bitmap.FromRGBA(*self.GetSize())
        self._gc = wx.GraphicsContext.Create(wx.MemoryDC(self._bg))
        self._gc.SetFont(self.GetFont(), self._fgcolor)
        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
            self._gc.DrawText(self._text[i % lens][1], offx, 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
        region = wx.Region(self._bg, wx.TransparentColour)
        # SetShape 会阻塞主线程
        # wx.CallAfter(self.SetShape, region)
        t = threading.Thread(target=wx.CallAfter, args=(self.SetShape, region))
        t.start()

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

    def OnLeftDown(self, evt):
        self.CaptureMouse()
        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.HasCapture():
            self.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])
            self.Move(fp)


class App(wx.App):
    def OnInit(self):
        tk = Ticker(parent=None, id=-1, text="你好,世界!Hello World!",
                    fgcolor="#ff0000", bgcolor="#ffff00", start=True,
                    ppf=2, fps=20, direction="rtl",
                    size=(1075, 53),
                    style=wx.FRAME_SHAPED | wx.NO_BORDER | wx.STAY_ON_TOP | wx.TRANSPARENT_WINDOW,
                    name="Ticker")
        tk.SetFont(wx.Font(24, family=wx.SWISS, style=wx.NORMAL, weight=wx.BOLD, faceName=u"宋体"))
        tk.SetGap(5)
        tk.SetText("CSDN")
        tk.SetText("SmileBasic")
        tk.Start()
        tk.Show()

        return True

    def OnExit(self):
        return 0


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

# ticker.py
#----------------------------------------------------------------------
# Name:        wx.lib.ticker
# Purpose:     A news-ticker style scrolling text control
#
# Author:      Chris Mellon
#
# Created:     29-Aug-2004
# Copyright:   (c) 2004 by Chris Mellon
# Licence:     wxWindows license
# Tags:        phoenix-port, unittest, documented, py3-port
#----------------------------------------------------------------------

"""
News-ticker style scrolling text control

    * Can scroll from right to left or left to right.

    * Speed of the ticking is controlled by two parameters:

      - Frames per Second(FPS): How many times per second the ticker updates

      - Pixels per Frame(PPF): How many pixels the text moves each update

Low FPS with high PPF will result in "jumpy" text, lower PPF with higher FPS
is smoother (but blurrier and more CPU intensive) text.
"""

import wx

#----------------------------------------------------------------------

class Ticker(wx.Frame):
    def __init__(self,
            parent,
            id=-1,
            text=wx.EmptyString,
            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"
        ):
        """
        Default class constructor.

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

        """
        wx.Frame.__init__(self, parent, id=id, pos=pos, size=size, style=style, name=name)
        self.timer = wx.Timer(owner=self)
        self._extent = (-1, -1)  #cache value for the GetTextExtent call
        self._offset = 0
        self._fps = fps  #frames per second
        self._ppf = ppf  #pixels per frame
        self.SetDirection(direction)
        self.SetText(text)
        self.SetInitialSize(size)
        self.SetForegroundColour(fgcolor)
        self.SetBackgroundColour(bgcolor)
        self.Bind(wx.EVT_TIMER, self.OnTick)
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnErase)
        if start:
            self.Start()


    def Stop(self):
        """Stop moving the text"""
        self.timer.Stop()


    def Start(self):
        """Starts the text moving"""
        if not self.timer.IsRunning():
            self.timer.Start(1000 // self._fps)


    def IsTicking(self):
        """Is the ticker ticking? ie, is the text moving?"""
        return self.timer.IsRunning()


    def SetFPS(self, fps):
        """
        Adjust the update speed of the ticker.

        :param int `fps`: frames per second.

        """
        self._fps = fps
        self.Stop()
        self.Start()


    def GetFPS(self):
        """
        Get the frames per second speed of the ticker.
        """
        return self._fps


    def SetPPF(self, ppf):
        """
        Set the number of pixels per frame the ticker moves - ie,
        how "jumpy" it is.

        :param int `ppf`: the pixels per frame setting.

        """
        self._ppf = ppf


    def GetPPF(self):
        """Get pixels per frame setting."""
        return self._ppf


    def SetFont(self, font):
        """
        Set the font for the control.

        :param wx.Font `font`: the font to be used.

        """
        self._extent = (-1, -1)
        wx.Control.SetFont(self, font)


    def SetDirection(self, dir):
        """
        Sets the direction of the ticker: right to left (rtl) or
        left to right (ltr).

        :param `dir`: the direction 'rtl' or 'ltr'

        """
        if dir == "ltr" or dir == "rtl":
            if self._offset != 0:
                #Change the offset so it's correct for the new direction
                self._offset = self._extent[0] + self.GetSize()[0] - self._offset
            self._dir = dir
        else:
            raise TypeError


    def GetDirection(self):
        """Get the set direction."""
        return self._dir


    def SetText(self, text):
        """
        Set the ticker text.

        :param string `text`: the ticker text

        """
        self._text = text
        self._extent = (-1, -1)
        if not self._text:
            self.Refresh() #Refresh here to clear away the old text.


    def GetText(self):
        """Get the current ticker text."""
        return self._text


    def UpdateExtent(self, dc):
        """
        Updates the cached text extent if needed.

        :param wx.DC `dc`: the dc to use.

        """
        if not self._text:
            self._extent = (-1, -1)
            return
        if self._extent == (-1, -1):
            self._extent = dc.GetTextExtent(self.GetText())


    def DrawText(self, dc):
        """
        Draws the ticker text at the current offset using the provided DC.

        :param wx.DC `dc`: the dc to use.

        """
        dc.SetTextForeground(self.GetForegroundColour())
        dc.SetFont(self.GetFont())
        self.UpdateExtent(dc)
        if self._dir == "ltr":
            offx = self._offset - self._extent[0]
        else:
            offx = self.GetSize()[0] - self._offset
        offy = (self.GetSize()[1] - self._extent[1]) / 2 #centered vertically
        dc.DrawText(self._text, int(offx), int(offy))


    def OnTick(self, evt):
        """
        Handles the ``wx.EVT_TIMER`` event for :class:`Ticker`.

        :param `evt`: a :class:`TimerEvent` event to be processed.

        """
        self._offset += self._ppf
        w1 = self.GetSize()[0]
        w2 = self._extent[0]
        if self._offset >= w1+w2:
            self._offset = 0
        self.Refresh()


    def OnPaint(self, evt):
        """
        Handles the ``wx.EVT_PAINT`` event for :class:`Ticker`.

        :param `evt`: a :class:`PaintEvent` event to be processed.

        """
        dc = wx.BufferedPaintDC(self)
        brush = wx.Brush(self.GetBackgroundColour())
        dc.SetBackground(brush)
        dc.Clear()
        self.DrawText(dc)


    def OnErase(self, evt):
        """
        Noop because of double buffering

        Handles the ``wx.EVT_ERASE_BACKGROUND`` event for :class:`Ticker`.

        :param `evt`: a :class:`EraseEvent` event to be processed.

        """
        pass


    def AcceptsFocus(self):
        """Non-interactive, so don't accept focus"""
        return False


    def DoGetBestSize(self):
        """
        Width we don't care about, height is either -1, or the character
        height of our text with a little extra padding
        """
        if self._extent == (-1, -1):
            if not self._text:
                h = self.GetCharHeight()
            else:
                h = self.GetTextExtent(self.GetText())[1]
        else:
            h = self._extent[1]
        return (100, h+5)


    def ShouldInheritColours(self):
        """Don't get colours from our parent."""
        return False



#testcase/demo
if __name__ == '__main__':
    app = wx.App()
    t = Ticker(None, text="Some sample ticker text", size=(1070, 50))
    t.Center()
    t.Show()
    app.MainLoop()


既然已经大致实现了透明背景动画效果窗体,那么做个桌面宠物也是可以的吧。桌面水印、抢购计时…以后再试试。

新的问题,滚动字幕通过视频号工具,添加窗口播放源会有黑色背景,添加进程则整个窗口都不显示,添加桌面倒是可以,但也不适合。
在这里插入图片描述

安装 pywin32 库,试试通过 AssociateHandle 关联其他程序窗口,获取并使用它的 DC。但是对系统的记事本有作用,对小程序没有影响。而且使用 dc.Clear 会影响原内容,不使用则在移动窗口时会显示历史文字。
在这里插入图片描述

# -*- coding: utf-8 -*-
import win32gui
import wx


class MyFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(MyFrame, self).__init__(*args, **kwargs)
        hld = win32gui.FindWindow(None, u"批量发文.txt - 记事本")
        print(hld)
        self.AssociateHandle(hld)

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnErase)

        self.timer = wx.Timer(owner=self)
        self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)
        self.timer.Start(100)

    def OnErase(self, event):
        pass

    def Draw(self, dc):
        w, h = self.GetClientSize()
        print(w, h)
        x, y = self.GetPosition()
        print(x, y)
        strs = "你好,世界!Hello World!"
        dc.SetTextForeground(wx.RED)
        dc.SetFont(wx.Font(32, wx.SWISS, wx.NORMAL, wx.BOLD))
        tw, th = dc.GetTextExtent(strs)
        dc.DrawText(strs, (w - tw) // 2, (h - th) // 2)

    def OnTimer(self, evt):
        dc = wx.ClientDC(self)
        # dc.Clear()
        self.Draw(dc)

    def OnPaint(self, evt):
        dc = wx.PaintDC(self)
        dc.Clear()
        self.Draw(dc)


app = wx.App()
frame = MyFrame(parent=None)
frame.Show()
app.MainLoop()

所以,不懂的太多了,还是向有背景方向美化吧。两种美化效果参考:一种是LED滚动显示屏,一种是比特小队游戏里的商店滚动屏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值