1
votes

I'm updating the question to remove irrelevant details. The conclusion I've drawn is that if a valid alpha channel exists, it will honor it, but if it doesn't (say a 24-bit PNG w/o alpha channel), it uses F0F0F0 as a transparent color.

I have an image being loaded into a static "picture control" (chosen in visual studio) in a dialog. I noticed that color 0xF0F0F0 is being displayed as a "transparent" color (background of the dialog bleeds through). The bitmap is loaded via CStatic::SetBitmap.

The Picture Control transparent flag is set to false.

The image is loaded via CImage::Load.

If I wanted to mask a color out of a CStatic bitmap set via SetBitmap, how would I do it? I don't, but maybe that would help me find the cause.

Minimum example below. I created a dialog project with the VS wizard, and added a picture control to the main dialog. Then I added only the following code:

//header code added
CPngImage logoImage;
CStatic pictureCtrl;
CBrush bgBrush;
....
afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);

//cpp code added
DDX_Control(pDX, IDC_STATICIMG, pictureCtrl);
....
ON_WM_CTLCOLOR()
....
bgBrush.CreateSolidBrush(RGB(0, 255, 0));
logoImage.LoadFromFile(_T("C:\\temp\\logo.png"));
pictureCtrl.SetBitmap(logoImage);
....
HBRUSH CMFCApplication1Dlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) {
    return bgBrush;
}

And here is the image file I'm testing with.

https://i.imgur.com/OA9CCh8.png

And here is what it looks like on the dialog:

enter image description here

// MFCApplication1Dlg.h : header file
//

#pragma once


// CMFCApplication1Dlg dialog
class CMFCApplication1Dlg : public CDialogEx
{
// Construction
public:
    CMFCApplication1Dlg(CWnd* pParent = nullptr);   // standard constructor
    CPngImage logoImage;
    CStatic pictureCtrl;
    CBrush bgBrush;

// Dialog Data
#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_MFCAPPLICATION1_DIALOG };
#endif

    protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support


// Implementation
protected:
    HICON m_hIcon;


    // Generated message map functions
    virtual BOOL OnInitDialog();
    afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
    afx_msg void OnPaint();
    afx_msg HCURSOR OnQueryDragIcon();
    DECLARE_MESSAGE_MAP()
public:
    afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
};




// MFCApplication1Dlg.cpp : implementation file
//

#include "stdafx.h"
#include "MFCApplication1.h"
#include "MFCApplication1Dlg.h"
#include "afxdialogex.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif


// CAboutDlg dialog used for App About

class CAboutDlg : public CDialogEx
{
public:
    CAboutDlg();

// Dialog Data
#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_ABOUTBOX };
#endif

    protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support

// Implementation
protected:
    DECLARE_MESSAGE_MAP()
};

CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX)
{
}

void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
}

BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx)
END_MESSAGE_MAP()


// CMFCApplication1Dlg dialog



CMFCApplication1Dlg::CMFCApplication1Dlg(CWnd* pParent /*=nullptr*/)
    : CDialogEx(IDD_MFCAPPLICATION1_DIALOG, pParent)
{
    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CMFCApplication1Dlg::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Control(pDX, IDC_STATICIMG, pictureCtrl);
}

BEGIN_MESSAGE_MAP(CMFCApplication1Dlg, CDialogEx)
    ON_WM_SYSCOMMAND()
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_WM_CTLCOLOR()
END_MESSAGE_MAP()


// CMFCApplication1Dlg message handlers

BOOL CMFCApplication1Dlg::OnInitDialog()
{
    CDialogEx::OnInitDialog();

    // Add "About..." menu item to system menu.

    // IDM_ABOUTBOX must be in the system command range.
    ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
    ASSERT(IDM_ABOUTBOX < 0xF000);

    CMenu* pSysMenu = GetSystemMenu(FALSE);
    if (pSysMenu != nullptr)
    {
        BOOL bNameValid;
        CString strAboutMenu;
        bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
        ASSERT(bNameValid);
        if (!strAboutMenu.IsEmpty())
        {
            pSysMenu->AppendMenu(MF_SEPARATOR);
            pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
        }
    }

    // Set the icon for this dialog.  The framework does this automatically
    //  when the application's main window is not a dialog
    SetIcon(m_hIcon, TRUE);         // Set big icon
    SetIcon(m_hIcon, FALSE);        // Set small icon

    bgBrush.CreateSolidBrush(RGB(0, 255, 0));
    logoImage.LoadFromFile(_T("C:\\temp\\logo.png"));
    pictureCtrl.SetBitmap(logoImage);

    return TRUE;  // return TRUE  unless you set the focus to a control
}

void CMFCApplication1Dlg::OnSysCommand(UINT nID, LPARAM lParam)
{
    if ((nID & 0xFFF0) == IDM_ABOUTBOX)
    {
        CAboutDlg dlgAbout;
        dlgAbout.DoModal();
    }
    else
    {
        CDialogEx::OnSysCommand(nID, lParam);
    }
}

// If you add a minimize button to your dialog, you will need the code below
//  to draw the icon.  For MFC applications using the document/view model,
//  this is automatically done for you by the framework.

void CMFCApplication1Dlg::OnPaint()
{
    if (IsIconic())
    {
        CPaintDC dc(this); // device context for painting

        SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);

        // Center icon in client rectangle
        int cxIcon = GetSystemMetrics(SM_CXICON);
        int cyIcon = GetSystemMetrics(SM_CYICON);
        CRect rect;
        GetClientRect(&rect);
        int x = (rect.Width() - cxIcon + 1) / 2;
        int y = (rect.Height() - cyIcon + 1) / 2;

        // Draw the icon
        dc.DrawIcon(x, y, m_hIcon);
    }
    else
    {
        CDialogEx::OnPaint();
    }
}

// The system calls this function to obtain the cursor to display while the user drags
//  the minimized window.
HCURSOR CMFCApplication1Dlg::OnQueryDragIcon()
{
    return static_cast<HCURSOR>(m_hIcon);
}



HBRUSH CMFCApplication1Dlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) {
    return bgBrush;
}
3
Are you sure the transparency is not defined in the PNG file? Even if the PNG has no alpha channel, it may define a transparency color key.zett42
I can reproduce the issue even with a 24 bpp BMP file (loaded through CImage::Load()). I have created a new bitmap from scratch and filled it partly with F0F0F0. The CStatic displays this color as transparent similar to your screenshot.zett42
Further experiments show that this is not related to CImage / CPngImage. I have created a memory DC and drawn an ellipse with a solid brush of 0xF0F0F0. Assigned the bitmap from the memory DC to the CStatic. Same behaviour, the color 0xF0F0F0 becomes transparent.zett42
My repro code. My OnCtlColor() is copy-pasted from yours.zett42
BTW, on my machine 0xF0F0F0 equals GetSysColor(COLOR_BTN_FACE), which is the default dialog background color.zett42

3 Answers

1
votes

On my system (Windows 10), the color 0xF0F0F0 equals GetSysColor(COLOR_BTNFACE) which is the default dialog background color. When drawing, the static control seems to replace this color in the background image with the brush returned from OnCtlColor() handler of the parent window. This does have the taste of a feature and not a bug (though I couldn't find anything in the reference that specifies this behaviour).

Here is a code snippet to reproduce this issue even without using CPngImage or CImage, just by drawing in a memory DC with color 0xF0F0F0.

As the behaviour only appears when the source image does not contain an alpha channel, a solution would be to convert the source image to 32-bpp ARGB format. This way we don't have to override CStatic::OnPaint():

// Set the alpha channel of a 32-bpp ARGB image to the given value.
HRESULT SetAlphaChannel( CImage& image, std::uint8_t alpha )
{
    if( ! image.GetBits() || image.GetBPP() != 32 )
        return E_INVALIDARG;

    GdiFlush(); // Make sure GDI has finished all drawing in source image.

    for( int y = 0; y < image.GetHeight(); ++y )
    {
        DWORD* pPix = reinterpret_cast<DWORD*>( image.GetPixelAddress( 0, y ) );
        for( int x = 0; x < image.GetWidth(); ++x, ++pPix )
        {
            *pPix = ( *pPix & 0xFFFFFF ) | ( alpha << 24 );
        }
    }

    return S_OK;        
}

// Load an image and convert to 32-bpp ARGB format, if necessary.
HRESULT LoadImageAndConvertToARGB32( CImage& image, LPCWSTR pFilePath )
{
    CImage tempImage;
    HRESULT hr = tempImage.Load( pFilePath );
    if( FAILED( hr ) )
        return hr;

    if( tempImage.GetBPP() == 32 )  // Assume 32 bpp image already has an alpha channel
    {
        image.Attach( tempImage.Detach() );
        return S_OK;
    }

    if( ! image.Create( tempImage.GetWidth(), tempImage.GetHeight(), 32, CImage::createAlphaChannel ) )
        return E_FAIL;

    HDC const imageDC = image.GetDC();
    BOOL const bitBltSuccess = tempImage.BitBlt( imageDC, 0, 0, SRCCOPY );
    image.ReleaseDC();

    if( ! bitBltSuccess )
        return E_FAIL;

    SetAlphaChannel( image, 255 );  // set alpha to opaque

    return S_OK;
}

Usage:

Replace call to CImage::Load() by:

LoadImageAndConvertToARGB32( m_image, filePath );

Notes:

There is another static control nastiness when you assign a 32-bpp bitmap with a non-zero alpha channel to the control¹ (as you do when following my solution). In this case, the static control will make a copy of the bitmap you passed in while you are responsible to destroy this copy!

Mandatory OldNewThing read:

"When will the static control automatically delete the image loaded into it, and when is it the responsibility of the application?"

¹) More precisely: When using version 6 of the common controls, which almost all applications do these days.

0
votes

It's not a PNG problem, it's a color depth problem.

According to your code, I converted 8-bit PNG picture into 8-bit BMP picture by format conversion tool, and the picture still show the background color.

So I saved 8-bit PNG picture to 32-bit png picture, and that's all right. 1

2

Why is that?

Answer: A GIF file has two parts: a color table and the image pixel data. The color table is a list of the colors used in that image (an 8-bit GIF can have up to 2^8 = 256 colors in the color table, but a 4-bit GIF can have only 2^4 = 16 colors), and each color is assigned a number. The image pixel data are for the image itself, and each pixel is assigned a number that points to its color in the color table. For example, if color #10 in the color table is red (#FF0000), then any pixel in the image with the number 10 will be displayed as red. The colors in the color table will vary from GIF file to GIF file based on the image itself; color #10 will not always be red. The color table is the set of up to 256 colors necessary to render that image.

When we add index transparency, every color in the color table is given a transparency designation in addition to its color data (i.e., RGB values):

zero (o = False in Boolean algebra) means do not display this color, or one (1 = True in Boolean Algebra) means display this color. There are no intermediate opacities; the color is either displayed or it is not. The end result is that a pixel with an index transparency color will not be displayed and whatever is in the background behind that pixel will show through. For example, if color #10 is red (#FF0000) and is designated as transparent (index transparency = 0), then any pixel that is color #10 will not be displayed and the background will show through.

There can be multiple transparent colors in index transparency, because every color in the color table has a designation of either opaque (1) or transparent (0). Most graphics programs assume that the canvas color (often white, but it could be any color) is the default transparent color, but you can specify any color (or any number of colors) as transparent or not.

This type of transparency is very common in GIF and PNG8 files and is easy to identify, because there is no fading, there are no partially transparent pixels, and the edges are often described as “hard” or “pixelated.”

3

0
votes

I'm afraid the following is the best answer I'm going to get. MFC/Win32 does something funny and it's not clear why. But the image has no problems displaying correctly if you draw it manually.

I created a custom (very rough) CStatic class and added the OnPaint below. In the screenshot, the first is using my class, and the second is using regular CStatic. Both are using an identical image (24 bit BMP).

struct AFX_CTLCOLOR {
    HWND hWnd;
    HDC hDC;
    UINT nCtlType;
};

void CMyStatic::OnPaint() {
    CPaintDC dc(this); // device context for painting
    CRect r;
    GetClientRect(r);

    WPARAM w = (WPARAM)&dc;
    AFX_CTLCOLOR ctl;
    ctl.hWnd = this->GetSafeHwnd();
    ctl.nCtlType = CTLCOLOR_STATIC;
    ctl.hDC = dc.GetSafeHdc();
    HBRUSH bg=(HBRUSH)::SendMessage(GetParent()->GetSafeHwnd(), WM_CTLCOLORSTATIC, w, (LPARAM)&ctl);
    CBrush cbg;
    cbg.Attach(bg);
    dc.FillRect(r, &cbg);
    cbg.Detach();

    HBITMAP hbmp=GetBitmap();
    BITMAP binfo;

    CBitmap* cbmp = CBitmap::FromHandle(hbmp);
    cbmp->GetBitmap(&binfo);
    CDC dcMem;
    dcMem.CreateCompatibleDC(&dc);
    dcMem.SelectObject(cbmp);

    dc.BitBlt((r.Width()-binfo.bmWidth)/2, (r.Height() - binfo.bmHeight) / 2, binfo.bmWidth, binfo.bmHeight, &dcMem, 0, 0, SRCCOPY);
}

enter image description here