Cesar
Cesar

Reputation: 399

Make Win32 edit control transparent (WM_CTLCOLOREDIT Nullbrush text issue)

When i type something on an edit control which got its background 'transparent' by returning a NULL_BRUSH as response to WM_CTLCOLOREDIT into the WindowProc, it behaves weird with some visual glitches:

https://i.imgur.com/y4DxkIA.gif

case WM_CTLCOLOREDIT:
{
    SetBkMode((HDC)wParam, TRANSPARENT);
    return (LRESULT)GetStockObject(NULL_BRUSH);
}

When i minimize/restore the GUI looks like it 'repaint' the control to the correct state.

What am I missing?

Upvotes: 3

Views: 944

Answers (2)

zett42
zett42

Reputation: 27806

AFAIK the only way to make the edit control appear transparent is to fake transparency by returning a brush from WM_CTLCOLOREDIT that looks like the background of the dialog. Transparent or NULL brushes are not supported.

This is trivial if the background is a single solid color. Just create a brush using CreateSolidBrush(). As IInspectable notes, another way (that avoids managing resources) is to return GetStockObject(DC_BRUSH) and call SetDCBrushColor() to set the color to use for this brush.

For complex backgrounds, use a bitmap and convert that to a HBRUSH using CreatePatternBrush(). Set the brush origin using SetBrushOrgEx() so that the bitmap brush is drawn at the correct offset in relation to the position of the edit control. Also, make sure to set the device context background mode to TRANSPARENT so the background of the edit control text is drawn using the brush instead of the DC background color.

Full self-contained sample:

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <unknwn.h>
#include <gdiplus.h>
#include <memory>
#include <cassert>

#pragma comment( lib, "gdiplus" )

namespace gp = Gdiplus;

// Forward declaration
INT_PTR CALLBACK MyDialogProc( HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam );

// RAII wrapper to initialize GDI+
struct InitGdiPlus {
    gp::GdiplusStartupInput gdiplusStartupInput;
    ULONG_PTR gdipToken = 0;
    InitGdiPlus() { gp::GdiplusStartup( &gdipToken, &gdiplusStartupInput, nullptr ); }
    ~InitGdiPlus() { gp::GdiplusShutdown( gdipToken ); }
};

HINSTANCE g_hInstance = nullptr;

//---------------------------------------------------------------------------------------------

int APIENTRY wWinMain( _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR    lpCmdLine,
    _In_ int       nCmdShow )
{
    g_hInstance = hInstance;

    InitGdiPlus initGdip;

    // Create and show dialog.

    struct MyDialog : DLGTEMPLATE {
        WORD dummy[ 3 ] = { 0 };  // unused menu, class and title
    }
    dlg;
    dlg.style = WS_POPUP | WS_CAPTION | WS_SYSMENU | DS_CENTER;
    dlg.dwExtendedStyle = 0;
    dlg.cdit = 0;  // no controls in template
    dlg.x = 0;
    dlg.y = 0;
    dlg.cx = 200;  // width in dialog units
    dlg.cy = 100;  // height in dialog units
      
    DialogBoxIndirectW( hInstance, &dlg, nullptr, MyDialogProc );

    return 0;
}

//---------------------------------------------------------------------------------------------

INT_PTR CALLBACK MyDialogProc( HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam )
{
    static HWND hEdit = nullptr;
    static HBRUSH hEditBrush = nullptr;
    static std::unique_ptr<gp::Bitmap> pBitmap; 

    switch( message )
    {
        case WM_INITDIALOG:
        {
            SetWindowTextW( hDlg, L"Edit control bitmap background demo" );

            RECT rc; GetClientRect( hDlg, &rc );
            gp::Rect rect( rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top );

            // Create bitmap with size of client area
            pBitmap.reset( new gp::Bitmap( rect.Width, rect.Height, PixelFormat32bppRGB ) );

            // Draw background into bitmap

            gp::Graphics gfx( pBitmap.get() );

            gp::LinearGradientBrush brush(
                gp::Point( 0, 0 ),
                gp::Point( rect.Width, rect.Height ),
                gp::Color( 255, 255, 255, 255 ),
                gp::Color( 255, 0, 0, 255 ) );

            gfx.FillRectangle( &brush, rect );

            // Create brush from bitmap

            HBITMAP hBitmap = nullptr;
            auto res = pBitmap->GetHBITMAP( gp::Color::White, &hBitmap );
            assert( res == gp::Status::Ok );

            hEditBrush = ::CreatePatternBrush( hBitmap );
            assert( hEditBrush != nullptr );

            // Create controls

            RECT rcEdit{ 30, 40, 100, 55 }; ::MapDialogRect( hDlg, &rcEdit );
            hEdit = ::CreateWindow( L"Edit", L"hello edit", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 
                                    rcEdit.left, rcEdit.top, rcEdit.right - rcEdit.left, rcEdit.bottom - rcEdit.top, 
                                    hDlg, nullptr, g_hInstance, 0 );

            return TRUE;
        }
        case WM_PRINTCLIENT: 
        {
            // Doing the actual drawing in WM_PRINTCLIENT supports transparency of some common controls (but not the edit control).
            HDC hdc = reinterpret_cast<HDC>( wParam );
            gp::Graphics gfx{ hdc };

            if( pBitmap ) {
                gfx.DrawImage( pBitmap.get(), 0, 0 );
            }

            return TRUE;
        }
        case WM_PAINT:
        {
            PAINTSTRUCT ps{ 0 };
            HDC hdc = BeginPaint( hDlg, &ps );

            // Delegate drawing to WM_PRINTCLIENT
            MyDialogProc( hDlg, WM_PRINTCLIENT, reinterpret_cast<WPARAM>( hdc ), 0 );

            EndPaint( hDlg, &ps );

            return TRUE;
        }
        case WM_CTLCOLOREDIT:
        {
            HDC hdc = reinterpret_cast<HDC>( wParam );
            ::SetBkMode( hdc, TRANSPARENT );     // required so it uses the brush instead of HDC background color
            ::SetTextColor( hdc, RGB(127,0,0) ); // optionally set edit control text color 

            // Set brush origin so that background will be drawn at correct offset depending on edit control position.
            // We basically map the top-left position of the client area to the coordinate space of the edit control.
            POINT offset{ 0, 0 }; ::MapWindowPoints( hDlg, hEdit, &offset, 1 );
            ::SetBrushOrgEx( hdc, offset.x, offset.y, nullptr );
        
            return reinterpret_cast<LONG_PTR>( hEditBrush );
        }
        case WM_NCDESTROY:
        {
            // Cleanup
            if( hEditBrush ) { ::DeleteObject( hEditBrush ); } hEditBrush = nullptr;
            if( pBitmap ) { pBitmap.reset(); }  // Must be deleted before GDI+ shuts down!
            return FALSE;
        }
        case WM_COMMAND:
        {
            WORD id = LOWORD( wParam );
            if( id == IDOK || id == IDCANCEL )
            {
                EndDialog( hDlg, id );
                return TRUE;
            }
            return FALSE;
        }
    }
    return FALSE; // return FALSE to let DefDialogProc handle the message
}

Screenshot:

Edit control bitmap background demo

Upvotes: 5

The edit control is trying to erase the wrong pixels by drawing the background colour over top of them, but now it's drawing a null brush so this doesn't actually erase the pixels and you see the leftover pixels.

You can't make a regular control transparent by making it not paint the background. That's just not a thing. Sorry.

The second time the control paints, it's not painting on top of a blank slate - it's painting on top of whatever it painted last time.

Upvotes: 3

Related Questions