0

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?

Jimi
  • 29,621
  • 8
  • 43
  • 61
LabRat
  • 1,996
  • 11
  • 56
  • 91

1 Answers1

2

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

Jimi
  • 29,621
  • 8
  • 43
  • 61
  • Thanks for the reply Jim. I'm not an advanced user. I am having trouble understanding where all the parts of your code go. To show I have made an effort I have 1) Start a new project, then I added Protected Overrides code you provided in the forms code right underneath where it says "Public Class Form 1". 2: created a new form PerPixelAlphaLayeredForm with your 2nd block of code in it and 3:Create a class NativeMethods which I added your last block of code. I've obviously done it wrong I don't know where though... Sorry – LabRat Mar 19 '20 at 20:47
  • `PerPixelAlphaLayeredForm` is, well, a Form name. You should have just renamed your `Form1` to `PerPixelAlphaLayeredForm`, pasted the code in the first block (from `Public Sub New()` onwards) inside it and nothing else. The `NativeMethods` class is usually a stand-alone class object: you should add a new Class to your Project, name it `NativeMethods`, then paste it the code here overwriting the existing (2 lines of) code. But you can also paste it inside your Form (your Form1, renamed or not renamed to `PerPixelAlphaLayeredForm`, its the same). – Jimi Mar 19 '20 at 22:01
  • Note that the Layered Form, as it's built now, is supposed to be instatiated from other code, since, as you can see, the Bitmap is assigned in its constructor and the `LayerBitmap As Bitmap` property is `Private ReadOnly`. This because it's supposed to be used as a *Splash Windows*, kept there for the time needed and then disposed of by the same calling code. It's not strictly necessary, but I suggest you use it like this, since a Layered Form is not exactly *usable*. So maybe, for testing, create it from a `Button.Click` event handler in another Form until you get the hang of it. – Jimi Mar 19 '20 at 22:11
  • Okay I have the grip on your 2nd reply no errors are coming up now. If I understand correctly a form named layered is created by a trigger like a button. the code behind that trigger is Dim layeredForm As New PerPixelAlphaLayeredForm(bitmap) layeredForm.Show() and now I get nothing? – LabRat Mar 19 '20 at 22:35
  • Do you have 2 Forms in your Project now? The starting Form and another named `PerPixelAlphaLayeredForm`? Are you passing a semi-transparent PNG Bitmap to the `PerPixelAlphaLayeredForm` Form constructor when you create it? Look, tell me what Framework version you're using (if you have 4.7.2 or 4.8 is better) and I'll post a sample Project in Google Drive or whatever. – Jimi Mar 19 '20 at 22:51
  • I have one form named PerPixelAlphaLayeredForm. the version i have is 4.7.2 – LabRat Mar 19 '20 at 23:15
  • [Sample Project on Google Drive](https://drive.google.com/open?id=1OFcN3DpDGUQk0VGgcJu6-JzTOuoxhf28) -- Download the Zip -> Extract here -> Open the Solution -> Run the Project. – Jimi Mar 19 '20 at 23:23
  • Okay a bit weird I was expecting to be completely wrong but it seems I wasn't off by that far although with that said in programming almost isn't near enougtht :) – LabRat Mar 20 '20 at 14:41
  • Just one other thing I was wanting to fade the form in and out. The most common way of doing this with normal forms is to use a timer which changes the forms opacity state giving it the apearance of fading in and/ or out. That dosen't work in these type of forms how would I be able to achive this? – LabRat Mar 20 '20 at 19:14
  • See the `Private Sub SelectBitmap(bitmap As Bitmap)` method? It then calls `NativeMethods.SelectBitmapToLayeredWindow(Me, bitmap, 255)`. Look at the definition of `SelectBitmapToLayeredWindow()`: the third argument specifies the `Opacity`. Now, try to set it to `128` instead of `255`. If you loop this range of values, don't start from `0`, start from `1`. – Jimi Mar 20 '20 at 20:04
  • I did see that and put it in a loop statement it will loop but not actually show any image till the loop is complete. assuming that NativeMethods.SelectBitmapToLayeredWindow(Me, bitmap, MYPOACITY-1) and Dim MYOPACITY as Integer= 255 are use in the code – LabRat Mar 20 '20 at 20:46
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/210033/discussion-between-abandond-acount-and-jimi). – LabRat Mar 20 '20 at 20:54
  • Just out of curiousity how do I hide the form once its faded out again, because I have tried to add me.hide() just after the next in the Private Async Function SelectBitmapFadeOut.... section (its a really nice form it just reacts diffrent than I'm use too) – LabRat Mar 29 '20 at 09:47
  • I have an aditional question to the above code. how do I close the newly created form, Ive tried Layerdform.close() but it dosen't react? – LabRat Apr 27 '20 at 07:08
  • Calling `Close()` will of course close the Form. Layered or not, it's still a Form. You're not trying to close a default instance, right? The Layered Form (here) is declared as `Dim layeredForm As New PerPixelAlphaLayeredForm(bitmap)`, so `layeredForm` is defined locally. You need a Field to store the instance if you want to refer to it from somewhere else: `Private layeredForm As PerPixelAlphaLayeredForm = Nothing`. Then, create it when needed: `layeredForm = New PerPixelAlphaLayeredForm([Bitmap])`. Then use the reference to close it: `layeredForm.Close()`. – Jimi Apr 27 '20 at 11:52