Win32 Theme Stacks¶
Four rendering stacks sit under every Win32 application on Windows 11, and each one explains a different part of what "the Windows look" meant in a given year. This study takes them apart layer by layer, measures what they produce on a current machine, and works out which of those looks a retained, Direct2D-backed framework can reproduce from an algorithm and which it can only imitate.
The measurements were taken on Windows 11 Enterprise build 26200 with the 10.0.26100.0 SDK headers, in dark mode. Values read off that machine are marked as measured; values taken from documentation are marked as documented.
A Win32 window on Windows 11 is four layers composited on top of each other:
| Layer | Since | What it is | Code or data |
|---|---|---|---|
| DWM | Vista | Composites every window and owns the frame, caption, rounded corners, drop shadow and Mica. The app can only set attributes. | Attributes |
| uxtheme + msstyles | XP | DrawThemeBackground blits nine-sliced 32-bit PNGs out of a theme file. This is what "themed" means. |
Almost entirely data |
| GDI+ | XP (redistributable to 98) | Antialiased, floating-point, alpha-blended. Optional; used by WinForms owner-draw paths. | Algorithm |
| GDI | 1.0 | Aliased, integer, no alpha. DrawEdge, FillRect, DrawText, DrawFocusRect. The "Windows Classic" look, and the fallback when visual styles are off. |
Algorithm |
The fact that shapes everything below: GDI and GDI+ are algorithms, uxtheme is a bitmap archive. Windows 95's look can be computed from five colors and a rule. Windows XP's and Vista's looks are pictures of themselves, and a framework that does not ship those pictures produces a homage, not a replica.
GDI¶
The Drawing Model¶
GDI is a stateful, integer, aliased rasterizer that paints opaque by default.1 A device context (HDC) holds the currently selected pen, brush, font, bitmap, clip region and, historically, a color palette, and every drawing call uses whatever is selected. The properties that produce the look:
- No antialiasing. Lines, rectangles, ellipses and polygons have hard pixel edges. GDI has no concept of coverage, so a 1 px line is exactly one pixel at an integer coordinate.
- No alpha. A
COLORREFis0x00BBGGRRand the high byte is ignored by essentially every core call. Translucency only arrived as bolt-ons (see below). - Integer coordinates, in logical units mapped through the mapping mode. For UI code that is
MM_TEXT, i.e. one unit per pixel. - Raster operations.
BitBltandPatBltcombine source, pattern and destination with a boolean ROP (SRCCOPY,PATINVERT,DSTINVERT). The focus rectangle, the drag rubber band and marching-ants selection are XOR operations, so drawing them twice erases them. - Palette-era colors.
#C0C0C0,#808080, navy#000080and teal#008080are entries of the 20-color default system palette of the 8-bit era, not design choices. Anything outside the palette was dithered, which is why GDI still has hatch and halftone brushes.
The resulting signature: hard edges, flat fills, no gradients, no shadows, no curves except circles, and a strict 1 px structural grid.
The 3-D Bevel Is DrawEdge¶
The whole Windows 95 to 2000 control language reduces to DrawEdge plus a short color ramp.2 DrawEdge(hdc, rc, uType, uFlags) draws up to two concentric 1 px rings, and uType picks each ring's polarity on its own:
BDR_RAISEDOUTER 0x0001 BDR_SUNKENOUTER 0x0002
BDR_RAISEDINNER 0x0004 BDR_SUNKENINNER 0x0008
EDGE_RAISED = RAISEDOUTER | RAISEDINNER a control that sticks out
EDGE_SUNKEN = SUNKENOUTER | SUNKENINNER a well: edit box, check box, status pane
EDGE_ETCHED = SUNKENOUTER | RAISEDINNER a group box or separator groove
EDGE_BUMP = RAISEDOUTER | SUNKENINNER a ridge
uFlags picks the sides (BF_LEFT | BF_TOP | BF_RIGHT | BF_BOTTOM) and modifiers (BF_MIDDLE fills the interior, plus BF_SOFT, BF_FLAT, BF_MONO, BF_ADJUST). The colors per ring and side, documented and matching what Wine reimplements from the published behavior:
uType |
Top/left outer | Top/left inner | Bottom/right outer | Bottom/right inner |
|---|---|---|---|---|
EDGE_RAISED |
COLOR_3DLIGHT |
COLOR_BTNHIGHLIGHT |
COLOR_3DDKSHADOW |
COLOR_BTNSHADOW |
EDGE_SUNKEN |
COLOR_BTNSHADOW |
COLOR_3DDKSHADOW |
COLOR_BTNHIGHLIGHT |
COLOR_3DLIGHT |
Two details make a hand-rolled bevel look "not quite right" when they are missed:
- On the shadow side the darkest color is outside. A raised button's bottom-right edge is
3DDKSHADOW(black) and thenBTNSHADOW(mid gray) moving inward, not the other way round. - On the lit side the outer ring is
COLOR_3DLIGHT, not white. In Windows 2000's scheme3DLIGHTequals the face color, so the outer ring vanishes into the parent and the eye reads one crisp white line; in Windows 95's it is#DFDFDF, a soft step between face and white. Either way the top-left reads as one highlight and the bottom-right as two shadows. That asymmetry comes from the palette, not the geometry, so an implementation should draw all four rings and let the colors collapse them. Hard-coding the asymmetry breaks every custom scheme where3DLIGHTdiffers from the face.
BF_SOFT adds a second raised shape. DrawFrameControl(DFC_BUTTON, DFCS_BUTTONPUSH) passes it for every command button, and it swaps the two top/left rings, so on a push button the white highlight is the outermost pixel. A window border, a menu edge or a scroll arrow uses the plain arrangement. Drawing every raised surface with one of the two is the easiest way to get the era subtly wrong. A third raised shape is the hot toolbar button: a single ring with no second one behind it.
DrawFrameControl layers state on top with DFCS_PUSHED, DFCS_CHECKED, DFCS_INACTIVE, DFCS_HOT, DFCS_FLAT and DFCS_MONO, and supplies the check box, radio, scroll-arrow and combo-arrow glyphs.3 A pressed push button is not a mirrored bevel: it is a single COLOR_BTNSHADOW frame around the face, with the content moved one pixel right and down. The default push button gets an extra 1 px COLOR_WINDOWFRAME frame outside the bevel, and its face shrinks by that pixel.
The System Color Table Is the Whole Palette¶
winuser.h defines 31 COLOR_* indices (0 to 30).4 Classic controls never hard-code a color, they call GetSysColor(i) or GetSysColorBrush(i). The ones that carry the 3-D language:
| Index | Name | Role |
|---|---|---|
| 15 | COLOR_BTNFACE / COLOR_3DFACE |
Every control's flat fill, and the dialog background |
| 20 | COLOR_BTNHIGHLIGHT / COLOR_3DHILIGHT |
The bright bevel edge |
| 22 | COLOR_3DLIGHT |
The outer light bevel edge |
| 16 | COLOR_BTNSHADOW / COLOR_3DSHADOW |
The mid-gray bevel edge |
| 21 | COLOR_3DDKSHADOW |
The dark outer bevel edge |
| 6 | COLOR_WINDOWFRAME |
1 px hard frames (default-button ring, list borders) |
| 5 / 8 | COLOR_WINDOW / COLOR_WINDOWTEXT |
Editable and scrollable surfaces: white, not face |
| 13 / 14 | COLOR_HIGHLIGHT / COLOR_HIGHLIGHTTEXT |
Selection |
| 17 | COLOR_GRAYTEXT |
Disabled text |
| 4 / 7 / 30 / 29 | COLOR_MENU / MENUTEXT / MENUBAR / MENUHILIGHT |
Menus |
| 2 / 3 / 9 / 27 / 28 | Caption colors, including the Windows 98 gradient pair | Title bars |
| 24 / 23 | COLOR_INFOBK / COLOR_INFOTEXT |
Tooltips (#FFFFE1) |
| 26 | COLOR_HOTLIGHT |
Hyperlinks |
| 0 / 12 | COLOR_SCROLLBAR / COLOR_APPWORKSPACE |
Scroll bar trough, MDI backdrop |
The split between WINDOW and BTNFACE is the most important structural distinction of the classic language. A dialog is face gray, but anything you can type into or scroll is COLOR_WINDOW white inside a sunken border. Getting that one relationship right does more for "this looks like Windows 98" than any amount of palette tuning.
Focus, Disabled State and Patterns¶
DrawFocusRectXORs a 50 % checkerboard brush along a 1 px rectangle. The dots alternate every pixel, not every two, and the pattern is aligned to the window origin so neighboring focus rects phase-match. Because it is XOR, a second call erases it. On screen it reads as a fine gray shimmer, not a dashed line.5- Disabled text is not simply gray.
DrawState(..., DSS_DISABLED)draws the string twice, first inCOLOR_3DHILIGHToffset by (1, 1), then inCOLOR_3DSHADOWat the origin, which gives the engraved label. Flat gray disabled text is an XP-and-later look.6 COLOR_GRAYTEXT == 0used to mean "no gray available": disabled menu text was then drawn with a 50 % pattern brush over the normal text.- Hatch and pattern brushes (
CreateHatchBrush,CreatePatternBrush) supply the drag-rectangle stipple, the disabled scroll bar trough and the classic desktop pattern.
Text¶
- Windows 3.x and 95 set UI text in raster fonts (
System,MS Sans Serif,Fixedsys,Terminal), hand-drawn glyph bitmaps at fixed sizes (8, 10, 12, 14, 18, 24 pt). They do not scale, hint or antialias, and the stepped diagonals of MS Sans Serif 8 pt are the visual texture of Windows 95. - MS Sans Serif is not installed on Windows 11 (measured: the installed font collection has
Microsoft Sans Serifbut notMS Sans Serif). A request for it silently gets the TrueTypeMicrosoft Sans Serif, which is metrically compatible but outline-based and antialiased.Fixedsys,TerminalandSystemare gone too. - Grayscale font smoothing arrived as an option with Windows 95 Plus! and Windows 98. ClearType (subpixel RGB) arrived in XP, off by default, and became the default in Vista. Era-faithful text is therefore aliased for 95/98/2000, grayscale for XP and ClearType from Vista on.
DrawText/DrawTextExown layout (DT_SINGLELINE | DT_VCENTER | DT_CENTER,DT_END_ELLIPSIS,DT_HIDEPREFIX), including the&mnemonic underline that only appears after the user presses Alt (WM_UPDATEUISTATE,UISF_HIDEACCEL).
The Bolt-Ons That Broke the Flat-Fill Rule¶
Three msimg32.dll additions made pre-uxtheme polish possible:7
GradientFillwithGRADIENT_FILL_RECT_H(0),GRADIENT_FILL_RECT_V(1) andGRADIENT_FILL_TRIANGLE(2). Two-vertex axis-aligned gradients; the Windows 98 gradient caption (COLOR_GRADIENTACTIVECAPTION) is exactly this.AlphaBlendwithBLENDFUNCTION{AC_SRC_OVER, 0, SourceConstantAlpha, AC_SRC_ALPHA}: constant and per-pixel alpha from a 32 bpp DIB. The only per-pixel translucency GDI ever had, and what themed 32-bit bitmaps blit through.TransparentBlt: color-key transparency, the magenta-mask idiom from before alpha.
Uxtheme and Msstyles¶
Class, Part, State¶
XP replaced "controls draw themselves from five system colors" with "controls ask a theme file what this part looks like in this state". The API is small:8
HTHEME OpenThemeData(HWND, L"BUTTON"); // or OpenThemeDataForDpi
DrawThemeBackground (hTheme, hdc, BP_PUSHBUTTON, PBS_HOT, &rc, NULL);
DrawThemeBackgroundEx(hTheme, hdc, part, state, &rc, &opts);
DrawThemeText / DrawThemeTextEx(hTheme, hdc, part, state, str, len, flags, &rc, &dttopts);
GetThemePartSize / GetThemeMargins / GetThemeColor / GetThemeFont / GetThemeInt
GetThemeSysColor(hTheme, COLOR_*); // the theme's own system colors
IsThemeBackgroundPartiallyTransparent(...); // paint the parent first if true
CloseThemeData(hTheme);
vsstyle.h holds the vocabulary: VSCLASS_BUTTON with parts BP_PUSHBUTTON, BP_RADIOBUTTON, BP_CHECKBOX, BP_GROUPBOX, BP_COMMANDLINK; PUSHBUTTONSTATES is PBS_NORMAL, PBS_HOT, PBS_PRESSED, PBS_DISABLED, PBS_DEFAULTED, PBS_DEFAULTED_ANIMATING. CHECKBOXSTATES has twenty entries (unchecked, checked, mixed, implicit and excluded, each normal, hot, pressed and disabled), and EDITPARTS tells EP_EDITBORDER_NOSCROLL apart from the _HSCROLL, _VSCROLL and _HVSCROLL borders. Two things follow for any theme model:
PBS_DEFAULTEDis its own state, with its own artwork, not "normal plus a ring".- State counts are per control and large. A single four-member state enum shared by every control runs out of room quickly.
How a Part Is Drawn¶
TMT_BGTYPE in vssym32.h picks one of three strategies per part and state:9
TMT_BGTYPE |
Value | Meaning |
|---|---|---|
BT_IMAGEFILE |
0 | Blit from a bitmap in the msstyles |
BT_BORDERFILL |
1 | Draw a border and a fill from properties |
BT_NONE |
2 | Draw nothing |
BT_BORDERFILL is a vector recipe: TMT_BORDERTYPE (BT_RECT, BT_ROUNDRECT, BT_ELLIPSE), TMT_BORDERCOLOR, TMT_BORDERSIZE, TMT_ROUNDCORNERWIDTH/HEIGHT, and TMT_FILLTYPE (FT_SOLID, FT_VERTGRADIENT, FT_HORZGRADIENT, FT_RADIALGRADIENT, FT_TILEIMAGE) with TMT_GRADIENTCOLOR1..5 and TMT_GRADIENTRATIO1..5. Five gradient stops with explicit ratios, four gradient geometries and three border shapes are the theme engine's whole vector vocabulary, and a reasonable target for what a retained renderer's brush model should express.
BT_IMAGEFILE is a nine-slice blit, described by:
TMT_IMAGEFILE, TMT_IMAGEFILE1..7 the bitmap(s)
TMT_IMAGECOUNT how many states are stacked in one strip
TMT_IMAGELAYOUT IL_VERTICAL | IL_HORIZONTAL
TMT_SIZINGTYPE ST_TRUESIZE | ST_STRETCH | ST_TILE
TMT_SIZINGMARGINS the nine-slice cuts (L, R, T, B)
TMT_CONTENTMARGINS where the caller's content goes
TMT_TRANSPARENT, TMT_TRANSPARENTCOLOR, TMT_ALPHALEVEL, TMT_ALPHATHRESHOLD
TMT_IMAGESELECTTYPE IST_NONE | IST_SIZE | IST_DPI
TMT_MINDPI1..7 the seven DPI rungs
TMT_SIZINGMARGINS is CSS border-image-slice and TMT_CONTENTMARGINS is padding. Drawing a themed part means picking the bitmap for the DPI, cutting it into nine, stretching or tiling edges and center, and laying the content out inside the content margins.
The rest of the 257 TMT_* properties cover glyphs (GT_IMAGEGLYPH, GT_FONTGLYPH), text decoration (TMT_TEXTSHADOWTYPE, TMT_TEXTGLOW, TMT_TEXTGLOWSIZE, TMT_TEXTBORDERSIZE), animation (TMT_TRANSITIONDURATIONS and friends) and composition (TMT_COMPOSITED, TMT_GLOWINTENSITY, TMT_COLORIZATIONCOLOR). TMT_CLASSICVALUE (3202) lets every themed part point back at what the classic renderer would have drawn: the engine was built with a classic fallback from day one, which is the same "one control, several appearances" split a multi-era framework needs.
What Is Inside Windows 11's aero.msstyles (Measured)¶
aero.msstyles is a resource-only PE. A byte-level scan of the 1'859'728-byte file on the test machine found:
| Measurement | Result |
|---|---|
| Embedded PNG images | 1'614 |
| Theme class names | Button, Explorer::Button, DarkMode_Explorer::Button, DarkMode_DarkTheme::Button, and 69 distinct DarkMode_* classes |
| Recurring size families | e.g. 19×21, 24×26, 29×32, 38×42, 47×52, 57×63, 76×84, sixteen images at each size |
| Tall narrow strips (h ≥ 3w, w ≤ 40) | 676 |
The seven widths 19, 24, 29, 38, 47, 57 and 76 are 19 × {1.00, 1.26, 1.53, 2.00, 2.47, 3.00, 4.00}: the 100/125/150/200/250/300/400 % DPI ladder, seven rungs, matching TMT_MINDPI1..7. The 676 tall strips are IL_VERTICAL multi-state image strips.
So the Windows 11 Win32 control look is a bitmap archive authored at seven fixed DPIs, not a computed style. A framework can imitate its geometry and color; it cannot derive it. The bitmaps are also copyrighted image content, so extracting or shipping them is off the table for any library.
Dark Mode Is Not a Second Theme File¶
aero.theme and dark.theme both point at the same file (Path=%ResourceDir%\Themes\Aero\Aero.msstyles) and differ only in SystemMode and AppMode. Dark mode for common controls comes from undocumented class names inside the same msstyles: an app calls SetWindowTheme(hwnd, L"DarkMode_Explorer", nullptr) and gets dark artwork from then on. Present in the binary: DarkMode::Menu, DarkMode::Toolbar, DarkMode::ItemsView, DarkMode_Explorer::{Button, Edit, ListView, ScrollBar, TreeView, Progress, Tooltip, TaskDialog}, DarkMode_DarkTheme::{Button, Combobox, Edit, Header, ListView, ScrollBar, Tab, Toolbar, TreeView} and more.
That makes Win32 dark mode opt-in per window, undocumented and partial, which is why Win32 and WinForms apps look broken in dark mode by default. A framework cannot get a dark classic palette from the OS and has to carry its own.
Glass Text and Transitions¶
DrawThemeTextExwithDTTOPTSis how Vista drew text on glass:DTT_GLOWSIZEandDTT_COMPOSITEDrender the string with an outward glow into a premultiplied top-down DIB so it stays readable over a blurred backdrop.10- Buffered animation (
BeginBufferedAnimation,BP_ANIMATIONSTYLE,GetThemeTransitionDuration,TMT_TRANSITIONDURATIONS) is how Vista and 7 cross-fade a button between normal and hot, over roughly 100 to 150 ms (documented). Architecturally that is the same thing as a compositor opacity cross-fade; the era differs only in duration and in which states animate. IsThemeBackgroundPartiallyTransparentplusDrawThemeParentBackgroundis the engine's answer to "this part has holes, paint the parent first".
GDI+¶
GDI+ (gdiplus.dll, XP onward, redistributable to 98) is a second rasterizer next to GDI, not a layer on top of it:11
| Axis | GDI | GDI+ |
|---|---|---|
| Coordinates | Integer | float, with a full 3×2 Matrix |
| Edges | Aliased | SmoothingMode: None, AntiAlias, HighQuality |
| Alpha | None (bolt-ons only) | Native ARGB, CompositingMode, CompositingQuality |
| Fills | Solid, hatch, pattern | Solid, LinearGradientBrush, PathGradientBrush, TextureBrush, hatch |
| Geometry | Rect, ellipse, polygon, polyline | GraphicsPath: Béziers, arcs, figures, widening, flattening |
| Text | DrawText |
Its own rasterizer, TextRenderingHint |
Four settings explain most "my GDI+ drawing looks wrong" reports:
SmoothingModedefaults toNoneon aGraphicsfromPaintEventArgs, so naive code looks like GDI. Switch onAntiAliasand every 1 px line becomes a 2 px smear, unless the next setting is also changed.PixelOffsetMode::Halfshifts sampling by half a pixel so integer rectangles land on pixel boundaries. Without it a 1 pxDrawRectangle(0, 0, w, h)under antialiasing straddles two pixel rows at 50 % each.TextRenderingHint:SingleBitPerPixelGridFitis aliased,AntiAliasGridFitgrayscale,ClearTypeGridFitsubpixel. GDI+ text was widely judged worse-spaced than GDI text, which is why .NET 2.0 switched WinForms toTextRenderer(aDrawTextWwrapper) withUseCompatibleTextRendering = falseas the default.- Gamma. GDI+ interpolates in non-linear sRGB unless
LinearGradientBrush.GammaCorrectionis set. Direct2D also blends in sRGB by default, so the two agree; a linear-light renderer would put the midpoint of the same two stops somewhere else.
GDI+ is not a look worth reproducing. It is the closest existing analog to a Direct2D UI renderer (float coordinates, antialiasing, alpha, gradient brushes), and its lesson is point 2: an antialiased float renderer must be able to switch antialiasing off and snap to the pixel grid, or it can never draw a convincing 1990s control.
WinForms¶
WinForms is the most useful reference here, because it had exactly this problem: one control model, several visual eras, on a renderer that is not the OS's own.
Three Painting Paths¶
- The OS paints it. Most WinForms controls are Win32 controls (
Buttonis aBUTTONwindow). With a comctl32 v6 manifest andApplication.EnableVisualStyles(),user32/comctl32paint them through uxtheme. - Managed, themed.
VisualStyleRendererand the per-control wrappers (ButtonRenderer,CheckBoxRenderer,ComboBoxRenderer,TabRenderer) callDrawThemeBackgroundfrom managed code. - Fully managed GDI+.
ControlPaintand theButtonInternaladapters draw the classic 3-D look from first principles whenever visual styles are off,FlatStyleisFlatorPopup, or the control is owner-drawn anyway (ToolStrip,DataGridView).
Application.RenderWithVisualStyles is the switch, and it is checked per paint, not once at start-up.
ControlPaint and HLSColor: The Ramp as an Algorithm¶
ControlPaint.Light, LightLight, Dark and DarkDark derive the bevel ramp from any face color, through a struct whose own comment says it copies the lightening and darkening logic from the Windows sources.12 Its shape:
- RGB is converted to integer HLS on a 0 to 240 range (
HLSMax = 240, the range of the Windows 3.x color picker), with luminosity(max + min) * 240 / 510, rounded. - Lightening moves luminosity toward a target scaled by
HighlightAdjustment = +500, darkening toward zero scaled byShadowAdjustment = -333. For a positiventhe rule islum * (1000 - n) / 1000 + 241 * n / 1000; for a negative onelum * (1000 + n) / 1000. - When the face is exactly
SystemColors.Control, it does not compute anything and returns the OS's ownControlLight,ControlLightLight,ControlDarkandControlDarkDark. The computed ramp and the shipped ramp disagree, and Microsoft picked the shipped one for the default scheme.
ButtonBaseAdapter.ColorOptions.Calculate() builds each button's ramp from that: the shadows and highlight from the system colors when the back color is Control, from Dark/DarkDark/LightLight otherwise, plus a LowButtonFace and LowHighlight 10 % brighter or darker depending on whether the face is bright or dark. That brightness test is how the classic look inverts itself on dark backgrounds instead of disappearing, and it is what makes user schemes like Hot Dog Stand or High Contrast Black work with the same code. Every color then still goes through hdc.FindNearestColor, a palette-era holdover.
FlatStyle: Four Looks, One Control¶
FlatStyle |
What it draws |
|---|---|
System |
Hands off to the Win32 control; the OS decides |
Standard |
Themed via ButtonRenderer with visual styles on, else the classic 2 px bevel from ControlPaint |
Flat |
1 px FlatAppearance.BorderColor border, flat fill, explicit hover and pressed colors |
Popup |
Flat at rest, raises to a 1 px bevel on hover: the Office 97 / IE4 toolbar idiom |
FlatStyle is a per-control choice of rendering strategy, orthogonal to the app-wide visual styles switch. Era belongs to the theme, but "this particular button is a flat toolbar button" belongs to the control.
ToolStrip and ProfessionalColors¶
ToolStripProfessionalRenderer with ProfessionalColorTable derives a whole Office 2003-style identity (MenuItemPressedGradientBegin/Middle/End, ButtonSelectedGradientBegin, SeparatorDark/Light and so on) from SystemColors, using the same ramp plus a color-scheme detector. It is a token-derivation function aimed at a different decade.
What .NET 9 and 10 Dark Mode Does¶
The newest evidence sits in ButtonInternal/DarkMode/. ModernButtonDarkModeRenderer is described as a "modern, WinUI-inspired push button renderer used when Control.VisualStylesMode is VisualStylesMode.Net11 or later", and it hand-draws Fluent geometry in GDI+:
| Property | Value |
|---|---|
| Corner radius | 8 logical px unfocused, 6 focused |
| Border thickness | 1 logical px |
| Focus ring | 2 px ring plus 1 px gap, white in dark mode |
| Dark normal / hover / pressed | #2D2D2D / #323232 / #2A2A2A, gap ring #0A0A0A |
| Light normal / hover / pressed | #FBFBFB / #F9F9F9 / #F5F5F5, border #D0D0D0 |
It also brings a ButtonBackColorAnimator for the state cross-fade. Microsoft's own answer to "make a Win32-era framework look like Windows 11" is to leave the theme engine alone and draw the Fluent geometry by hand. That is the approach libwinui takes natively, and it means the reverse direction, faithful older eras on a modern renderer, is where the open problem is.
Measured Windows 11 Behavior¶
The System Palette Is Frozen, Light-Only, and Not the Accent¶
Read with GetSysColor on a machine in dark mode (AppsUseLightTheme = 0, SystemUsesLightTheme = 0):
| Index | Value | Comment |
|---|---|---|
COLOR_BTNFACE |
#F0F0F0 |
The Windows 7 light gray, not #C0C0C0, not #D4D0C8, and not dark |
COLOR_WINDOW |
#FFFFFF |
White, in dark mode |
COLOR_WINDOWTEXT |
#000000 |
Black, in dark mode |
COLOR_BTNHIGHLIGHT |
#FFFFFF |
|
COLOR_3DLIGHT |
#E3E3E3 |
No longer equal to the face, so the ring collapse no longer happens |
COLOR_BTNSHADOW |
#A0A0A0 |
|
COLOR_3DDKSHADOW |
#696969 |
Not black any more |
COLOR_WINDOWFRAME |
#646464 |
|
COLOR_HIGHLIGHT |
#0078D7 |
The Windows 10 blue, frozen, not the user's accent |
COLOR_GRAYTEXT |
#6D6D6D |
|
COLOR_ACTIVECAPTION |
#99B4D1 |
The Windows 7 caption; DWM has drawn the caption since Vista |
COLOR_INFOBK |
#FFFFE1 |
The 1995 tooltip yellow, unchanged |
COLOR_MENU / MENUBAR |
#F0F0F0 |
|
COLOR_MENUHILIGHT |
#3399FF |
Meanwhile the DWM registry keys report the user's real accent: AccentColor = 0xFFD47800 (ABGR), i.e. #0078D4, and ColorizationColor = 0xC40078D4. Three conclusions:
GetSysColordoes not follow dark mode. A framework that builds its modern theme from it is wrong in dark mode by construction.COLOR_HIGHLIGHTis not the accent.#0078D7is not#0078D4, and the system value does not track the user setting. Selection color must come from the accent.GetSysColoris the right source for exactly one thing: high contrast, where the user picked those exact values on purpose.
libwinui follows all three. The Win32 theme source reads light/dark from AppsUseLightTheme and the accent from UISettings::GetColorValue, falling back to the DWM AccentColor key only when UISettings is unreachable (src/platform/windows/Win32ThemeSource.cpp). It reads GetSysColor only to fill HighContrastPalette while a contrast theme is on.
The DWM Layer¶
The app does not draw the frame, it configures it through DwmSetWindowAttribute (values from dwmapi.h):13
| Attribute | Value | Effect |
|---|---|---|
DWMWA_USE_IMMERSIVE_DARK_MODE |
20 | Dark caption and frame (19 before Windows 10 build 19041) |
DWMWA_WINDOW_CORNER_PREFERENCE |
33 | DWMWCP_DEFAULT, DONOTROUND, ROUND, ROUNDSMALL |
DWMWA_BORDER_COLOR |
34 | The 1 px frame line |
DWMWA_CAPTION_COLOR |
35 | Caption fill |
DWMWA_TEXT_COLOR |
36 | Caption text |
DWMWA_VISIBLE_FRAME_BORDER_THICKNESS |
37 | Reads the real border thickness |
DWMWA_SYSTEMBACKDROP_TYPE |
38 | DWMSBT_AUTO, NONE, MAINWINDOW (Mica), TRANSIENTWINDOW (Acrylic), TABBEDWINDOW |
Windows 11 rounds top-level windows by default, and a Windows 95 theme inside a rounded, drop-shadowed Windows 11 frame does not read as Windows 95. An era theme has to at least ask for DWMWCP_DONOTROUND.
Why a WinForms App Looks "Off" on Windows 11¶
Put together, a stock WinForms app today is a DWM-drawn, rounded Windows 11 frame around a Windows 7 gray #F0F0F0 client area from a frozen color table, holding Windows 8-era flat bitmap controls from aero.msstyles that ignore dark mode unless each HWND opts into an undocumented DarkMode_* class, set in Segoe UI 9 pt while the shell around it uses Segoe UI Variable, with a #0078D7 selection that ignores the user's #0078D4 accent. That is five visual eras in one window, and the reason a framework that renders every client pixel from one coherent theme does not inherit the layering.
Typography¶
| Metric | Value on the test machine |
|---|---|
CaptionFont, MenuFont, MessageFont, StatusFont, SmCaptionFont, IconFont |
All Segoe UI, lfWeight = 400, lfHeight = -18 at 144 DPI, i.e. 9 pt |
SystemFonts.MessageBoxFont |
Segoe UI, 9pt |
Segoe UI Variable Text |
Present |
Segoe UI Variable as a family name |
Absent; the families are Segoe UI Variable Text, Small and Display |
Segoe Fluent Icons, Segoe MDL2 Assets |
Present |
Tahoma |
Present, so an XP theme's font is the real one |
Microsoft Sans Serif |
Present |
MS Sans Serif, Fixedsys, Terminal, System |
Absent |
Win32 on Windows 11 is still Segoe UI 9 pt. Segoe UI Variable is a XAML and WinUI choice.
Era Catalog¶
What each era is, expressed as primitives a renderer needs, and whether it can be produced from an algorithm without shipping Microsoft assets:
| Era | Defining primitives | Font | Focus | Reproducible? |
|---|---|---|---|---|
| Windows 95 | Flat fills, 2 px DrawEdge bevels from a short ramp, square corners, 1 px XOR dotted focus, aliased everything, navy #000080 selection, teal desktop |
MS Sans Serif 8 pt (bitmap) | Dotted rect, inset | Yes, fully: an algorithm over a handful of colors |
| Windows 98 | As 95, plus hot tracking, gradient captions (GradientFill), flat toolbars |
MS Sans Serif 8 pt | Dotted rect | Yes |
| Windows 2000 | As 98, plus menu fade and slide, gradient captions on by default, warmer #D4D0C8 face |
Tahoma 8 pt | Dotted rect | Yes |
| Windows XP (Luna) | 3 to 5 stop vertical gradients, 3 px rounded buttons, #003C74 blue borders, amber hover, #ECE9D8 dialog face, glossy caption; image-based |
Tahoma 8 pt | Dotted rect | Homage only: the real thing is luna.msstyles bitmaps |
| Windows Vista / 7 (Aero) | DWM blur-behind glass with colorization, glossy two-tone buttons, animated hot-track cross-fade, glow text on glass, 32 bpp themed bitmaps | Segoe UI 9 pt | Dotted rect | Approximation: client-area glass was removed after 7 |
| Windows 8 (Metro) | Flat, no glass, no bevels, square, a saturated accent, thin borders | Segoe UI 9 pt | Dotted rect | Yes, flat is easy |
| Windows 10 (MDL2) | Flat plus accent plus 1 px borders plus Segoe MDL2 Assets glyphs; Reveal in UWP only |
Segoe UI 9 pt | Dotted rect (Win32), accent ring (UWP) | Yes |
| Windows 11 (Fluent) | 4 to 8 px corners, layered elevation stroke gradients, Mica and Acrylic, Segoe Fluent Icons, two-ring focus visual |
Segoe UI Variable (XAML), Segoe UI (Win32) | 2 px outer and 1 px inner ring | Yes |
The table is for the Win32 side of each era. The XAML-era pages (Metro, and the theme pages linked above) cover what Windows 8 and later looked like in their own frameworks.
What libwinui Can Express¶
The study started from a renderer with eight commands (filled and stroked rounded rects, their gradient variants, a line, a text run and an axis-aligned clip) and a Theme shaped entirely around buttons. Against the era catalog that setup had three problems worth more than all palette work put together: geometry went through an antialiasing rasterizer, text was always grayscale-smoothed, and layout produced fractional bounds. A Windows 95 button is defined by hard 1 px edges, so the colors were right while the rasterization came from the wrong century.
The capability list that came out of it, and where libwinui stands now:
| Capability | Needed by | libwinui now |
|---|---|---|
| Aliased geometry, aliased or grayscale or ClearType text, device-pixel snapping | Every classic era | SetRasterization{geometry, text, snap_to_device_pixels} (render/Renderer.hpp), emitted once per frame from Theme::rasterization |
Four-ring bevel with the right order, BF_SOFT, per-side flags |
95 / 98 / 2000 | draw_bevel_border() with BevelSides, soft and rings (render/Bevel.hpp); draw_win3x_border() for the asymmetric 3.x edge |
| 1-on-1-off dotted focus rect | 95 to 7 | draw_dotted_rect() with dash_length = 1 and FocusVisual::ClassicDotted (render/FocusRing.hpp) |
| 50 % stipple | Disabled dither, drag rect, scroll bar trough | FillRectPattern with Pattern::Checkerboard50 |
| Horizontal and diagonal gradients | XP and 98 captions, ToolStrip | GradientAxis::{Vertical, Horizontal, Diagonal} (render/Brush.hpp) |
| Radial gradients | FT_RADIALGRADIENT, the Aero orb |
RadialGradientBrush |
| Real ellipses | Classic circular radio bevel | FillEllipse, StrokeEllipse, PushClipEllipse |
| Engraved disabled text | Classic disabled labels | TextEffect{TextEffectKind::Emboss, ...} on TextStyle::effect (render/Text.hpp); no glow or shadow kind |
| Nine-slice bitmap chrome | Authentic XP / Vista | DrawImageNineSlice, backed by an internal image theme pack; libwinui ships no images |
| Group opacity | Fluent overlays | PushOpacityLayer / PopOpacityLayer |
| Backdrop blur | Aero glass, Mica, Acrylic | An AcrylicBrush fill blurs what was drawn before it; Mica comes from DWM |
| Drop shadow | Vista, Fluent flyouts | Approximated by stacked translucent shapes (render/OverlayShadow.hpp); there is no blur primitive for shadows |
Rasterization is a command in the stream, not hidden renderer state, so TestRenderer records it and the semantic goldens print it. "Does the Windows 95 theme ask for aliased rendering" is then a cross-platform assertion instead of something only a pixel golden can catch. Snapping happens in the Direct2D backend, at device pixels and at emit time, so the logical layout stays resolution-independent and headless tests do not move. The per-era policies:
| Themes | Geometry | Text | Snap |
|---|---|---|---|
| Early Windows, Windows 3.x, Windows 9x | Aliased | Aliased | Yes |
| Luna | Antialiased | Grayscale | No |
| Aero, MDL2, Fluent | Antialiased | Subpixel | No |
| Metro, Fluent 2 (the default rasterization) | Antialiased | Grayscale | No |
On the theme side the button-shaped struct got role colors, Theme::colors() (ThemeColors in ui/themes/Theme.hpp), whose fields mirror COLOR_* indices: field (COLOR_WINDOW), selection (COLOR_HIGHLIGHT), text_disabled and link. A second block, Theme::surfaces (Surfaces), holds the era-specific menu and tooltip colors, the etched separator pair and the scroll bar parts. Lists, trees, text fields and menus draw from those roles instead of borrowing a button's colors, which gives the classic eras their white wells on a gray dialog and navy-on-white selection.
The classic eras also got their own token tier, following the WinForms model. ClassicPalette (ui/themes/ClassicPalette.hpp) is modeled on the roughly twenty COLOR_* indices that carry the 3-D language, and Theme::classic() derives a whole Theme from one. classic_lighter() and classic_darker() port the integer-HLS rule described above, and classic_bevel_ramp_from_face() derives the four bevel colors from any face, with the same override WinForms uses: for the two canonical grays it returns the published ramps instead of the computed ones.
| Face | 3DLIGHT |
BTNHIGHLIGHT |
BTNSHADOW |
3DDKSHADOW |
Used by |
|---|---|---|---|---|---|
#C0C0C0 |
#DFDFDF |
#FFFFFF |
#808080 |
#000000 |
Windows 95, 98 SE (ClassicPalette::windows_standard()) |
#D4D0C8 |
#D4D0C8 |
#FFFFFF |
#808080 |
#404040 |
Windows 2000, XP Classic (ClassicPalette::windows_standard_2000()) |
| anything else | computed | computed | computed | computed | User schemes via ClassicPalette::from_face() |
A user scheme is one literal away: ThemeRequest::classic_scheme swaps any ClassicPalette into the Windows 9x variant. High-contrast classic schemes, where no lighten or darken of black gives the right ramp, set ClassicPalette::bevel directly.
Window chrome follows through Theme::chrome (ChromeHints), which the Win32 host applies with DwmSetWindowAttribute. Every pre-Fluent era asks for CornerPreference::DoNotRound and disallows a system backdrop.
What stays out of reach:
- Microsoft's bitmaps. XP and Vista are bitmap themes, so the shipped Luna and Aero packs are computed homages. The image backend exists, but only for originally authored or freely licensed art.
- Raster fonts. The Windows 9x theme asks for
MS Sans Serifand getsMicrosoft Sans Serif, which is outline-based. Short of bundling a licensed clone there is no fix. - Aero glass. The client-area glass API was removed after Windows 7. A translucent look-alike is the ceiling.
- The era's corners and shadow. With
Frame::Themedeach era pack draws its own caption and border, but the window stays a DWM-framed window. Its corners are square, never Luna's top-only rounding, and its shadow is Windows 11's.
Measured Data¶
All values read on Windows 11 Enterprise 26200 with SDK 10.0.26100.0, in dark mode.
GetSysColor, Complete¶
SCROLLBAR #C8C8C8 BTNFACE #F0F0F0
BACKGROUND #000000 BTNSHADOW #A0A0A0
ACTIVECAPTION #99B4D1 GRAYTEXT #6D6D6D
INACTIVECAPTION #BFCDDB BTNTEXT #000000
MENU #F0F0F0 INACTIVECAPTIONTEXT #000000
WINDOW #FFFFFF BTNHIGHLIGHT #FFFFFF
WINDOWFRAME #646464 3DDKSHADOW #696969
MENUTEXT #000000 3DLIGHT #E3E3E3
WINDOWTEXT #000000 INFOTEXT #000000
CAPTIONTEXT #000000 INFOBK #FFFFE1
ACTIVEBORDER #B4B4B4 HOTLIGHT #0066CC
INACTIVEBORDER #F4F7FC GRADIENTACTIVECAPTION #B9D1EA
APPWORKSPACE #ABABAB GRADIENTINACTIVECAPTION #D7E4F2
HIGHLIGHT #0078D7 MENUHILIGHT #3399FF
HIGHLIGHTTEXT #FFFFFF MENUBAR #F0F0F0
Theme and Accent State¶
HKCU\...\Themes\Personalize AppsUseLightTheme = 0 SystemUsesLightTheme = 0
HKCU\SOFTWARE\Microsoft\Windows\DWM
ColorizationColor = 0xC40078D4
AccentColor = 0xFFD47800 (ABGR, i.e. #0078D4)
ColorPrevalence = 0
aero.theme [VisualStyles] Path=...\Aero.msstyles SystemMode=Light AppMode=Light
dark.theme [VisualStyles] Path=...\Aero.msstyles SystemMode=Dark AppMode=Dark
aero.msstyles¶
size 1'859'728 bytes
embedded PNGs 1'614
DarkMode_* classes 69 (incl. DarkMode_Explorer::Button, DarkMode_DarkTheme::Button)
DarkMode:: classes Menu, Toolbar, ItemsView, Navigation, ProperTree, SearchBox,
PreviewPane, ReadingPane, ExplorerNavPane, TaskManager, ...
DPI ladder 19x21, 24x26, 29x32, 38x42, 47x52, 57x63, 76x84 (16 images each)
= 19 x {1.00, 1.26, 1.53, 2.00, 2.47, 3.00, 4.00}
= 100/125/150/200/250/300/400 % = TMT_MINDPI1..7
vertical strips 676 images with h >= 3w and w <= 40
Fonts¶
present : Microsoft Sans Serif, Tahoma, Segoe UI, Segoe UI Variable Text,
Segoe Fluent Icons, Segoe MDL2 Assets, Courier New (310 families)
absent : MS Sans Serif, Fixedsys, Terminal, System, "Segoe UI Variable"
system UI font : Segoe UI, weight 400, 9 pt (all six WindowMetrics LOGFONTs)
Constants Referenced¶
winuser.h COLOR_* 0..30 ; BDR_/EDGE_/BF_* ; DFC_/DFCS_*
wingdi.h GRADIENT_FILL_RECT_H=0, _RECT_V=1, _TRIANGLE=2 ; AC_SRC_OVER=0, AC_SRC_ALPHA=1
vssym32.h 257 TMT_* properties ; BGTYPE, FILLTYPE, BORDERTYPE, SIZINGTYPE,
IMAGELAYOUT, IMAGESELECTTYPE, GLYPHTYPE, TEXTSHADOWTYPE
vsstyle.h BUTTONPARTS, PUSHBUTTONSTATES (6), CHECKBOXSTATES (20), EDITPARTS (9),
EDITTEXTSTATES (8)
uxtheme.h DTBG_*, DTT_*, BPBF_*, BPPF_*, BP_ANIMATIONSTYLE
dwmapi.h DWMWA_USE_IMMERSIVE_DARK_MODE=20, WINDOW_CORNER_PREFERENCE=33,
BORDER_COLOR=34, CAPTION_COLOR=35, TEXT_COLOR=36,
VISIBLE_FRAME_BORDER_THICKNESS=37, SYSTEMBACKDROP_TYPE=38
Sources¶
Headers from the Windows SDK 10.0.26100.0: winuser.h, wingdi.h, uxtheme.h, vsstyle.h, vssym32.h, dwmapi.h. The WinForms sources referenced are in the dotnet/winforms repository under src/System.Windows.Forms/: ControlPaint.cs, ControlPaint.HLSColor.cs, Controls/Buttons/ButtonInternal/* and its DarkMode/ folder.
-
Microsoft Learn, Windows GDI. ↩
-
Microsoft Learn, DrawEdge function (winuser.h). ↩
-
Microsoft Learn, DrawFrameControl function (winuser.h). ↩
-
Microsoft Learn, GetSysColor function (winuser.h). ↩
-
Microsoft Learn, DrawFocusRect function (winuser.h). ↩
-
Microsoft Learn, DrawStateW function (winuser.h). ↩
-
Microsoft Learn, GradientFill, AlphaBlend and TransparentBlt. ↩
-
Microsoft Learn, Visual Styles and the uxtheme.h reference. ↩
-
Microsoft Learn, Property Identifiers and Parts and States. ↩
-
Microsoft Learn, DrawThemeTextEx function (uxtheme.h) and DTTOPTS structure. ↩
-
ControlPaint.HLSColor.csin dotnet/winforms, and Microsoft Learn, ControlPaint Class. ↩ -
Microsoft Learn, DWMWINDOWATTRIBUTE enumeration (dwmapi.h). ↩