Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TransparentPaintWindow #1839

Open
Metallicow opened this issue Nov 5, 2020 · 21 comments
Open

TransparentPaintWindow #1839

Metallicow opened this issue Nov 5, 2020 · 21 comments

Comments

@Metallicow
Copy link
Contributor

Metallicow commented Nov 5, 2020

I managed to dig thru some of my zillion alpha test samples and came across one that I had abandoned at the time for unknown reasons(probably this or I forgot why). This one is back from/updated from late 2013/early 2014 when Jon was toying with EmbroiderModder Stitchify on his Arduino. https://hackaday.com/2014/02/15/generating-embroidery-with-an-arduino/
Anyhow, around that time I was also working on my ShapedBitmapButton for my MCOW library which had intentions of supporting vector and alpha images. Needless to say, I had to put that on hold until wxPy4.1.x because of various hiccups and trying to get it to work a zillion ways with various libraries, but each library had its own limitations/problems.

This is probably the 3rd sample I want to try and get added to the demo by years end, dealing with wrapping up the lingering msw alpha issues. Other alpha threads of note are these two.
#1544
#1463

@kdschlosser Yep, it can be done in pure wxpy... This sample does not rely on your mswalpha.py . It may not be cross-platform tho... ...remember baby steps... :/

TransparentAlphaWindow

And here is the sample to be tested.

As a zip with images
TransparentPaintWindow.zip
And here is just the code.

TransparentPaintWindow.py Sample Code - Click to expand
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Pure python window with alpha.

The docs say that
wx.TRANSPARENT_WINDOW style is windows only.
Not sure if that is still true... Would this work on Linux/Mac?
Tested on:
wxPython 4.0.7.post2, Python 2.7.18, Windows7
wxPython 4.1.x, Python 3.8.6, Windows7

Found Issues: The transform matrix wasn't working on wxPy4.0 with gcdc. Works Ok on wxPy4.1.x
Maybe this was why I was having problems with nothing showing in
earlier ShapedBitmapButton animation tests.

- Metallicow
"""

# Imports.---------------------------------------------------------------------
# -Python Imports.
import os
import sys
from math import radians

# -wxPython Imports.
import wx


class MyPanel(wx.Panel):
    """"""
    def __init__(self, parent, id=wx.ID_ANY,
                 pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.BORDER_SUNKEN, name='panel'):
        """"""
        wx.Panel.__init__(self, parent, id, pos, size, style, name)
        # Set a background color for sanity check.
        self.SetBackgroundColour('#3D8838')

        # Define TransparentPaintWindow first so it draws over the buttons,
        # and not the the buttons over the transwin.
        self.transwin = TransparentPaintWindow(self, pos=(20, 20), size=(250, 250))
        # Make some buttons along the animation track so we can test alpha blending.
        p = 42
        for i in range(1, 12):
            btn = wx.Button(self, -1, "MyButton", pos=(p*i, p*i))
        ## print(self.transwin.GetPosition())

        # Reduce flicker of TransparentPaintWindow refreshing.
        # And to Show the buttons thru. Background widgets wont draw correctly if commented out.
        # Also they might smear if moved outside of the panels size.
        if not self.IsDoubleBuffered():
            self.SetDoubleBuffered(True)

class TransparentPaintWindow(wx.Window):
    """"""
    def __init__(self, parent, id=wx.ID_ANY,
                 pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.TRANSPARENT_WINDOW, name='window'):
        """"""
        wx.Window.__init__(self, parent, id, pos, size, style, name)

        self.SetupWindowProperties()
        self.BindEvents()

        print('GetBackgroundStyle()', self.GetBackgroundStyle())
        print(self.GetBackgroundStyle() == wx.BG_STYLE_TRANSPARENT, 'wx.BG_STYLE_TRANSPARENT')
        # Apparantly BG_STYLE_PAINT and BG_STYLE_CUSTOM both equal 2. Is one deprecated?
        print(self.GetBackgroundStyle() == wx.BG_STYLE_CUSTOM, 'wx.BG_STYLE_CUSTOM')
        print('CanSetTransparent()', self.CanSetTransparent())

        # self.SetTransparent(100)  # Nope. doesnt seem to work on a window...

        self._rotate = 0
        # Define 2 bitmaps with alpha to test with. One for gcdc and the other for gc.
        self.bmp = wx.Bitmap('phoenix64.png', wx.BITMAP_TYPE_PNG)
        self.bmp2 = wx.Bitmap('brushwithalpha.png', wx.BITMAP_TYPE_PNG)


    def SetupWindowProperties(self):
        """
        Setup the `wx.Window` properties for :class:`TransparentPaintWindow`.
        """
        self.SetBackgroundStyle(wx.BG_STYLE_PAINT)

    def BindEvents(self):
        """
        Bind various event types for :class:`TransparentPaintWindow`.
        """
        self_Bind = self.Bind
        # OnEraseBackground handler not needed when using BG_STYLE_PAINT.
        ## self_Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
        self_Bind(wx.EVT_PAINT, self.OnPaint)
        self_Bind(wx.EVT_SIZE, self.OnSize)

    # def OnEraseBackground(self, event):
    #     """
    #     Handles the ``wx.EVT_ERASE_BACKGROUND`` event for :class:`TransparentPaintWindow`.
    #
    #     :param `event`: A `wx.EraseEvent` to be processed.
    #     :type `event`: `wx.EraseEvent`
    #     """
    #     pass  # Reduce flicker with BufferedPaint.

    def OnSize(self, event):
        """
        Handles the ``wx.EVT_SIZE`` event for :class:`TransparentPaintWindow`.

        :param `event`: A `wx.SizeEvent` to be processed.
        :type `event`: `wx.SizeEvent`
        """
        event.Skip()
        self.Refresh()

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

        :param `event`: A `wx.PaintEvent` to be processed.
        :type `event`: `wx.PaintEvent`
        """
        # Seems like I have to use just a PaintDC for this to avoid BLACK issues
        # or it just not drawing at all(transparent).
        dc = wx.PaintDC(self)
        ## dc = wx.AutoBufferedPaintDC(self)
        ## dc = wx.BufferedPaintDC(self)
        ## dc = wx.AutoBufferedPaintDCFactory(self)
        gcdc = wx.GCDC(dc)
        ## gcdc.SetBrush(wx.TRANSPARENT_BRUSH)
        ## gcdc.SetPen(wx.TRANSPARENT_PEN)
        ## gcdc.Clear()
        gc = wx.GraphicsContext.Create(dc)

        graphicsMatrix = gc.CreateMatrix(a=1.0, b=0.0, c=0.0, d=1.0, tx=0.0, ty=0.0)
        graphicsMatrix.Rotate(radians(self._rotate))
        gc.SetTransform(graphicsMatrix)

        affineMatrix2D = wx.AffineMatrix2D()
        ## affineMatrix2D = gcdc.GetTransformMatrix()
        affineMatrix2D.Rotate(cRadians=radians(self._rotate))
        affineMatrix2D.Scale(xScale=1.0-radians(self._rotate), yScale=1.0-radians(self._rotate))
        # gcdc matrix transform NOT working on wxPy4.0.7post2 Py2 Win7
        # It is working fine on wxPy4.1.x Py3 Win7
        gcdc.SetTransformMatrix(affineMatrix2D)
        ## print(gcdc.SetTransformMatrix(affineMatrix2D))  # Returns False if it isn't gonna work.

        gcdc.SetBrush(wx.TRANSPARENT_BRUSH)
        gcdc.SetPen(wx.Pen(wx.RED, 1, wx.PENSTYLE_SOLID))
        gcdc.DrawCircle(x=40, y=40, radius=20)
        gcdc.SetPen(wx.Pen(wx.WHITE, 2, wx.PENSTYLE_DOT_DASH))
        gcdc.DrawCircle(x=60, y=40, radius=20)
        gcdc.SetPen(wx.Pen(wx.BLUE, 3, wx.PENSTYLE_CROSSDIAG_HATCH))
        gcdc.DrawRectangle(x=80, y=40, width=80+20, height=40+20)

        gcdc.DrawBitmap(self.bmp, 42, 42, True)

        # Draw BORDER_SIMPLE like rectangle for sanity check.
        # When animating this visually seems to be a bit slow drawing the edges
        # with TRANSPARENT_WINDOW.
        # Freeze/Thaw fixes this when modding the matrix values and refreshing.
        gcdc.ResetTransformMatrix()
        gcdc.SetBrush(wx.TRANSPARENT_BRUSH)
        gcdc.SetPen(wx.Pen(wx.BLACK, 10, wx.PENSTYLE_SOLID))
        gcdc.DrawRectangle(x=1, y=1, width=self.ClientSize[0]-1, height=self.ClientSize[1]-1)

        font = wx.Font(14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_ITALIC, wx.FONTWEIGHT_BOLD, False, "")
        graphicsFont = gc.CreateFont(font, wx.CYAN)
        gc.SetFont(graphicsFont)
        graphicsBrush = gc.CreateBrush(wx.Brush(wx.YELLOW, wx.BRUSHSTYLE_CROSS_HATCH))
        gc.DrawText("gc.DrawRotatedText", x=30, y=50, angle=0.333, backgroundBrush=graphicsBrush)

        gc.DrawBitmap(self.bmp2, x=30, y=50, w=self.bmp2.Width, h=self.bmp2.Height)


class MyFrame(wx.Frame):
    """"""
    def __init__(self, parent, id=wx.ID_ANY, title=wx.EmptyString,
                 pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.DEFAULT_FRAME_STYLE, name='frame'):
        """"""
        wx.Frame.__init__(self, parent, id, title, pos, size, style, name)
        global gMainWin
        gMainWin = self
        wxVER = 'wxPython %s' % wx.version()
        pyVER = 'python %d.%d.%d.%s' % sys.version_info[0:4]
        versionInfos = '%s %s' % (wxVER, pyVER)
        self.CreateStatusBar().SetStatusText(versionInfos)

        # Set a background color for sanity check.
        self.SetBackgroundColour('#AFC0DB')

        self.panel = MyPanel(self)
        # self.panel = TransparentPaintWindow(self)  # Flickers bad on resize.

        self.slider = wx.Slider(self, -1, value=0, minValue=0, maxValue=359)
        self.slider.Bind(wx.EVT_SLIDER, self.OnSlider)

        # Add 2 widgets to sizer so frame doesn't try to wreck a single widgets size by expanding.
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 1, wx.EXPAND | wx.ALL, 0)
        sizer.Add(self.slider, 0, wx.EXPAND | wx.ALL, 0)
        self.SetSizer(sizer)

        self.timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.OnTimer)
        # self.timer.Start(16)  # Uncomment to animate on a timer.
        self.direction = True

        self.Bind(wx.EVT_CLOSE, self.OnDestroy)

    def OnSlider(self, event):
        value = self.slider.GetValue()
        x = int(value)
        y = int(value)
        inc = 2
        self.panel.transwin._rotate = x
        self.panel.Freeze()
        self.panel.transwin.SetPosition((x+inc, y+inc))
        self.panel.Thaw()

    def OnTimer(self, event):
        x, y = self.panel.transwin.GetPosition()
        if self.direction:
            inc = 2
            if x >= 360:
                self.direction = False
                inc = -2
        else:
            inc = -2
            if x - inc < inc:
                self.direction = True
                inc = 2

        self.panel.transwin._rotate = x
        self.panel.Freeze()
        self.panel.transwin.SetPosition((x+inc, y+inc))
        self.panel.Thaw()
        self.slider.SetValue(x)
        # Sanity check.
        # Update a button label to show it is rendering right thru the transwin
        children = self.panel.GetChildren()
        children[7].SetLabel('%s' % x)

        ## self.panel.transwin.Refresh()
        ## self.panel.Refresh()
        ## self.Refresh()

    def OnDestroy(self, event):
        """"""
        self.timer.Stop()
        self.Destroy()

class MyApp(wx.App):
    """"""
    def OnInit(self):
        """"""
        self.SetClassName('MyApp')
        self.SetAppName('MyApp')
        gMainWin = MyFrame(None, size=(600, 600))
        gMainWin.SetTitle('TransparentPaintWindow')
        self.SetTopWindow(gMainWin)
        gMainWin.Center()
        gMainWin.Show()
        return True

if __name__ == '__main__':
    gApp = MyApp(redirect=False,
                 filename=None,
                 useBestVisual=False,
                 clearSigInt=True)
    gApp.MainLoop()

@tatarize This may be of interest to you also if you was having nightmares with the alpha embroidery matrix stuff on wxPy4.0

@RobinD42 I really didn't realize that the matrix transforms wasn't working completely correctly on gcdc wxPy4.0 until I revisited this sample. The gc stuff works but the gcdc stuff didn't. This just goes to show the amount oh headaches and rage quits I was having knowing that I had somehow on one occasion got it to sorta work somehow and then probably broke it by changing one line.
Chances are I might have used a slow pure-python matrix to hack the drawing to work or somesuch or used a different lib implementation but that was real slow because I was dealing with attempting to translating light reflections from 3D into 2D similar to Jon's pseudo rendering which would look alot better, but alas was python based speed wise(and would take around 20 sec to do calculations so it def wasn't realtime by any means)
If you manage to make 1 last Py2 build by the year end(pretty please), call it 4.0.8 or whatever and be the last Py2 build for 4.0, this would be an extremely important bug/fix(maybe it wasn't finished/wipz). I would request that this be looked into and included if at all possible, as without this, it creates a bit of trial/error issues.
Also the Phoenix docs say that wx.TRANSPARENT_WINDOW is windows only... Is this still true with wxPy4.1? I don't have access to a mac to test. Maybe you can give a suggestion of what would work similarly so as to make a cross platform file if possible. Also I don't have a linux box handy atm that has wxPy built for it, so test/ideas/translations of code on that end are welcome also.

Another question I had(tho while a bit off subject) was if there is a way to convert a path to points(for instance GraphicsPath or somesuch) similar to this image. https://github.com/Shinao/PathToPoints . I recall I tried before but don't recall I ever asked or got an answer if it could be done in pure wxpy. The whole goal was to be able to convert for example a path/mathematica curve to x amount of points from length which then could be like connect-the-dots and pseudo rendered upon a ShapedBitmapButton/etc...

@kdschlosser
Copy link
Contributor

That is not a transparent window/frame. The background if fully opaque.

This is a transparent frame
import ctypes.wintypes
import ctypes
import wx

HANDLE = ctypes.wintypes.HANDLE
LPWSTR = ctypes.wintypes.LPWSTR
HRESULT = ctypes.HRESULT
LONG = ctypes.wintypes.LONG
HWND = ctypes.wintypes.HWND
INT = ctypes.wintypes.INT
HDC = ctypes.wintypes.HDC
HGDIOBJ = ctypes.wintypes.HGDIOBJ
BOOL = ctypes.wintypes.BOOL
DWORD = ctypes.wintypes.DWORD
UBYTE = ctypes.c_ubyte
COLORREF = DWORD

GWL_EXSTYLE = -20
WS_EX_LAYERED = 0x00080000
ULW_ALPHA = 0x00000002
AC_SRC_OVER = 0x00000000
AC_SRC_ALPHA = 0x00000001


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


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)
    ]

byref = ctypes.byref

kernel32 = ctypes.windll.Kernel32

GetTempPathW = kernel32.GetTempPathW
GetTempPathW.restype = DWORD
GetTempPathW.argtypes = [DWORD, LPWSTR]


gdi32 = ctypes.windll.Gdi32

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

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

# BOOL DeleteDC(
#   HDC hdc
# );

DeleteDC = gdi32.DeleteDC
DeleteDC.restype = BOOL


shell32 = ctypes.windll.Shell32

SHGetFolderPathW = shell32.SHGetFolderPathW
SHGetFolderPathW.restype = HRESULT
SHGetFolderPathW.argtypes = [HWND, INT, HANDLE, DWORD, LPWSTR]


user32 = ctypes.windll.User32

# 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

# 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

import math


class AlphaFrame(wx.Frame):
    _xml = None
    _vehicle = None

    def __init__(self, parent=None, size=(800, 800), style=wx.TRANSPARENT_WINDOW):
        wx.Frame.__init__(
            self,
            parent,
            -1,
            style=(
                wx.NO_BORDER |
                wx.FRAME_NO_TASKBAR |
                style
            )
        )

        self.SetSize(size)
        self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)

    def Draw(self):
        width, height = self.GetClientSize()
        bmp = wx.Bitmap.FromRGBA(width, height)
        dc = wx.MemoryDC()
        dc.SelectObject(bmp)

        gc = wx.GraphicsContext.Create(dc)

        gc.SetPen(gc.CreatePen(wx.Pen(wx.Colour(255, 0, 0, 175), 10)))
        # gc.SetBrush(gc.CreateBrush(wx.Brush(wx.Colour(0, 0, 255, 100))))

        gc.DrawRoundedRectangle(20, 20, width - 40, height / 2, 5)

        text = 'This is a transparent frame'
        text_len = len(text)

        text_width, char_height = self.GetFullTextExtent(text)[:2]

        radius = (min(width, height) * 0.90) / 2

        circumference = radius * math.pi * 2
        angle_range = 320.0 - 190.0
        avg_char_width = text_width / text_len
        angle_ratio = angle_range / 360.0
        arc_length = circumference * angle_ratio
        num_steps = arc_length / avg_char_width
        angle_spacing = angle_range / num_steps

        pixels_per_degree = circumference / 360.0

        angle = 190.0 + (angle_spacing / 2)

        font = self.GetFont()
        font.SetStyle(wx.FONTSTYLE_ITALIC)
        font.MakeBold()
        font.SetFractionalPointSize(font.GetFractionalPointSize() * 3)
        gc.SetFont(font, wx.Colour(0, 0, 0, 1))

        center_x = width / 2.0
        center_y = height / 2.0

        for char in list(text):
            char_width = gc.GetFullTextExtent(char)[0]
            angle_offset = (char_width / 2) / pixels_per_degree

            spacing = (angle_range / text_len) + angle_offset

            for _ in range(2):
                radians = math.radians(angle - angle_offset)

                cos = math.cos(radians)
                sin = math.sin(radians)
                x = center_x + (radius * cos)
                y = center_y + (radius * sin)

                text_radians = math.radians(angle - angle_offset + 90.0)

                gc.DrawText(char, x, y, -text_radians)
                angle_offset -= 0.3

            angle += spacing

        angle = 190.0 + (angle_spacing / 2)
        gc.SetFont(font, wx.Colour(0, 255, 0, 200))

        for char in list(text):

            char_width = gc.GetFullTextExtent(char)[0]
            angle_offset = (char_width / 2) / pixels_per_degree

            spacing = (angle_range / text_len) + angle_offset
            radians = math.radians(angle - angle_offset - 1)

            cos = math.cos(radians)
            sin = math.sin(radians)
            x = center_x + (radius * cos)
            y = center_y + (radius * sin)

            text_radians = math.radians(angle - angle_offset + 90.0)

            gc.DrawText(char, x, y, -text_radians)

            angle += spacing

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

        dc.Destroy()
        del dc

        self.Render(bmp)


    def Render(self, bmp, transparency=255):
        x, y = self.GetPosition()
        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(int(x), int(y))
        psize = SIZE(bmp.GetWidth(), bmp.GetHeight())
        pptSrc = POINT(0, 0)
        crKey = RGB(0, 0, 0)

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

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

        DeleteDC(HDC(hdcDst))
        DeleteDC(HDC(hdcSrc))


app = wx.App()

frame = AlphaFrame()
frame.Show()
frame.Draw()
app.MainLoop()

@Metallicow
Copy link
Contributor Author

Metallicow commented Nov 6, 2020

Basically what was trying to be achieved here was in the creation of the shapedbitmapbutton widget I was looking for ways to make it cross platform and just use pure wxpy without ctypes/etc. Like I said above, I'm not sure if this might work on mac/linux since wxPy4.1 changes a bit of things. Ex: maybe the docs didn't get updated...
And of course your sample is windows only, so until I can figure out if there is a relatively ez way to do the same thing on mac/linux, then I might be stuck still doing things the way I have been by calculating out all the quads. When I did it this way it back then it was developed on Classic and the new alpha classes wasn't fully fleshed out back then to do everything the way I needed it to. This isn't intended to be a shaped frame or top level window. Just a standard window that is alpha and can be painted on(so yes it has to have a parent in order to work).

The main thing to note with this sample is that it just uses wxpy, but also it points out that the matrix transform method wasn't working fully(specifically gcdc.SetTransformMatrix(affineMatrix2D)), so when I would test on another platform or wxpy version it often seemed like it wasn't drawing/animating at all.
This explains why I wasn't quite sure when I started stacking widgets that the clickthru wasn't working correctly in an event fashion akin to back-to-the-future, so that all siblings/family can communicate with one another no matter their position in the stack or family tree/timeline if they "get deleted" in some alternate family tree event universe.

@Metallicow
Copy link
Contributor Author

Updated collapsed sample code in OP to reflect these small changes regarding the edges not drawing up to speed than the painting was occuring.

        # Draw BORDER_SIMPLE like rectangle for sanity check.
        # When animating this visually seems to be a bit slow drawing the edges
        # with TRANSPARENT_WINDOW.
        # Freeze/Thaw fixes this when modding the matrix values and refreshing.
        
        .... in slider and timer event wrapped SetPosition call with Freeze/Thaw ...
        self.panel.Freeze()
        self.panel.transwin.SetPosition((x+inc, y+inc))
        self.panel.Thaw()

@Metallicow
Copy link
Contributor Author

Metallicow commented Nov 7, 2020

Updated OP with apng so as to show what this widget is actually supposed to do.

@kdschlosser
Copy link
Contributor

GTK is how to di it. and it can be done. I was looking into it. There are Python libraries for GTK that you should be able to leverage to do what is called alpha blending so do a search for "GTK alpha blending"

@Metallicow
Copy link
Contributor Author

Obviously, If I can paint on a wxWindow "Live" from my Krita plugin in wxPython/OpenGL/Vulkan, then there is at least a way to do this in QT by default with no hassles. This "thing", I'm trying to get closed, is the cross-compatable code part in pure wxPy.
@kdschlosser If you have links to helpful zlib/MIT stuff or have wxWidgets compatible code, please do post it.
This has been a headache for ages.

@vadz Maybe the 'Flying V' can help solve the OS compat issue more accurately, since he works on the C family side of things.... Is this widget feature possible yet cross-platform?

@kdschlosser
Copy link
Contributor

OK I now remember what the issue is. If you render inside of the wxFrame paint event using GraphicsContext in the frame you will get what appears to be at first a transparent frame. But when you move the window that is behind that frame you then see that the frame is not actually transparent. the reason this is working for you is because you are using a window and that window is limited to the bounds of the frame. so you will not have this problem, because you are redrawing each time a move takes place the data that is supposed to be an alpha channel gets updated. If you did this as a test you would see what I am talking about.

Modift you code so that when you move the slider it updates the window first and then changes all of the button labels to something different. You will see that the labels for the buttons that are "behind" the window do not change.

@Metallicow
Copy link
Contributor Author

Metallicow commented Nov 8, 2020

Seems to work fine for me. It updates when transparent paint window is over the button.

mod- add child change at end of timer handler.

        children = self.panel.GetChildren()
        children[7].SetLabel('%s' % x)

        ## self.panel.transwin.Refresh()
        ## self.panel.Refresh()
        ## self.Refresh()

The order of operations is important oftentimes when playing around with this stuff to get it to look right. This would be handled on the programmers end, not the widget.

Edit: Added this sanity check to collapsible code in OP to show that it is rendering thru the transwin.

@Metallicow
Copy link
Contributor Author

Metallicow commented Nov 8, 2020

@kdschlosser Oftentimes, the smearing or draw over effect isn't desirable, but take for example when you win a game of solitaire on windows. The cards start jumping off the stacks and draw over themselves. This is a good example of how this undesirable way of doing things sometimes turn it into "Art".
https://duckduckgo.com/?t=ffsb&q=win+solitaire&iax=images&ia=images

@kdschlosser
Copy link
Contributor

what funny is the wx.TRANSPARENT_WINDOW style I believe is supposed to map to the Windows API WS_EX_TRANSPARENT. And this is only supposed to allow click through meaning that mouse clicks will pass through the window to the window underneath it. It is not supposed to have anything to do with alpha blending. Yet it does to some extent in wxWidgets but only for a wxWindow instance and not a wxFrame instance.

Now I wanted to show you what I was talking about earlier as far as the background data not getting updated properly
When you run the code below it is going to render what appears to be is a transparent frame without using ctypes or any of the windows API. But if you move a window that is behind the transparent frame you will see something interesting happen..

The other thing is that using a PaintDC solves the issue with the font not having an alpha channel. You will also notice that I am not using the wx.TRANSPARENT_WINDOW style. If this was fixed so that it did a per pixel alpha blend and updated when things change behind it then it would be cross platform. This has got to be a bug in wxWidgets

Example code (MSW only)
import wx
import math

class AlphaFrame(wx.Frame):
    _xml = None
    _vehicle = None

    def __init__(self, parent=None, size=(800, 800)):
        wx.Frame.__init__(
            self,
            parent,
            -1,
            style=(
                wx.NO_BORDER |
                wx.FRAME_NO_TASKBAR
            )
        )
        self.SetSize(size)
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)

    def Draw(self):
        self.Refresh()
        self.Update()

    def OnPaint(self, _):
        width, height = self.GetClientSize()
        dc = wx.PaintDC(self)

        gc = wx.GraphicsContext.Create(dc)

        gc.SetPen(gc.CreatePen(wx.Pen(wx.Colour(255, 0, 0, 175), 10)))
        gc.DrawRoundedRectangle(20, 20, width - 40, height / 2, 5)

        text = 'Is this what you are trying to do?'
        text_len = len(text)

        text_width, char_height = self.GetFullTextExtent(text)[:2]

        radius = (min(width, height) * 0.90) / 2

        circumference = radius * math.pi * 2
        angle_range = 320.0 - 180.0
        avg_char_width = text_width / text_len
        angle_ratio = angle_range / 360.0
        arc_length = circumference * angle_ratio
        num_steps = arc_length / avg_char_width
        angle_spacing = angle_range / num_steps

        pixels_per_degree = circumference / 360.0

        angle = 180.0 + (angle_spacing / 2)

        font = wx.Font(14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_ITALIC, wx.FONTWEIGHT_BOLD, False, "")
        graphicsFont = gc.CreateFont(font, wx.Colour(0, 0, 0, 50))
        gc.SetFont(graphicsFont)

        center_x = width / 2.0
        center_y = height / 2.0

        for char in list(text):
            char_width = gc.GetFullTextExtent(char)[0]
            angle_offset = (char_width / 2) / pixels_per_degree

            spacing = (angle_range / text_len) + angle_offset

            for _ in range(2):
                radians = math.radians(angle - angle_offset)

                cos = math.cos(radians)
                sin = math.sin(radians)
                x = center_x + (radius * cos)
                y = center_y + (radius * sin)

                text_radians = math.radians(angle - angle_offset + 90.0)
                gc.DrawText(char, x, y, -text_radians)
                angle_offset -= 0.3

            angle += spacing

        angle = 180.0 + (angle_spacing / 2)
        graphicsFont = gc.CreateFont(font, wx.Colour(0, 255, 0, 50))
        gc.SetFont(graphicsFont)

        for char in list(text):

            char_width = gc.GetFullTextExtent(char)[0]
            angle_offset = (char_width / 2) / pixels_per_degree

            spacing = (angle_range / text_len) + angle_offset
            radians = math.radians(angle - angle_offset - 1)

            cos = math.cos(radians)
            sin = math.sin(radians)
            x = center_x + (radius * cos)
            y = center_y + (radius * sin)

            text_radians = math.radians(angle - angle_offset + 90.0)
            gc.DrawText(char, x, y, -text_radians)

            # gc.DrawText(char, x, y, -text_radians)

            angle += spacing

        gc.Destroy()
        del gc

app = wx.App()

frame = AlphaFrame()
frame.Show()
frame.Draw()
app.MainLoop()

@Metallicow
Copy link
Contributor Author

Metallicow commented Nov 8, 2020

I wouldnt exactly call that a bug. probably more or less a detail of the frame implementation. I've tried that long time ago and got the same results. It does have a couple uses if you need to capture a screenie on frame creation. but yes, the frame has to have something, so basically it bakes the texture to become the frame. weird but whatever...

With the frame transparency stuff that is usually got to do with the OS or the desktop shell. In this case your mswalpha.py with the ctypes stuff is basically what needed to be implemented at the wxWidgets level and tweaked to deal with the antialiasing and everything not going BLACK if a single pixel had alpha in 1-254 range and also being able to place widgets on top of it without a float frame like in my other sample.

@kdschlosser
Copy link
Contributor

on OSX to render a transparent frame like in my examples a call need to be made to wx.Frame.SetBackgroundStyle(wx.BG_STYLE_TRANSPARENT)

That is all that should need to be done.

I am going to dig into Linux next.

@kdschlosser
Copy link
Contributor

As I was saying about there being a bug. If setting the style when using osx works then wxWidgets is not properly rendering on MSW. This would be a bug.
I do not have a mac so I am not able to test it to see if it works.

@kdschlosser
Copy link
Contributor

on linux passing wx.BG_STYLE_TRANSPARENT to the constructor should enable per pixel alpha blending. again if both this and the mac version work properly then there is 100% a bug.

so that would be the cross platform support. it is fairly easy enough to do a check for windows and use the ctypes code.

@Metallicow
Copy link
Contributor Author

It throws this error with wx.BG_STYLE_TRANSPARENT. I think what that means is since msw isn't double buffered by default you may need to try and do a 2 step creation, tho I'm not sure. It works fine on wxPy4.0 and wxPy4.1 the way it is now, so unless there is any sort of benefit to complicating things, then it works fine for windows.

    self.SetBackgroundStyle(wx.BG_STYLE_TRANSPARENT)
wx._core.wxAssertionError: C++ assertion ""(style != wxBG_STYLE_TRANSPARENT) ||
!GetHandle()"" failed at ..\..\src\common\wincmn.cpp(1641) in wxWindowBase::SetB
ackgroundStyle(): wxBG_STYLE_TRANSPARENT style can only be set before Create()-i
ng the window.
OnInit returned false, exiting...

The only problem I ran into so far was the gcdc matrix wasnt working on wxPy4.0.7
When I get to test it on a linux box, I'll see if it throws the same exception...

And chances are that I will use wx.Control when I get to updating the shapedbitmapbutton code and refactoring it since it makes doing a few things easier on the users end.

@Metallicow
Copy link
Contributor Author

@kdschlosser If your really interested in how to easily swap it to solitaire mode,
Then start it up with the timer and add this or use a button event.

    def OnLeftDown(self, event):
        self.ToggleWindowStyle(wx.TRANSPARENT_WINDOW)
        self.Refresh()

And if you are so concerned about the ctypes stuff, As I said before, this isn't intended to be a ShapedFrame or TopLevelWindow. If you want to take the other samples I whipped up that uses the mswalpha.py that was working fine and try to attempt to put one of these on it, it may actually work somehow if you try hard enough, tho I'm not interested in reinventing the wheel with the shaped frame alpha and to try and get every single event thing working atm. Plus multiplying alpha with alpha will probably result in the transpaintwindow not working properly or could be black and x3DoubleBuffering would only complicate things even more.

As far as clickthru, shapedbitmapbutton already handles all that in pure wxpy event fashion, so if you like to toy around with layers something along these lines would get you close to handling whatever you would need in a class dict. I think you said at one time, there was a way to set clickthru with ctypes..., chances are if so, that may be faster if desired and no widgets reside upon the window could make an option for it...

    def OnButton(self, event):
        print('OnButton')
        evtObj = event.GetEventObject()
        evtObj = self.transwin
        # for child in self.GetChildren():
            # if child == evtObj:
                # continue
            # self.RemoveChild(evtObj)
            # self.AddChild(evtObj)
            # evtObj.Raise()
        evtObj.Lower()

    def OnRaise(self, event):
        evtObj = event.GetEventObject()
        evtObj.Raise()

    def OnLower(self, event):
        evtObj = event.GetEventObject()
        evtObj.Lower()

As far as the mswalpha.py, I'll want to have you at least review the doc stuff I've done on it and have a few samples that use it, before sending a pullreqz. As if any of my widgets rely on it, then it will need to be in wx.lib, before I pullreqz any MCOW widgets that might use it if running on msw.

@kdschlosser
Copy link
Contributor

kdschlosser commented Nov 9, 2020

@Metallicow

I never stated anything about setting wx.BG_STYLE_TRANSPARENT for MSW. that style would need to be set into the style parameter when calling the constructor.

I know you were looking for a cross platform solution for rendering transparent frames/windows and you have all of the pieces needed to successfully do that.

OSX: wx.Frame.SetBackgroundStyle(wx.BG_STYLE_TRANSPARENT)
Linux: wx.Frame.__init__(style=wx.BG_STYLE_TRANSPARENT)
MSW: supposed to be wx.Frame.__init__(style=wx.BG_STYLE_TRANSPARENT) but doesn't work. alternative is to use ctypes to render the frame.

@Metallicow
Copy link
Contributor Author

Metallicow commented Nov 9, 2020

Ok, so if I do a wx.Platform check right before the constructor that might work also.... That seems to be the answer you managed to dig up? Is that correct? I couldn't manage to dig up a test sample anywhere that had any lines written that way.
Yea, having that written 3 different ways does seem misleading if that is the case. And the fact that there is the method to change them. Maybe the docs might need updated and this also better explained in the sample.

It would appear that the wx.TRANSPARENT_WINDOW flag should have been enough by looking at the docs, but needed to be double buffered. Everything here has been trial and error to get it working to this point correctly without using a python matrix also.

Also, when I trimmed trailing whitespace this came up, but I cannot get it to work.
https://github.com/wxWidgets/Phoenix/blob/master/docs/sphinx/rest_substitutions/snippets/python/converted/wx.Window.SetBackgroundStyle.1.py

@Metallicow
Copy link
Contributor Author

@kdschlosser Ok. It seems that that snippet is close.

This worked also. But needed to keep wx.TRANSPARENT_WINDOW in style and not set the frame to BG_STYLE_PAINT(it complained when I did this after using the BG_STYLE_TRANSPARENT)

Seems to work the same. both ways. will need to test further. Also Double Buffering seems to still be needed.

class TransparentPaintWindow(wx.Window):
    """"""
    def __init__(self, parent, id=wx.ID_ANY,
                 pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.TRANSPARENT_WINDOW, name='window'):
        """"""
        wx.Window.__init__(self)
        # Do this first:
        self.SetBackgroundStyle(wx.BG_STYLE_TRANSPARENT)

        # And really create the window afterwards:
        self.Create(parent, id, pos, size, style, name)

        # self.SetupWindowProperties()

@Metallicow
Copy link
Contributor Author

print('IsTransparentBackgroundSupported', self.IsTransparentBackgroundSupported(reason=None))
It also appears that this is always returning False, and in my opinion is wrong. If I had to give a reason it would probably have to say "might isn't an answer, it is a guess. use your eyes and test for yourself..."

@RobinD42 This stuff appears to need updated in the docs to be more accurately explained for each platform as all the method docs referencing this are confusingly worded.
Also by using the two line bits from the snippet the GetBackgroundStyle method now actually states that the style is wx.BG_STYLE_TRANSPARENT instead of wx.BG_STYLE_PAINT. I'm not exactly sure what to think of this since I think most of us could still consider the dc to be using TransparentPaint(or clear coat in a paint sense).

@kdschlosser I'll update the OP sample after a bit more testing and sanity tests. I'll try to see if the 2 stage creation might work on Frame also, tho I won't cross my fingers on that one. I do believe that the ctypes mswalpha.py for it appears to be the correct answer for it antialiasing wise.

@kdschlosser
Copy link
Contributor

I am only going by what I see in the source code for wxWidgets. on MSW when the background gets erased a check is done to see if the style wx.BG_STYLE_TRANSPARENT has been set. If it has then it will not render the background with a comment line stating that it will be handled in the paint event. This does not actually make the background transparent by doing this.

I am 100% certain that on MSW the style wx.TRANSPARENT_WINDOW is only supposed to change the "click through" of the mouse. why you are ending up with a black background with this flag is not set I am not sure. This is not supposed to be like that.

you should have to set wx.TRANSPARENT_WINDOW and wx.BG_STYLE_TRANSPARENT if you wanted to have a transparent background and click through.

when I test the wx.TRANSPARENT_WINDOW style using the ctypes method of rendering the only thing that changes is the click through of the mouse.

on Windows you cannot set the wx.BG_STYLE_TRANSPARENT this way self.SetBackgroundStyle(wx.BG_STYLE_TRANSPARENT) it will throw an error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants