GraphicsContext does not use alpha channel properly when rendering fonts/text
Operating system: MSW 7 x64 wxPython version & source: 4.1.0 msw (phoenix) wxWidgets 3.1.4, 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)]
Description of the problem: when rendering text using a GraphicsContext instance the alpha channel causes a change in the brightness but not a change in opacity
Code Example (MSW only)
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()
I'm not exactly sure why it is giving me errors about sending a wx.GraphicsFont. It appears you got something screwed up since it is accepting a wx.Font.
- I see that you are not using a PaintEvent.
- You are using FromRGBA might be causing issues.
- You are using a MemoryDC.
... so not exactly sure what the real problem is.
If you modify my TransparentPaintWindow Sample you can indeed see that the GraphicsFont alpha is working correctly.

I think somehow something got screwed up, since you are using your ctypes stuff...
Depending on the results you want, you can do like I did in the previous sample where I used your mswalpha for the transparent frame, and then create a 2nd float on parent frame for the mswalpha affected one and use a region for the text and then set the transparency on the frame. See this sample https://github.com/wxWidgets/Phoenix/issues/1544#issuecomment-661862752 That I think should allow you to use a PaintEvent and might work like you are expecting... Tho antialiasing may or may not work properly on the edges...
You have to use FromRGBA otherwise the bitmap will not have an alpha channel... the MemoryDC is only being used for the purposes of selecting the bitmap. I am creating a GraphicsContext instance from the MemoryDC instance. This allows me to render to the bitmap using the Graphics context.
As you stated, The GraphicsContext.SetFont method should not be allowing a wxFont instance to be passed to it.
The only thing the ctypes stuff does is it draws the bitmap to the screen. That is it. The font is being rendered to the bitmap without an alpha channel and this is how it gets drawn on the screen by the ctypes stuff. The problem is NOT with that portion of the code.
There is a GraphicsContext.CreateFont method and I have tried using this as well but the results are exactly the same.
gc.SetFont(gc.CreateFont(font, wx.Colour(0, 255, 0, 100)))
I would have thought that this would utilize the alpha channel properly but it does not.
This old mailing list sample converted to phoenix might help.
Shaped Text Frame - Click to expand
import wx
class ShapedText(wx.Frame):
def __init__(self, text="Scrolling text!"):
wx.Frame.__init__(self, None, style=
wx.FRAME_SHAPED | wx.NO_BORDER | wx.FRAME_NO_TASKBAR | wx.STAY_ON_TOP)
if not self.IsDoubleBuffered():
self.SetDoubleBuffered(True) # Reduce flicker.
# Set up the timer which will move the text and paint the screen.
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.OnTimer, source=self.timer)
self.timer.Start(10)
# Make sure we are using our custom paint handler.
self.Bind(wx.EVT_PAINT, self.OnPaint)
screenwidth = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_X)
screenheight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)
framewidth = int(screenheight * .75)
frameheight = 100
framex = int(screenwidth/2 - framewidth/2)
framey = screenheight - 150
# Create the bitmap.
self.textFont = wx.Font(36, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
self.bmp = wx.Bitmap(framewidth, frameheight)
self.SetScrolledString(text)
self.SetWindowShape()
self.SetPosition((framex, framey))
self.SetSize((framewidth, self.textHeight+5))
self.SetTransparent(100)
def SetScrolledString(self, txt):
self.scrolledStr = txt
self.textOffset = 0
# Calculate the text width.
memDC = wx.MemoryDC()
memDC.SetFont(self.textFont)
self.textWidth, self.textHeight, heightLine = memDC.GetFullMultiLineTextExtent(self.scrolledStr)
memDC.Destroy()
def SetWindowShape(self):
# Use the bitmap's mask to determine the region.
r = wx.Region(self.bmp)
self.SetShape(r)
def OnPaint(self, event):
# Draw the bitmap on the screen.
dc = wx.PaintDC(self)
dc.DrawBitmap(self.bmp, 0, 0, useMask=True)
def OnTimer(self, event):
# Draw the text on the bitmap, set a mask, and paint.
x, y = self.Size
memDC = wx.MemoryDC(self.bmp)
memDC.SetPen(wx.WHITE_PEN)
memDC.DrawRectangle(0, 0, x, y)
memDC.SetFont(self.textFont)
memDC.DrawText(self.scrolledStr, x-self.textOffset, 0)
memDC.Destroy()
mask = wx.Mask(self.bmp, wx.WHITE)
self.bmp.SetMask(mask) # Comment this line to disable transparency.
self.textOffset += 1
# Stop the timer when the text has scrolled the entire way across.
# print(self.textOffset-self.Size[0], self.textWidth)
if self.textOffset - x == self.textWidth:
# print(self.textOffset-self.Size[0], textWidth)
self.timer.Stop()
self.Destroy()
self.SetWindowShape()
self.Refresh()
if __name__ == "__main__":
app = wx.App()
frame = ShapedText()
frame.Show(True)
app.MainLoop()
nope that is not going to support alpha channels
Some implementation details that may help, if you haven't already dug into the relevant C++ code: In the GDI+ GC backend the colour value is used to create a gdi+ SolidBrush, and that is then held by the wxGDIPlusFontData class along with the gdi+ Font object. The Font and Brush are then used when calling the gdi+ DrawString API. There really isn't a lot of complexity there, just a lot of little things working together to tie the two APIs together.
The alpha value is used when creating the brush, but I don't know why it wouldn't be using it. I do know that GDI+ has some holes in its functionality, perhaps drawing text with a partially transparent brush is one of them? Have you tried the Direct2D GC backend? If you're just interested in getting a bitmap out of this then using the Cairo backend would probably be an option as well.
@RobinD42
I have also tried to wrap MemoryDC with GCDC and GraphicsContext with GCDC and the results are the same. Now.. If I create a GraphicsContext instance from a PaintDC it does render properly.
I am guessing I am probably going to have to learn more about Cairo
The GCDC is still going to use the default renderer backend, which on Windows is GDI+, so I wouldn't expect any difference when using the GCDC vs what you were doing before. To use a non-default you need to explicitly create the renderer and then use it to create the GraphicsContext and related objects. For example, something like this:
if 'wxMSW' in wx.PlatformInfo:
renderer = wx.GraphicsRenderer.GetDirect2DRenderer() # or GetCairoRenderer
else:
renderer = wx.GraphicsRenderer.GetDefaultRenderer()
ctx = renderer.CreateContext(dc)
If wanted, you can create the renderer once and reuse the same one throughout the application's lifetime.
There is also a module implementing a GraphicsContext-like in Python using Cairo, in wx.lib.Graphics. If you want to use the Cairo API directly then the wx.lib.wxcairo package provides some code to help you use Cairo on wx.DCs, wx.Bitmaps, etc.
@RobinD42
If I use the Direct2D renderer as you have shown above. Nothing gets drawn.
I have the same issue (plus the text looks quite bad).
This is what it looks like (text is drawn on a WS_EX_LAYERED window, with a white window behind it)
To fix it, we have the use SetTextRenderingHint to TextRenderingHintAntiAliasGridFit. This fixes both the alpha channel not applying and the the text rendering.
Unfortunately I couldn't find a way to call SetTextRenderingHint in wx, so had to use a .dll with
extern "C" __declspec(dllexport) int SetTextRenderingHint(void* v, int renderingHint) {
Gdiplus::Graphics* g = reinterpret_cast<Gdiplus::Graphics*>(v);
return g->SetTextRenderingHint(static_cast<Gdiplus::TextRenderingHint>(renderingHint));
}
and ctypes in python
stdc = ctypes.cdll.LoadLibrary(R"PyWxGdiPlus.dll")
TextRenderingHintAntiAliasGridFit = 3
r = stdc.SetTextRenderingHint(ctypes.c_void_p(gc.GetNativeContext().__int__()), TextRenderingHintAntiAliasGridFit)
edit: and I don't believe this is a wx bug, same problem happens using gdiplus directly.
edit: and I don't believe this is a wx bug, same problem happens using gdiplus directly.
This is good to know.
You should be able to make those function calls without having to make a dll.
something along these lines.
import ctypes
GpStatus = ctypes.HRESULT
gdiplus = ctypes.windll.gdiplus
GdipSetTextRenderingHint = gdiplus.GdipSetTextRenderingHint
GdipSetTextRenderingHint.restype = GpStatus
and then in a paint event
TextRenderingHintAntiAliasGridFit = 3
def OnPaint(self, event):
# Create paint DC
dc = wx.PaintDC(self)
# Create graphics context from it
gc = wx.GraphicsContext.Create(dc)
if gc:
GdipSetTextRenderingHint(ctypes.byref(gc.GetNativeContext()), TextRenderingHintAntiAliasGridFit)
something like that. I have not tested the above but it would be something really similar I would imagine.
If you look at the Windows SDK header file um\gdiplusgraphics.h and at line 199 is the method SetTextRenderingHint
Status SetTextRenderingHint(IN TextRenderingHint newMode)
{
return SetStatus(DllExports::GdipSetTextRenderingHint(nativeGraphics,
newMode));
}
and all the method is doing is calling the exported GdipSetTextRenderingHint function. So it is a matter of passing the native context to the exported function.
If you look at um\gdiplusflat.h at line 1615 you have the following code.
GpStatus WINGDIPAPI
GdipSetTextRenderingHint(GpGraphics *graphics, TextRenderingHint mode);
In you code example you are getting the handle of the native context and creating the GpGraphics instance in c code using that handle then calling the method which in turn calls the exported function. Since there is already an instance of the native context that can be gotten on the python side of things there should be a way to pass that native context to the exported function directly without having to compile a dll to do it for us,
code correction.
TextRenderingHintAntiAliasGridFit = 3
def OnPaint(self, event):
# Create paint DC
dc = wx.PaintDC(self)
# Create graphics context from it
gc = wx.GraphicsContext.Create(dc)
if gc:
GdipSetTextRenderingHint(gc.GetNativeContext(), TextRenderingHintAntiAliasGridFit)
I think that should work. GetNativeContext returns a pointer to GpGraphics I believe.
Thanks! Didn't realize there was a wrapper api for gdiplus.
I can't seem to get the parameters right for the call however. GetNativeContext() gives a <class 'sip.voidptr'>
so this should work:
GdipSetTextRenderingHint(ctypes.c_void_p(gc.GetNativeContext().__int__()), TextRenderingHintAntiAliasGridFit)
however I'm getting a InvalidParameter result.
This:
GdipSetTextRenderingHint(gc.GetNativeContext(), TextRenderingHintAntiAliasGridFit)
results in:
[ctypes.ArgumentError: argument 1: <class 'TypeError'>: Don't know how to convert parameter 1]
OK try wrapping the NativeContex in ctypes.byref()
You can also try doing this
p = ctypes.c_void_p(gc.GetNativeContext().__int__())
GdipSetTextRenderingHint(ctypes.byref(p), TextRenderingHintAntiAliasGridFit)
I have another.
Here are 4 ways you can try it
p = gc.GetNativeContext()
GdipSetTextRenderingHint(ctypes.byref(p), TextRenderingHintAntiAliasGridFit)
p = ctypes.c_void_p(gc.GetNativeContext().__int__())
GdipSetTextRenderingHint(ctypes.byref(p), TextRenderingHintAntiAliasGridFit)
p = ctypes.cast(gc.GetNativeContext(), ctypes.POINTER(ctypes.c_void_p))
GdipSetTextRenderingHint(p, TextRenderingHintAntiAliasGridFit)
p = ctypes.cast(gc.GetNativeContext(), ctypes.POINTER(ctypes.c_void_p))
GdipSetTextRenderingHint(ctypes.byref(p), TextRenderingHintAntiAliasGridFit)
No, it doesn't work...
GdipSetTextRenderingHint expects a Gdiplus::GpGraphics* and what we have is Gdiplus::Graphics*, and I'm not sure how to convert between the two
not sure then.