Form's TransparencyKey leaves ghastly colored edging [closed]

When the TransparencyKey is used to make a transparent Form and a PNG with transparent pixels is placed as the background image, the PNG image has an unsightly colored edge along most of its image outline (notably the color used to set the forms transparency key too... In my case Magenta).

I have tried for hours too come up with a solution but can't find one.

What did almost work was to use multiple Forms with different TransparencyKey colors and match the Forms pixel by pixel if the pixel was not matched by color or position in one of the other Forms it was excluded if there was a match it was written pixel by pixel on a new Form.

It wasn't perfect but came very close. However this method took literally 2.5 hours which is way too long for a small logo to be processed.

How can I fix this issue?


Solution 1:

This code is a translation (with minor interpretations) of the code found here:
Windows Form Transparent Background Image.
Originally from the Microsoft samples code-base (at least it was, before they killed it).


When a Form is rendered transparent, setting its TransparencyKey to the same Color used as the BackGroundColor and then draw a semi-transparent Bitmap on the transparent surface of the Form, the anti-aliased parts of the Bitmap are not blended with whatever there is behind the Form.
The Color used as TransparencyKey may affect the rendering result, but the semi-transparent pixels (especially the pixels near the edges of the Bitmap) will always be visible on the different backgrounds, since there's no blending.

To solve the problem, we can build a Layered Window:

The system automatically composes and repaints layered windows and the windows of underlying applications. As a result, layered windows are rendered smoothly, without the flickering typical of complex window regions. In addition, layered windows can be partially translucent, that is, alpha-blended.

To create a Layered Form, we can set the WS_EX_LAYERED extended style overriding the Form's CreateParams property:

Protected Overrides ReadOnly Property CreateParams As CreateParams
    Get
        Dim parms As CreateParams = MyBase.CreateParams
        parms.ExStyle = parms.ExStyle Or WS_EX_LAYERED
        Return parms
    End Get
End Property

Windows 8+: The WS_EX_LAYERED style is supported for top-level windows and child windows. Previous Windows versions support this style only for top-level windows.

To draw a Bitmap that can blend with the background, we select a Bitmap into the Window Device Context, then call UpdateLayeredWindow, specifying the type of rendering using a BLENDFUNCTION structure.
This structure allows to define (BlendOp) how the source and destination Bitmaps are blended (actually, the only possible operation is Source Over, AC_SRC_OVER), the level of opacity applied to the source Bitmap (SourceConstantAlpha: 255 = opaque, 0 = fully transparent) and how the Colors of the source and destination bitmaps are interpreted (AlphaFormat).

Here, we want to blend a source Bitmap that has an Alpha Channel (per-pixel alpha), so it's semi-transparent: we specify AC_SRC_ALPHA as the AlphaFormat (see the Docs about how Color blending is interpreted based on the Color type of the source bItmap).

That's all.

To build a Layered Form, add a new Form to a Project, change the Constructor as shown here, add the CreateParams override, the WndProc override if the Form can be moved dragging it and the SelectBitmap() method call, which activates the alpha blending of the source Bitmap (passed in the constructor) and the Form's DC.
Also, add the NativeMethods support class to the Project:

► The Form can be created as usual, in this case passing a Bitmap object to its constructor:
(the Bitmap format must be a 32bit ARGB - a PNG with alpha channel will do)

Dim layeredForm As New PerPixelAlphaLayeredForm(bitmap)
layeredForm.Show()

Public Class PerPixelAlphaLayeredForm
    Public Sub New()
        Me.New(Nothing)
    End Sub

    Public Sub New(bitmap As Bitmap)
        InitializeComponent()
        Me.LayerBitmap = bitmap
    End Sub

    Private ReadOnly Property LayerBitmap As Bitmap

    Protected Overrides Sub OnLoad(e As EventArgs)
        MyBase.OnLoad(e)
        If Me.LayerBitmap IsNot Nothing Then
            Me.ClientSize = Me.LayerBitmap.Size
            Dim screenSize = Screen.FromHandle(Me.Handle).Bounds.Size
            Me.Location = New Point((screenSize.Width - Me.Width) \ 2, (screenSize.Height - Me.Height) \ 2)
            SelectBitmap(Me.LayerBitmap)
            ' Or, call the SelectBitmapFadeOut() method
            ' Task.Run(Function() SelectBitmapFadeOut(Me.LayerBitmap))
        End If
        Me.TopMost = True
    End Sub

    Private Sub SelectBitmap(bitmap As Bitmap)
        NativeMethods.SelectBitmapToLayeredWindow(Me, bitmap, 255)
    End Sub

    Private Async Function SelectBitmapFadeOut(bitmap As Bitmap) As Task
        Dim fadeProgress As Integer = 255
        For i = fadeProgress To 1 Step -1
            BeginInvoke(New MethodInvoker(Sub() NativeMethods.SelectBitmapToLayeredWindow(Me, bitmap, fadeProgress)))
            fadeProgress -= 1
            Await Task.Delay(10)
        Next
    End Function

    Protected Overrides ReadOnly Property CreateParams As CreateParams
        Get
            Dim parms As CreateParams = MyBase.CreateParams
            If Not DesignMode Then parms.ExStyle = parms.ExStyle Or NativeMethods.WS_EX_LAYERED
            Return parms
        End Get
    End Property

    Protected Overrides Sub WndProc(ByRef m As Message)
        If m.Msg = NativeMethods.WM_NCHITTEST Then
            m.Result = New IntPtr(NativeMethods.HTCAPTION)
        Else
            MyBase.WndProc(m)
        End If
    End Sub
End Class

NativeMethods support class:

Imports System.Drawing.Imaging
Imports System.Runtime.InteropServices

Friend Class NativeMethods
    Public Const HTCAPTION As Integer = &H2
    Public Const WM_PAINT = &HF
    Public Const WM_NCHITTEST As Integer = &H84
    Public Const WS_EX_LAYERED As Integer = &H80000

    Public Const AC_SRC_OVER As Byte = 0
    Public Const AC_SRC_ALPHA As Byte = 1

    <Flags>
    Friend Enum ULWFlags
        ULW_COLORKEY = &H1
        ULW_ALPHA = &H2
        ULW_OPAQUE = &H4
        ULW_EX_NORESIZE = &H8
    End Enum

    <StructLayout(LayoutKind.Sequential)>
    Friend Structure POINT
        Public x As Integer
        Public y As Integer
        Public Sub New(X As Integer, Y As Integer)
            Me.x = X
            Me.y = Y
        End Sub
    End Structure

    <StructLayout(LayoutKind.Sequential)>
    Friend Structure SIZE
        Public cx As Integer
        Public cy As Integer
        Public Sub New(cX As Integer, cY As Integer)
            Me.cx = cX
            Me.cy = cY
        End Sub
    End Structure

    <StructLayout(LayoutKind.Sequential, Pack:=1)>
    Friend Structure ARGB
        Public Blue As Byte
        Public Green As Byte
        Public Red As Byte
        Public Alpha As Byte
    End Structure

    <StructLayout(LayoutKind.Sequential, Pack:=1)>
    Friend Structure BLENDFUNCTION
        Public BlendOp As Byte
        Public BlendFlags As Byte
        Public SourceConstantAlpha As Byte
        Public AlphaFormat As Byte
    End Structure

    <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Friend Shared Function UpdateLayeredWindow(hWnd As IntPtr, hdcDst As IntPtr, ByRef pptDst As POINT,
        ByRef psize As SIZE, hdcSrc As IntPtr, ByRef pprSrc As POINT, crKey As Integer,
        ByRef pblend As BLENDFUNCTION, dwFlags As ULWFlags) As Boolean
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Friend Shared Function SetLayeredWindowAttributes(hWnd As IntPtr, crKey As Integer,
        bAlpha As Byte, dwFlags As ULWFlags) As Boolean
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Friend Shared Function ReleaseDC(hWnd As IntPtr, hDC As IntPtr) As Integer
    End Function

    <DllImport("gdi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Friend Shared Function CreateCompatibleDC(hDC As IntPtr) As IntPtr
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Friend Shared Function GetDC(hWnd As IntPtr) As IntPtr
    End Function

    <DllImport("gdi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Friend Shared Function DeleteDC(hdc As IntPtr) As Boolean
    End Function

    <DllImport("gdi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Friend Shared Function SelectObject(hDC As IntPtr, hObject As IntPtr) As IntPtr
    End Function

    <DllImport("gdi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Friend Shared Function DeleteObject(hObject As IntPtr) As Boolean
    End Function

    Public Shared Sub SelectBitmapToLayeredWindow(form As Form, bitmap As Bitmap, opacity As Integer)

        If bitmap.PixelFormat <> PixelFormat.Format32bppArgb Then
            Throw New ApplicationException("The bitmap must be 32bpp with alpha-channel.")
        End If

        Dim screenDc As IntPtr = GetDC(IntPtr.Zero)
        Dim sourceDc As IntPtr = CreateCompatibleDC(screenDc)
        Dim hBitmap As IntPtr = IntPtr.Zero
        Dim hOldBitmap As IntPtr = IntPtr.Zero

        Try
            ' Get handle to the New bitmap and select it into the current device context.
            hBitmap = bitmap.GetHbitmap(Color.FromArgb(0))
            hOldBitmap = SelectObject(sourceDc, hBitmap)

            Dim windowLocation As New POINT(form.Left, form.Top)
            Dim windowSize As New SIZE(bitmap.Width, bitmap.Height)
            Dim sourceLocation As New POINT(0, 0)
            Dim blend As New BLENDFUNCTION() With {
                .BlendOp = AC_SRC_OVER,
                .BlendFlags = 0,
                .SourceConstantAlpha = CType(opacity, Byte),
                .AlphaFormat = AC_SRC_ALPHA
            }

            ' Update the window.
            ' Handle =>         Handle to the layered window
            ' screenDc =>       Handle to the screen DC
            ' windowLocation => Screen position of the layered window
            ' windowSize =>     SIZE of the layered window
            ' sourceDc =>       Handle to the layered window surface DC
            ' sourceLocation => Location of the layer in the DC
            ' 0 =>              Color key of the layered window
            ' blend =>          Transparency of the layered window
            ' ULW_ALPHA =>      Use blend as the blend function
            UpdateLayeredWindow(form.Handle, screenDc, windowLocation, windowSize,
                                sourceDc, sourceLocation, 0, blend, ULWFlags.ULW_ALPHA)
        Finally
            ' Release device context.
            ReleaseDC(IntPtr.Zero, screenDc)
            If hBitmap <> IntPtr.Zero Then
                SelectObject(sourceDc, hOldBitmap)
                DeleteObject(hBitmap)
            End If
            DeleteDC(sourceDc)
        End Try
    End Sub
End Class

You can download a Sample Project from Google Drive.
Built with .Net Framework 4.7.2 - any other Framework, 4.5.2+, will do