|
|
|
#Requires AutoHotkey v2.0 |
|
|
|
OnMessage( 0xF, PaintTitle) |
|
OnMessage( 0x14, EraseBkgnd) |
|
OnMessage( 0x83, NcCalcSize) |
|
OnMessage( 0x84, NcHitTest) |
|
OnMessage( 0xA4, NcRButtonDown) |
|
OnMessage( 0x2A2, NcMouseLeave) |
|
|
|
G := Gui('+Resize', 'Custom Titlebar Proof of Concept') |
|
G.OnEvent('Escape', G => G.Destroy()) |
|
G.OnEvent('Size', GuiResize) |
|
|
|
; Add a replacement for the window icon normally shown in the titlebar. |
|
G.AddPic('x16 y16 w16 vCaptionIcon', A_AhkPath) |
|
; Set the title font. |
|
G.SetFont('s9', "Segoe UI") |
|
; Add a hidden Text control to simplify font handling. |
|
; Height was tweaked so that the next control is added at the right position. |
|
G.Captn := G.AddText('x+14 h26 Hidden', G.Title) |
|
; Give us some titlebar area that we can draw on. |
|
ExtendFrameIntoClientArea(G.hwnd, 14) |
|
|
|
; Add some test controls. |
|
G.SetFont('s8', "") |
|
G.AddText('xm', "Normal Text controls work here but not on the titlebar.") |
|
g.AddEdit(, "Likewise for Edit controls.") |
|
g.AddButton(, "&Close").OnEvent('Click', (*) => G.Destroy()) |
|
|
|
G.Show('w400') |
|
|
|
; Reapply frame on resize, basically just to support per-monitor DPI awareness. |
|
GuiResize(G, minMax, width, height) { |
|
if minMax != -1 |
|
ExtendFrameIntoClientArea(G.hwnd, 14) |
|
} |
|
|
|
; ** Message handling callback functions ** |
|
|
|
; Paints the title onto the titlebar. |
|
PaintTitle(wParam, lParam, nmsg, hwnd) { |
|
if hwnd = G.hwnd { |
|
if hTheme := DllCall("uxtheme\OpenThemeData", 'ptr', 0, 'str', "DWMWindow;Window", 'ptr') { |
|
paint := Buffer(72) |
|
hDC := DllCall("BeginPaint", 'ptr', hwnd, 'ptr', paint, 'ptr') |
|
static init := DllCall("uxtheme\BufferedPaintInit", 'hresult') |
|
r := Buffer(16, 0) |
|
; Get our hidden Text control's rectangle relative to the GUI's client area. |
|
DllCall("GetClientRect", 'ptr', G.Captn.hwnd, 'ptr', r) |
|
DllCall("MapWindowPoints", 'ptr', G.Captn.hwnd, 'ptr', G.hwnd, 'ptr', r, 'int', 2) |
|
; Add some padding, exclusively for the glow effect on Windows 7. |
|
; Height is used because it already scales with font size and DPI. |
|
pad := NumGet(r, 12, 'int') - NumGet(r, 4, 'int') |
|
NumPut( |
|
'int', NumGet(r, 0, 'int') - pad, |
|
'int', NumGet(r, 4, 'int'), |
|
'int', NumGet(r, 8, 'int') + pad, |
|
'int', NumGet(r, 12, 'int'), |
|
rbuf := Buffer(16, 0)) |
|
; This essentially creates (or retrieves from cache) a GDI bitmap which we can paint into. |
|
if hBuf := DllCall("uxtheme\BeginBufferedPaint", 'ptr', hDC, 'ptr', rbuf, 'int', BPBF_TOPDOWNDIB := 2, 'ptr', 0, 'ptr*', &hBufDC:=0, 'ptr') { |
|
; Select the font of our hidden title Text control. |
|
hFont := SendMessage(49,,, G.Captn) ; WM_GETFONT |
|
hOldFont := DllCall("SelectObject", 'ptr', hBufDC, 'ptr', hFont, 'ptr') |
|
; Prepare drawing options. DTT_COMPOSITED is the essential option to output |
|
; correct alpha values. DTT_GLOWSIZE is only effective on Windows 7/Vista. |
|
; We could select the caption font indirectly via these options, but in that |
|
; case it would be sized for 96 DPI (100% scaling). |
|
dttopts := Buffer(56 + A_PtrSize * 2, 0) |
|
NumPut( |
|
'uint', dttopts.Size, |
|
'uint', (DTT_COMPOSITED := 0x2000) | (DTT_GLOWSIZE := 0x800), |
|
dttopts) |
|
NumPut( |
|
'int', 5, ; iGlowSize |
|
dttopts, 52 |
|
) |
|
; Draw the title text into the buffer. |
|
DllCall("uxtheme\DrawThemeTextEx", 'ptr', hTheme, 'ptr', hBufDC, 'int', 0, 'int', 0, 'str', G.Title, 'int', -1 |
|
, 'uint', DT_WORD_ELLIPSIS := 0x40000, 'ptr', r, 'ptr', dttopts, 'hresult') |
|
; Deselect our font. |
|
DllCall("SelectObject", 'ptr', hBufDC, 'ptr', hOldFont, 'ptr') |
|
; Paint the buffer onto the window. |
|
DllCall("uxtheme\EndBufferedPaint", 'ptr', hBuf, 'int', true, 'hresult') |
|
} |
|
DllCall("EndPaint", 'ptr', hwnd, 'ptr', paint) |
|
DllCall("uxtheme\CloseThemeData", 'ptr', hTheme) |
|
return 0 |
|
} |
|
} |
|
} |
|
|
|
; Erases the background of the area where the DWM window frame will be rendered. |
|
EraseBkgnd(hDC, lParam, nmsg, hwnd) { |
|
if hwnd = G.hwnd { |
|
m := GetFrameMetrics(hwnd) |
|
r := Buffer(16, 0) |
|
DllCall("GetClientRect", 'ptr', hwnd, 'ptr', r) |
|
|
|
; Fill the entire titlebar area with black. Because GDI doesn't understand alpha, |
|
; the alpha channel will be 0, which is what we want for the DWM frame to take over. |
|
; Areas covered by DwmExtendFrameIntoClientArea but not erased here tend to end up |
|
; white, which looks out of place if the frame isn't supposed to be white. |
|
bottom := NumGet(r, 12, 'int') |
|
NumPut('int', G.TopBarHeight, r, 12) ; r.bottom |
|
DllCall("FillRect", 'ptr', hDC, 'ptr', r, 'ptr', DllCall("GetStockObject", 'int', 4, 'ptr')) ; BLACK_BRUSH=4 |
|
|
|
; Option A: fill the remainder of the GUI and then suppress default handling. |
|
; NumPut('int', NumGet(r, 12, 'int'), r, 4) |
|
; NumPut('int', bottom, r, 12) |
|
; DllCall("FillRect", 'ptr', hDC, 'ptr', r, 'ptr', 16) ; COLOR_3DFACE+1 |
|
; return true |
|
|
|
; Option B: exclude the titlebar from the default handling (which respects Gui.BackColor). |
|
DllCall("ExcludeClipRect", 'ptr', hDC, 'int', NumGet(r, 0, 'int'), 'int', NumGet(r, 4, 'int'), 'int', NumGet(r, 8, 'int'), 'int', NumGet(r, 12, 'int')) |
|
} |
|
} |
|
|
|
; Handles WM_NCCALCSIZE to remove the standard titlebar area. |
|
NcCalcSize(wParam, lParam, nmsg, hwnd) { |
|
if !(IsSet(G) && hwnd = G.hwnd && wParam = true) |
|
return |
|
; Calculate the new client area size, to exclude the standard titlebar. |
|
m := GetFrameMetrics(hwnd) |
|
NumPut( |
|
'int', NumGet(lParam, 0, 'int') + (m.frame_x + m.padding), ; left |
|
'int', NumGet(lParam, 4, 'int') + (WinGetMinMax(hwnd) = 1 ? m.padding : 0), ; top |
|
'int', NumGet(lParam, 8, 'int') - (m.frame_x + m.padding), ; right |
|
'int', NumGet(lParam, 12, 'int') - (m.frame_y + m.padding), ; bottom |
|
lParam |
|
) |
|
return 0 |
|
} |
|
|
|
; Restores functionality to the titlebar buttons and frame (if resizable). |
|
NcHitTest(wParam, lParam, nmsg, hwnd) { |
|
if hwnd = G.hwnd { |
|
; This handles hit testing for the caption buttons, although it appears to fail |
|
; while the window is maximized (observed on Windows 11 24H2 and Windows 7): |
|
if DllCall("dwmapi\DwmDefWindowProc", 'ptr', hwnd, 'int', nmsg, 'ptr', wParam, 'ptr', lParam, 'ptr*', &r:=0) |
|
return r |
|
|
|
WinGetClientPos &gx, &gy, &gw,, hwnd |
|
x := (lParam << 48 >> 48) - gx |
|
y := (lParam << 32 >> 48) - gy |
|
|
|
; Both Windows 7 and 24H2 handle the sizing margins appropriately (if we don't override), |
|
; except for the top margin, presumably because it's the only side that doesn't have an |
|
; extra, invisible sizing margin. |
|
if y < G.TopBarHeight && x >= 0 && x < gw { |
|
if WinGetMinMax(hwnd) = 1 { |
|
; Manual testing for the buttons is needed due to an apparent bug in DwmDefWindowProc. |
|
tbi := Buffer(140, 0) ; TITLEBARINFOEX |
|
NumPut('uint', tbi.Size, tbi) |
|
SendMessage(WM_GETTITLEBARINFOEX := 0x33F,, tbi, hwnd) |
|
static ButtonRV := [8, 9, 21, 20] ; min, max, help, close |
|
pt := ((gx + x) & 0xffffffff) | ((gy + y) << 32) |
|
Loop 4 |
|
if DllCall("PtInRect", 'ptr', tbi.ptr + 60 + A_Index*16, 'int64', pt) |
|
return ButtonRV[A_Index] |
|
} |
|
; Practical testing on Windows 24H2 showed a margin of 7 when at 100% DPI |
|
; and 8 at 250% DPI, which is just stupid. Make it 7 * display scale. |
|
top_sizing_margin := 7 * G.dpi // 96 |
|
return y < top_sizing_margin ? 12 : 2 ; HTTOP : HTCAPTION |
|
} |
|
} |
|
} |
|
|
|
; Restores right-click functionality on the titlebar. |
|
NcRButtonDown(wParam, lParam, nmsg, hwnd) { |
|
if hwnd = G.hwnd { |
|
if wParam = 2 { |
|
; DefWindowProc tracks the mouse and doesn't return until the button is released, |
|
; but only does it if the coords are within the real caption area, so we emulate. |
|
KeyWait SysGet(23) ? 'LButton' : 'RButton' |
|
CoordMode 'Mouse' |
|
MouseGetPos &x, &y, &w |
|
WinGetClientPos , &gy,,, hwnd |
|
if w = hwnd && (y - gy) < G.TopBarHeight { |
|
; This is an undocumented message, used instead of: |
|
; - WM_CONTEXTMENU because it only shows a menu "if the specified position is in the window's caption", |
|
; and that apparently doesn't take WM_NCCALCSIZE into consideration. |
|
; - GetSystemMenu() because it actually creates a menu on first use with a given window. |
|
PostMessage 0x313,, (x & 0xffff) | (y << 16), hwnd |
|
} |
|
return 0 |
|
} |
|
} |
|
} |
|
|
|
; Removes the highlighting from the hovered caption buttons when the mouse leaves the window. |
|
NcMouseLeave(wParam, lParam, nmsg, hwnd) { |
|
if hwnd = G.hwnd { |
|
if DllCall("dwmapi\DwmDefWindowProc", 'ptr', hwnd, 'int', nmsg, 'ptr', wParam, 'ptr', lParam, 'ptr*', &r:=0) |
|
return r |
|
} |
|
} |
|
|
|
; ** Utility functions ** |
|
|
|
ExtendFrameIntoClientArea(hwnd, extra := 0) { |
|
m := GetFrameMetrics(hwnd) |
|
; Cache the DPI and height for use in WM_NCHITTEST handler. |
|
G.dpi := m.dpi |
|
G.TopBarHeight := m.frame_y + m.padding + m.caption_y + (extra * m.dpi // 96) |
|
; Extend the DWM frame into the client area to give us a titlebar area |
|
; (since our WM_NCCALCSIZE handler removes the standard titlebar area). |
|
margins := Buffer(16, 0) |
|
NumPut('int', 0, 'int', 0, 'int', G.TopBarHeight, 'int', 0, margins) |
|
DllCall("dwmapi\DwmExtendFrameIntoClientArea", 'ptr', hwnd, 'ptr', margins) |
|
} |
|
|
|
GetFrameMetrics(hwnd) { |
|
if VerCompare(A_OSVersion, '>=10.0.14393') { |
|
; Support per-monitor DPI awareness if it happens to be enabled and implemented |
|
; (either by the script or by built-in capabilities of AutoHotkey v2.1-alpha.16+). |
|
dpi := DllCall("GetDpiForWindow", 'ptr', hwnd) |
|
get := DllCall.Bind("GetSystemMetricsForDpi", 'int', , 'int', dpi) |
|
} |
|
else ; Dpi functions not available, so per-monitor DPI awareness is unsupported. |
|
dpi := A_ScreenDPI, get := SysGet |
|
return { |
|
dpi: dpi, |
|
frame_x: get(SM_CXFRAME := 32), |
|
frame_y: get(SM_CYFRAME := 33), |
|
padding: get(SM_CXPADDEDBORDER := 92), |
|
caption_y: get(SM_CYCAPTION := 4) |
|
} |
|
} |