wxWidgets / Phoenix

wxPython's Project Phoenix. A new implementation of wxPython, better, stronger, faster than he was before.
http://wxpython.org/
2.22k stars 509 forks source link

A few rendering problems with alpha channel #1544

Closed kdschlosser closed 4 years ago

kdschlosser commented 4 years ago

Operating system:

Windows 7 x64 SP1

wxPython version & source:

4.0.7.post1 msw (phoenix) wxWidgets 3.0.5
pypi

Python version & source:

3.7.5 Stackless 3.7 (tags/v3.7.5-slp:f7925f2a02, Oct 20 2019, 15:28:53) [MSC v.1916 64 bit (AMD64)] (cpython)
https://github.com/stackless-dev/stackless

Description of the problem: There are a few issues I am running into. I have attached an image to show the problems that are taking place.

I may be doing something incorrect, tho I do not think that I am.

image

Here is the frame as seen without the white behind it.

image

This example script will ONLY run on Windows.

import wx
import ctypes

from ctypes.wintypes import LONG, HWND, INT, HDC, HGDIOBJ, BOOL, DWORD

UBYTE = ctypes.c_ubyte
GWL_EXSTYLE = -20
WS_EX_LAYERED = 0x00080000
ULW_ALPHA = 0x00000002
AC_SRC_OVER = 0x00000000
AC_SRC_ALPHA = 0x00000001

class POINT(ctypes.Structure):
    _fields_ = [
        ('x', LONG),
        ('y', LONG)
    ]

class SIZE(ctypes.Structure):
    _fields_ = [
        ('cx', LONG),
        ('cy', LONG)
    ]

class BLENDFUNCTION(ctypes.Structure):
    _fields_ = [
        ('BlendOp', UBYTE),
        ('BlendFlags', UBYTE),
        ('SourceConstantAlpha', UBYTE),
        ('AlphaFormat', UBYTE)
    ]

user32 = ctypes.windll.User32
gdi32 = ctypes.windll.Gdi32

# LONG GetWindowLongW(
#   HWND hWnd,
#   int  nIndex
# )

GetWindowLongW = user32.GetWindowLongW
GetWindowLongW.restype = LONG

# LONG SetWindowLongW(
#   HWND hWnd,
#   int  nIndex,
#   LONG dwNewLong
# );
SetWindowLongW = user32.SetWindowLongW
SetWindowLongW.restype = LONG

# HDC GetDC(
#   HWND hWnd
# );
GetDC = user32.GetDC
GetDC.restype = HDC

# HWND GetDesktopWindow();
GetDesktopWindow = user32.GetDesktopWindow
GetDesktopWindow.restype = HWND

# HDC CreateCompatibleDC(
#   HDC hdc
# );
CreateCompatibleDC = gdi32.CreateCompatibleDC
CreateCompatibleDC.restype = HDC

# HGDIOBJ SelectObject(
#   HDC     hdc,
#   HGDIOBJ h
# );
SelectObject = gdi32.SelectObject
SelectObject.restype = HGDIOBJ

# BOOL UpdateLayeredWindow(
#   HWND          hWnd,
#   HDC           hdcDst,
#   POINT         *pptDst,
#   SIZE          *psize,
#   HDC           hdcSrc,
#   POINT         *pptSrc,
#   COLORREF      crKey,
#   BLENDFUNCTION *pblend,
#   DWORD         dwFlags
# );
UpdateLayeredWindow = user32.UpdateLayeredWindow
UpdateLayeredWindow.restype = BOOL

COLORREF = DWORD

def RGB(r, g, b):
    return COLORREF(r | (g << 8) | (b << 16))

class Frame(wx.Frame):

    def __init__(self):
        wx.Frame.__init__(
            self,
            None,
            -1,
            style=(
                wx.FRAME_SHAPED |
                wx.NO_BORDER |
                wx.FRAME_NO_TASKBAR |
                wx.STAY_ON_TOP
            ),
            size=(600, 300),
            pos=(400, 400)
        )
        self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)
        self.Bind(wx.EVT_PAINT, self.OnPaint)

    def OnPaint(self, evt):
        width, height = self.GetClientSize()

        text = 'Alpha test'
        font = self.GetFont()
        font.SetPointSize(font.GetPointSize() + 16)
        font = font.MakeBold()
        font = font.MakeItalic()

        bmp = wx.Bitmap(width, height)
        bmp2 = wx.Bitmap(width, height)
        bmp3 = wx.Bitmap(width, height)

        dc = wx.MemoryDC()

        dc.SelectObject(bmp3)
        dc.SetTextForeground(wx.Colour(255, 255, 255, 255))
        dc.SetFont(font)

        text_width, text_height = dc.GetFullTextExtent(text)[:2]

        text_x = (width / 2) - (text_width / 2)
        text_y = (height / 2) - (text_height / 2)

        dc.DrawText(text, text_x, text_y)

        dc.SelectObject(bmp2)

        dc.SetPen(wx.Pen(wx.Colour(255, 255, 255, 255), 6))
        dc.SetBrush(wx.Brush(wx.Colour(255, 255, 255, 255)))
        dc.DrawRoundedRectangle(103, 78, width - 200 - 3, height - 150 - 3, 5)

        dc.SelectObject(bmp)
        gc = wx.GraphicsContext.Create(dc)
        gcdc = wx.GCDC(gc)

        region1 = wx.Region(bmp)
        region2 = wx.Region(bmp2, wx.Colour(0, 0, 0, 0))
        region1.Subtract(region2)

        gcdc.SetDeviceClippingRegion(region1)
        gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 140), 6))
        gc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 150)))
        gc.DrawRoundedRectangle(3, 3, width-6, height-6, 5)

        gcdc.DestroyClippingRegion()

        gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 120), 6))
        gc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 0)))
        gc.DrawRoundedRectangle(103, 78, width - 200 - 3, height - 150 - 3, 5)

        region1 = wx.Region(bmp)
        region2 = wx.Region(bmp3, wx.Colour(0, 0, 0, 0))
        region1.Subtract(region2)
        gcdc.SetDeviceClippingRegion(region1)
        gcdc.SetTextForeground(wx.Colour(0, 0, 0, 130))
        gcdc.SetTextBackground(wx.TransparentColour)
        gcdc.SetFont(font)
        gc.DrawText(text, text_x - 4, text_y - 4)
        gcdc.DestroyClippingRegion()

        gcdc.SetTextForeground(wx.Colour(0, 255, 0, 150))
        gcdc.SetTextBackground(wx.TransparentColour)
        gc.DrawText(text, text_x, text_y)

        gcdc.Destroy()
        del gcdc

        dc.SelectObject(wx.NullBitmap)
        dc.Destroy()
        del dc

        region = wx.Region(bmp)
        self.SetShape(region)

        self.draw_alpha(bmp)

    def draw_alpha(self, bmp):
        hndl = self.GetHandle()
        style = GetWindowLongW(HWND(hndl), INT(GWL_EXSTYLE))
        SetWindowLongW(HWND(hndl), INT(GWL_EXSTYLE), LONG(style | WS_EX_LAYERED))

        hdcDst = GetDC(GetDesktopWindow())
        hdcSrc = CreateCompatibleDC(HDC(hdcDst))

        pptDst = POINT(*self.GetPosition())
        psize = SIZE(*self.GetClientSize())
        pptSrc = POINT(0, 0)
        crKey = RGB(0, 0, 0)

        pblend = BLENDFUNCTION(AC_SRC_OVER, 0, 255, AC_SRC_ALPHA)

        SelectObject(HDC(hdcSrc), HGDIOBJ(bmp.GetHandle()))
        UpdateLayeredWindow(
            HWND(hndl),
            HDC(hdcDst),
            ctypes.byref(pptDst),
            ctypes.byref(psize),
            HDC(hdcSrc),
            ctypes.byref(pptSrc),
            crKey,
            ctypes.byref(pblend),
            DWORD(ULW_ALPHA)
        )

app = wx.App()

frame = Frame()
frame.Show()
app.MainLoop()

@Metallicow Here is an example how how to overcome the alpha issue with shaped frames, this example only works on Windows. I would think that there are similiar API functions for OSX and Linux that can be used to achieve the same results

Metallicow commented 4 years ago

Hmmm well whatdayaknow... it sorta works. Still has the BLACK issue on wxPy4.1 tho.... I'm beginning to think that whatever changed in wxWidgets is what may be causing the performance issues on wxPy4.1 also, but am not sure...

wx40alpha

wx41alpha

brushwithalpha.png brushwithalpha

Collapsible Content - Click to expand ```python #!/usr/bin/env python # -*- coding: utf-8 -*- """""" # Imports.-------------------------------------------------------------------- # -Python Imports. import os import sys import ctypes from ctypes.wintypes import LONG, HWND, INT, HDC, HGDIOBJ, BOOL, DWORD # -wxPython Imports. import wx wxVER = 'wxPython %s' % wx.version() pyVER = 'python %d.%d.%d.%s' % sys.version_info[0:4] versionInfos = '%s %s' % (wxVER, pyVER) UBYTE = ctypes.c_ubyte GWL_EXSTYLE = -20 WS_EX_LAYERED = 0x00080000 ULW_ALPHA = 0x00000002 AC_SRC_OVER = 0x00000000 AC_SRC_ALPHA = 0x00000001 class POINT(ctypes.Structure): _fields_ = [ ('x', LONG), ('y', LONG) ] class SIZE(ctypes.Structure): _fields_ = [ ('cx', LONG), ('cy', LONG) ] class BLENDFUNCTION(ctypes.Structure): _fields_ = [ ('BlendOp', UBYTE), ('BlendFlags', UBYTE), ('SourceConstantAlpha', UBYTE), ('AlphaFormat', UBYTE) ] user32 = ctypes.windll.User32 gdi32 = ctypes.windll.Gdi32 # LONG GetWindowLongW( # HWND hWnd, # int nIndex # ); GetWindowLongW = user32.GetWindowLongW GetWindowLongW.restype = LONG # LONG SetWindowLongW( # HWND hWnd, # int nIndex, # LONG dwNewLong # ); SetWindowLongW = user32.SetWindowLongW SetWindowLongW.restype = LONG # HDC GetDC( # HWND hWnd # ); GetDC = user32.GetDC GetDC.restype = HDC # HWND GetDesktopWindow(); GetDesktopWindow = user32.GetDesktopWindow GetDesktopWindow.restype = HWND # HDC CreateCompatibleDC( # HDC hdc # ); CreateCompatibleDC = gdi32.CreateCompatibleDC CreateCompatibleDC.restype = HDC # HGDIOBJ SelectObject( # HDC hdc, # HGDIOBJ h # ); SelectObject = gdi32.SelectObject SelectObject.restype = HGDIOBJ # BOOL UpdateLayeredWindow( # HWND hWnd, # HDC hdcDst, # POINT *pptDst, # SIZE *psize, # HDC hdcSrc, # POINT *pptSrc, # COLORREF crKey, # BLENDFUNCTION *pblend, # DWORD dwFlags # ); UpdateLayeredWindow = user32.UpdateLayeredWindow UpdateLayeredWindow.restype = BOOL COLORREF = DWORD def RGB(r, g, b): return COLORREF(r | (g << 8) | (b << 16)) class Frame(wx.Frame): def __init__(self, parent, id=wx.ID_ANY, title=wx.EmptyString, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.FRAME_SHAPED | wx.NO_BORDER | wx.FRAME_NO_TASKBAR | wx.STAY_ON_TOP , name='frame'): wx.Frame.__init__(self, parent, id, title, pos, size, style, name) self.delta = (0, 0) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_MOTION, self.OnMouseMove) self.Bind(wx.EVT_RIGHT_UP, self.OnExit) self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None) self.Bind(wx.EVT_PAINT, self.OnPaint) def OnExit(self, event): self.Close() def OnLeftDown(self, event): self.CaptureMouse() x, y = self.ClientToScreen(event.GetPosition()) originx, originy = self.GetPosition() dx = x - originx dy = y - originy self.delta = ((dx, dy)) def OnLeftUp(self, event): if self.HasCapture(): self.ReleaseMouse() def OnMouseMove(self, event): if event.Dragging() and event.LeftIsDown(): x, y = self.ClientToScreen(event.GetPosition()) fp = (x - self.delta[0], y - self.delta[1]) self.Move(fp) def OnPaint(self, event): width, height = self.GetClientSize() text = 'Alpha test' font = self.GetFont() font.SetPointSize(font.GetPointSize() + 16) font = font.MakeBold() font = font.MakeItalic() bmp = wx.Bitmap(width, height) bmp2 = wx.Bitmap(width, height) bmp3 = wx.Bitmap(width, height) dc = wx.MemoryDC() dc.SelectObject(bmp3) dc.SetTextForeground(wx.Colour(255, 255, 255, 255)) dc.SetFont(font) text_width, text_height = dc.GetFullTextExtent(text)[:2] text_x = (width / 2) - (text_width / 2) text_y = (height / 2) - (text_height / 2) dc.DrawText(text, text_x, text_y) dc.SelectObject(bmp2) dc.SetPen(wx.Pen(wx.Colour(255, 255, 255, 255), 6)) dc.SetBrush(wx.Brush(wx.Colour(255, 255, 255, 255))) dc.DrawRoundedRectangle(103, 78, width - 200 - 3, height - 150 - 3, 5) dc.SelectObject(bmp) gc = wx.GraphicsContext.Create(dc) gcdc = wx.GCDC(gc) region1 = wx.Region(bmp) region2 = wx.Region(bmp2, wx.Colour(0, 0, 0, 0)) region1.Subtract(region2) gcdc.SetDeviceClippingRegion(region1) gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 140), 6)) gc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 150))) gc.DrawRoundedRectangle(3, 3, width-6, height-6, 5) gcdc.DestroyClippingRegion() gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 120), 6)) gc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 0))) gc.DrawRoundedRectangle(103, 78, width - 200 - 3, height - 150 - 3, 5) region1 = wx.Region(bmp) region2 = wx.Region(bmp3, wx.Colour(0, 0, 0, 0)) region1.Subtract(region2) gcdc.SetDeviceClippingRegion(region1) gcdc.SetTextForeground(wx.Colour(0, 0, 0, 130)) gcdc.SetTextBackground(wx.TransparentColour) gcdc.SetFont(font) gc.DrawText(text, text_x - 4, text_y - 4) gcdc.DestroyClippingRegion() gcdc.SetTextForeground(wx.Colour(0, 255, 0, 150)) gcdc.SetTextBackground(wx.TransparentColour) gc.DrawText(text, text_x, text_y) ## vippibmp = wx.Bitmap('Vippi50.png', wx.BITMAP_TYPE_PNG) ## gcdc.DrawBitmap(vippibmp, x=0, y=0, useMask=False) brushbmp = wx.Bitmap('brushwithalpha.png', wx.BITMAP_TYPE_PNG) gcdc.DrawBitmap(brushbmp, x=200, y=10, useMask=False) gcdc.Destroy() del gcdc dc.SelectObject(wx.NullBitmap) dc.Destroy() del dc region = wx.Region(bmp) self.SetShape(region) self.draw_alpha(bmp) def draw_alpha(self, bmp): hndl = self.GetHandle() style = GetWindowLongW(HWND(hndl), INT(GWL_EXSTYLE)) SetWindowLongW(HWND(hndl), INT(GWL_EXSTYLE), LONG(style | WS_EX_LAYERED)) hdcDst = GetDC(GetDesktopWindow()) hdcSrc = CreateCompatibleDC(HDC(hdcDst)) pptDst = POINT(*self.GetPosition()) psize = SIZE(*self.GetClientSize()) pptSrc = POINT(0, 0) crKey = RGB(0, 0, 0) pblend = BLENDFUNCTION(AC_SRC_OVER, 0, 255, AC_SRC_ALPHA) SelectObject(HDC(hdcSrc), HGDIOBJ(bmp.GetHandle())) UpdateLayeredWindow( HWND(hndl), HDC(hdcDst), ctypes.byref(pptDst), ctypes.byref(psize), HDC(hdcSrc), ctypes.byref(pptSrc), crKey, ctypes.byref(pblend), DWORD(ULW_ALPHA) ) if __name__ == '__main__': print(versionInfos) app = wx.App() frame = Frame(None, size=(600, 300), pos=(400, 400)) frame.Show() app.MainLoop() ```

I'm guessing you ripped the constants out of win32 to make it purepython. Not sure what you would need to do that with linux or mac...

With the stacked frames approach I made I eventually got to a point where I rage quit about 10 times because flattening out the font into a png made the stroke look worse and worse as I went. Then I came across the left edge wasn't rendering the first pixels on each line for who knows what reason. Hardcoding -1 didn't work either... I figured I could paint the logo into a walrus sitting on a couch, and combine it with my animated python powered apng and rerender it so it might be able to be ripped into a throbber for a animated splash screen. Needless to say, I kept getting frustrated by the little problems. shapedwintest part of the issue is that the font isn't free so that's why I was rendering it as a png.

I think with your approach combined with 2 stacked frames it will be possible to do a fancy animated splash with alpha. I even spent the time to write a custom fade in timer for it that looks nice. ... But yea I might have to add +1 or +2 to the frame size to work around the edge issue. That likely will uglyfy the code a bit by scattering extra variables everywhere which most folks will then ask "Why did you add this hardcoded +2?"

Also with your approach live painting directly from krita onto a frame will now support a alpha background, which is nice.

kdschlosser commented 4 years ago

I do not believe in the phrase "It cannot be done", I will however accept "With out present state of technology we cannot do it." LOL

I knew there was a better way, I have been scouring over the Windows SDK.. actually I have ported almost a million lines of it to Python (pure).

This is the real bonus of the way I have gone about it is if you wanted to draw an alpha frame that had a colored background, then you draw a png on top of it. If that png is not 100% opaque you would normally end up with the color from the background changing the colors in the png. The mechanics are there to stop that from happening.

You like how i made the hole through the middle of the frame?

both the text alpha and the overlap of the pen and brush can be coded around. doing so is expensive and it would be nice to have those issues fixed at the lower level of wxPython/wxWidgets.

I did want to let you know that a fade in and out is super simple to accomplish.

click the arrow to see the fade in/fade out example ```python #!/usr/bin/env python # -*- coding: utf-8 -*- """""" # Imports.-------------------------------------------------------------------- # -Python Imports. import os import sys import threading import ctypes from ctypes.wintypes import LONG, HWND, INT, HDC, HGDIOBJ, BOOL, DWORD # -wxPython Imports. import wx wxVER = 'wxPython %s' % wx.version() pyVER = 'python %d.%d.%d.%s' % sys.version_info[0:4] versionInfos = '%s %s' % (wxVER, pyVER) UBYTE = ctypes.c_ubyte GWL_EXSTYLE = -20 WS_EX_LAYERED = 0x00080000 ULW_ALPHA = 0x00000002 AC_SRC_OVER = 0x00000000 AC_SRC_ALPHA = 0x00000001 class POINT(ctypes.Structure): _fields_ = [ ('x', LONG), ('y', LONG) ] class SIZE(ctypes.Structure): _fields_ = [ ('cx', LONG), ('cy', LONG) ] class BLENDFUNCTION(ctypes.Structure): _fields_ = [ ('BlendOp', UBYTE), ('BlendFlags', UBYTE), ('SourceConstantAlpha', UBYTE), ('AlphaFormat', UBYTE) ] user32 = ctypes.windll.User32 gdi32 = ctypes.windll.Gdi32 # LONG GetWindowLongW( # HWND hWnd, # int nIndex # ); GetWindowLongW = user32.GetWindowLongW GetWindowLongW.restype = LONG # LONG SetWindowLongW( # HWND hWnd, # int nIndex, # LONG dwNewLong # ); SetWindowLongW = user32.SetWindowLongW SetWindowLongW.restype = LONG # HDC GetDC( # HWND hWnd # ); GetDC = user32.GetDC GetDC.restype = HDC # HWND GetDesktopWindow(); GetDesktopWindow = user32.GetDesktopWindow GetDesktopWindow.restype = HWND # HDC CreateCompatibleDC( # HDC hdc # ); CreateCompatibleDC = gdi32.CreateCompatibleDC CreateCompatibleDC.restype = HDC # HGDIOBJ SelectObject( # HDC hdc, # HGDIOBJ h # ); SelectObject = gdi32.SelectObject SelectObject.restype = HGDIOBJ # BOOL UpdateLayeredWindow( # HWND hWnd, # HDC hdcDst, # POINT *pptDst, # SIZE *psize, # HDC hdcSrc, # POINT *pptSrc, # COLORREF crKey, # BLENDFUNCTION *pblend, # DWORD dwFlags # ); UpdateLayeredWindow = user32.UpdateLayeredWindow UpdateLayeredWindow.restype = BOOL COLORREF = DWORD def RGB(r, g, b): return COLORREF(r | (g << 8) | (b << 16)) class Frame(wx.Frame): def __init__(self, parent, id=wx.ID_ANY, title=wx.EmptyString, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.FRAME_SHAPED | wx.NO_BORDER | wx.FRAME_NO_TASKBAR | wx.STAY_ON_TOP , name='frame'): wx.Frame.__init__(self, parent, id, title, pos, size, style, name) self.delta = (0, 0) self.fade_event = threading.Event() self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_MOTION, self.OnMouseMove) self.Bind(wx.EVT_RIGHT_UP, self.OnExit) self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_CLOSE, self.on_close) self.transparency = 0 def on_close(self, evt): self.fade_out() evt.Skip() def OnExit(self, event): self.Close() def OnLeftDown(self, event): self.CaptureMouse() x, y = self.ClientToScreen(event.GetPosition()) originx, originy = self.GetPosition() dx = x - originx dy = y - originy self.delta = ((dx, dy)) def OnLeftUp(self, event): if self.HasCapture(): self.ReleaseMouse() def OnMouseMove(self, event): if event.Dragging() and event.LeftIsDown(): x, y = self.ClientToScreen(event.GetPosition()) fp = (x - self.delta[0], y - self.delta[1]) self.Move(fp) def OnPaint(self, event): width, height = self.GetClientSize() text = 'Alpha test' font = self.GetFont() font.SetPointSize(font.GetPointSize() + 16) font = font.MakeBold() font = font.MakeItalic() bmp = wx.Bitmap(width, height) bmp2 = wx.Bitmap(width, height) bmp3 = wx.Bitmap(width, height) dc = wx.MemoryDC() dc.SelectObject(bmp3) dc.SetTextForeground(wx.Colour(255, 255, 255, 255)) dc.SetFont(font) text_width, text_height = dc.GetFullTextExtent(text)[:2] text_x = (width / 2) - (text_width / 2) text_y = (height / 2) - (text_height / 2) dc.DrawText(text, text_x, text_y) dc.SelectObject(bmp2) dc.SetPen(wx.Pen(wx.Colour(255, 255, 255, 255), 6)) dc.SetBrush(wx.Brush(wx.Colour(255, 255, 255, 255))) dc.DrawRoundedRectangle(103, 78, width - 200 - 3, height - 150 - 3, 5) dc.SelectObject(bmp) gc = wx.GraphicsContext.Create(dc) gcdc = wx.GCDC(gc) region1 = wx.Region(bmp) region2 = wx.Region(bmp2, wx.Colour(0, 0, 0, 0)) region1.Subtract(region2) gcdc.SetDeviceClippingRegion(region1) gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 140), 6)) gc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 150))) gc.DrawRoundedRectangle(3, 3, width-6, height-6, 5) gcdc.DestroyClippingRegion() gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 120), 6)) gc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 0))) gc.DrawRoundedRectangle(103, 78, width - 200 - 3, height - 150 - 3, 5) region1 = wx.Region(bmp) region2 = wx.Region(bmp3, wx.Colour(0, 0, 0, 0)) region1.Subtract(region2) gcdc.SetDeviceClippingRegion(region1) gcdc.SetTextForeground(wx.Colour(0, 0, 0, 130)) gcdc.SetTextBackground(wx.TransparentColour) gcdc.SetFont(font) gc.DrawText(text, text_x - 4, text_y - 4) gcdc.DestroyClippingRegion() gcdc.SetTextForeground(wx.Colour(0, 255, 0, 150)) gcdc.SetTextBackground(wx.TransparentColour) gc.DrawText(text, text_x, text_y) ## vippibmp = wx.Bitmap('Vippi50.png', wx.BITMAP_TYPE_PNG) ## gcdc.DrawBitmap(vippibmp, x=0, y=0, useMask=False) brushbmp = wx.Bitmap('brushwithalpha.png', wx.BITMAP_TYPE_PNG) gcdc.DrawBitmap(brushbmp, x=200, y=10, useMask=False) gcdc.Destroy() del gcdc dc.SelectObject(wx.NullBitmap) dc.Destroy() del dc region = wx.Region(bmp) self.SetShape(region) self.bmp = bmp self.draw_alpha(self.transparency) def fade_in(self): def _do(): for i in range(self.transparency, 256): self.transparency = i self.draw_alpha(i) self.fade_event.wait(0.02) if self.fade_event.is_set(): break threading.Thread(target=_do).start() def fade_out(self): self.fade_event.set() for i in range(self.transparency, -1, -1): self.transparency = i self.draw_alpha(i) self.fade_event.wait(0.02) if self.fade_event.is_set(): break def draw_alpha(self, transparency): hndl = self.GetHandle() style = GetWindowLongW(HWND(hndl), INT(GWL_EXSTYLE)) SetWindowLongW(HWND(hndl), INT(GWL_EXSTYLE), LONG(style | WS_EX_LAYERED)) hdcDst = GetDC(GetDesktopWindow()) hdcSrc = CreateCompatibleDC(HDC(hdcDst)) pptDst = POINT(*self.GetPosition()) psize = SIZE(*self.GetClientSize()) pptSrc = POINT(0, 0) crKey = RGB(0, 0, 0) pblend = BLENDFUNCTION(AC_SRC_OVER, 0, transparency, AC_SRC_ALPHA) SelectObject(HDC(hdcSrc), HGDIOBJ(self.bmp.GetHandle())) UpdateLayeredWindow( HWND(hndl), HDC(hdcDst), ctypes.byref(pptDst), ctypes.byref(psize), HDC(hdcSrc), ctypes.byref(pptSrc), crKey, ctypes.byref(pblend), DWORD(ULW_ALPHA) ) if __name__ == '__main__': print(versionInfos) app = wx.App() frame = Frame(None, size=(600, 300), pos=(400, 400)) frame.Show() frame.fade_in() app.Main ```

I have not tested this code, But it will give you the general idea anywho.

kdschlosser commented 4 years ago

also you can ditch the use of GraphicsContext all together and only use GCDC, the output is the same. I had it in there for purposes of testing the text alpha. the black issue might go away if you do that.

Metallicow commented 4 years ago

Yea timer fade is simple enough. Mine is basically like that with range 256 but it skips a bit slower each iteration to make is a slowing curve, tho I don't use thread, just a sleep call. Then I invert to logic and run it about x5 faster on destruction. The curve makes it look a bit nicer than just a hard number for the timer. It still causes issues with wxPy4.1 tho atm. I did actually write a starter for a pie menu im working on which is similar to the cutout. Instead of cutting out, I invert the logic to make slices in a drawn graphic, then convert the graphic to a mask and replace the green with black or whatever color I want when I have my bitmap to splat on the frame mask. You have to keep track of the regions to tell if mouse is over a slice section(button etc) piemenu

Metallicow commented 4 years ago

@kdschlosser If you really want a single file test piece, you can use my python powered logo apng to test. I know there isn't a lot of files that can be ripped into all the tests but it does 99% of them. apngdis/apngasm can do most of the take apart and put back to gether work along with pillow. AnimPythonPoweredLogo120fr512x128 This one renders slow on browsers, since a gif.

If you really want the apng source files in blender I can send em to ya, tho I would bet 99% of em will cause a BLACK screen on wxPy4.1 if rendered tho

Animated3DPythonPoweredLogo120fr512x128px1_60secAPNG .

If you are reading this line, you will notice the difference between gif and apng in your browser. My opinion is it should look the same way with alpha in a basic demo with like the brush alpha png i provided.

kdschlosser commented 4 years ago

OK so I dod some messing about with your animated PNG file. It is not that hard to create a wx Animation or splash screen from it. The apng specification is an easy specification. There is actually a python library that will break the apng apart into separate png images. go figure on this.. the name of the library is "apng" LOL..

It also provides you the frame header data. so you know how long to wait between frame renderings and how to clear the old frame and ly in the new frame. That is the part you would have to figure out how to do with masks. It did a quick and dirty. it didn't come out 100% correct but the animation portion of it works nd it works just fine. You would need to create a thread to handle the renderings.

The hardest part of the whole deal is going to be the masks. because apng has some goofy ways of handling how a new frame gets drawn. it can either get drawn over the old, or the old needs to get blanked to black with an alpha of 0 first then the new frame gets drawn. or the hole animation takes a back step by a single frame and the new frame gets drawn on top of the frame prior to the one that would normally be getting written on top of. and with the animation above the whole thing does not get drawn each time. only the spinning bit does. so you need to mask off portions of the main image in order to be able to draw it properly.

It is something that is very achievable. I have gotten fairly close to getting it right. But seeing as how apng is just about a dead image format anywho I do not see any kind of real need figure it out. apng never really took off because it had to many downfalls like not being able to reuse already added images. While it was a great attempt at improving upon the animated GIF it never really became widely adopted

kdschlosser commented 4 years ago

Here is a partially working example of the apng. There are some rendering anomalies that still need to be sorted out. read the comments before running the script.

Example Code ```python path = r'c:\PATH\TO\APNG' # you need to install the apng library import apng import wx import ctypes import threading import tempfile from ctypes.wintypes import LONG, HWND, INT, HDC, HGDIOBJ, BOOL, DWORD temp_dir = tempfile.gettempdir() UBYTE = ctypes.c_ubyte GWL_EXSTYLE = -20 WS_EX_LAYERED = 0x00080000 ULW_ALPHA = 0x00000002 AC_SRC_OVER = 0x00000000 AC_SRC_ALPHA = 0x00000001 class POINT(ctypes.Structure): _fields_ = [ ('x', LONG), ('y', LONG) ] class SIZE(ctypes.Structure): _fields_ = [ ('cx', LONG), ('cy', LONG) ] class BLENDFUNCTION(ctypes.Structure): _fields_ = [ ('BlendOp', UBYTE), ('BlendFlags', UBYTE), ('SourceConstantAlpha', UBYTE), ('AlphaFormat', UBYTE) ] user32 = ctypes.windll.User32 gdi32 = ctypes.windll.Gdi32 # LONG GetWindowLongW( # HWND hWnd, # int nIndex # ) GetWindowLongW = user32.GetWindowLongW GetWindowLongW.restype = LONG # LONG SetWindowLongW( # HWND hWnd, # int nIndex, # LONG dwNewLong # ); SetWindowLongW = user32.SetWindowLongW SetWindowLongW.restype = LONG # HDC GetDC( # HWND hWnd # ); GetDC = user32.GetDC GetDC.restype = HDC # HWND GetDesktopWindow(); GetDesktopWindow = user32.GetDesktopWindow GetDesktopWindow.restype = HWND # HDC CreateCompatibleDC( # HDC hdc # ); CreateCompatibleDC = gdi32.CreateCompatibleDC CreateCompatibleDC.restype = HDC # HGDIOBJ SelectObject( # HDC hdc, # HGDIOBJ h # ); SelectObject = gdi32.SelectObject SelectObject.restype = HGDIOBJ # BOOL UpdateLayeredWindow( # HWND hWnd, # HDC hdcDst, # POINT *pptDst, # SIZE *psize, # HDC hdcSrc, # POINT *pptSrc, # COLORREF crKey, # BLENDFUNCTION *pblend, # DWORD dwFlags # ); UpdateLayeredWindow = user32.UpdateLayeredWindow UpdateLayeredWindow.restype = BOOL COLORREF = DWORD def RGB(r, g, b): return COLORREF(r | (g << 8) | (b << 16)) # frame_control.depose_op # no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is. APNG_DISPOSE_OP_NONE = 0 # the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame. APNG_DISPOSE_OP_BACKGROUND = 1 # the frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame. APNG_DISPOSE_OP_PREVIOUS = 2 # frame_control.blend_op # all color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region. APNG_BLEND_OP_SOURCE = 0 # frame should be composited onto the output buffer based on its alpha, using a simple OVER operation APNG_BLEND_OP_OVER = 1 splash_png = apng.APNG.open(path) bitmaps = [] app = wx.App() import os print(wx.version()) first_frame = splash_png.frames[0][1] apng_width = first_frame.width apng_height = first_frame.height dc = wx.MemoryDC() frames = [] last_frame = None class AnimationFrame(object): def __init__(self, index, png, frame_control): self.index = index self.png = png self.width = frame_control.width self.height = frame_control.height self.x_offset = frame_control.x_offset self.y_offset = frame_control.y_offset # These 2 variables are what are to be used when determining # how to draw the next frame. # I have not spent enough time tinkering with it to get the values # of these 2 set up correctly to render the frames properly. # this is the reason why you are seeing the black in the animation. self.depose_op = frame_control.depose_op self.blend_op = frame_control.blend_op delay_num = frame_control.delay delay_den = frame_control.delay_den if delay_num == 0: self.delay = 0 else: if delay_den == 0: delay_den = 100 self.delay = delay_num / delay_den # I was lasy and didn't spend the time to figure out how to # collect the PNG data and send it dorectly into wx without the need for # making png files and opening them using wx.Bitmap. I know there is a # way to do it. I really do not remember how off the top of my head. save_path = os.path.join(temp_dir, 'Frame_' + str(i) + '.png') with open(save_path, 'wb') as f: f.write(png.to_bytes()) bitmap = wx.Bitmap(save_path, wx.BITMAP_TYPE_PNG) new_bitmap = wx.Bitmap(self.width, self.height) test_bitmap = wx.Bitmap(self.width, self.height) dc = wx.MemoryDC() dc.SelectObject(new_bitmap) gc = wx.GraphicsContext.Create(dc) gcdc = wx.GCDC(gc) # gcdc.SetPen(wx.TRANSPARENT_PEN) # gcdc.SetBrush(wx.Brush(wx.Colour(255, 255, 255, 150))) # gcdc.DrawRectangle(0, 0, self.width, self.height) # gcdc.DrawBitmap(bitmap, 0, 0) region1 = wx.Region(new_bitmap) region2 = wx.Region(bitmap, wx.Colour(0, 0, 0, 0)) region1.Subtract(region2) gcdc.Destroy() del gcdc dc.SelectObject(test_bitmap) gc = wx.GraphicsContext.Create(dc) gcdc = wx.GCDC(gc) gcdc.SetDeviceClippingRegion(region1) gc.DrawBitmap(bitmap, 0, 0, self.width, self.height) gcdc.DestroyClippingRegion() dc.SelectObject(wx.NullBitmap) gcdc.Destroy() del gcdc dc.Destroy() del dc self.bitmap = bitmap for i, (png, frame_control) in enumerate(splash_png.frames): frames += [AnimationFrame(i, png, frame_control)] class Frame(wx.Frame): def __init__(self): wx.Frame.__init__( self, None, -1, style=( wx.FRAME_SHAPED | wx.NO_BORDER | wx.FRAME_NO_TASKBAR | wx.STAY_ON_TOP ), size=(800, 400), pos=(400, 400) ) base_frame = frames[0] base_bitmap = wx.Bitmap(base_frame.bitmap.GetWidth(), base_frame.bitmap.GetHeight()) second_frame = frames[1] dc = wx.MemoryDC() dc.SelectObject(base_bitmap) gc = wx.GraphicsContext.Create(dc) gcdc = wx.GCDC(gc) gc.DrawBitmap(base_frame.bitmap, 0, 0, base_frame.width, base_frame.height) gcdc.SetPen(wx.TRANSPARENT_PEN) gcdc.SetBrush(wx.Brush(wx.Colour(254, 255, 252, 255))) gcdc.DrawRectangle(0, 0, second_frame.width, second_frame.height) dc.SelectObject(wx.NullBitmap) gcdc.Destroy() del gcdc dc.Destroy() del dc self.alpha_bitmap = base_bitmap self.bmp = base_bitmap self._thread = None self.frame = 1 self.close_event = threading.Event() self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None) self.Bind(wx.EVT_PAINT, self.OnPaint) def run(self): while not self.close_event.is_set(): frame = frames[self.frame] self.draw() self.close_event.wait(frame.delay) def start(self): self._thread = threading.Thread(target=self.run) self._thread.daemon = True self._thread.start() def OnPaint(self, evt): bitmap = wx.Bitmap(*self.GetClientSize()) dc = wx.MemoryDC() dc.SelectObject(bitmap) gc = wx.GraphicsContext.Create(dc) gcdc = wx.GCDC(gc) gcdc.DrawBitmap(self.bmp, 0, 0) gcdc.Destroy() del gcdc dc.Destroy() del dc self.draw_alpha(bitmap) def draw(self): frame = frames[self.frame] alpha_bitmap = wx.Bitmap(self.alpha_bitmap.GetWidth(), self.alpha_bitmap.GetHeight()) dc = wx.MemoryDC() dc.SelectObject(alpha_bitmap) gcdc = wx.GCDC(dc) gcdc.DrawBitmap(self.alpha_bitmap, 0, 0) gcdc.DrawBitmap(frame.bitmap, frame.x_offset, frame.y_offset) dc.SelectObject(wx.NullBitmap) gcdc.Destroy() del gcdc if self.frame + 1 == len(frames): self.frame = 1 else: self.frame += 1 self.bmp = alpha_bitmap self.Refresh() self.Update() def draw_alpha(self, bmp): hndl = self.GetHandle() style = GetWindowLongW(HWND(hndl), INT(GWL_EXSTYLE)) SetWindowLongW(HWND(hndl), INT(GWL_EXSTYLE), LONG(style | WS_EX_LAYERED)) hdcDst = GetDC(GetDesktopWindow()) hdcSrc = CreateCompatibleDC(HDC(hdcDst)) pptDst = POINT(*self.GetPosition()) psize = SIZE(*self.GetClientSize()) pptSrc = POINT(0, 0) crKey = RGB(0, 0, 0) pblend = BLENDFUNCTION(AC_SRC_OVER, 0, 255, AC_SRC_ALPHA) SelectObject(HDC(hdcSrc), HGDIOBJ(bmp.GetHandle())) UpdateLayeredWindow( HWND(hndl), HDC(hdcDst), ctypes.byref(pptDst), ctypes.byref(psize), HDC(hdcSrc), ctypes.byref(pptSrc), crKey, ctypes.byref(pblend), DWORD(ULW_ALPHA) ) frme = Frame() frme.Show() frme.start() app.MainLoop() ```
kdschlosser commented 4 years ago

@Metallicow

Here is the APNG support you have requested. You should be able to modify this script to work with any apng file.

I embedded the apng file above into the script.

I did not do a whole mess of performance tweaks and there are things that could use to be cleaned up and changed. It does provide full apng animation support. It works without any anomalies in the rendering.

apng_script.zip

Metallicow commented 4 years ago

Had a few min to look over your code. Many strange things occured but I managed to code yet another hmmmOpps() function into SourceCoder. I can manage to get a screenie this time before hard crashing.

Just to note, I dont use the apng lib, I have used the official apngasm/dis from the kickstarter onwards and other image libs to handle most of what I do with them.

1st weird thing. This is just plain stupid. Probably an issue with apng lib itself.

Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.

C:\Users\testuser\Desktop>tmprun.py
You need to install the apng library in order to use this script.
pip install apng

wtf... Ok... reading the file your code. checked location.... looks ok but could be better... Let's do a runitanyway in SourceCoder... wow it actually worked... sorta until crash...

  1. probably a problem with your thread logic or whatever weirdness apng lib cant import

apngchokes

...apparently both your examples start this way. The apng starts running at the speed of light but over time it slows down until it finally crashes.

kdschlosser commented 4 years ago

how long does it take until the crash happens?

I am not using the any of the events in wx to handle the drawing operations like EVT_PAINT. so there is nothing that is changing hands to other thenreads other then the main thread passing off to a worker thread and that worker thread is what is handling the drawing. so there shouldn't be anything that "starts off at the speed of light and slows down".

I have had the animation running for hours and not had it crash. I will start it up again and let it run and see what happens. I will also have it log how long each iteration of the animation it takes.

the apng package does not use any external software to extract the frames inside of an animation. this is the reason why I used it. I had started to write this ability myself. but once I discovered the apng library and it was done fairly close to what I would have done I decided not to.

kdschlosser commented 4 years ago

In your test did you run the script exactly how it is right now? or did you modify it in some way?

Metallicow commented 4 years ago

Nope. no mod. I'd lay a guess if I run it in x64 just the crash might occur faster. ...maybe how the apng lib is loading and sending info... idunno It seems like the thread is outrunning the draw function, but cant be sure. It takes like 4-8 sec to crash, depending on if it was precompiled before running.

kdschlosser commented 4 years ago

OK so I did run the test and after 27 iterations I did produce the error you pointed out.

That Error has got to be a bug in wxWidgets. The same operation is being performed over and over again and does not have an issue for 27 times then all of a sudden out of the blue it is saying that I have not selected a bitmap before I am creating a GCDC..

and the thread is running at the same pace or really close to the same pace.

4.180239200592041
3.711212396621704
3.713212251663208
3.713212490081787
3.712212324142456
3.713212251663208
3.712212562561035
3.713212490081787
3.728212833404541
4.181239366531372
3.9932281970977783
3.713212490081787
3.712212562561035
3.71421217918396
3.713212490081787
3.712212324142456
3.727213144302368
3.714212417602539
3.727213144302368
3.713212251663208
3.7282135486602783
3.72821307182312
3.822218656539917
3.714212417602539
3.7582149505615234
3.728213310241699
3.729213237762451

I would need to compile a debugging version of wx and also python to be able to track down the issue.

kdschlosser commented 4 years ago

OK so this is the portion of the wxWidgets code where the problem is stemming from in wxWidgets.

wxGraphicsContext * wxGDIPlusRenderer::CreateContext( const wxMemoryDC& dc)
{
    ENSURE_LOADED_OR_RETURN(NULL);
#if wxUSE_WXDIB
    // It seems that GDI+ sets invalid values for alpha channel when used with
    // a compatible bitmap (DDB). So we need to convert the currently selected
    // bitmap to a DIB before using it with any GDI+ functions to ensure that
    // we get the correct alpha channel values in it at the end.

    wxBitmap bmp = dc.GetSelectedBitmap();
    wxASSERT_MSG( bmp.IsOk(), "Should select a bitmap before creating wxGCDC" );

I am in fact selecting the bitmap before the call to construct the GCDC. It turns out that the bitmap for some reason is not returning True from IsOk(). I need to figure out why.

I did however discover there is a missing method in wxPython. there is no method MemoryDC.GetSelectedBitmap. I would love to be able to make a comparison between the one that MemoryDC has selected and the one I have created.

I still need to do some more testing and find out why the bitmap is not ok. and this seems to be happening always on the 27th loop of the animation on the 24th frame of that loop. That is remaining a constant.

Metallicow commented 4 years ago

We have this little python game we play sometimes... Actually i kinda invented it. I like to call it pyTink but phonetic for "think". verbal vs interpreter Well, there is a piece of hardware that needed to be retired. I didn't want to just destroy it like all the rest, so I made it into a bork machine.... When we had game night, most everyone there knew at least basics of python. I then installed python on it and said guess what!?! nor more computers vs games tonight. Anyhow we managed to make up some [if, elif, exec, etc] cards all python based.... The rule of the game was simple: defeat your opponents and be last man standing. Card deck on table with obviously "dangeous" ways to play them in it. Each player is givin 1-3 variables to input into the interpreter to play the game. No other players know what you put into it LOL. Then let the cards fly and have the python god to be doomed sort out who is winner. Interpreter is of course the game rules so its answer is final.. After many iterations one night we had me vs linuxguru as last surviving players that night. I had 1 card left and he had all 20. Obviously I had no chance in my mind... so I just flopped next top card over on table and it was CHEESE! that's like a wild card with another card played. I just so happened to have the "1-liner" card. so after like 10 min of arguing who would win, we input it into interpreter. 1 card was obviously if True: run() It did some weird things and has never worked the same since. I still can't stop rolling on the floor laughing everytime I ask him a question nowadays. replies to anything, I ask as "Is that TRUE?" ...somehow I got a extra long exception named after me in his codebase hahaha. it violates all pep8 rules

kdschlosser commented 4 years ago

I found the problem.

There is a memory leak. It is not in wxWidgets or wxPython bit it was in the script code. I was not cleaning up properly. I needed to release and delete some of the windows handles. But also I needed to call wxBitmap.Destroy() and delete the python object.

This solved the memory leak problem.

The error that is produced by wxWidgets is misleading. Because wxWidgets uses wxBitmap.IsOk to determine if the memory dc has selected a bitmap the error gets created if IsOk returns False. The issue there is that there is more then one reason why IsOk would return False. One of them being insufficient memory.

I have modified the script to fix the memory leak, and I also modified it to provide a proper error if there is no enough memory to render the animation.

apng_script.zip

kdschlosser commented 4 years ago

But that should solve the issue with the crash. I was unable to locate an issue with the speeding up problem you encountered. One of the things you have to remember is when dealing with any graphics animations any large rendering that takes place by any application is going to cause a slowdown. even if the rendering is being performed by a completely separate process. I think there is only a single thread/process that actually handles drawing to the screen. so anything that is to be drawn has to "wait in line". This would cause a slowdown. I have not looked into the inner working of the Windows GDI so I am not able to tell you if that really is the case. But it does make sense.

The animation should have a total of about 3.7 seconds for a runtime. I would have to go and check and see what each frames delay time is and add them up to see how accurate the script is actually running.

kdschlosser commented 4 years ago

I just checked the delays. each frame has a delay of 0.016666666666666666 seconds. and all frames added up ends up being 1.9833333333333312 seconds. so my time of 3.7ish is not correct.

I am going to have to add in a high precision timer to time how long the drawing takes and subtract that from the delay. and if there is anything left over then wait that time. and if not then continue on to render the next frame.

I will add that now. Tho the speed looks good how it is I still want it to be proper!!

kdschlosser commented 4 years ago

OK so I have gotten the animation time to be better then it was.

I am down to 2.264129400253296 seconds total animation run time. I am not able to get it to be exact with what is in the apng. This is due to how long is takes wxPython to do it's rendering operations and also how ,ong it takes for the windows api call to do it's thing.

264ms off over 118 frame is not horrible. If I used Cython and converted the script into c code and compiled it into a python extension I am sure we could get the times to be perfect. I think that a 264ms error over 118 frames is not awful considering the speed at which each frame should be rendered.

apng_script.zip

kdschlosser commented 4 years ago

It was taking a HUGE amount of time to crash on my machine because I am running 64gb of ram. when I tested it after you reported the problem I had a project that was open in my IDE that is massive at close to a million lines of code. So my IDE had a grip of RAM all gobbled up. so there was far less available. When I had the animation running for a really long time to test it I didn't have much open. so most of the 64gb was available. and at 20meg or so being consumed each loop it would have taken a really long time to run out.

kdschlosser commented 4 years ago

here is a newer version that fixes an issue where the apng will not render properly if frame 0 has an alpha channel apng_script.zip

Metallicow commented 4 years ago

Adding wx.MilliSleep(8) to the end of the update animation method fixes the draw rate. Github garbles the filename when attaching files. The apng should be 120frames a second. Gifs have problems running at full speed in all browsers. The last script you uploaded seems to run fine by adding the millisleep line. I have other versions that have fewer frames but that one is 120 a sec.

Metallicow commented 4 years ago

I recall that I had an animation where I had my snakey icon bonce around a screen, and had a few different ways of doing it. Regular timer in wxPython would only render at like 15 on MSW. this is like a limitation. But using pygame and thread got it to run faster on windows. Tho I scrapped my idea when I started figuring out how bad crashes was when using pygame embedded in wxPy. Tho on linux the icon bounced around at the speed of light. But yea, I recall I used a thread to bypass the frame draw limitation.

Metallicow commented 4 years ago

https://commons.wikimedia.org/wiki/File:Animated_PNG_example_bouncing_beach_ball.png This one is still showing as black background. self.apng = apng.APNG.open(file=r'Animated_PNG_example_bouncing_beach_ball.png') the calculation for the delay I think needs more work, but at least the page tells how many frames and a delay. The python apng has a background on the first frame, the bouncing ball doesn't. maybe that has something to do with it...

Metallicow commented 4 years ago

hmmm I think I might restart over from the one that was working fine and incorporate the latter stuff in bit by bit. If you got the unanimated one to work with alpha, then somehow something broke inbetween. @kdschlosser This is how you would load the png from data, the "supposedly" not lazy way.

import io
img = wx.Image(io.BytesIO(png.to_bytes()), wx.BITMAP_TYPE_PNG)
kdschlosser commented 4 years ago

I have the black background issue fixes.

I am not sure where you are coming up with the 120 frames a second. You have each frame set to a 16 millisecond render time.

16 * 120 = 1920 = 1.92 seconds

If you add any additional delays or waits you are going to slow the rendering time down even further. Right now it is getting as close as it can to the 16 milliseconds per frame as it can. the rendering time is longer then 16 milliseconds. it takes on the order of 17 milliseconds to render each frame.

The only thing I do not like about wx.Millisleep is the inability to exit the sleep. the program has to wait until the sleep is finished. This is not a good thing when dealing with threads. If a thread needs to be shut down it usually has to be done at the snap of the fingers. This is the same reason why you do not want to use time.sleep() in a thread. using threading.Event.wait() you have the ability to exit the wait from another thread if needed by calling threading.Event.set(). The same threading.Event instance can also be used to keep a loop going inside of the thread. so if you want to have the thread exit you can do so with 2 commands. one to set the event, and the second to join the thread so your program will not continue until the thread has shutdown. This is the way to do it in order to not have PyDeadObject tracebacks take place when closing an application GUI.

Here is an updated version of the script. I fixes the issue with the background not having an alpha channel. It also removes the writing of the temporary files.

You can run the script from a shell and supply a command line argument which would be a path to an apng file you want to load instead of the embedded one.

I changed the embedded apng to the bouncy ball. apng_script.zip

kdschlosser commented 4 years ago

Oh and the calculation for the delay is exactly to the apng specification that is written here,

The delay_num and delay_den parameters together specify a fraction indicating the time to display the current frame, in seconds. If the denominator is 0, it is to be treated as if it were 100 (that is, delay_num then specifies 1/100ths of a second). If the the value of the numerator is 0 the decoder should render the next frame as quickly as possible, though viewers may impose a reasonable lower bound.

Frame timings should be independent of the time required for decoding and display of each frame, so that animations will run at the same rate regardless of the performance of the decoder implementation.

I have done this to the best of my ability. Because of how long it takes to render the bmp to the screen (something I do not have control over) I time how long that rendering process takes. I subtract that time from the delay that is provided in the apng frame. if the remaining value is > 0 then I have the program wait for that remainder. This is the best I am able to do in order to get the delays as close as possible. I am handling as much of the rendering of the frame as possible ahead of time so the only thing that ends up needing to be done is setting up a region that defines the alpha and copying the buffered frame to a new bmp and that new bmp is what gets drawn to the screen.

I might be able to squeeze it for better performance by reusing some of the windows structures and possible setting it up so that I do not have to keep on creating and destroying windows handles.

Metallicow commented 4 years ago

@kdschlosser I ripped everything out that wasn't needed with apng and started over from the sample that worked on wxPy4.0. I think this should work. Please test. Bouncy ball apng works now with alpha.

testapng.py - Click to expand sample sample apng image used with alpha Animated_PNG_example_bouncing_beach_ball.png ![Animated_PNG_example_bouncing_beach_ball](https://user-images.githubusercontent.com/4668356/77041392-d3045780-6987-11ea-975c-4cd056de7004.png) ```python #!/usr/bin/env python # -*- coding: utf-8 -*- """Shaped Frame apng sample with alpha""" # Imports.-------------------------------------------------------------------- # -Python Imports. import os import sys import threading import ctypes from ctypes.wintypes import LONG, HWND, INT, HDC, HGDIOBJ, BOOL, DWORD # -wxPython Imports. import wx # -apng Imports. try: import apng except ImportError: print('You need to install the apng library in order to use this script.') print('pip install apng') sys.exit(1) # -Platform check. if not sys.platform.startswith('win'): print('This script will only run on Microsoft Windows.') sys.exit(1) wxVER = 'wxPython %s' % wx.version() pyVER = 'python %d.%d.%d.%s' % sys.version_info[0:4] versionInfos = '%s %s' % (wxVER, pyVER) UBYTE = ctypes.c_ubyte GWL_EXSTYLE = -20 WS_EX_LAYERED = 0x00080000 ULW_ALPHA = 0x00000002 AC_SRC_OVER = 0x00000000 AC_SRC_ALPHA = 0x00000001 class POINT(ctypes.Structure): _fields_ = [ ('x', LONG), ('y', LONG) ] class SIZE(ctypes.Structure): _fields_ = [ ('cx', LONG), ('cy', LONG) ] class BLENDFUNCTION(ctypes.Structure): _fields_ = [ ('BlendOp', UBYTE), ('BlendFlags', UBYTE), ('SourceConstantAlpha', UBYTE), ('AlphaFormat', UBYTE) ] user32 = ctypes.windll.User32 gdi32 = ctypes.windll.Gdi32 # LONG GetWindowLongW( # HWND hWnd, # int nIndex # ); GetWindowLongW = user32.GetWindowLongW GetWindowLongW.restype = LONG # LONG SetWindowLongW( # HWND hWnd, # int nIndex, # LONG dwNewLong # ); SetWindowLongW = user32.SetWindowLongW SetWindowLongW.restype = LONG # HDC GetDC( # HWND hWnd # ); GetDC = user32.GetDC GetDC.restype = HDC # HWND GetDesktopWindow(); GetDesktopWindow = user32.GetDesktopWindow GetDesktopWindow.restype = HWND # HDC CreateCompatibleDC( # HDC hdc # ); CreateCompatibleDC = gdi32.CreateCompatibleDC CreateCompatibleDC.restype = HDC # HGDIOBJ SelectObject( # HDC hdc, # HGDIOBJ h # ); SelectObject = gdi32.SelectObject SelectObject.restype = HGDIOBJ # BOOL UpdateLayeredWindow( # HWND hWnd, # HDC hdcDst, # POINT *pptDst, # SIZE *psize, # HDC hdcSrc, # POINT *pptSrc, # COLORREF crKey, # BLENDFUNCTION *pblend, # DWORD dwFlags # ); UpdateLayeredWindow = user32.UpdateLayeredWindow UpdateLayeredWindow.restype = BOOL # int ReleaseDC( # HWND hWnd, # HDC hDC # ); ReleaseDC = user32.ReleaseDC ReleaseDC.restype = INT # BOOL DeleteDC( # HDC hdc # ); DeleteDC = gdi32.DeleteDC DeleteDC.restype = BOOL COLORREF = DWORD def RGB(r, g, b): return COLORREF(r | (g << 8) | (b << 16)) # This class sets up the objects needed to be # used in a better fashion then the apng library has. # it keeps the code quite a bit cleaner. class AnimationFrame(object): def __init__(self, index, png, frame_control): self.index = index self.png = png self.width = frame_control.width self.height = frame_control.height self.x_offset = frame_control.x_offset self.y_offset = frame_control.y_offset self.depose_op = frame_control.depose_op self.blend_op = frame_control.blend_op self.delay = frame_control.delay self.delay_den = frame_control.delay_den self.bitmap = wx.Bitmap.NewFromPNGData(data=png.to_bytes(), size=-1) if not self.bitmap.IsOk(): raise MemoryError('Insufficent memory to render animation.') class TimerThread(threading.Thread): """TimerThread to regulate animation.""" def __init__(self, event, windo): """Default class constructor.""" threading.Thread.__init__(self) self.windo = windo self.close_event = event def run(self): """Run the TimerThread.""" windo = self.windo frames = windo.frames while not self.close_event.is_set(): try: windo.Refresh() except RuntimeError: # Crash on closing. break frame = frames[windo.current_frame] delay = frame.delay wx.MilliSleep(delay) if windo.current_frame + 1 == len(windo.frames): windo.current_frame = 0 else: windo.current_frame += 1 def stop(self): """Stop the TimerThread.""" self.close_event.set() def stopped(self): """Is the TimerThread stopped?""" return self.close_event.isSet() class Frame(wx.Frame): def __init__(self, parent, id=wx.ID_ANY, title=wx.EmptyString, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.FRAME_SHAPED | wx.NO_BORDER | wx.FRAME_NO_TASKBAR | wx.STAY_ON_TOP , name='frame'): wx.Frame.__init__(self, parent, id, title, pos, size, style, name) self.frames = [] # self.apng = apng.APNG.from_bytes(base64.b64decode(APNG)) self.apng = apng.APNG.open(file='Animated_PNG_example_bouncing_beach_ball.png') self.bitmaps = [] self.current_frame = 0 self._thread = None # creating the helper class instances for i, (png, frame_control) in enumerate(self.apng.frames): frame = AnimationFrame(i, png, frame_control) self.frames += [frame] # the base frame or "preview png" is always going to be frame 0. # this is designed so that an apng can be loaded as a normal png # in the event that animation is not supported. # This is also where the animation starts and is going to be # what is used to set up the animation width and height. base_frame = self.frames[0] self.base_frame = self.frames[0] self.SetSize((base_frame.width, base_frame.height)) self.delta = (0, 0) self.BindEvents() def BindEvents(self): self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_MOTION, self.OnMouseMove) self.Bind(wx.EVT_RIGHT_UP, self.OnExit) self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None) self.Bind(wx.EVT_PAINT, self.OnPaint) def StartThread(self): close_event = threading.Event() self._thread = TimerThread(close_event, self) self._thread.daemon = True self._thread.start() def OnExit(self, event): self._thread.stop() wx.CallAfter(self.Destroy) # self.Close() def OnLeftDown(self, event): self.CaptureMouse() x, y = self.ClientToScreen(event.GetPosition()) originx, originy = self.GetPosition() dx = x - originx dy = y - originy self.delta = ((dx, dy)) def OnLeftUp(self, event): if self.HasCapture(): self.ReleaseMouse() def OnMouseMove(self, event): if event.Dragging() and event.LeftIsDown(): x, y = self.ClientToScreen(event.GetPosition()) fp = (x - self.delta[0], y - self.delta[1]) self.Move(fp) def OnPaint(self, event): width, height = self.GetClientSize() bmp = wx.Bitmap(width, height) dc = wx.MemoryDC() dc.SelectObject(bmp) gc = wx.GraphicsContext.Create(dc) gcdc = wx.GCDC(gc) gcdc.DrawBitmap(self.frames[self.current_frame].bitmap, x=0, y=0, useMask=True) # Draw frame rect with transparent brush for sanity check. gcdc.SetPen(wx.BLACK_PEN) gcdc.SetBrush(wx.TRANSPARENT_BRUSH) gcdc.DrawRectangle(0, 0, width, height) gcdc.Destroy() del gcdc dc.SelectObject(wx.NullBitmap) dc.Destroy() del dc region = wx.Region(bmp) self.SetShape(region) self.draw_alpha(bmp) def draw_alpha(self, bmp): hndl = self.GetHandle() style = GetWindowLongW(HWND(hndl), INT(GWL_EXSTYLE)) SetWindowLongW(HWND(hndl), INT(GWL_EXSTYLE), LONG(style | WS_EX_LAYERED)) hdcDst = GetDC(GetDesktopWindow()) hdcSrc = CreateCompatibleDC(HDC(hdcDst)) pptDst = POINT(*self.GetPosition()) psize = SIZE(*self.GetClientSize()) pptSrc = POINT(0, 0) crKey = RGB(0, 0, 0) pblend = BLENDFUNCTION(AC_SRC_OVER, 0, 255, AC_SRC_ALPHA) SelectObject(HDC(hdcSrc), HGDIOBJ(bmp.GetHandle())) UpdateLayeredWindow( HWND(hndl), HDC(hdcDst), ctypes.byref(pptDst), ctypes.byref(psize), HDC(hdcSrc), ctypes.byref(pptSrc), crKey, ctypes.byref(pblend), DWORD(ULW_ALPHA) ) # cleanup to avoid a memory leak. DeleteDC(HDC(hdcSrc)) ReleaseDC(HWND(hndl), HDC(hdcDst)) ##bmp.Destroy() ##del bmp if __name__ == '__main__': print(versionInfos) app = wx.App() frame = Frame(None) frame.Centre() frame.Show() frame.StartThread() app.MainLoop() ```
Metallicow commented 4 years ago

The python apng has 120 frames and should be 1/60 second so 1000/120 = 8 milliseconds is how i calculated it. so in order for it to look proper one rotation should take 1 second

Metallicow commented 4 years ago

The animation speed is sporadic on your last example and speeds up and slows down, but yea you managed to get the alpha to look right.

Metallicow commented 4 years ago

I also noticed that the ball reflection on your sample isn't semi-transparent....

Metallicow commented 4 years ago

With my example, I need to do some sort or position coding on each frame to get it to look right. It works, but each frame is bouncing against the left wall instead of forcing the python logo to be square. also 16 milliseconds looks more proper. Something is wrong with the division somewhere. The ball animation looks OK speedwise tho. I think the issue is that since apng was never accepted initially, everyone else nowadays has a different idea as to how the delays and everything should be formatted. Tho the optimized way I rendered the python apng is actually only drawing background on 1st frame, so I may need to adjust that bit... with the drawing constants somehow... there never really was an official spec, so I think apng lib being used might be off also... Still waiting for Pillow to push thru the apng support they are working on.

Metallicow commented 4 years ago

It appears that depending on the era that the apng was made you might get different results because whatever the math may be the numbers might be flipped and for example if you feed that number into Sleep(), MilliSleep(), or MicroSleep() you will get a totally different result. Plus your example does calculations for the delay which are subtracted, which should be already part of the delay spec in any sane guess. The weird part is that most browsers nowadays render them fine, so however firefox/chrome/opera is doing the figuring out of the delay will probably be the accepted way in the end. The two different apngs render fine in the browser, but depending on if I use your example or mine, one is right and the other is wrong and vise versa. My calculation of 8 milliseconds on your example with python png runs fine, but the ball example works with my minimal example. But both render just fine in the browser... so obviously the browser code has a way of figuring out what the delay spec should be.

kdschlosser commented 4 years ago

OK so I have trimmed the fat off of the Windows API function calls. When I use the apng you made above the rendering time is down to 6.89748699999998 milliseconds. So now the program is able to properly render the frames at the 16 millisecond speed that is enbedded into the frames.

apng_script.zip

kdschlosser commented 4 years ago

The delay the browser is using is the exact same delay that I am using. The problem was stemming from the rendering time being greater then the delay. This would make the animation slower. I reorked the Windows API calls so they reuse portions of it between frame renders instead of creating each piece new every frame. This has reduced the overhead quite a bit.

I checked the apng file you attached with an apng disassembler and it too states that you have a 16ms delay set for each frame. So I know my calculations are correct. The specification states that the rendering portions of the code should not affect the speed in which the apng can be displayed. I have done as much rendering as possible before the animation starts. I wish there was a way I could remove having to copy the bmp so we can draw it on the screen. But there is no way to achieve that with the Windows calls.

If I set this up to not use the alpha shaped frame. and set it up to be a panel instead I could simply write the data to a ClientDC instead and I would not have to have the additional overhead caused by the copying of the bmp.

I am not sure how much that would speed things up I do not believe it would be all that much.

I would love to extend the wx.Bitmap class so it can handle APNG files. There would need to be a whole lot of Monkey patching taking place in order to handle the drawing of each frame. for each of the various widgets that you can use a wx.Bitmap with. I am fairly certain that the bitmaps are not "drawn" by wxWidgets but instead has the data copied out of it and placed into a Windows bitmap buffer and that buffer gets passed to a Windows API call to be drawn. Because of the nature of that process there are not going to be repeated calls unless there is a need to refresh the information on that portion of the screen. So once it is drawn it does not get redrawn at some kind of an interval.

So those redrawing mechanics would need to be added to each of the available widgets. The question is how is the data contained within the wx.Bitmap retrieved when the bitmap is to be drawn on the screen. If I knew that bit of information I would be able to refresh and update which would trigger a collection of data to occur from the wx.Bitmap instance. at that point I would be able to return the proper frame.

All of this could be handled internally to the bitmap if the bitmap had a "parent" that would be the widget. I do not know if this exists. I do not think that it does.

I am going to poke about the wx.Bitmap class and see if I am able to isolate the mechanism that gets used to get the data,

Metallicow commented 4 years ago

Ok. checked out your sample and made afew modifications to fix the sporadic timing issues im getting... The reflection on the ball is still incorrect tho. still not semi-transparent.

  1. zero speed just runs as fast as it can render so it is actually valid, so changed > to >=. placed sanity check in print statement. if it wasn't in a thread it is throttled/limited to 15ms how wxPython/Widgets deals with windows, so the way you got it bypasses that just fine in a GUI without causing any issues.

    def run(self):
        while not self.close_event.is_set():
            start = perf_counter()
            self._update_animation()
            frame = self.frames[self.current_frame]
            stop = perf_counter()
            delay = frame.delay - (stop - start)
    
            print('debug %d' % delay)
            if delay >= 0:
                print('delay > 0: %d' % delay)
                self.close_event.wait(delay)
                # wx.MilliSleep(delay)
    
            if self.current_frame + 1 == len(self.frames):
                self.current_frame = 0
            else:
                self.current_frame += 1
  2. In order to get it to work right with the thread timer the delay_den should be wrapped as a float in order to accomodate python 2 otherwise it always ends up as 0.

  3. change your import to be Py2/3 phoenix friendly for perf_counter

    try:
    from time import perf_counter # NOQA
    except ImportError:
    from time import clock as perf_counter # NOQA

Also tested with this also. The antialias on the edges look grainy and flattened. no semi transparency. https://commons.wikimedia.org/wiki/File:APNG_throbber.png APNG_throbber If you can get the edges to look proper on the 2 with the transparency, then I think everything else looks fine with the python powered one with this iteration.

Edit also I think the perf_counter probably isnt needed anyhow.... cant see any reason why to do the extra calc...

Metallicow commented 4 years ago

with the float fix, this would be the optimized run method.

    def run(self):
        frames = self.frames
        len_frames = len(self.frames)
        _update_animation = self._update_animation
        close_event = self.close_event
        while not close_event.is_set():
            current_frame = self.current_frame
            _update_animation()
            close_event.wait(frames[current_frame].delay)

            if current_frame + 1 == len_frames:
                self.current_frame = 0
            else:
                self.current_frame += 1
Metallicow commented 4 years ago

creating a value or func/meth to tell if all the values are the same would be useful also... how useful would depend on the users use of the value, if even used at all. for example any apng with all the same delays could be scaled as anything to look quote "right" to the user, tho an amination that takes a minute might be scaled elsewise and look ugly....

kdschlosser commented 4 years ago

wxPython 4.x only runs on Python 3.x so adding the float() is moot and does not need to be done.

That sanity check of >= does not need to be done. It is an additional function call that gets done if delay == 0 and that call does not need to be made. That call does take time to process. so why even do it when it is not necessary?? All it does is adds additional time to process thus slowing down the animation. Now it may not seem like much but if you keep on doing that over and over again it will accumulate.

I can also add in a "frame skip" if needed. it would do this when the returned value of running_time % animation_loop_time is >= a frame's delay time in the animation. The skipping of the frame only means that it is not going to get drawn to the screen. The only frame that cannot get skipped would be frame 0.

This would keep the animation run time correct. I use a similiar process when I am rendering my security cameras using wxPython. The other option we can add is turning off anti-aliasing when rendering. This should also speed up the rendering.

I have been writing an apng addon for wxPython I am removing the use of the apng library and adding in the full specification for apng into it. I am also breaking down the png specification so a user would be able to set the metadata for each frame if they wanted to.

It is also going to handle sequencing of the frames, this is something the apng library does not have the ability to do. The specification does not mandate that the frames be coded in any specific order.

The delay is not something that should be "scaled" the delay would always remain a constant. Right now we are only rendering the apng in the exact size it is made in. This is going to change as well. we will scale the size of the apng when a size event occurs. so the smaller an apng is the faster it is going to render and the delay adjustment code is going to handle that change in rendering speed as needed. the displayed size coupled with the users computer speed is what is going to determine exactly how fast we re able to render each frame. because both of those factors are ?'s the only thing we can do in terms of producing a correct animation speed is what I have done above, other then adding in a frame skip. The frame skip is something that we can add in as an option that can be turned on or off. The other thing we can add is an event that will signal the application that there is an overrun occurring this way the application then can make a decision as to whether or not it wants to resize the apng or to turn on frame skipping. In the event object we can add in the amount of overrun that is occurring.

I am writing this thing to also allow a user to create a new animation. a frame in an animation can be any file type that wx Image is able to convert into a wxBitmap. the apng would also be able to be saved to a file or a file like object.

The png and apng specification is really not all that complex. Most of the png specific bits are handled by wxPython/wxWidgets all that has to be done is we have to separate the IDAT chunk from a PNG and place it into an fdAT chunk inside the apng. there needs to also be an fcTL chunk added for each frame which provides width, height, x offset, y offset, blend op, delay and dispose op data that is needed to know how to render each frame.

I will create a repo for the apng library and you can help out with it if you like. Probably best to move the conversation to there anyway. This topic is way off track. I will probably close the issue and possibly reopen it. I have some code examples of what can be done to band-aide the problems that I will post in the new issue.

In the next day or so I will open the repo. I will post the link to it in here. then we can continue the conversation there. Even tho the apng specification is almost dead it is a great way to go about providing animation support for any image type that wxPython supports. This is because it is a really simple specification. Adding support for other animation types can be derived from this model.

One thing I have not checked is how an animation gets loaded using wx.Image. I know that wx.Image supports multiple "layers" or images in a single object. Does it load each frame if i load an apng?? gonna have to try it.

Metallicow commented 4 years ago

Phoenix has ran on Py2 and 3 since it was launched... I ported the whole demo... Im running all your code on Py 2 too lol. The sample on Py2 is ending up doing integer division is more specific with the float part. Edit: when you are calculating this in the animation class, this will always do true division on Py2/3. Then when run in thread.wait() it will get a float instead of 0 or whatever self.delay = self.delay_num / float(self.delay_den)

kdschlosser commented 4 years ago

I didn't say Phoenix. I said wxPython 4.x I didn't think that wxPython 3.x was being developed anymore.

either or it is not a big problem to add the float().

I did however run into a road block. the components needed from wxWigets to be able to add custom animation decoders have not been added to wxPython. So I am not able to do what I wanted to without having to write a new control. The reason why I wanted to add a new decoder was because the rendering of the animation to the screen was handled in c code and it would be faster. a good comparison is racing a top fuel drag car (c code) against a pinto (python). Plus everything is already in place to handle sizing events and the running of the animation. It would simply be the best and easiest way to go about it.

Metallicow commented 4 years ago

wxPy 4 is officially Phoenix launch. If it doesnt say phoenix when you do wx.version, then you are using classic. There was some versions in wxPy 3 that worked but they was still alpha/beta testing. If it isnt on PyPI then it is probably classic.

kdschlosser commented 4 years ago

well I'll be damned. last time I had checked (don't know when that was) Phoenix was not available for Python 2.x I just checked and it is there now....

So my bad on mentioning 3.x I have them all mixed up. the 3.x is because last I knew 3.x was the latest release that was available for Python 2.x That is no longer the case.

so the float does in fact need to be there you are correct.

kdschlosser commented 4 years ago

OK so here is the repository that adds APNG support to wxPython. It is not tested and is a WIP.

https://github.com/kdschlosser/wxAnimation

What I am doing is extending wx.adv.AnimationCtrl and wx.adv.Animation and adding a python version of wxAnimationDecoder. I have also added the methods for AnimationCtrl and Animation that have not been included in wxPython. I am setting it up so that the original classes will get used if an animation is an ANI or GIF. otherwise it will use the pure python version of it.

There is actually a problem with the original C code, It uses wx.Timer which cannot be run using a timeout value of 0. I would have to check but I believe that wxTimer has resolution down to 1 millisecond. So all frames will have at least a 1 millisecond delay. I am going to alter the code so it will do a proper update without having to have a delay of 1ms for a frame that may have a delay of 0.

I am also going to add in code to correct the rendering time vs delay time.

Metallicow commented 4 years ago

iirc wx.Timer was the one restricted to like 15ms on windows. MilliSleep or Thread should work around the limitation tho. Might ask Robin again but I recall that was the case when I ran into it the first time.

kdschlosser commented 4 years ago

I like the approach oif using threads anyhow. because it will not tie up the main thread. I have corrected some of the initial problems with the code I posted. I have to figure out why it is not rendering correctly. and I also have to add in support for transparency. as it seems from looking at the code that the original animation code does not support the rendering of alpha.

Metallicow commented 4 years ago

It appears that pillow finally is getting apng support/imagegrab pushed with 7.1.0. https://github.com/python-pillow/Pillow/issues/4354#issuecomment-606950009 Might be something to look into when it gets released. Tho pillow 7.1.0 won't support python 2 so it you are looking for that then might have to backport or use alternate apng lib or wxAnimationDecoder stuff when it matures.

Metallicow commented 3 years ago

@kdschlosser

I managed to get this to work with the alpha on wxPy 4.1 by doing this. I didnt have the BLACK problem. ... now to figure out how to get rid of the PIL dependency and do it straight in wxPython to create a proper alpha image.

        ##from PIL import Image
        pil_im = Image.new(mode='RGBA', size=(width, height), color=(0, 0, 0, 0))
        bmp = wx.Bitmap.FromBufferRGBA(width, height, data=pil_im.tobytes())

tested on wxPython 4.1.1a1.dev4883+75f1081f msw (phoenix) wxWidgets 3.1.4 Scintilla 3.7.2 Python 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)] on win32

Collapsible Content - Click to expand ```python #!/usr/bin/env python # -*- coding: utf-8 -*- """""" # Imports.-------------------------------------------------------------------- # -Python Imports. import os import sys if not sys.platform.startswith('win'): print('This script will only run on Microsoft Windows.') sys.exit(1) import ctypes from ctypes.wintypes import LONG, HWND, INT, HDC, HGDIOBJ, BOOL, DWORD # -wxPython Imports. import wx # -Pillow Imports. from PIL import Image wxVER = 'wxPython %s' % wx.version() pyVER = 'python %d.%d.%d.%s' % sys.version_info[0:4] versionInfos = '%s %s' % (wxVER, pyVER) UBYTE = ctypes.c_ubyte GWL_EXSTYLE = -20 WS_EX_LAYERED = 0x00080000 ULW_ALPHA = 0x00000002 AC_SRC_OVER = 0x00000000 AC_SRC_ALPHA = 0x00000001 class POINT(ctypes.Structure): _fields_ = [ ('x', LONG), ('y', LONG) ] class SIZE(ctypes.Structure): _fields_ = [ ('cx', LONG), ('cy', LONG) ] class BLENDFUNCTION(ctypes.Structure): _fields_ = [ ('BlendOp', UBYTE), ('BlendFlags', UBYTE), ('SourceConstantAlpha', UBYTE), ('AlphaFormat', UBYTE) ] user32 = ctypes.windll.User32 gdi32 = ctypes.windll.Gdi32 # LONG GetWindowLongW( # HWND hWnd, # int nIndex # ); GetWindowLongW = user32.GetWindowLongW GetWindowLongW.restype = LONG # LONG SetWindowLongW( # HWND hWnd, # int nIndex, # LONG dwNewLong # ); SetWindowLongW = user32.SetWindowLongW SetWindowLongW.restype = LONG # HDC GetDC( # HWND hWnd # ); GetDC = user32.GetDC GetDC.restype = HDC # HWND GetDesktopWindow(); GetDesktopWindow = user32.GetDesktopWindow GetDesktopWindow.restype = HWND # HDC CreateCompatibleDC( # HDC hdc # ); CreateCompatibleDC = gdi32.CreateCompatibleDC CreateCompatibleDC.restype = HDC # HGDIOBJ SelectObject( # HDC hdc, # HGDIOBJ h # ); SelectObject = gdi32.SelectObject SelectObject.restype = HGDIOBJ # BOOL UpdateLayeredWindow( # HWND hWnd, # HDC hdcDst, # POINT *pptDst, # SIZE *psize, # HDC hdcSrc, # POINT *pptSrc, # COLORREF crKey, # BLENDFUNCTION *pblend, # DWORD dwFlags # ); UpdateLayeredWindow = user32.UpdateLayeredWindow UpdateLayeredWindow.restype = BOOL COLORREF = DWORD def RGB(r, g, b): return COLORREF(r | (g << 8) | (b << 16)) class Frame(wx.Frame): def __init__(self, parent, id=wx.ID_ANY, title=wx.EmptyString, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.FRAME_SHAPED | wx.NO_BORDER | wx.FRAME_NO_TASKBAR | wx.STAY_ON_TOP , name='frame'): wx.Frame.__init__(self, parent, id, title, pos, size, style, name) self.delta = (0, 0) self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_MOTION, self.OnMouseMove) self.Bind(wx.EVT_RIGHT_UP, self.OnExit) self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None) self.Bind(wx.EVT_PAINT, self.OnPaint) def OnExit(self, event): self.Close() def OnLeftDown(self, event): self.CaptureMouse() x, y = self.ClientToScreen(event.GetPosition()) originx, originy = self.GetPosition() dx = x - originx dy = y - originy self.delta = ((dx, dy)) def OnLeftUp(self, event): if self.HasCapture(): self.ReleaseMouse() def OnMouseMove(self, event): if event.Dragging() and event.LeftIsDown(): x, y = self.ClientToScreen(event.GetPosition()) fp = (x - self.delta[0], y - self.delta[1]) self.Move(fp) # self.Refresh() def OnPaint(self, event): width, height = self.GetClientSize() text = 'Alpha test' font = self.GetFont() font.SetPointSize(font.GetPointSize() + 16) font = font.MakeBold() font = font.MakeItalic() ##from PIL import Image pil_im = Image.new(mode='RGBA', size=(width, height), color=(0, 0, 0, 0)) bmp = wx.Bitmap.FromBufferRGBA(width, height, data=pil_im.tobytes()) bmp2 = wx.Bitmap.FromBufferRGBA(width, height, data=pil_im.tobytes()) bmp3 = wx.Bitmap.FromBufferRGBA(width, height, data=pil_im.tobytes()) # bmp = wx.Bitmap(width, height) # bmp2 = wx.Bitmap(width, height) # bmp3 = wx.Bitmap(width, height) dc = wx.MemoryDC() dc.SelectObject(bmp3) dc.SetTextForeground(wx.Colour(255, 255, 255, 255)) dc.SetFont(font) text_width, text_height = dc.GetFullTextExtent(text)[:2] text_x = (width / 2) - (text_width / 2) text_y = (height / 2) - (text_height / 2) dc.DrawText(text, text_x, text_y) dc.SelectObject(bmp2) dc.SetPen(wx.Pen(wx.Colour(255, 255, 255, 255), 6)) dc.SetBrush(wx.Brush(wx.Colour(255, 255, 255, 255))) dc.DrawRoundedRectangle(103, 78, width - 200 - 3, height - 150 - 3, 5) dc.SelectObject(bmp) gc = wx.GraphicsContext.Create(dc) gcdc = wx.GCDC(gc) region1 = wx.Region(bmp) region2 = wx.Region(bmp2, wx.Colour(0, 0, 0, 0)) region1.Subtract(region2) gcdc.SetDeviceClippingRegion(region1) gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 140), 6)) gc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 150))) gc.DrawRoundedRectangle(3, 3, width-6, height-6, 5) gcdc.DestroyClippingRegion() gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 120), 6)) gc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 0))) gc.DrawRoundedRectangle(103, 78, width - 200 - 3, height - 150 - 3, 5) region1 = wx.Region(bmp) region2 = wx.Region(bmp3, wx.Colour(0, 0, 0, 0)) region1.Subtract(region2) gcdc.SetDeviceClippingRegion(region1) gcdc.SetTextForeground(wx.Colour(0, 0, 0, 130)) gcdc.SetTextBackground(wx.TransparentColour) gcdc.SetFont(font) gc.DrawText(text, text_x - 4, text_y - 4) gcdc.DestroyClippingRegion() gcdc.SetTextForeground(wx.Colour(0, 255, 0, 150)) gcdc.SetTextBackground(wx.TransparentColour) gc.DrawText(text, text_x, text_y) brushbmp = wx.Bitmap('brushwithalpha.png', wx.BITMAP_TYPE_PNG) gcdc.DrawBitmap(brushbmp, x=200, y=10, useMask=False) gcdc.Destroy() del gcdc # dc.SelectObject(wx.NullBitmap) dc.Destroy() del dc region = wx.Region(bmp) self.SetShape(region) print('OnPaint') bmp.SetMaskColour(wx.TransparentColour) self.draw_alpha(bmp) def draw_alpha(self, bmp): print('draw_alpha') hndl = self.GetHandle() style = GetWindowLongW(HWND(hndl), INT(GWL_EXSTYLE)) SetWindowLongW(HWND(hndl), INT(GWL_EXSTYLE), LONG(style | WS_EX_LAYERED)) hdcDst = GetDC(GetDesktopWindow()) hdcSrc = CreateCompatibleDC(HDC(hdcDst)) pptDst = POINT(*self.GetPosition()) psize = SIZE(*self.GetClientSize()) pptSrc = POINT(0, 0) crKey = RGB(0, 0, 0) pblend = BLENDFUNCTION(AC_SRC_OVER, 0, 255, AC_SRC_ALPHA) SelectObject(HDC(hdcSrc), HGDIOBJ(bmp.GetHandle())) UpdateLayeredWindow( HWND(hndl), HDC(hdcDst), ctypes.byref(pptDst), ctypes.byref(psize), HDC(hdcSrc), ctypes.byref(pptSrc), crKey, ctypes.byref(pblend), DWORD(ULW_ALPHA) ) if __name__ == '__main__': print(versionInfos) app = wx.App() frame = Frame(None, size=(600, 300), pos=(400, 400)) frame.Show() app.MainLoop() ```
kdschlosser commented 3 years ago

@Metallicow Embed any images into an example if you could please :smile: If you run the script below and enter the filename of the image it will print out a base64 encoded image.

# Python 2.7 and 3.5+ supported

from __future__ import print_function
import base64

try:
    file_name = raw_input('Enter File Name\n')
except NameError:
    file_name = input('Enter File Name\n')

with open(file_name, 'rb') as f:
    data = f.read()

encoded = base64.b64encode(data)

lines = []
line = "    b'"
for item in encoded:
    try:
        line += chr(item)
    except TypeError:
        line += item

    if len(line) + 1 == 78:
        line += "'"
        lines += [line]
        line = "    b'"

if len(line) > 6:
    line += "'"
    lines += [line]

print('IMAGE = (')
print('\n'.join(lines))
print(')')

copy and paste the output into your example and then add the following lines of code to it.


from io import BytesIO
stream = BytesIO(base64.b64decode(IMAGE))
stream.seek(0)

You should be able to pass the "stream" file object to most classes/functions that deal with handling images.

One other thing I did was I wrote a cross platform high precision timer for Python and this timer has microsecond resolution... We cannot use anything that is built into Python to handle stalling a thread or the program in a traditional manner because these mechanisms are not stable and can return before the wanted time or after it. the one that I wrote will return either on time of 4-5 microseconds after the desired time. But not before it and never 10-12 milliseconds after it. This fixes the inconsistent animation speeds.

I was able to overcome the black by using masks. @RobinD42 extended the Python access to the c functions and methods So now they can be properly overridden. I have not had the opportunity yet to update the code to work with the new changes.

kdschlosser commented 3 years ago

One thing I would like to know and maybe @RobinD42 can answer this. Is there a way to draw the frame like what is being done in the examples? To make the frame transparent without having to use a shaped frame? This is because a shaped frame is not able to be created properly using an image that has an alpha channel. If a frame can be drawn like what is being done above but for other OS's we can write an animation handler that will deal with APNG files and then separately write a shaped frame that will handle alpha channels. The shaped frame portion needs to be able to be cross platform.