bitmap transparency in Visual Studio 2013 C++

2019-01-19 09:45发布

I'm puzzled on this one. I use a program recommended on the forums to convert 32bit PNG to 32bit bitmaps with alpha channel. I add these to the resource editor and place them via the toolbox onto a Dialog. I have read (what I thought was extensively) on bitmap transparency and the limitations in Visual Studio.
What's puzzling for me is that I add the picture control via the Visual Studio resource editor to my dialog box. For example, I have two red balls, one in 24bit bitmap and one with 32bit bitmap. In the test mode of visual studio and when opening the .rc with the resource editor the transparency looks fine. Test mode of visual studio

However, when I call DialogBox programmatically, I don't get transparency.

DialogBox(GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_DIALOG1),
    NULL, DialogProc);

Dialog when DialogBox is called programmatically When I click the test button in Visual Studio, it must be calling a routine DialogBox or similar to show the bitmap. When I place the bitmap in the resource editor to the dialog it shows transparency. What is it Microsoft does that I'm not doing?
I'm developing without MFC, intentionally. Is that the issue, only in MFC can Dialogbox's be loaded with transparency (I realize it reduces to CreateWindowEX). I realize that various bitblt methods also can be used. Is that what Visual Studio does behind the scenes? Also reviewed material on WM_CTLCOLORSTATIC etc. What do you all think? Is there some simple way to call DialogBox and get transparent BMPs on the dialog? Or we're all forced to use MFC? or the user has to write the routines to erase the background/paint etc.

It seems a bit odd to me that without an image with transparency, a dialogbox is easy. Need a non-square image? That's somehow a problem. Now the software engineer has to add lots of code to the Dialog Callback or program. Seems more of a bug to me.

Thank you for your review.

 #include <windows.h>
 #include <winuser.h>
 #include "resource.h"


 INT_PTR CALLBACK DialogProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM   lParam)
  {
  int wmId, wmEvent;

  switch (message)
   {
   case WM_COMMAND:
    wmId = LOWORD(wParam);
    wmEvent = HIWORD(wParam);
    // Parse the menu selections:
    switch (wmId)
    {
    case IDOK:
        EndDialog(hwnd, LOWORD(wParam));
        break;
    case IDCANCEL:
        EndDialog(hwnd, LOWORD(wParam));
        return TRUE;

    }

case WM_PAINT:
    break;

case WM_DESTROY:
    EndDialog(hwnd, LOWORD(wParam));
    break;
}
return FALSE; 
}


 int APIENTRY WinMain(HINSTANCE hInstance,
 HINSTANCE hPrevInstance,
 LPSTR     lpCmdLine,
 int       nCmdShow)
 {


DialogBox(GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_DIALOG1),
    NULL, DialogProc);

return 0;
 }

2条回答
Emotional °昔
2楼-- · 2019-01-19 10:05

There are two possible solutions to the problem which both have advantages and disadvantages.

Solution 1 fixes the original problem stated by the OP by simply adding an application manifest file to the resources. There is no coding required for this solution. The transparency achieved with that solution is not perfect, but is supported on all Windows versions since Windows XP.

Solution 2 is more advanced as it creates a layered child window that provides true transparency of the image over the dialog background aswell as any overlapping child controls. The disadvantages are that at least Windows 8 is required and a decent amount of non-trivial code must be written (but you are lucky as I already did this for you ;-)).

Solution 1 - add an application manifest

The native static control supports bitmaps with alpha transparency only if you add an application manifest that specifies common controls version 6.0.0.0. From the "old-school" look of the controls in your screenshot we can see that you don't have such a manifest yet.

Save the following snippet into a file called "manifest.xml" and put it into your application resource folder. In Visual Studio, right-click your project, go to "manifest tool" > "input and output" > "additional manifest files" > enter relative path of "manifest.xml" without quotation marks.

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> 
    <application>
        <!-- Windows 10 --> 
        <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
        <!-- Windows 8.1 -->
        <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
        <!-- Windows 8 -->
        <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
        <!-- Windows 7 -->
        <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
        <!-- Windows Vista -->
        <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> 
    </application>
  </compatibility>
  <dependency>
    <dependentAssembly>
        <assemblyIdentity
            type="win32"
            name="Microsoft.Windows.Common-Controls"
            version="6.0.0.0"
            processorArchitecture="*"
            publicKeyToken="6595b64144ccf1df"
            language="*"
        />
    </dependentAssembly>
  </dependency>
</assembly>

No further coding is required, just select the bitmap resource for the picture control (which actually is a static control) in the resource editor.

The bitmap to use should be a 32 bpp bitmap, bottom-up row order, non-premultiplied alpha. If you use PixelFormer to convert from PNG, use format A8:R8:G8:B8 (32 bpp) without selecting the other check boxes in the export dialog. If you use XnView to convert, simply save as BMP, it will use this format by default.

Result:

enter image description here

As we can see, we only get "fake" transparency. Any other controls underneath the image will be clipped at the boundaries of the static control.

Solution 2 - using a layered child window

True transparency is possible using a layered child window (WS_EX_LAYERED extended style). This is supported since Windows 8. It requires some coding though.

I wrapped the required code into a function SetLayeredWindowFromBitmapResource() which might be called from the WM_INITDIALOG handler of the dialog. The function throws any error as std::system_error exception, so you must add a try/catch block to handle the errors (this is shown further below in the "usage" example).

#include <system_error>

/// Turn given window into a layered window and load a bitmap from given resource ID 
/// into it.
/// The window will be resized to fit the bitmap.
/// Bitmap must be 32bpp, top-down row order, premultiplied alpha.
///
/// \note For child windows, this requires Win 8 or newer OS
///       (and "supportedOS" element for Win 8 in application manifest)  
///
/// \exception Throws std::system_error in case of any error.

void SetLayeredWindowFromBitmapResource( 
    HWND hwnd, UINT bitmapResourceId, HINSTANCE hInstance = nullptr ) 
{
    // Enable "layered" mode for the child window. This enables full alpha channel 
    // transparency.

    // GetWindowLong() won't reset the last error in case of success.
    // As we can't judge from the return value of GetWindowLong() alone if 
    // the function was successful (0 may be returned even in case of
    // success), we must reset the last error to reliably detect errors.
    ::SetLastError( 0 ); 
    DWORD exStyle = ::GetWindowLong( hwnd, GWL_EXSTYLE );
    if( !exStyle )
    {
        // NOTE: Call GetLastError() IMMEDIATELY when a function's return value 
        // indicates failure and it is documented that said function supports 
        // GetLastError().
        // ANY other code (be it your own or library code) before the next line 
        // must be avoided as it may invalidate the last error value.
        if( DWORD err = ::GetLastError() )
            throw std::system_error( static_cast<int>(err), 
                std::system_category(),
                "SetLayeredWindowFromBitmapResource: Could not get extended window style" );
    }

    // SetWindowLong() won't reset the last error in case of success.
    // As we can't judge from the return value of GetWindowLong() alone if 
    // the function was successful (0 may be returned even in case of
    // success), we must reset the last error to reliably detect errors.
    ::SetLastError( 0 ); 
    if( !::SetWindowLong( hwnd, GWL_EXSTYLE, exStyle | WS_EX_LAYERED ) ) 
    {
        if( DWORD err = ::GetLastError() )
            throw std::system_error( static_cast<int>(err), 
                std::system_category(),
                "SetLayeredWindowFromBitmapResource: Could not set extended window style" );
    }

    // Use RAII ( https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization )
    // to cleanup resources even in case of exceptions.
    // This greatly simplifies the code because now we don't have to manually cleanup the 
    // resources at every location in the code where we throw an exception.
    struct Resources {
        HBITMAP hImage = nullptr;
        HGDIOBJ hOldImage = nullptr;
        HDC hMemDC = nullptr;

        // This destructor will be automatically called before the function 
        // SetLayeredWindowFromBitmapResource() returns aswell as any locations 
        // in the code where the "throw" keyword is used to throw an exception.
        ~Resources() 
        {
            if( hMemDC ) 
            {
                if( hOldImage )
                    ::SelectObject( hMemDC, hOldImage );
                ::DeleteDC( hMemDC );
            }
            if( hImage )
                ::DeleteObject( hImage );
        }
    } res;

    // Make it possible to use nullptr as an argument for the hInstance parameter of 
    // this function. This means we will load the resources from the current executable 
    // (instead of another DLL).
    if( ! hInstance )
        hInstance = ::GetModuleHandle( nullptr );

    // Load bitmap with alpha channel from resource. 
    // Flag LR_CREATEDIBSECTION is required to create a device-independent bitmap that 
    // preserves the alpha channel.
    res.hImage = reinterpret_cast<HBITMAP>(::LoadImage(
        hInstance, MAKEINTRESOURCE( bitmapResourceId ), IMAGE_BITMAP, 
        0, 0, LR_CREATEDIBSECTION ));
    if( !res.hImage )
    {
        DWORD err = ::GetLastError();
        throw std::system_error( static_cast<int>(err),
            std::system_category(),
            "SetLayeredWindowFromBitmapResource: Could not load bitmap resource" );
    }

    // Get bitmap information (width, height, etc.)
    BITMAP imgInfo{ 0 };
    if( !::GetObject( res.hImage, sizeof( imgInfo ), &imgInfo ) )
    {
        DWORD err = ::GetLastError();
        throw std::system_error( static_cast<int>(err),
            std::system_category(),
            "SetLayeredWindowFromBitmapResource: Could not get bitmap information" );
    }

    if( imgInfo.bmBitsPixel != 32 || imgInfo.bmPlanes != 1 )
    {
        // Use a constant error value here because this is our own error condition.
        // Of course GetLastError() wouldn't return anything useful in this case.
        DWORD err = ERROR_INVALID_DATA;
        throw std::system_error( err, std::system_category(),
            "SetLayeredWindowFromBitmapResource: bitmap must be 32 bpp, single plane" );
    }

    // Create a memory DC that will be associated with the image.
    // UpdateLayeredWindow() can't use image directly, it must be in a memory DC.
    res.hMemDC = ::CreateCompatibleDC( nullptr );
    if( !res.hMemDC )
    {
        DWORD err = ::GetLastError();
        throw std::system_error( static_cast<int>(err), 
            std::system_category(),
            "SetLayeredWindowFromBitmapResource: Could not create memory DC" );
    }

    res.hOldImage = ::SelectObject( res.hMemDC, res.hImage );
    if( !res.hOldImage )
    {
        DWORD err = ::GetLastError();
        throw std::system_error( static_cast<int>(err), 
            std::system_category(),
            "SetLayeredWindowFromBitmapResource: Could not select bitmap into memory DC" );
    }

    // Assign the image to the child window, making it transparent.
    SIZE size{ imgInfo.bmWidth, imgInfo.bmHeight };
    POINT ptSrc{ 0, 0 };
    BLENDFUNCTION blend{ AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
    if( !::UpdateLayeredWindow( hwnd, nullptr, nullptr, &size, res.hMemDC, &ptSrc, 
                                0, &blend, ULW_ALPHA ) )
    {
        DWORD err = ::GetLastError();
        throw std::system_error( static_cast<int>(err), 
            std::system_category(),
            "SetLayeredWindowFromBitmapResource: Could not update layered window" );
    }

    // Destructor of res object will cleanup resources here!
}

Usage:

The function may be called in WM_INITDIALOG handler of your dialog box procedure, see example below. The example also shows how to handle errors.

NOTE: I'm calling MessageBoxA() here because std::exception::what() returns a const char* which is apparently a multibyte (ANSI) encoded string that contains a localized error message from the OS (with VS2015 or newer).

#include <sstream>

/// Dialog box procedure.
INT_PTR CALLBACK TestDialogProc( HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam ) {
    UNREFERENCED_PARAMETER( lParam );
    switch( message ) {
        case WM_INITDIALOG: {
            // This is the child window where we want to show the image (e. g. a static).
            if( HWND hwndImage = ::GetDlgItem( hDlg, IDC_IMAGE ) ){
                try{
                    SetLayeredWindowFromBitmapResource( hwndImage, IDB_BITMAP1 );
                }
                catch( std::system_error& e ){
                    std::ostringstream msg;
                    msg << e.what() << std::endl << "Error code: " << e.code();
                    ::MessageBoxA( hDlg, msg.str().c_str(), L"Error", MB_ICONERROR );
                }
            }
            return TRUE;
        }
        case WM_COMMAND: {
            if( LOWORD( wParam ) == IDOK || LOWORD( wParam ) == IDCANCEL ){
                EndDialog( hDlg, LOWORD( wParam ) );
                return TRUE;
            }
            break;
        }
    }
    return FALSE;
}

Result:

Screenshot

Pitfalls:

Application must have a manifest resource specifying at least Win 8 compatibility:

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> 
    <application>
        <!-- Windows 10 --> 
        <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
        <!-- Windows 8.1 -->
        <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
        <!-- Windows 8 -->
        <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>   
    </application>
  </compatibility>
  <dependency>
    <dependentAssembly>
        <assemblyIdentity
            type="win32"
            name="Microsoft.Windows.Common-Controls"
            version="6.0.0.0"
            processorArchitecture="*"
            publicKeyToken="6595b64144ccf1df"
            language="*"
        />
    </dependentAssembly>
  </dependency>
</assembly>

The image to load must be a 32 bpp, top-down bitmap with premultiplied alpha channel.

A regular PNG can be converted into this format using PixelFormer for instance. Open image, then File > Export. Select bitmap, format A8:R8:G8:B8 (32 bpp), premultiplied alpha, top-down row order.

查看更多
家丑人穷心不美
3楼-- · 2019-01-19 10:07

Building on @Zett42's answer, a simple pragma can also direct the compiler to use the 6 0 0 0 controls rather than using the manifest file.

#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' " "version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") 
查看更多
登录 后发表回答