Gabriel Santi
Gabriel Santi

Reputation: 83

Control a window's alpha blending and events transparency in Win API and Direct 2D (circular window example)

I'm trying to create a circular window using the Win32 API and Direct2D in C++. From what I understand, the only way to achieve this is by creating a WS_POPUP window and handling all the drawing manually. My goal is for the window to include a custom shadow around it. This shadow should be painted with transparency so that the desktop or other windows remain visible behind it. Additionally, it must be transparent to mouse events, allowing users to interact with other applications through the shadowed area.

I found that using the WS_EX_LAYERED style along with SetLayeredWindowAttributes() allows mouse events to be detected only in areas painted with Direct2D, effectively enabling a circular window. However, painting with transparency (alpha values) does not seem to work as expected—rather than blending with the background behind the window, it blends with a solid black background. That said, I was able to make the window transparent to mouse events by adding the WS_EX_TRANSPARENT flag.

Result using a layered window:

image

Another approach I considered was avoiding WS_EX_LAYERED and instead using DwmExtendFrameIntoClientArea(). This method allows proper transparency blending, but WS_EX_TRANSPARENT no longer works, meaning mouse events are not ignored in transparent areas. Additionally, I noticed some flickering when moving the window from outside the screen back into view, which makes this approach less ideal.

Result using DWM. I did not find a way to make the mouse events pass through:

image

Here is the flicker effect that I discovered and I don't like:

image

Here is my full code, the variable isDWM is used to switch from one approach to the other:

#include <dwmapi.h>
#include <windows.h>
#include <iostream>
#include <d2d1_1.h>

#pragma once
#pragma comment(lib, "d2d1.lib")
#pragma comment(lib, "dwrite.lib")
#pragma comment(lib, "Dwmapi.lib")

#define INIT_X_RATIO    0.7f
#define INIT_Y_RATIO    0.7f
#define BORDER_SIZE     2       // pixels
#define SHADOW_OFF      100     // pixels

ID2D1Factory* fact;
ID2D1HwndRenderTarget* renderTarget;
ID2D1SolidColorBrush* brush;
ID2D1RadialGradientBrush* pRadialGradientBrush;

D2D1::ColorF ColorRefToColorF(COLORREF color);
void paint_frame(HWND hwnd);
bool init_d2d1(HWND hwnd);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
void CreateConsole();

int x, y, w, h;
bool isDWM = false; // Set to true to use DWM and see semi-transparent painting
LPCTSTR cursors[] = {IDC_SIZENESW, IDC_SIZENWSE};

D2D1::ColorF ColorRefToColorF(COLORREF color){
    float r = GetRValue(color) / 255.0f;
    float g = GetGValue(color) / 255.0f;
    float b = GetBValue(color) / 255.0f;
    return D2D1::ColorF(r, g, b);
}

void paint_frame(HWND hwnd) {
    UINT dpi = GetDpiForSystem();
    float ratio = (float)dpi / 96.0f;
    float shadow_off = SHADOW_OFF / ratio / 2.0f;
    D2D1_SIZE_F rtSize = renderTarget->GetSize();
    renderTarget->BeginDraw();
    renderTarget->Clear(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f));
    
    // Create shadow circle
    float cx = rtSize.width / 2.0f;
    float cy = rtSize.height / 2.0f;
    float rad = min(rtSize.width, rtSize.height) / 2.0f - shadow_off;
    D2D1_ELLIPSE circle = D2D1::Ellipse(
        D2D1::Point2F(cx, cy + shadow_off),
        rad,
        rad   
    );
    brush->SetColor(D2D1::ColorF(0.01f, 0.01f, 0.01f, 0.5f));
    renderTarget->FillEllipse(circle, brush);

    // Paint border circle
    circle.point = D2D1::Point2F(cx, cy - shadow_off);
    COLORREF borderColor = GetSysColor(COLOR_ACTIVEBORDER); // Get system border color
    D2D1::ColorF d2dColor = ColorRefToColorF(borderColor);
    brush->SetColor(d2dColor);
    renderTarget->FillEllipse(circle, brush);

    // Paint Content circle
    rad -= BORDER_SIZE / ratio;
    circle.radiusX = rad;
    circle.radiusY = rad;
    brush->SetColor(D2D1::ColorF(0.18f, 0.18f, 0.19f));
    renderTarget->FillEllipse(circle, brush);

    renderTarget->EndDraw();
}

bool init_d2d1(HWND hwnd) {
    // Create factory
    HRESULT hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &fact);
    if (hr != S_OK) return false;
    RECT r;
    GetClientRect(hwnd, &r);

    // Create render target
    hr = fact->CreateHwndRenderTarget(
        D2D1::RenderTargetProperties(D2D1_RENDER_TARGET_TYPE_DEFAULT, D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED)),
        D2D1::HwndRenderTargetProperties(hwnd, D2D1::SizeU(r.right, r.bottom)),
        &renderTarget
    );

    if (hr != S_OK) return false;
    renderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);

    // Create brush
    hr = renderTarget->CreateSolidColorBrush(D2D1::ColorF(0.0f, 0.0f, 0.0f, 1.0f), &brush);
    if (hr != S_OK) return false;

    return true;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
    case WM_DESTROY: {
        PostQuitMessage(0);
        return 0;
    }

    case WM_PAINT: {
        paint_frame(hwnd);
        ValidateRect(hwnd, NULL);
        break;
    }

    case WM_NCHITTEST: 
        return HTCAPTION;  // Enable dragging of the window

    case WM_NCMOUSEMOVE: {
        float xPos = (float) LOWORD(lParam);
        float yPos = (float) HIWORD(lParam);
        std::cout << "Mouse Position: X = " << xPos << ", Y = " << yPos << std::endl;
        break;
    }

    default:
        return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
}

void CreateConsole() {
    AllocConsole();                                     // Allocate a new console
    freopen_s((FILE**)stdout, "CONOUT$", "w", stdout);  // Redirect stdout to console
    freopen_s((FILE**)stdin, "CONIN$", "r", stdin);     // Redirect stdin to console
    freopen_s((FILE**)stderr, "CONOUT$", "w", stderr);  // Redirect stderr to console
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    int screen_width = GetSystemMetrics(SM_CXSCREEN);
    int screen_height = GetSystemMetrics(SM_CYSCREEN);
    w = (int)((double)screen_width * INIT_X_RATIO);
    h = (int)((double)screen_height * INIT_Y_RATIO);

    // Assure even number
    w += w % 2 == 1;
    h += h % 2 == 1;

    // Square window
    w = min(w, h);
    h = w;
    x = (screen_width - w) / 2;
    y = (screen_height - h) / 2;

    WNDCLASS wc = {};
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = L"SampleWindowClass";
    wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    RegisterClass(&wc);

    HWND hwnd = CreateWindowEx(
        0,
        L"SampleWindowClass",
        L"Classic Windows App",
        WS_POPUP,
        x, y, w, h,
        nullptr, nullptr, hInstance, nullptr);

    if (!hwnd) return 0;
    if (!init_d2d1(hwnd)) return 0;
    CreateConsole();

    if (isDWM) {
        MARGINS margins = { -1 };
        DwmExtendFrameIntoClientArea(hwnd, &margins);
    }
    else {
        // Set Layered window (transparent)
        SetWindowLong(hwnd, GWL_EXSTYLE, GetWindowLong(hwnd, GWL_EXSTYLE) | WS_EX_LAYERED);
        SetLayeredWindowAttributes(hwnd, RGB(0, 0, 0), 0, LWA_COLORKEY);
    }

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    MSG msg = {};
    while (GetMessage(&msg, nullptr, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return static_cast<int>(msg.wParam);
}

Note that I want to use DirectX for the painting (No GDI/GDI+).

What would be the best way to achieve my goal?

Upvotes: 4

Views: 159

Answers (2)

Gabriel Santi
Gabriel Santi

Reputation: 83

I found a way to paint with alpha blending in DirectX while having transparency to events by using Direct Composition (thanks to this repo).

  1. Create a parent window with the WS_EX_TRANSPARENT style and paint the shadow only.

  2. Create a child window, set its region with CreateEllipticRgn() and SetWindowRgn() and paint the border and content. I made sure that the elliptic region is slightly bigger than the painted content so that the circle benefits from anti-aliasing. This child window handles all the events in its procedure.

  3. Since the child window handles the events, dragging the window does not move the parent automatically with it. So when the child receives a WM_MOVE message, it sets its parent's new position using SetWindowPos().

I played around with a radial brush for the shadow effect. The event transparency seems to work:

Click-through is working fine

However, moving a parent window through its child is not ideal: there is a slight delay before the parent window repaints. To see this, I painted on the shadow window a red circle about the size of the child window. If the two windows were correctly "glued" together, this red circle should never be visible.

Moving delay of the parent window.

My full code:

#pragma once

#include <windows.h>
#pragma comment(lib, "user32.lib")
#include <wrl.h>
#include <dxgi1_3.h>
#include <d3d11_2.h>
#include <d2d1_2.h>
#include <d2d1_2helper.h>
#include <dcomp.h>
#pragma comment(lib, "dxgi")
#pragma comment(lib, "d3d11")
#pragma comment(lib, "d2d1")
#pragma comment(lib, "dcomp")
#include <iostream>
// https://docs.microsoft.com/en-us/archive/msdn-magazine/2014/june/windows-with-c-high-performance-window-layering-using-the-windows-composition-engine

using namespace Microsoft::WRL;
#define INIT_X_RATIO        0.9f
#define INIT_Y_RATIO        0.9f

#define BORDER_SIZE         1.0f
#define PADDING_SIZE        8.0f
#define SHADOW_SIZE         100

#define CONTENT_COLOR       0.18f, 0.18f, 0.19f, 1.0f
#define BORDER_COLOR        1.0f, 1.0f, 1.0f, 1.0f
#define GRADIENT_COLOR_1    1.0f, 1.0f, 1.0f, 0.7f
#define GRADIENT_COLOR_2    0.0f, 0.0f, 1.0f, 0.0f
#define BEHIND_COLOR        1.0f, 0.0f, 0.0f, 1.0f


int x, y, w, h;
HINSTANCE instance;
LPCWSTR pClass = L"pClass";
LPCWSTR pName = L"Circular window";
LPCWSTR cClass = L"cClass";
LPCWSTR cName = L"Circular content";

struct ComException{
    HRESULT result;
    ComException(HRESULT const value) :
        result(value)
    {}
};

struct DirectRessources {
    ComPtr<ID3D11Device> direct3dDevice;
    ComPtr<IDXGIDevice> dxgiDevice;
    ComPtr<IDXGIFactory2> dxFactory;
    ComPtr<IDXGISwapChain1> swapChain;
    ComPtr<ID2D1Factory2> d2Factory;
    ComPtr<ID2D1Device1> d2Device;
    ComPtr<ID2D1DeviceContext> dc;
    ComPtr<IDXGISurface2> surface;
    ComPtr<ID2D1Bitmap1> bitmap;
    ComPtr<IDCompositionDevice> dcompDevice;
    ComPtr<IDCompositionTarget> target;
    ComPtr<IDCompositionVisual> visual;
    ComPtr<ID2D1SolidColorBrush> brush;
    ID2D1GradientStopCollection* pGradientStopCollection;
    ComPtr<ID2D1RadialGradientBrush> radBrush;
};

DirectRessources pR; // Parent ressources
DirectRessources cR; // Child ressources

void HR(HRESULT const result){
    if (S_OK != result)
    {
        throw ComException(result);
    }
}

D2D1::ColorF ColorRefToColorF(COLORREF color) {
    float r = GetRValue(color) / 255.0f;
    float g = GetGValue(color) / 255.0f;
    float b = GetBValue(color) / 255.0f;
    return D2D1::ColorF(r, g, b);
}

void InitDirect2D(HWND window, DirectRessources* res){
    HR(D3D11CreateDevice(nullptr,    // Adapter
        D3D_DRIVER_TYPE_HARDWARE,
        nullptr,    // Module
        D3D11_CREATE_DEVICE_BGRA_SUPPORT,
        nullptr, 0, // Highest available feature level
        D3D11_SDK_VERSION,
        &res->direct3dDevice,
        nullptr,    // Actual feature level
        nullptr));  // Device context

    HR(res->direct3dDevice.As(&res->dxgiDevice));

    HR(CreateDXGIFactory2(
        DXGI_CREATE_FACTORY_DEBUG,
        __uuidof(res->dxFactory),
        reinterpret_cast<void**>(res->dxFactory.GetAddressOf())));

    DXGI_SWAP_CHAIN_DESC1 description = {};
    description.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
    description.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    description.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
    description.BufferCount = 2;
    description.SampleDesc.Count = 1;
    description.AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED;

    RECT rect = {};
    GetClientRect(window, &rect);
    description.Width = rect.right - rect.left;
    description.Height = rect.bottom - rect.top;

    HR(res->dxFactory->CreateSwapChainForComposition(res->dxgiDevice.Get(),
        &description,
        nullptr, // Don’t restrict
        res->swapChain.GetAddressOf()));

    // Create a single-threaded Direct2D factory with debugging information
    D2D1_FACTORY_OPTIONS const options = { D2D1_DEBUG_LEVEL_INFORMATION };
    HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
        options,
        res->d2Factory.GetAddressOf()));

    // Create the Direct2D device that links back to the Direct3D device
    HR(res->d2Factory->CreateDevice(res->dxgiDevice.Get(),
        res->d2Device.GetAddressOf()));

    // Create the Direct2D device context that is the actual render target and exposes drawing commands
    HR(res->d2Device->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
        res->dc.GetAddressOf()));

    // Retrieve the swap chain's back buffer
    HR(res->swapChain->GetBuffer(
        0, // index
        __uuidof(res->surface),
        reinterpret_cast<void**>(res->surface.GetAddressOf())));

    // Create a Direct2D bitmap that points to the swap chain surface
    D2D1_BITMAP_PROPERTIES1 properties = {};
    properties.pixelFormat.alphaMode = D2D1_ALPHA_MODE_PREMULTIPLIED;
    properties.pixelFormat.format = DXGI_FORMAT_B8G8R8A8_UNORM;
    properties.bitmapOptions = D2D1_BITMAP_OPTIONS_TARGET |
        D2D1_BITMAP_OPTIONS_CANNOT_DRAW;
    HR(res->dc->CreateBitmapFromDxgiSurface(res->surface.Get(),
        properties,
        res->bitmap.GetAddressOf()));
    // Point the device context to the bitmap for rendering
    res->dc->SetTarget(res->bitmap.Get());

    HR(DCompositionCreateDevice(
        res->dxgiDevice.Get(),
        __uuidof(res->dcompDevice),
        reinterpret_cast<void**>(res->dcompDevice.GetAddressOf())));


    HR(res->dcompDevice->CreateTargetForHwnd(window,
        true, // Top most
        res->target.GetAddressOf()));

    HR(res->dcompDevice->CreateVisual(res->visual.GetAddressOf()));
    HR(res->visual->SetContent(res->swapChain.Get()));
    HR(res->target->SetRoot(res->visual.Get()));
    HR(res->dcompDevice->Commit());

    D2D1_COLOR_F const brushColor = D2D1::ColorF(0.18f,  // red
                                                 0.55f,  // green
                                                 0.34f,  // blue
                                                 0.75f); // alpha
    HR(res->dc->CreateSolidColorBrush(brushColor,
        res->brush.GetAddressOf()));
}

void paint_content() {
    cR.dc->BeginDraw();
    cR.dc->Clear(D2D1::ColorF(0.0f, 0.0f, 1.0f, 0.0f));

    D2D1_SIZE_F rtSize = cR.dc->GetSize();
    UINT dpi = GetDpiForSystem();
    float ratio = (float)dpi / 96.0f;
    float rad = min(rtSize.width, rtSize.height) / 2.0f - PADDING_SIZE/ratio;

    cR.brush->SetColor(D2D1::ColorF(BORDER_COLOR)); // Border color
    D2D1_POINT_2F ellipseCenter = D2D1::Point2F(rtSize.width / 2.0f, rtSize.height / 2.0f);
    D2D1_ELLIPSE ellipse = D2D1::Ellipse(ellipseCenter, rad, rad);
    cR.dc->FillEllipse(ellipse, cR.brush.Get());

    cR.brush->SetColor(D2D1::ColorF(CONTENT_COLOR)); // Content background color
    rad -= BORDER_SIZE / ratio;
    ellipse.radiusX = rad;
    ellipse.radiusY = rad;
    cR.dc->FillEllipse(ellipse, cR.brush.Get());

    HR(cR.dc->EndDraw());

    // Make the swap chain available to the composition engine
    HR(cR.swapChain->Present(1,   // sync
        0)); // flags
}

void CreateConsole() {
    AllocConsole();                                     // Allocate a new console
    freopen_s((FILE**)stdout, "CONOUT$", "w", stdout);  // Redirect stdout to console
    freopen_s((FILE**)stdin, "CONIN$", "r", stdin);     // Redirect stdin to console
    freopen_s((FILE**)stderr, "CONOUT$", "w", stderr);  // Redirect stderr to console
}

void paint_shadow() {
    pR.dc->BeginDraw();
    pR.dc->Clear(D2D1::ColorF(1.0f, 1.0f, 1.f, 0.0f));

    D2D1_SIZE_F rtSize = pR.dc->GetSize();
    UINT dpi = GetDpiForSystem();
    float ratio = (float)dpi / 96.0f;
    float rad = min(rtSize.width, rtSize.height) / 2.0f;
    D2D1_POINT_2F center = D2D1::Point2F(rtSize.width / 2.0f, rtSize.height / 2.0f);
    float start = 1.0f - ((SHADOW_SIZE + PADDING_SIZE) / (rad*ratio));  

    D2D1_GRADIENT_STOP gradientStops[] = {
    { start, D2D1::ColorF(GRADIENT_COLOR_1) },      // Start color
    { 1.0f, D2D1::ColorF(GRADIENT_COLOR_2) }        // End color
    };
    pR.dc->CreateGradientStopCollection(
        gradientStops,
        2,  // Number of stops aka size of gradientStops
        D2D1_GAMMA_2_2,
        D2D1_EXTEND_MODE_CLAMP,
        &pR.pGradientStopCollection
    );

    D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES radialProps =
        D2D1::RadialGradientBrushProperties(
            center,  // Gradient center
            D2D1::Point2F(0, 0),  // Gradient offset
            rad,                    // radX
            rad                     // radY
        );

    pR.dc->CreateRadialGradientBrush(
        radialProps,
        pR.pGradientStopCollection,
        &pR.radBrush
    );
    
    D2D1_POINT_2F ellipseCenter = D2D1::Point2F(rtSize.width /2.0f, rtSize.height / 2.0f);
    D2D1_ELLIPSE ellipse = D2D1::Ellipse(ellipseCenter, rad, rad); // y radius
    pR.dc->FillEllipse(ellipse, pR.radBrush.Get());

    rad -= (SHADOW_SIZE + BORDER_SIZE + PADDING_SIZE) / ratio;
    ellipse.radiusX = rad;
    ellipse.radiusY = rad;
    pR.brush->SetColor(D2D1::ColorF(BEHIND_COLOR));
    pR.dc->FillEllipse(ellipse, pR.brush.Get());

    HR(pR.dc->EndDraw());

    // Make the swap chain available to the composition engine
    HR(pR.swapChain->Present(1,   // sync
                          0)); // flags
}
LRESULT CALLBACK ChildProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg)
    {

    case WM_NCHITTEST: {
        return HTCAPTION;
    }

    case WM_PAINT: {
        paint_content();
        break;
    }

    case WM_DESTROY: {
        PostQuitMessage(0);
        return 0;
    }
    case WM_SIZE: {
        return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }

    case WM_MOVE: {
        int xPos = (int)((short)LOWORD(lParam)); 
        int yPos = (int)((short)HIWORD(lParam)); 
        HWND parent = GetParent(hwnd);
        x = xPos - SHADOW_SIZE;
        y = yPos - SHADOW_SIZE;

        RECT r;
        GetWindowRect(hwnd, &r);
        std::cout << "Rect upper left corner (" << r.left << ", " << r.top << ")" << std::endl;
        std::cout << "Rect lower right corner (" << r.right << ", " << r.bottom << ")" << std::endl;
        std::cout << "Dimensions (" << r.right - r.left << ", " << r.bottom - r.top << ")" << std::endl;
        RECT pr;
        GetWindowRect(parent, &pr);
        std::cout << "Rect upper left corner (" << pr.left << ", " << pr.top << ")" << std::endl;
        std::cout << "Rect lower right corner (" << pr.right << ", " << pr.bottom << ")" << std::endl;
        std::cout << "Dimensions (" << pr.right - pr.left << ", " << pr.bottom - pr.top << ")" << std::endl;

        std::cout << "Offset (" << r.left - pr.left << ", " << r.top - pr.top << ")\n" << std::endl;
        SetWindowPos(parent, 0, x, y, w, h, TRUE);
        return 1;
    }
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg)
    {
    case WM_CREATE: {
        int cx = x + SHADOW_SIZE;
        int cy = y + SHADOW_SIZE;
        int cw = w - 2*SHADOW_SIZE;
        int ch = h - 2*SHADOW_SIZE;

        HWND child = CreateWindowEx(0,
            cClass, cName,
            WS_VISIBLE | WS_CHILD | WS_POPUP , // | WS_MAXIMIZEBOX | WS_THICKFRAME | WS_MINIMIZEBOX | WS_CAPTION,
            cx, cy,
            cw, ch,
            hwnd, nullptr, nullptr, nullptr);;

        RECT r;
        GetWindowRect(child, &r);
        HRGN hRgn = CreateEllipticRgn(0, 0, r.right - r.left, r.bottom - r.top);
        SetWindowRgn(child, hRgn, TRUE);

        InitDirect2D(child, &cR);
    }

    case WM_NCHITTEST:{
        return 0;
    }

    case WM_PAINT:{
        paint_shadow();
        break;
    }
    case WM_DESTROY:{
        PostQuitMessage(0);
        return 0;
    }
    
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

int __stdcall wWinMain(HINSTANCE module, HINSTANCE, PWSTR, int){    
    CreateConsole();
    instance = module;
    int screen_width = GetSystemMetrics(SM_CXSCREEN);
    int screen_height = GetSystemMetrics(SM_CYSCREEN);
    w = (int)((double)screen_width * INIT_X_RATIO);
    h = (int)((double)screen_height * INIT_Y_RATIO);

    // Assure even number
    w += w % 2 == 1;
    h += h % 2 == 1;

    // Square window
    w = min(w, h);
    h = w;
    x = (screen_width - w) / 2;
    y = (screen_height - h) / 2;

    WNDCLASS wc = {};
    wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wc.hInstance = instance;
    wc.lpszClassName = pClass;
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = WindowProc;
    RegisterClass(&wc);

    wc = {};
    wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wc.hInstance = instance;
    wc.lpszClassName = cClass;
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = ChildProc;
    RegisterClass(&wc);

    DWORD overlay_style = WS_EX_TRANSPARENT | WS_EX_LAYERED;
    HWND window = CreateWindowEx(overlay_style,
        pClass, pName,
        WS_POPUP | WS_VISIBLE,
        x, y,
        w, h,
        nullptr, nullptr, instance, nullptr);

    InitDirect2D(window, &pR);
    paint_shadow();

    MSG message;
    while (BOOL result = GetMessage(&message, 0, 0, 0)){
        if (-1 != result) DispatchMessage(&message);
    }
    return 0;
}

EDIT: By using PostMessage(parent, WM_NCLBUTTONDOWN, HTCAPTION, lParam); when the child receives a WM_LBUTTONDOWN message, I was able to drag the parent through the child. But, in my example, the child window has the WS_POPUP style which means that it no longer follows its parent. When removing the WS_POPUP style from the child window, it is no longer visible, since its parent is a WS_EX_LAYERED window. So, in my opinion, it leaves 3 solutions:

  1. Find a way to make visible the child window of a WS_EX_LAYERED window.
  2. Make WS_EX_TRANSPARENT work without WS_EX_LAYERED (with DWM).
  3. "Glue" a WS_POPUP child window to its parent.

NOTE: I noticed that when reaching the parent with PostMessage() and making the parent window manually move the child with SetWindowPos(), the delay was smaller, but still present.

Upvotes: 2

user7860670
user7860670

Reputation: 37578

Window proc is broken. After handling WM_PAINT and WM_NCMOUSEMOVE it just ends without executing return statement, which is Undefined Behavior. The nasty flickering is most likely caused by letting default window proc handle WM_ERASEBKGND so it tries to fill whole window with background colour. You should handle this message and return 1 to indicate that background was erased.

So the fixed proc looks like this:


LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, PARAM lParam)
{
    switch (uMsg)
    {
        case WM_DESTROY:
        {
            PostQuitMessage(0);
            return 0;
        }
        case WM_ERASEBKGND:
        {
            return 1; // done erasing
        }
        case WM_PAINT:
        {
            paint_frame(hwnd);
            ValidateRect(hwnd, NULL);
            return 0;
        }
        case WM_NCHITTEST:
        {
            return HTCAPTION;  // Enable dragging of the window
        }
        case WM_NCMOUSEMOVE:
        {
            float xPos = (float) LOWORD(lParam);
            float yPos = (float) HIWORD(lParam);
            std::cout << "Mouse Position: X = " << xPos << ", Y = " << yPos << std::endl;
        }
        [[fallthrough]];
        default:
        {
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
        }
    }
}

Upvotes: 4

Related Questions