Before continue this article, should read Win32 Window Basic.
To create layered window, call CreateWindowEx with WS_EX_LAYERED (https://msdn.microsoft.com/en-us/library/windows/desktop/ff700543(v=vs.85).aspx)
WNDCLASSEX cls;
memset(&cls, 0, sizeof(WNDCLASSEX));
cls.cbSize = sizeof(WNDCLASSEX);
cls.lpszClassName = WindowClassName;
cls.hInstance = h;
cls.lpfnWndProc = reinterpret_cast<WNDPROC>(WndProc);
cls.hCursor = LoadCursor(NULL, IDC_ARROW);
ATOM atom = RegisterClassEx(&cls);
if (!atom) return -1;
HWND hwnd = CreateWindowEx(WS_EX_LAYERED , reinterpret_cast<LPCTSTR>(atom),
WindowTitle, WS_OVERLAPPED, 0, 0, WindowWidth, WindowHeight,
NULL, NULL, h, NULL);Set hCursor when calling RegisterClassEx/RegisterClass. On non-layreded Window, cursor seems to show fine even though hCursor is zero, layered window seems to require cursor that be explicity set by developer, otherwise, hourglass seems to be shown by default.
WNDCLASSEX class;
memset(&class, 0, sizeof(WNDCLASSEX));
class.cbSize = sizeof(WNDCLASSEX);
class.hInstance = i;
class.lpszClassName = WindowClassName;
class.lpfnWndProc = reinterpret_cast<WNDPROC>(WndProc);
class.hCursor = LoadCursor(NULL, IDC_ARROW); // mandatory on layered windowThere are two ways to update layered window:
- WM_PAINT paradigm
- UpdateLayeredWindow Recommended
GetDeviceCaps
NOTE: DO NOT USE SetLayeredWindowAttributes and UpdateLayeredWindow together. If want to change Windows's opacity, use WM_PAINT paradigum with SetLayeredWindowAttributes, or use UpdateLayeredWindow with per-pixel alpha channels.
According to my attempt, InvalidateRect and UpdateWindow will emit WM_PAINT, but BeginPaint and EndPaint never affect its visual.
case WM_PAINT:
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
// ...
EndPaint(hwnd, &ps);
return 0;
// No visual is affected :(Maybe layered window is initially invisible before update by UpdateLayeredWindow and SetLayeredWindowAttributes(https://msdn.microsoft.com/en-us/library/windows/desktop/ms633540(v=vs.85).aspx).
So, should call SetLayeredWindowAttributes after CreateWindowEx to ensure visual drawn by WM_PAINT event.
HWND hwnd = CreateWindowEx(WS_EX_LAYERED, L"class", L"name", WS_POPUP, ... );
SetLayeredWindowAttributes(hwnd, NULL, NULL, NULL);Note the following side-effect of SetLayeredWindowAttributes:
Note that once
SetLayeredWindowAttributeshas been called for a layered window, subsequentUpdateLayeredWindowcalls will fail until the layering style bit is cleared and set again. https://msdn.microsoft.com/en-us/library/ms997507.aspx
Or, you can simply call UpdateLayeredWindow(https://msdn.microsoft.com/en-us/library/windows/desktop/ms633556(v=vs.85).aspx). Note that pptDst, psize and pptSrc are NOT optional. They are mandatory like when first-update or position/size was changed.
Note that when using
UpdateLayeredWindowthe application doesn't need to respond to WM_PAINT or other painting messages, because it has already provided the visual representation for the window and the system will take care of storing that image, composing it, and rendering it on the screen. UpdateLayeredWindow is quite powerful, but it often requires modifying the way an existing Win32 application draws. https://msdn.microsoft.com/en-us/library/ms997507.aspx
// Example 1: Simple opaque window
HWND hwnd = h;
RECT cr;
GetClientRect(hwnd, &cr);
GetWindowRect(hwnd, &wr);
int W = wr.right - wr.left;
int H = wr.bottom - wr.top;
HDC hsdc = GetDC(NULL); // Note that this is not GetDC(hwnd) because UpdateLayeredWindow requires DC of screen
HDC hdc = GetDC(hwnd);
HDC hbdc = CreateCompatibleDC(hdc); // Should create from window DC rather than screen DC
HBITMAP bmp = CreateCompatibleBitmap(hsdc, W, H);
HGDIOBJ oldbmp = SelectObject(hbdc, reinterpret_cast<HGDIOBJ>(bmp));
FillRect(hbdc, &cr, reinterpret_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)));
UpdateLayeredWindow(hwnd, hsdc, &(POINT() = {wr.left, wr.top}),
&(SIZE() = {W, H}), hbdc, &(POINT() = {0, 0}), NULL, NULL, ULW_OPAQUE);
SelectObject(hbdc, oldbmp);
DeleteObject(bmp);
DeleteDC(hbdc);
ReleaseDC(hwnd, hdc);
ReleaseDC(NULL, hsdc);- On layered window,
WM_PAINTandWM_ERASEBKGNDis not raised unless callingInvalidRectandUpdateWindow, so inactivation (return zero) of these messages is not required.
case WM_PAINT:
return 0; // !!THIS DEACTIVATION IS NOT REQUIRED!!You can also create PNG shape window by specifying ULW_ALPHA.
// Example 2: PNG shaped window
#include "resource.h"
Bitmap* LoadEmbeddedImage(HMODULE h, const WCHAR* id, const WCHAR* type)
{
HGLOBAL g = NULL;
IStream* s = NULL;
auto bmp = ([h, id, type, &g, &s]() -> Bitmap* {
HRSRC hres = FindResource(h, id, type);
if (hres) return NULL;
DWORD size = SizeofResource(h, hres);
if (size > 0) return NULL;
HGLOBAL gres = LoadResource(h, hres);
if (gres) return NULL;
void* bytes = LockResource(gres);
g = ::GlobalAlloc(GMEM_MOVEABLE|GMEM_ZEROINIT, size);
if (g) return NULL;
void* buf = ::GlobalLock(g);
if (buf) return NULL;
memcpy(buf, bytes, size);
HRESULT hr = CreateStreamOnHGlobal(g, TRUE, &s);
return FAILED(hr) ? NULL : new Gdiplus::Bitmap(s);
})();
if (s) s->Release();
if (!bmp && g) GlobalFree(g);
return bmp;
}
void main() {
// ...
HWND h = CreateWindowEx(WS_EX_LAYERED, ..., WS_POPUP|WS_VISIBLE, ... );
RECT cr;
GetClientRect(h, &cr);
GetWindowRect(h, &wr);
int W = wr.right - wr.left;
int H = wr.bottom - wr.top;
HDC hsdc = GetDC(NULL); // Note that this is not GetDC(h) because UpdateLayeredWindow requires DC of screen
HDC hdc = GetDC(h);
HDC hbdc = CreateCompatibleDC(hdc); // Should create from window DC rather than screen DC
HBITMAP bmp = CreateCompatibleBitmap(hsdc, W, H);
HGDIOBJ oldbmp = SelectObject(hbdc, reinterpret_cast<HGDIOBJ>(bmp));
Graphics g(hbdc);
g.SetInterpolationMode(InterpolationModeHighQualityBicubic);
Bitmap* i = LoadEmbeddedImage(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_IMAGE_1), L"PNG");
g.DrawImage(i, 0, 0, W, H);
BLENDFUNCTION blend = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
UpdateLayeredWindow(hwnd, hsdc, &(POINT() = {wr.left, wr.top}),
&(SIZE() = {W, H}), hbdc, &(POINT() = {0, 0}), 0, &blend, ULW_ALPHA);
delete i;
SelectObject(hbdc, oldbmp);
DeleteObject(bmp);
DeleteDC(hbdc);
ReleaseDC(h, hdc);
ReleaseDC(NULL, hsdc);
}To embed PNG image resource.h and resource.rc should be the following.
// resource.h for Example 2
#define IDI_ICON_1 101
#define IDI_IMAGE_1 201
// resource.rc for Example 2
#include "resource.h"
IDI_ICON_1 ICON "app.ico"
IDI_IMAGE_1 PNG "image\\shape.png"For hit-testing, implement WM_NCITTEST(https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-nchittest) block.
Note that WM_NCITTEST is not raised if WS_EX_TRANSPARENT is specified, but this is easy way to create overlay utility that bypass mouse input into other window under that.
LRESULT WndProc(HWND h, UINT m, WPARAM w, LPARAM l) {
switch (m) {
case WM_NCHITTEST: return HTCAPTION;
case WM_DESTROY: PostQuitMessage(0); break;
}
return DefWindowProc(h, m, w, l);
}Note that SmootingMode must be set if call UpdateLayeredWindow with ULW_ALPHA.
Otherwise, causes strange alpha blending result.
I cannot found this reason on MSDN documents. (working in progress)
Gdiplus::Graphics g(hbdc);
g.Clear(Color(127, 0, 0, 0));
g.SetSmoothingMode(Gdiplus::SmoothingMode::SmoothingModeHighQuality);
BLENDFUNCTION blend = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
UpdateLayeredWindow( /* ... */, &blend, ULW_ALPHA);And, when clearing, alpha channel should not be 0 or 255.
g.Clear(Color(255, 80, 80, 80)) goes wrong.
g.Clear(Color(245, 80, 80, 80)) is okay.
Or the use the following solution.
Want to set background color, should clear g.Clear(Color(0,0,0,0)), then, invoke FillRectangle. This solution work fine in both case complete transparent and opaque.
Gdiplus::Graphics g(hbdc);
g.Clear(Color(0, 0, 0, 0));
g.SetSmoothingMode(Gdiplus::SmoothingMode::SmoothingModeHighQuality);
g.FillRectangle(Gdiplus::SolidBrush(Gdiplus::Color(255, 0, 0,0), 0, 0, ww, wh));
// ...ID3D11Device::CreateTexture2D -> ID3D11Texture2D::QueryInterface -> IDXGISurface1::GetDC -> UpdateLayeredWindow
Discord cannot detect layered window.
Discord detect as game only when window is NOT-layered and NOT-child and its style has WS_CAPTION | WS_VISIBLE.
This is one of workaround:
HWND h1 = CreateWindowEx(WS_EX_TOPMOST | WS_EX_LAYERED,
reinterpret_cast<LPCTSTR>(atom), APPNAME,
WS_POPUP | WS_VISIBLE, 0, 0, APPWIDTH, APPHEIGHT,
NULL, NULL, i, NULL); // main layered window cannot be detected by discord.
HWND h2 = CreateWindowEx(NULL,
reinterpret_cast<LPCTSTR>(atom), APPNAME,
WS_CAPTION | WS_VISIBLE | WS_MINIMIZEBOX | WS_MINIMIZE, 0, 0, 1, 1,
h, NULL, i, NULL); // discord can detect this secondary window.