Creating Irregular Shaped Window

Before Windows 2000 the only way of creating irregular shaped windows seems to be using regions. Windows 2000 introduced a new function UpdateLayeredWindow which is powerful and is more flexible of creating irregular shaped windows. Basically by supplying a device context which contains a 32-bit bitmap with alpha channel, Windows will alpha blending the window and pass through the mouse messages to the window below for the transparent pixels.

Following is a simple C++ class demonstrates how to create a window with ellipse shape:

#pragma once

class CEllipseWindow : public CWindowImpl<CEllipseWindow>
{
public:
    BEGIN_MSG_MAP_EX(CEllipseWindow)
        MSG_WM_CREATE(OnCreate)
        MSG_WM_NCHITTEST(OnNcHitTest)
        MSG_WM_KEYDOWN(OnKeyDown)
    END_MSG_MAP()

    int OnCreate(LPCREATESTRUCT lpCreateStruct)
    {
        // Add style WS_EX_LAYERED to the window
        ModifyStyleEx(0, WS_EX_LAYERED);

        CRect rectClient;
        GetClientRect(&rectClient);

        // Use ATL::CImage class to create a 32-bit bitmap with alpha channel
        CImage img;
        if(img.Create(rectClient.Width(), rectClient.Height(), 32, CImage::createAlphaChannel))
        {
            Gdiplus::Graphics graphics(img.GetDC());
            graphics.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality);

            Gdiplus::Rect rect(0, 0, img.GetWidth() – 1, img.GetHeight() – 1);
            Gdiplus::LinearGradientBrush brush(rect,
                                               Gdiplus::Color(0, 0, 0, 0),
                                               Gdiplus::Color(255, 0, 0, 255),
                                               Gdiplus::LinearGradientModeForwardDiagonal);
            // Fill the ellipse with the linear brush, GDI+ will take care of alpha blending
            graphics.FillEllipse(&brush, rect);

            // Dig a "hole" with a smaller ellipse by using a solid transparent brush (alpha is 0) and set composite mode
            // to source copy instead of source over
            rect.Inflate(-rect.Width / 4, -rect.Height / 4);
            graphics.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
            Gdiplus::SolidBrush brush2(Gdiplus::Color(0, 0, 0, 0));
            graphics.FillEllipse(&brush2, rect);
        }
        img.ReleaseDC();

        /* Following code can be used to fine tune the bitmap pixel by pixel if needed
        LPDWORD pdwbits = (LPDWORD)img.GetPixelAddress(0, rectClient.Height() – 1);
        for(LONG ny = 0; ny < rectClient.Height(); ++ny)
        {
            for(LONG nx = 0; nx < rectClient.Width(); ++nx)
            {
                DWORD& dwPixel = pdwbits[ny * rectClient.Width() + nx]; // dwPixel is in ARGB format
            }
        }
        */

        POINT pt = {0, 0};
        SIZE size = {rectClient.Width(), rectClient.Height()};

        BLENDFUNCTION bf = {0};
        bf.BlendOp = AC_SRC_OVER;
        bf.BlendFlags = 0;
        bf.AlphaFormat = AC_SRC_ALPHA;
        bf.SourceConstantAlpha = 178;    // in addition to alpha channel in bitmap, this adds additional transparency

        // supply the bitmap to Windows for rendering the window
        UpdateLayeredWindow(m_hWnd, 0, 0, &size, img.GetDC(), &pt, 0, &bf, ULW_ALPHA);
        img.ReleaseDC();

        return 0;
    }
    UINT OnNcHitTest(CPoint point)
    {
        return HTCAPTION;
    }

    void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
    {
        if(VK_ESCAPE == nChar)
            DestroyWindow();
    }

    virtual void OnFinalMessage(HWND)
    {
        PostQuitMessage(0);
    }
};

This is sample code of how to use it:

#include "stdafx.h"
#include "EllipseWindow.h"

CAppModule _Module;

int Run(LPTSTR /*lpstrCmdLine*/ = NULL, int nCmdShow = SW_SHOWDEFAULT)
{
    CMessageLoop theLoop;
    _Module.AddMessageLoop(&theLoop);
    RECT rect = {100, 100, 300, 200};
    CEllipseWindow wnd;
    if(NULL != wnd.Create(0, rect, 0, WS_POPUP | WS_VISIBLE))
    {
        wnd.ShowWindow(nCmdShow);
        theLoop.Run();
    }
    _Module.RemoveMessageLoop();
    return 0;
}

int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR lpstrCmdLine, int nCmdShow)
{
    HRESULT hRes = ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
    ::DefWindowProc(NULL, 0, 0, 0L);
    AtlInitCommonControls(ICC_BAR_CLASSES);    // add flags to support other controls

    ULONG_PTR token = 0;
    Gdiplus::GdiplusStartupInput input;
    Gdiplus::GdiplusStartup(&token, &input, 0);

    hRes = _Module.Init(NULL, hInstance);
    int nRet = Run(lpstrCmdLine, nCmdShow);
    _Module.Term();

    Gdiplus::GdiplusShutdown(token);
    ::CoUninitialize();
    return nRet;
}

This is the order of ATL includes incase you got compiling error of ambiguous symbol for CPoint, CRect etc:

#define _WTL_NO_WTYPES

#include <atlstr.h>
#include <atlimage.h>
#include <atlbase.h>
#include <atlapp.h>
#include <atlwin.h>
#include <atlcrack.h>
#include <atlmisc.h>

#include <gdiplus.h>

And this is result:

Short description about using UpdateLayeredWindow to create irregular shaped windows:

  1. Apply style WS_EX_LAYERED flag to the window;
  2. Prepare a device context with a 32-bit bitmap; ATL::CImage can be used for this job, it is quite easy to use it to create a 32-bit DIB with alpha channel and it also has a built-in device context. The only catch with it is to be careful with its function GetBits() as for bottomed up DIB it returns address to the first scan line instead of the beginning of buffer. The code above retrieves address of the beginning of buffer by getting pixel address of last scan line. Please note that this is only for bottomed up bitmaps;
  3. Drawing can be done with GDI+ as it takes care of alpha blending which is hard to do using GDI;
  4. Call UpdateLayeredWindow to supply the content for rendering, user can add additional transparency by setting value SourceConstantAlpha of BLENDFUNCTION;

Further reading:

Windows Vista for Developers – Part 3 – The Desktop Window Manager

Alpha Blended Splash Screen in Delphi Part 1 and Alpha Blended Splash Screen in Delphi Part 2

There is another great article regarding how to paint standard controls on windows which glass effect was enabled: Controls and the Desktop Window Manager. The basic idea is to use subclass the standard control, create a double paint buffer for the dc when handling WM_PAINT message and send WM_PRINTCLIENT message to edit control to force it to paint on buffered dc, then copy buffered dc to paint dc with opacity of 255 (virtually disables transparency). The subclass procedure also needs to handle EN_CHANGE notification and invalidate the window for force a paint. WTL 8.0/8.1 has a sample project Aero which provides a C++ class CAeroEdit which is using same idea.

Advertisements

Posted on January 19, 2010, in Uncategorized. Bookmark the permalink. Leave a comment.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: