Skip to content

Instantly share code, notes, and snippets.

@Lexikos
Last active February 22, 2025 14:53
Show Gist options
  • Save Lexikos/eaffb9a99cafa0a3a21455c5e3efa55f to your computer and use it in GitHub Desktop.
Save Lexikos/eaffb9a99cafa0a3a21455c5e3efa55f to your computer and use it in GitHub Desktop.
Creating a GUI with custom titlebar in AutoHotkey v2.0

CustomTitleBar

This script for AutoHotkey v2.0 demonstrates a technique for customizing the titlebar of a GUI.

Although this script only emulates a taller titlebar, it is possible to render other text or graphics.

→ Screenshots (not included inline due to gist limitations)

Known Issues

When text is drawn by GDI, it sets the alpha channel to 0. As a result, most Win32 controls aren't fully usable within the titlebar.

While the window is maximized and the mouse is hovering over a caption button, it is not highlighted. This is likely related to an OS bug where DwmDefWindowProc does not process messages if the window is maximized.

The background of the icon is not transparent. This is a limitation of the Picture (Win32 "Static") control. Drawing the icon manually would likely work, but I didn't bother since it would generally have no visible effect on Windows 8 and later.

Drawing artifacts become visible if the DPI changes and per-monitor DPI awareness is enabled. (The latter is a feature of AutoHotkey v2.1-alpha.16+, but could also be enabled and implemented by a v2.0 script.)

Alternatives

The Windows App SDK provides AppWindowTitleBar.ExtendsContentIntoTitleBar. This closed-source part of the SDK appears to essentially eliminate the standard non-client area (like we do by handling WM_NCCLIENT) and then emulate the caption buttons using a custom control. This can be used from AutoHotkey, but no public examples exist at the time of writing.

#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)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment