Minimal custom title bar window (win32 API)

Setting up a window with a custom title bar (or a "borderless" window in older Windows versions) via the Windows win32 API is notoriously annoying to get right. Information about it is sparse and often comes with its own issues and edge cases.

Here I want to share the method that I have been using for quite a while now without encountering any issues (even though I am sure there are some). Unlike other solutions I have found online, this does not call any DWM- or other obscure win32 functions. Shortcuts, drop-shadows, resizing etc. works as expected.

Note that this sample code does not actually render a fancy title bar. It's just sets up the window correctly.

The code snippet is written in Odin:


package main

import "core:fmt"
import "base:intrinsics"
import win32 "core:sys/windows"

foreign import user32 "system:User32.lib"
@(default_calling_convention = "system")
foreign user32 {
	IsZoomed :: proc(win32.HWND) -> win32.BOOL ---
}

shouldExit := false
TITLEBAR_HEIGHT: i32 = 30

WndProc :: proc "system" (hwnd : win32.HWND, msg : win32.UINT, wParam : win32.WPARAM, lParam : win32.LPARAM) -> win32.LRESULT { 
  //WM_NCCALCSIZE and WM_NCHITTEST are the only messages relevant to making the window borderless.
  //The other message are just for clearing the window and rendering a titlebar.
  BORDERLESS :: true
  switch msg {
    case win32.WM_NCCALCSIZE:
      if BORDERLESS {
        dpi := win32.GetDpiForWindow(hwnd)
        frame_x := i32(win32.GetSystemMetricsForDpi(win32.SM_CXFRAME, dpi))
        frame_y := i32(win32.GetSystemMetricsForDpi(win32.SM_CYFRAME, dpi))
        padding := i32(win32.GetSystemMetricsForDpi(win32.SM_CXPADDEDBORDER, dpi))
        
        params := (^win32.NCCALCSIZE_PARAMS)(uintptr(lParam))
        rect := wParam == 0 ? (^win32.RECT)(uintptr(lParam)) : &(params.rgrc[0])
        rect.right  -= frame_x + padding;
        rect.left   += frame_x + padding;
        rect.bottom -= frame_y + padding;
        if IsZoomed(hwnd) {
          rect.top += frame_y + padding
          
          // If we do not do this, hidden taskbar can not be unhidden on mouse hover
          // Unfortunately it can create an ugly bottom border when maximized...
          rect.bottom -= 1; 
        }
        return 0
      }
      else {
        return win32.DefWindowProcW(hwnd, msg, wParam, lParam)
      }
    case win32.WM_NCHITTEST:
      if BORDERLESS {
        hit := win32.DefWindowProcW(hwnd, msg, wParam, lParam)
        switch hit {
        case win32.HTNOWHERE    : fallthrough
        case win32.HTRIGHT      : fallthrough
        case win32.HTLEFT       : fallthrough
        case win32.HTTOPLEFT    : fallthrough
        case win32.HTTOPRIGHT   : fallthrough
        case win32.HTBOTTOMRIGHT: fallthrough
        case win32.HTBOTTOM     : fallthrough
        case win32.HTBOTTOMLEFT:
          return hit
        }
        
        //Adjustment happening in NCCALCSIZE are messing with detection
        //of the top hit area so manually checking that.
        dpi := win32.GetDpiForWindow(hwnd)
        frame_y := i32(win32.GetSystemMetricsForDpi(win32.SM_CYFRAME, dpi))
        padding := i32(win32.GetSystemMetricsForDpi(win32.SM_CXPADDEDBORDER, dpi))
        mousePosScreen : win32.POINT
        mousePosScreen.x = win32.GET_X_LPARAM(lParam)
        mousePosScreen.y = win32.GET_Y_LPARAM(lParam)
        
        mousePosClient := mousePosScreen
        win32.ScreenToClient(hwnd, &mousePosClient)
        if !IsZoomed(hwnd) && mousePosClient.y > 0 && mousePosClient.y < frame_y + padding do return win32.HTTOP
        
        //Use your custom titlebar hit test here
        if mousePosClient.y >= 0 && mousePosClient.y < TITLEBAR_HEIGHT do return win32.HTCAPTION
        
        return win32.HTCLIENT
      }
      else {
        return win32.DefWindowProcW(hwnd, msg, wParam, lParam)
      }
    case win32.WM_CLOSE:
      shouldExit = true
      return 0
    case win32.WM_PAINT:
      ps: win32.PAINTSTRUCT
      hdc := win32.BeginPaint(hwnd, &ps)
      clientRect: win32.RECT
      win32.GetClientRect(hwnd, &clientRect)
    
      titlebarRect := win32.RECT {
        left = 0,
        top = 0,
        right = clientRect.right,
        bottom = TITLEBAR_HEIGHT,
      }
      
      win32.FillRect(hdc, &clientRect     , (win32.HBRUSH)(uintptr(win32.COLOR_WINDOW + 1)));
      win32.FillRect(hdc, &titlebarRect   , (win32.HBRUSH)(uintptr(win32.COLOR_MENU   + 1)));
      win32.TextOutW(hdc, 0, titlebarRect.bottom, intrinsics.constant_utf16_cstring("Hello"), 5)
      win32.EndPaint(hwnd, &ps)
    case win32.WM_SIZING:
      win32.InvalidateRect(hwnd, nil, false)
  }

  return win32.DefWindowProcW(hwnd, msg, wParam, lParam);
}

main :: proc() {
  //The following function call is not strictly necessary to create a borderless window.
  //Without it however, functions like 'GetDpiForWindow' (which we use in WndProc) will always return 96
  //and Window's DPI scaling fallback will kick in. This may or may not be what you want, depending on your
  //application's support for DPI scaling.
  //Note that IF you call the function you have to either use DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 or
  //DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE. With DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE you additionally have 
  //to call EnableNonClientDpiScaling() after creating your window. Otherwise the borderless code will have issues.
  win32.SetProcessDpiAwarenessContext(win32.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)

  //Your regular window creation code. Nothing fancy in regards to borderless is happening here.
  UTF_16 :: intrinsics.constant_utf16_cstring
  hInstance := win32.GetModuleHandleW(nil)

  wndClass : win32.WNDCLASSEXW
  wndClass.cbSize = size_of(win32.WNDCLASSEXW)
  wndClass.style  = win32.CS_OWNDC | win32.CS_BYTEALIGNCLIENT
  wndClass.lpfnWndProc = WndProc
  wndClass.hInstance   = win32.HINSTANCE(hInstance);
  wndClass.lpszClassName = UTF_16("MyWindowClass")
  wndClass.hCursor = win32.LoadCursorA(nil, win32.IDC_ARROW)

  reg := win32.RegisterClassExW(&wndClass)
  if reg == 0 {
    fmt.println("RegisterClass failed")
  }

  window := win32.CreateWindowW(
    UTF_16("MyWindowClass"),
    UTF_16("Borderless"),
    //MAXIMIZEBOX and MINIMIZEBOX are necessary to enable proper maximizing/minimizing, even if we do not use/show the buttons
    win32.WS_CAPTION | win32.WS_SYSMENU | win32.WS_MAXIMIZEBOX | win32.WS_MINIMIZEBOX | win32.WS_SIZEBOX, 
    win32.CW_USEDEFAULT,
    win32.CW_USEDEFAULT,
    win32.CW_USEDEFAULT,
    win32.CW_USEDEFAULT,
    nil,
    nil,
    win32.HINSTANCE(hInstance),
    nil)

  if window == nil {
    fmt.println("CreateWindow failed")
  }

  win32.ShowWindow(window, win32.SW_MAXIMIZE)

  for msg: win32.MSG; !shouldExit && win32.GetMessageW(&msg, nil, 0, 0) != 0; {
    win32.TranslateMessage(&msg)
    win32.DispatchMessageW(&msg)
  }
}