Search code examples
.netvb.netwinformstransparency

Form's TransparencyKey leaves ghastly colored edging


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

  • 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