Share via

How to Create custom shadows on windows

Omar Mohamed 160 Reputation points
2026-05-24T08:31:21.3266667+00:00

Hello,

I'm trying to create a GUI Framework where I'm using D2D with Direct Composition to Draw the window instead of relying on GDI and I managed to do that but I also want to add custom shadows to the window instead of the bad shadows of windows but I couldn't find anyway to do that I managed to increase the HWND size but it is not that good because many issues started occurring like bad Maximizing and another issues so could any one help me

void VX::VX_WindowsWindow::DrawWindow() {
	VX_D;

	d->directX11->ClearScreen({ 0.0f, 0.0f, 0.0f, 0.0f });

	float m = isMaximized == VX_TRUE ? 0.0f : 30.0f;

	if (isMaximized == VX_TRUE) {
		// Fill entire surface, no shadow, no rounded corners
		d->directX11->FillRoundedRectangle(
			{ 0.0f, 0.0f, float(width), float(height) },
			{ 0, 0 },
			{ 0.12f, 0.12f, 0.12f, 1.0f }
		);
	} else {
		d->directX11->FillRoundedRectangleWithShadow(
			{ m, m, float(width) - m * 2.0f, float(height) - m * 2.0f },
			{ 8, 8 },
			{ 0.12f, 0.12f, 0.12f, 1.0f },
			10.0f, 0.0f, 4.0f, 0.7f
		);
	}

	d->closeButtonWidth = 22;
	d->closeButtonHeight = 22;
	d->closeButtonX = float(width) - m - d->closeButtonWidth - 12;
	d->closeButtonY = m + 12;

	d->maximizeButtonWidth = 22;
	d->maximizeButtonHeight = 22;
	d->maximizeButtonX = d->closeButtonX - d->closeButtonWidth - 8;
	d->maximizeButtonY = m + 12;

	d->minimizeButtonWidth = 22;
	d->minimizeButtonHeight = 22;
	d->minimizeButtonX = d->maximizeButtonX - d->maximizeButtonWidth - 8;
	d->minimizeButtonY = m + 12;

	// Then buttons with shadow — now shadow is visible against grey
	d->directX11->FillRoundedRectangleWithShadow(
		{ d->closeButtonX, d->closeButtonY, d->closeButtonWidth, d->closeButtonHeight },
		{ 4, 4 },
		d->closeButtonColor,
		3.5f, 0.0f, 0.0f, 0.7f
	);

	d->directX11->FillRoundedRectangleWithShadow(
		{ d->maximizeButtonX, d->maximizeButtonY, d->maximizeButtonWidth, d->maximizeButtonHeight },
		{ 4, 4 },
		d->maximizeButtonColor,
		3.5f, 0.0f, 0.0f, 0.7f
	);

	d->directX11->FillRoundedRectangleWithShadow(
		{ d->minimizeButtonX, d->minimizeButtonY, d->minimizeButtonWidth, d->minimizeButtonHeight },
		{ 4, 4 },
		d->minimizeButtonColor,
		3.5f, 0.0f, 0.0f, 0.7f
	);

	VX_TextFormat format = {};
	format.FontFamily = "Arial";
	format.FontSize = 16.0f;
	format.Bold = true;
	format.Italic = false;
	format.paragraphAlignment = VX_ParagraphAlignment::Center;
	format.textAlignment = VX_TextAlignment::Center;

	d->directX11->RenderText(title, { m + 12, m + 12, 200, 32 }, format, VX_Colors::VX_White);
}

void VX::VX_WindowsWindow::SetSize(VX_UNSIGNEDINT width, VX_UNSIGNEDINT height) {
	VX_D;

	const unsigned int SHADOW_MARGIN = 30;

	this->width = width;
	this->height = height;

	if (d->hwnd) {

		VX_UNSIGNEDINT hwndWidth = width + SHADOW_MARGIN * 2;
		VX_UNSIGNEDINT hwndHeight = height + SHADOW_MARGIN * 2;

		VX_UNSIGNEDINT hwndX = x - SHADOW_MARGIN;
		VX_UNSIGNEDINT hwndY = y - SHADOW_MARGIN;

		SetWindowPos(d->hwnd, nullptr,
			hwndX, hwndY,
			hwndWidth, hwndHeight,
			SWP_NOZORDER
		);
	}
}
VX_BOOL VX::VX_WindowsWindow::Initialize()
{
	VX_D;

	if (!d->isClassRegistered)
	{
		WNDCLASSEXW wc = {};
		wc.cbSize = sizeof(WNDCLASSEXW);
		wc.style = 0;
		wc.lpfnWndProc = WindowProc;
		wc.hInstance = d->hInstance;
		wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
		wc.hbrBackground = nullptr;
		wc.lpszClassName = d->className;

		if (!RegisterClassExW(&wc))
			return VX_FALSE;

		d->isClassRegistered = VX_TRUE;
	}

	wchar_t* wTitle = ToWideChar(title);

	const unsigned int SHADOW_MARGIN = 30;

	VX_UNSIGNEDINT hwndWidth = width + SHADOW_MARGIN * 2;
	VX_UNSIGNEDINT hwndHeight = height + SHADOW_MARGIN * 2;

	VX_UNSIGNEDINT hwndX = x - SHADOW_MARGIN;
	VX_UNSIGNEDINT hwndY = y - SHADOW_MARGIN;

	d->hwnd = CreateWindowExW(
		WS_EX_NOREDIRECTIONBITMAP,
		d->className,
		wTitle,
		WS_POPUP,
		hwndX, hwndY,
		hwndWidth, hwndHeight,
		nullptr,
		nullptr,
		d->hInstance,
		d
	);

	delete[] wTitle;

	if (!d->hwnd)
		return VX_FALSE;

	VX_BOOL result = VX_FALSE;

	MARGINS margins = { 0, 0, 0, 0 };
	DwmExtendFrameIntoClientArea(d->hwnd, &margins);

	result = d->directX11->Initialize(this);

	isInitialized = result;

	return result;
}
case WM_NCCALCSIZE:
	if (wParam == TRUE)
	{
		result = 0;
		wasHandled = VX_TRUE;
	}
	break;
case WM_ERASEBKGND:
	result = 1;
	wasHandled = VX_TRUE;
	break;
case WM_NCHITTEST:
{
	POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
	ScreenToClient(hwnd, &pt);

	RECT rc;
	GetClientRect(hwnd, &rc);

	const int m = pThis->q_ptr->isMaximized == VX_TRUE ? 0.0f : 30.0f;
	const int border = 8;
	const int titleBarHeight = 32;

	if (pt.x < m || pt.y < m
		|| pt.x > rc.right - m || pt.y > rc.bottom - m)
	{
		result = HTTRANSPARENT;
		wasHandled = VX_TRUE;
		break;
	}

	bool left = pt.x < m + border;
	bool right = pt.x >= rc.right - m - border;
	bool top = pt.y < m + border;
	bool bottom = pt.y >= rc.bottom - m - border;

	if (left && top) { result = HTTOPLEFT; wasHandled = VX_TRUE; break; }
	if (right && top) { result = HTTOPRIGHT; wasHandled = VX_TRUE; break; }
	if (left && bottom) { result = HTBOTTOMLEFT; wasHandled = VX_TRUE; break; }
	if (right && bottom) { result = HTBOTTOMRIGHT; wasHandled = VX_TRUE; break; }
	if (top) { result = HTTOP; wasHandled = VX_TRUE; break; }
	if (left) { result = HTLEFT; wasHandled = VX_TRUE; break; }
	if (right) { result = HTRIGHT; wasHandled = VX_TRUE; break; }
	if (bottom) { result = HTBOTTOM; wasHandled = VX_TRUE; break; }

	if (pt.x >= pThis->closeButtonX
		&& pt.x <= pThis->closeButtonX + pThis->closeButtonWidth
		&& pt.y >= pThis->closeButtonY
		&& pt.y <= pThis->closeButtonY + pThis->closeButtonHeight)
	{
		result = HTCLIENT;
		wasHandled = VX_TRUE;
		break;
	}

	// Minimize button
	if (pt.x >= pThis->minimizeButtonX
		&& pt.x <= pThis->minimizeButtonX + pThis->minimizeButtonWidth
		&& pt.y >= pThis->minimizeButtonY
		&& pt.y <= pThis->minimizeButtonY + pThis->minimizeButtonHeight)
	{
		result = HTCLIENT;
		wasHandled = VX_TRUE;
		break;
	}

	// Maximize button
	if (pt.x >= pThis->maximizeButtonX
		&& pt.x <= pThis->maximizeButtonX + pThis->maximizeButtonWidth
		&& pt.y >= pThis->maximizeButtonY
		&& pt.y <= pThis->maximizeButtonY + pThis->maximizeButtonHeight)
	{
		result = HTCLIENT;
		wasHandled = VX_TRUE;
		break;
	}

	if (pt.y <= m + titleBarHeight)
	{
		result = HTCAPTION;
		wasHandled = VX_TRUE;
		break;
	}
	result = HTCLIENT;
	wasHandled = VX_TRUE;
	break;
}
case WM_GETMINMAXINFO:
{
	MINMAXINFO* mmi = (MINMAXINFO*)lParam;

	HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
	MONITORINFO mi = { sizeof(mi) };
	GetMonitorInfo(monitor, &mi);

	int m = 30;

	mmi->ptMaxPosition.x = mi.rcWork.left - m;
	mmi->ptMaxPosition.y = mi.rcWork.top - m;
	mmi->ptMaxSize.x = (mi.rcWork.right - mi.rcWork.left) + m * 2;
	mmi->ptMaxSize.y = (mi.rcWork.bottom - mi.rcWork.top) + m * 2;

	wasHandled = VX_TRUE;
	result = 0;
	break;
}
case WM_SIZING:
{
	RECT* rc = reinterpret_cast<RECT*>(lParam);
	unsigned int newWidth = rc->right - rc->left;
	unsigned int newHeight = rc->bottom - rc->top;

	if (pThis && pThis->q_ptr->isInitialized && newWidth > 0 && newHeight > 0)
	{
		pThis->q_ptr->width = newWidth;
		pThis->q_ptr->height = newHeight;

		pThis->directX11->Resize(newWidth, newHeight,
			[pThis]() { pThis->q_ptr->Render(); });
	}

	result = TRUE;
	wasHandled = VX_TRUE;
	break;
}
case WM_SIZE:
{
	pThis->q_ptr->isMaximized = (wParam == SIZE_MAXIMIZED);

	unsigned int newWidth = LOWORD(lParam);
	unsigned int newHeight = HIWORD(lParam);

	if (pThis && newWidth > 0 && newHeight > 0)
	{
		pThis->q_ptr->width = newWidth;
		pThis->q_ptr->height = newHeight;

		if (pThis->q_ptr->isInitialized)
		{
			pThis->directX11->Resize(newWidth, newHeight,
				[pThis]() { pThis->q_ptr->Render(); });

			pThis->q_ptr->window->SizeChanged(
				pThis->q_ptr->window, { newWidth, newHeight });
			pThis->isDirty = VX_TRUE;
		}
	}
	result = 0;
	wasHandled = VX_TRUE;
	break;
}
case WM_MOUSEMOVE:
{
	POINT p;
	GetCursorPos(&p);
	ScreenToClient(hwnd, &p);

	bool onCloseButton = p.x >= pThis->closeButtonX
		&& p.x <= pThis->closeButtonX + pThis->closeButtonWidth
		&& p.y >= pThis->closeButtonY
		&& p.y <= pThis->closeButtonY + pThis->closeButtonHeight;

	bool onMinimizeButton = p.x >= pThis->minimizeButtonX
		&& p.x <= pThis->minimizeButtonX + pThis->minimizeButtonWidth
		&& p.y >= pThis->minimizeButtonY
		&& p.y <= pThis->minimizeButtonY + pThis->minimizeButtonHeight;

	bool onMaximizeButton = p.x >= pThis->maximizeButtonX
		&& p.x <= pThis->maximizeButtonX + pThis->maximizeButtonWidth
		&& p.y >= pThis->maximizeButtonY
		&& p.y <= pThis->maximizeButtonY + pThis->maximizeButtonHeight;

	if (onCloseButton)
	{
		pThis->q_ptr->SetTitleBarButtons(VX_TitleBarButton::Close);
		pThis->minimizeButtonColor = VX_Colors::VX_Green;
		pThis->maximizeButtonColor = VX_Colors::VX_Yellow;
	}
	else if (onMinimizeButton)
	{
		pThis->q_ptr->SetTitleBarButtons(VX_TitleBarButton::Minimize);
		pThis->closeButtonColor = VX_Colors::VX_Red;
		pThis->maximizeButtonColor = VX_Colors::VX_Yellow;
	}
	else if (onMaximizeButton)
	{
		pThis->q_ptr->SetTitleBarButtons(VX_TitleBarButton::Maximize);
		pThis->closeButtonColor = VX_Colors::VX_Red;
		pThis->minimizeButtonColor = VX_Colors::VX_Green;
	}
	else
		pThis->q_ptr->SetTitleBarButtons(VX_TitleBarButton::None);

	result = 0;
	wasHandled = VX_TRUE;
	break;
}
case WM_LBUTTONDOWN:
{
	POINT p;
	GetCursorPos(&p);
	ScreenToClient(hwnd, &p);

	bool onCloseButton = p.x >= pThis->closeButtonX
		&& p.x <= pThis->closeButtonX + pThis->closeButtonWidth
		&& p.y >= pThis->closeButtonY
		&& p.y <= pThis->closeButtonY + pThis->closeButtonHeight;

	bool onMinimizeButton = p.x >= pThis->minimizeButtonX
		&& p.x <= pThis->minimizeButtonX + pThis->minimizeButtonWidth
		&& p.y >= pThis->minimizeButtonY
		&& p.y <= pThis->minimizeButtonY + pThis->minimizeButtonHeight;

	bool onMaximizeButton = p.x >= pThis->maximizeButtonX
		&& p.x <= pThis->maximizeButtonX + pThis->maximizeButtonWidth
		&& p.y >= pThis->maximizeButtonY
		&& p.y <= pThis->maximizeButtonY + pThis->maximizeButtonHeight;

	if (onCloseButton)
	{
		pThis->q_ptr->Close();
	}
	else if (onMinimizeButton)
	{
		pThis->q_ptr->Minimize();
	}
	else if (onMaximizeButton)
	{
		if (!pThis->q_ptr->isMaximized)
			pThis->q_ptr->Maximize();
		else
			pThis->q_ptr->Restore();
	}

	result = 0;
	wasHandled = VX_TRUE;
	break;
}

#include "VX_DirectX11.h"

#include "VX_DirectX11_P.h"

#include "VX_WindowsWindow.h"

#include "VX_WindowsApplication.h"

#include "VX_ToWideChar.h"

VX::VX_DirectX11::VX_DirectX11()

: VX_Object(*new VX_DirectX11Private)

{

VX_D;

d->d3dDevice = nullptr;

d->d3dDeviceContext = nullptr;

d->dxgiSwapChain = nullptr;

d->d2dFactory = nullptr;

d->d2dDevice = nullptr;

d->d2dDeviceContext = nullptr;

d->d2dFactory = nullptr;

d->d2dRenderTarget = nullptr;

d->dcompDevice = nullptr;

d->dcompTarget = nullptr;

d->dcompVisual = nullptr;

d->backBuffer = nullptr;

d->bgBrush = nullptr;

d->currentHeight = 0;

d->currentWidth = 0;

d->hasDrawingAtLeastOnce = VX_FALSE;

d->isDrawing = VX_FALSE;

d->isInitialized = VX_FALSE;

d->isInitialized = VX_FALSE;

d->isResizing = VX_FALSE;

d->titleBarHovered = VX_FALSE;

d->windowFocused = VX_FALSE;

d->hoveredButton = VX_TitleBarButton::None;

}

VX::VX_DirectX11::~VX_DirectX11()

{

}

VX_BOOL VX::VX_DirectX11::Initialize(VX_WindowsWindow* window)

{

VX_D;

if (FAILED(CreateD3DDeviceAndDeviceContext())) { return VX_FALSE; }

if (FAILED(CreateDXGISwapChain(window->width, window->height))) { return VX_FALSE; }

if (FAILED(CreateD2DFactory())) { return VX_FALSE; }

if (FAILED(CreateD2DDeviceAndDeviceContext())) { return VX_FALSE; }

if (FAILED(CreateD2DBitmap())) { return VX_FALSE; }

if (FAILED(CreateDWriteFactory())) { return VX_FALSE; }

d->d2dDeviceContext->CreateSolidColorBrush(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f), d->bgBrush.GetAddressOf());

if (FAILED(CreateDirectComposition(window->GetHandle()))) { return VX_FALSE; }

d->isInitialized = VX_TRUE;

return VX_TRUE;

}

void VX::VX_DirectX11::ClearScreen(VX_Color color)

{

VX_D;

auto& ctx = d->d2dDeviceContext;

ctx->Clear(D2D1::ColorF(color.r, color.g, color.b, color.a));

}

void VX::VX_DirectX11::FillRoundedRectangle(VX_Coordinates c, VX_Radius radius, VX_Color color)

{

VX_D;

if (d->isInitialized == VX_FALSE) return;



d->bgBrush->SetColor(D2D1::ColorF{ color.r, color.g, color.b, color.a });

D2D1_ROUNDED_RECT rect = {

	D2D1::RectF(

		c.x, c.y, c.x + c.w, c.y + c.h

	),

	radius.x,

	radius.y

};

d->d2dDeviceContext->FillRoundedRectangle(rect, d->bgBrush.Get());

}

void VX::VX_DirectX11::FillRoundedRectangleWithShadow(

VX_Coordinates c, VX_Radius radius, VX_Color color,

float shadowBlur, float shadowOffsetX, float shadowOffsetY, float shadowOpacity) {

VX_D;

if (!d->isInitialized) return;

auto& ctx = d->d2dDeviceContext;

// Extra space around the shape so blur doesn't get clipped

float margin = shadowBlur * 3.0f;

D2D1_SIZE_F size = D2D1::SizeF(c.w + margin * 2.0f, c.h + margin * 2.0f);

// 1. Create an offscreen render target — has its own BeginDraw/EndDraw

ComPtr<ID2D1BitmapRenderTarget> compatRT;

ctx->CreateCompatibleRenderTarget(

	&size, nullptr, nullptr,

	D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE,

	compatRT.GetAddressOf()

);

// 2. Draw shape into it (shifted by margin so blur has room)

ComPtr<ID2D1SolidColorBrush> tempBrush;

compatRT->CreateSolidColorBrush(

	D2D1::ColorF(color.r, color.g, color.b, color.a),

	tempBrush.GetAddressOf()

);

D2D1_ROUNDED_RECT localRect = {

	D2D1::RectF(margin, margin, margin + c.w, margin + c.h),

	radius.x, radius.y

};

compatRT->BeginDraw();

compatRT->Clear(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f));

compatRT->FillRoundedRectangle(localRect, tempBrush.Get());

compatRT->EndDraw();

// 3. Get bitmap from the offscreen target

ComPtr<ID2D1Bitmap> shapeBitmap;

compatRT->GetBitmap(shapeBitmap.GetAddressOf());

// 4. Apply shadow effect to the bitmap

ComPtr<ID2D1Effect> shadowEffect;

ctx->CreateEffect(CLSID_D2D1Shadow, shadowEffect.GetAddressOf());

shadowEffect->SetInput(0, shapeBitmap.Get());

shadowEffect->SetValue(D2D1_SHADOW_PROP_BLUR_STANDARD_DEVIATION, shadowBlur);

shadowEffect->SetValue(D2D1_SHADOW_PROP_COLOR,

	D2D1::Vector4F(0.0f, 0.0f, 0.0f, shadowOpacity));

// 5. Draw shadow behind (offset accounts for the margin we added)

D2D1_POINT_2F shadowPos = D2D1::Point2F(

	c.x - margin + shadowOffsetX,

	c.y - margin + shadowOffsetY

);

ctx->DrawImage(

	shadowEffect.Get(),

	&shadowPos,

	nullptr,

	D2D1_INTERPOLATION_MODE_LINEAR,

	D2D1_COMPOSITE_MODE_SOURCE_OVER

);

// 6. Draw original shape on top

D2D1_ROUNDED_RECT worldRect = {

	D2D1::RectF(c.x, c.y, c.x + c.w, c.y + c.h),

	radius.x, radius.y

};

d->bgBrush->SetColor(D2D1::ColorF(color.r, color.g, color.b, color.a));

ctx->FillRoundedRectangle(worldRect, d->bgBrush.Get());

}

void VX::VX_DirectX11::FillRoundedRectangleWithShadowSpecifiyShadowWidthAndHeight(VX_Coordinates c, VX_Radius radius, VX_Color color, VX_Coordinates shadowCoordinates, float shadowBlur, float shadowOffsetX, float shadowOffsetY, float shadowOpacity) {

VX_D;

if (!d->isInitialized) return;

auto& ctx = d->d2dDeviceContext;

// Extra space around the shape so blur doesn't get clipped

float margin = shadowBlur * 3.0f;

D2D1_SIZE_F size = D2D1::SizeF(shadowCoordinates.w + margin * 2.0f, shadowCoordinates.h + margin * 2.0f);

// 1. Create an offscreen render target — has its own BeginDraw/EndDraw

ComPtr<ID2D1BitmapRenderTarget> compatRT;

ctx->CreateCompatibleRenderTarget(

	&size, nullptr, nullptr,

	D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE,

	compatRT.GetAddressOf()

);

// 2. Draw shape into it (shifted by margin so blur has room)

ComPtr<ID2D1SolidColorBrush> tempBrush;

compatRT->CreateSolidColorBrush(

	D2D1::ColorF(color.r, color.g, color.b, color.a),

	tempBrush.GetAddressOf()

);

D2D1_ROUNDED_RECT localRect = {

	D2D1::RectF(margin, margin, margin + shadowCoordinates.w, margin + shadowCoordinates.h),

	radius.x, radius.y

};

compatRT->BeginDraw();

compatRT->Clear(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f));

compatRT->FillRoundedRectangle(localRect, tempBrush.Get());

compatRT->EndDraw();

// 3. Get bitmap from the offscreen target

ComPtr<ID2D1Bitmap> shapeBitmap;

compatRT->GetBitmap(shapeBitmap.GetAddressOf());

// 4. Apply shadow effect to the bitmap

ComPtr<ID2D1Effect> shadowEffect;

ctx->CreateEffect(CLSID_D2D1Shadow, shadowEffect.GetAddressOf());

shadowEffect->SetInput(0, shapeBitmap.Get());

shadowEffect->SetValue(D2D1_SHADOW_PROP_BLUR_STANDARD_DEVIATION, shadowBlur);

shadowEffect->SetValue(D2D1_SHADOW_PROP_COLOR,

	D2D1::Vector4F(0.0f, 0.0f, 0.0f, shadowOpacity));

// 5. Draw shadow behind (offset accounts for the margin we added)

D2D1_POINT_2F shadowPos = D2D1::Point2F(

	shadowCoordinates.x - margin + shadowOffsetX,

	shadowCoordinates.y - margin + shadowOffsetY

);

ctx->DrawImage(

	shadowEffect.Get(),

	&shadowPos,

	nullptr,

	D2D1_INTERPOLATION_MODE_LINEAR,

	D2D1_COMPOSITE_MODE_SOURCE_OVER

);

// 6. Draw original shape on top

D2D1_ROUNDED_RECT worldRect = {

	D2D1::RectF(c.x, c.y, c.x + c.w, c.y + c.h),

	radius.x, radius.y

};

d->bgBrush->SetColor(D2D1::ColorF(color.r, color.g, color.b, color.a));

ctx->FillRoundedRectangle(worldRect, d->bgBrush.Get());

}

void VX::VX_DirectX11::RenderText(const char* text, VX_Coordinates c, VX_TextFormat textFormat, VX_Color color)

{

VX_D;

const wchar_t* wcharText = ToWideChar(text);

const wchar_t* wcharTextFormatFontFamily = ToWideChar(textFormat.FontFamily);

ComPtr<IDWriteTextFormat> format = nullptr;

d->dwriteFactory->CreateTextFormat(

	wcharTextFormatFontFamily,

	NULL,

	textFormat.Bold ? DWRITE_FONT_WEIGHT_BOLD : DWRITE_FONT_WEIGHT_NORMAL,

	textFormat.Italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL,

	DWRITE_FONT_STRETCH_NORMAL,

	textFormat.FontSize,

	L"en-us",

	&format

);

delete wcharTextFormatFontFamily;

d->textBrush->SetColor(

	D2D1::ColorF(color.r, color.g, color.b, color.a)

);

D2D1_RECT_F layoutRect = D2D1::RectF(c.x, c.y, c.x + c.w, c.y + c.h);

d->d2dDeviceContext->DrawText(

	wcharText,

	wcslen(wcharText),

	format.Get(),

	layoutRect,

	d->textBrush.Get()

);

delete wcharText;



format.Reset();

}

}

Windows development | Windows API - Win32
0 comments No comments

Answer accepted by question author

Taki Ly (WICLOUD CORPORATION) 2,225 Reputation points Microsoft External Staff Moderator
2026-05-25T03:30:54.7433333+00:00

Hello @Omar Mohamed ,

The maximizing and hit-testing issues you're running into are likely caused by inflating the HWND size with SHADOW_MARGIN. When you tell Windows the window is physically larger than it visually appears, features like Aero Snap and Maximize tend to miscalculate the dimensions.

I've experimented a bit with a minimal setup using your DirectComposition method, and I found a reliable way to solve those issues. I would suggest completely abandoning the HWND size inflation. Instead, keep the HWND strictly matching the true resolution of your rendering surface, and simply render your main solid content slightly smaller (inward) to leave transparent space for the shadow.

A quick concept pattern of how I approached it. First, avoid adding margins during initialization and resizing:

void VX::VX_WindowsWindow::SetSize(VX_UNSIGNEDINT width, VX_UNSIGNEDINT height) {
    VX_D;
    this->width = width;
    this->height = height;
    if (d->hwnd) {
        // Just use the raw requested size, no SHADOW_MARGIN here
        SetWindowPos(d->hwnd, nullptr, x, y, width, height, SWP_NOZORDER);
    }
}

Then, inside your DrawWindow or Render logical block, shrink the rectangle so the shadow spills out within the bounds of your transparent render target:

void VX::VX_WindowsWindow::DrawWindow() {
    VX_D;
    // ... [Setup Direct2D Context and Clear background to transparent]
    float margin = isMaximized == VX_TRUE ? 0.0f : 30.0f; // E.g., your SHADOW_MARGIN
    // 1. Defind the rect shrunk by the margin
    D2D1_ROUNDED_RECT contentRect = D2D1::RoundedRect(
        D2D1::RectF(
            margin, 
            margin, 
            (float)width - margin, 
            (float)height - margin
        ),
        isMaximized ? 0.0f : 10.0f, 
        isMaximized ? 0.0f : 10.0f
    );
    if (isMaximized != VX_TRUE) {
        // 2. Generate the shadow based on 'contentRect' in an off-screen target
        // ... (Keep your CreateCompatibleRenderTarget & CLSID_D2D1Shadow logic)
        // 3. Draw the shadow effect (it will bleed into the transparent margin)
        d->directX11->DrawImage(shadowEffect.Get());
    }
    // 4. Finally, draw your solid window background strictly inside 'contentRect'
    d->directX11->FillRoundedRectangle(contentRect, d->bgBrush.Get());
    // ... [Render buttons and text inside the bounds of contentRect]
}

And in your WM_NCHITTEST, you can just return HTTRANSPARENT for any coordinates that fall in that outer 30px margin zone, so mouse clicks will gracefully pass through the drop shadow to the desktop behind it.

For more context on how Direct2D handles off-screen rendering and compositing which allows this technique to work smoothly, you can refer to these official documentations:

I hope this conceptual approach gives you a helpful direction to explore. If you found my response helpful, I would greatly appreciate it if you could follow this guide for your confirmation.

Thank you.

Was this answer helpful?

1 person found this answer helpful.

1 additional answer

Sort by: Most helpful
  1. AI answer

    2026-05-24T08:31:41.09+00:00

    Custom window shadows with Direct2D/DirectComposition are created the same way as any other blurred shape: draw the shape to an off‑screen render target, turn it into a bitmap, apply a blur/shadow effect, then composite that bitmap behind the window content. The code already does this correctly with ID2D1BitmapRenderTarget and CLSID_D2D1Shadow; the remaining issues are mostly window sizing and hit‑testing, not the shadow rendering itself.

    Key points based on the shown code:

    1. Shadow rendering pattern

    The FillRoundedRectangleWithShadow implementation follows the standard Direct2D pattern:

    1. Create a compatible render target:
         ComPtr<ID2D1BitmapRenderTarget> compatRT;
         ctx->CreateCompatibleRenderTarget(
             &size, nullptr, nullptr,
             D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE,
             compatRT.GetAddressOf());
      
      This uses ID2D1RenderTarget::CreateCompatibleRenderTarget, which is the correct API for drawing into an off‑screen bitmap render target.
    2. Draw the rounded rect into that RT, with a margin so blur is not clipped.
    3. Call compatRT->GetBitmap(...) to obtain an ID2D1Bitmap.
    4. Create a CLSID_D2D1Shadow effect, set the bitmap as input, configure blur and color.
    5. Draw the effect output with DrawImage at an offset so it appears as a shadow.
    6. Draw the original rounded rect on top using FillRoundedRectangle.

    This is exactly how Direct2D’s bitmap/effect pipeline is intended to be used. The use of CreateCompatibleRenderTarget and GetBitmap matches the documented pattern for generating a bitmap and then using it as an effect input.

    1. Avoid resizing the HWND for the shadow

    The main functional issues (bad maximize, sizing, hit‑test) come from inflating the HWND by SHADOW_MARGIN and then trying to compensate in WM_NCHITTEST, WM_GETMINMAXINFO, etc.

    A simpler and more robust approach is:

    • Keep the HWND size equal to the logical client area (no extra margins for the shadow).
    • Render the shadow fully inside the client area using Direct2D, as is already done in DrawWindow.
    • When maximized, skip the shadow and rounded corners (the code already does this with isMaximized and FillRoundedRectangle).

    That means:

    • Remove the SHADOW_MARGIN adjustments in Initialize and SetSize:
      • Create the window at (x, y, width, height).
      • On resize, call Resize(width, height, ...) with the actual client size.
    • In DrawWindow, keep using a margin m (e.g. 30) to position the inner rounded rect and its shadow, but that margin is purely visual, not part of the HWND size.
    1. Hit‑testing and resize borders

    With the shadow drawn inside the client area, hit‑testing becomes straightforward:

    • WM_NCCALCSIZE returning 0 is correct for a borderless window.
    • In WM_NCHITTEST, treat the outer m pixels as non‑interactive shadow area if desired, or allow resize from that region.
    • For example, to allow resize from the visual border:
      • Use border (e.g. 8) inside the client rect to return HTLEFT/HTRIGHT/HTTOP/HTBOTTOM.
      • Keep HTCAPTION for the top area above the buttons.

    The existing WM_NCHITTEST logic already does this; once the HWND is no longer inflated by SHADOW_MARGIN, the calculations become consistent with what is drawn.

    1. Direct2D APIs involved

    The shadow implementation uses these Direct2D concepts:

    • ID2D1RenderTarget::CreateCompatibleRenderTarget to create an ID2D1BitmapRenderTarget for off‑screen drawing.
    • ID2D1BitmapRenderTarget::GetBitmap to obtain an ID2D1Bitmap from the off‑screen content.
    • ID2D1RenderTarget::DrawBitmap or ID2D1DeviceContext::DrawImage to composite bitmaps/effects into the main render target.

    DrawBitmap is documented as drawing a bitmap scaled into a destination rectangle and optionally from a source rectangle:

    void DrawBitmap(
      ID2D1Bitmap *bitmap,
      const D2D1_RECT_F &destinationRectangle,
      FLOAT opacity,
      D2D1_BITMAP_INTERPOLATION_MODE interpolationMode,
      const D2D1_RECT_F &sourceRectangle
    );
    

    This is useful if a custom blur implementation is preferred over the built‑in CLSID_D2D1Shadow effect (for example, pre‑blurring into a bitmap and then drawing it with DrawBitmap).

    1. Text and other content

    Text rendering is already correctly using DirectWrite with Direct2D via DrawText on the ID2D1DeviceContext, which is the recommended way to render text in a Direct2D‑based UI.

    Summary of changes to stabilize custom shadows:

    • Do not enlarge the HWND for the shadow; keep HWND size equal to the logical client area.
    • Draw the shadow entirely inside the client area using the existing FillRoundedRectangleWithShadow implementation.
    • Keep WM_NCCALCSIZE returning 0 for a borderless window and simplify WM_GETMINMAXINFO/WM_NCHITTEST to work with the real client size instead of a shadow‑inflated size.
    • Continue using CreateCompatibleRenderTarget + GetBitmap + shadow effect as implemented; this is the correct Direct2D pattern.

    References:

    AI-generated content may be incorrect. Read our transparency notes for more information.

    Was this answer helpful?

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.