Borderless Window with Drop Shadow
Asked Answered
N

1

9

I'm trying to achieve something like Visual Studio installer does with borderless window and drop shadow:

screenshot

I tried various options like CS_DROPSHADOW and DWM API, but as soon as I apply the WS_THICKFRAME style the shadow disappears.

This is my code for creating and centering a window:

RECT R = {0, 0, _clientWidth, _clientHeight};
AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false);
_mainWnd = CreateWindow(L"D3DWndClassName", _mainWndCaption.c_str(), WS_OVERLAPPEDWINDOW, 100, 100, R.right, R.bottom, nullptr, nullptr, _appInst, nullptr);

if(!_mainWnd){
    MessageBox(nullptr, L"CreateWindow FAILED", nullptr, 0);
    PostQuitMessage(0);
}

RECT rc;

GetWindowRect(_mainWnd, &rc);

LONG lStyle = GetWindowLong(_mainWnd, GWL_STYLE);
lStyle &= ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZE | WS_MAXIMIZE | WS_SYSMENU );
SetWindowLong(_mainWnd, GWL_STYLE, lStyle);


int xPos = (GetSystemMetrics(SM_CXSCREEN) - rc.right) / 2;
int yPos = (GetSystemMetrics(SM_CYSCREEN) - rc.bottom) / 2;

SetWindowPos(_mainWnd, 0, xPos, yPos, _clientWidth, _clientHeight, SWP_NOZORDER);

ShowWindow(_mainWnd, SW_SHOW);
UpdateWindow(_mainWnd);
Nonobedience answered 6/5, 2017 at 7:43 Comment(3)
There is no class style CS_SHADOW. What have you really tried?Logography
Sorry, I meant CS_DROPSHADOW. The effect of it is kinda different than the one I provided as an example. ComparisonNonobedience
Fire up the Spy++ tool and inspect the window whose behavior and attributes you want to mirror.Wentworth
H
20

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, DWLP_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, 
                static_cast<INT>( ps.rcPaint.left ), static_cast<INT>( ps.rcPaint.top ),
                static_cast<INT>( ps.rcPaint.right - ps.rcPaint.left ), static_cast<INT>( 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, DWLP_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
}
Hominid answered 6/5, 2017 at 10:28 Comment(12)
Thank you very much for your detailed explanation. Although... I managed to draw a shadow but now I'm getting this 1px frame which is green in my case. Any ideas how to get rid of that? ImageNonobedience
@wajsic Did you draw using GDI+?Hominid
No. I'm using DirectXNonobedience
@wajsic Can you reproduce the issue using my code sample? Otherwise it looks like your DirectX drawing code either doesn't fill the whole client area or the alpha channel is not opaque.Hominid
@Hominid yes, the same could be reproduced in your test application if you change the window background to e.g. Color{ 0x40, 0x40, 0x40 } instead of the white color you use.Misfire
Awesome man, I was looking for a solution to do that in java with JNA and with our comment I was able to get one.Subdivision
Your comment and code are not same. "Returning 0 from the message when wParam is TRUE....." but code in code you are returning TRUE(1). Am I missing something?Boatload
@VallabhPatade Thanks for the correction. I have changed the wording of the comment so it is hopefully more clear now.Hominid
Thanks for the explanation. I am trying to have Drop Shadow for my window which is not a dialog. There is no any other message result. <WinType>WL_MSGRESULT. How can I achieve this?Boatload
@VallabhPatade In this case you would replace return TRUE with returning the value that is the 3rd argument to SetWindowLong() and replace return FALSE with return DefWindowProc(hwnd, message, wParam, lParam).Hominid
It still get rid of the caption bar as well as drop shadow. I do want to expand the window into caption bar but keep the drop shadow.Boatload
@VallabhPatade I suggest you create a new question, post your current code and I will see what is wrong. Drop me a note here when you have done, as I'm not actively watching the winapi tag.Hominid

© 2022 - 2024 — McMap. All rights reserved.