You can create this effect by using a combination of DwmExtendFrameIntoClientArea()
and setting 0
as the message result of WM_NCCALCSIZE
if wParam is TRUE
. Detailed steps below.
- Window style should be such that normally the whole frame would be shown (
WS_CAPTION|WS_POPUP
works well for me), but don't include any of WS_MINIMIZE
, WS_MAXIMIZE
, WS_SYSMENU
.
- Call
DwmExtendFrameIntoClientArea()
with MARGINS{0,0,0,1}
. We don't really want a transparent frame, so setting the bottom margin only is enough.
- Call
SetWindowPos(hWnd, nullptr, 0, 0, 0, 0, SWP_NOZORDER|SWP_NOOWNERZORDER|SWP_NOMOVE|SWP_NOSIZE|SWP_FRAMECHANGED)
to let the system recalculate NC area.
- Return 0 from
WM_NCCALCSIZE
if wParam is TRUE
. This has the effect of extending the client area to the window size including frame, but excluding the shadow. See remarks section of the documentation.
- In
WM_PAINT
draw your frame and content area as you like but make sure to use an opaque alpha channel (value of 255) for the margin area defined by the DwmExtendFrameIntoClientArea()
call. Otherwise part of regular frame would be visible in this area. You may use GDI+ for that as most regular GDI functions ignore alpha channel. BitBlt()
with a 32bpp source bitmap containing an opaque alpha channel also works.
- You may handle
WM_NCHITTEST
if you want a resizable window.
The effect of all this is, that you paint "over" the regular window frame which is now inside the client area due to the DWM calls, but keep the regular window shadow. Don't worry the "paint over" doesn't create any flickering, even if you make the window resizable.
You can put any standard or user-defined controls into this window. Just make sure child controls don't overlap the margin defined by the DwmExtendFrameIntoClientArea()
call because most GDI-based controls ignore the alpha channel.
Here is a minimal, self-contained example application:
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <dwmapi.h>
#include <unknwn.h>
#include <gdiplus.h>
#pragma comment( lib, "dwmapi" )
#pragma comment( lib, "gdiplus" )
namespace gdip = Gdiplus;
INT_PTR CALLBACK MyDialogProc( HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam );
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
// Initialize GDI+
gdip::GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdipToken = 0;
gdip::GdiplusStartup( &gdipToken, &gdiplusStartupInput, nullptr );
struct MyDialog : DLGTEMPLATE {
WORD dummy[3] = { 0 }; // unused menu, class and title
}
dlg;
dlg.style = WS_POPUP|WS_CAPTION|DS_CENTER;
dlg.dwExtendedStyle = 0;
dlg.cdit = 0; // no controls in template
dlg.x = 0;
dlg.y = 0;
dlg.cx = 300; // width in dialog units
dlg.cy = 200; // height in dialog units
DialogBoxIndirectW( hInstance, &dlg, nullptr, MyDialogProc );
gdip::GdiplusShutdown( gdipToken );
return 0;
}
INT_PTR CALLBACK MyDialogProc( HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam )
{
switch( message )
{
case WM_INITDIALOG:
{
SetWindowTextW( hDlg, L"Borderless Window with Shadow" );
// This plays together with WM_NCALCSIZE.
MARGINS m{ 0, 0, 0, 1 };
DwmExtendFrameIntoClientArea( hDlg, &m );
// Force the system to recalculate NC area (making it send WM_NCCALCSIZE).
SetWindowPos( hDlg, nullptr, 0, 0, 0, 0,
SWP_NOZORDER|SWP_NOOWNERZORDER|SWP_NOMOVE|SWP_NOSIZE|SWP_FRAMECHANGED);
return TRUE;
}
case WM_NCCALCSIZE:
{
// Setting 0 as the message result when wParam is TRUE removes the
// standard frame, but keeps the window shadow.
if( wParam == TRUE )
{
SetWindowLong( hDlg, DWL_MSGRESULT, 0 );
return TRUE;
}
return FALSE;
}
case WM_PAINT:
{
PAINTSTRUCT ps{ 0 };
HDC hdc = BeginPaint( hDlg, &ps );
// Draw with GDI+ to make sure the alpha channel is opaque.
gdip::Graphics gfx{ hdc };
gdip::SolidBrush brush{ gdip::Color{ 255, 255, 255 } };
gfx.FillRectangle( &brush, ps.rcPaint.left, ps.rcPaint.top,
ps.rcPaint.right - ps.rcPaint.left, ps.rcPaint.bottom - ps.rcPaint.top );
EndPaint( hDlg, &ps );
return TRUE;
}
case WM_NCHITTEST:
{
// Setting HTCAPTION as the message result allows the user to move
// the window around by clicking anywhere within the window.
// Depending on the mouse coordinates passed in LPARAM, you may
// set other values to enable resizing.
SetWindowLong( hDlg, DWL_MSGRESULT, HTCAPTION );
return TRUE;
}
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
}