Keyboard cues are disabled by default on newer versions of Windows. Some win32 common controls, e.g., button doesn't draw a focus rect when SystemParametersInfoW(SPI_GETKEYBOARDCUES) reports FALSE.
Drawing a focus rect when the button is really small creates an effect that makes the button look corrupted. On Windows, the focus rect is not drawn when keyboard cues are disabled, which is the default.
Other controls also have this behavior, so they're included as well. There are some exceptions, for example, SysMonthCal32 always draws a focus rect, even when keyboard cues are disabled.
From: Zhiyi Zhang zzhang@codeweavers.com
Keyboard cues are disabled by default on newer versions of Windows. Some win32 common controls, e.g., button doesn't draw a focus rect when SystemParametersInfoW(SPI_GETKEYBOARDCUES) reports FALSE. --- dlls/win32u/sysparams.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dlls/win32u/sysparams.c b/dlls/win32u/sysparams.c index 463b6b00f68..55234fea97f 100644 --- a/dlls/win32u/sysparams.c +++ b/dlls/win32u/sysparams.c @@ -5262,7 +5262,7 @@ static WCHAR desk_wallpaper_path[MAX_PATH]; static PATH_ENTRY( DESKPATTERN, DESKTOP_KEY, "Pattern", desk_pattern_path ); static PATH_ENTRY( DESKWALLPAPER, DESKTOP_KEY, "Wallpaper", desk_wallpaper_path );
-static BYTE user_prefs[8] = { 0x30, 0x00, 0x00, 0x80, 0x12, 0x00, 0x00, 0x00 }; +static BYTE user_prefs[8] = { 0x10, 0x00, 0x00, 0x80, 0x12, 0x00, 0x00, 0x00 }; static BINARY_ENTRY( USERPREFERENCESMASK, user_prefs, DESKTOP_KEY, "UserPreferencesMask" );
static FONT_ENTRY( CAPTIONLOGFONT, FW_BOLD, METRICS_KEY, "CaptionFont" );
From: Zhiyi Zhang zzhang@codeweavers.com
--- dlls/user32/tests/sysparams.c | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+)
diff --git a/dlls/user32/tests/sysparams.c b/dlls/user32/tests/sysparams.c index 870b7f1cc93..1234a4766d0 100644 --- a/dlls/user32/tests/sysparams.c +++ b/dlls/user32/tests/sysparams.c @@ -2574,6 +2574,38 @@ static void test_WM_DISPLAYCHANGE(void) displaychange_test_active = FALSE; }
+static void test_SPI_SETKEYBOARDCUES( void ) /* 0x100B */ +{ + BOOL ret, values[2], result; + unsigned int i; + + trace( "testing SPI_{GET,SET}KEYBOARDCUES\n" ); + SetLastError( 0xdeadbeef ); + ret = SystemParametersInfoA( SPI_GETKEYBOARDCUES, 0, &result, 0 ); + if (!test_error_msg( ret, "SPI_{GET,SET}KEYBOARDCUES" )) + return; + ok( result == FALSE, "Expected keyboard cues disabled by default.\n" ); + values[1] = result; + values[0] = !result; + + for (i = 0; i < ARRAY_SIZE( values ); i++) + { + ret = SystemParametersInfoA( SPI_SETKEYBOARDCUES, 0, IntToPtr(values[i]), SPIF_UPDATEINIFILE | SPIF_SENDCHANGE ); + if (!test_error_msg( ret, "SPI_SETKEYBOARDCUES" )) + break; + ok( ret, "%d: ret=%d err=%ld\n", i, ret, GetLastError() ); + test_change_message( SPI_SETKEYBOARDCUES, 1 ); + + ret = SystemParametersInfoA( SPI_GETKEYBOARDCUES, 0, &result, 0 ); + ok( ret, "%d: ret=%d err=%ld\n", i, ret, GetLastError() ); + eq( result, values[i], "SPI_GETKEYBOARDCUES", "%d" ); + } + + ret = SystemParametersInfoA( SPI_SETKEYBOARDCUES, 0, IntToPtr(values[1]), SPIF_UPDATEINIFILE ); + ok( ret, "***warning*** failed to restore the original value: ret=%d err=%ld\n", ret, GetLastError()); + flush_change_messages(); +} + /* * Registry entries for the system parameters. * Names are created by 'SET' flags names. @@ -2625,6 +2657,7 @@ static DWORD WINAPI SysParamsThreadFunc( LPVOID lpParam ) test_SPI_SETMENUSHOWDELAY(); /* 107 */ test_SPI_SETWHEELSCROLLCHARS(); /* 108 */ test_SPI_SETWALLPAPER(); /* 115 */ + test_SPI_SETKEYBOARDCUES(); /* 0x100B */
SendMessageA( ghTestWnd, WM_DESTROY, 0, 0 );
From: Zhiyi Zhang zzhang@codeweavers.com
Drawing a focus rect when the button is really small creates an effect that makes the button look corrupted. On Windows, the focus rect is not drawn when keyboard cues are disabled, which is the default. --- dlls/comctl32/button.c | 18 +++++++++--------- dlls/comctl32/comctl32.h | 1 + dlls/comctl32/commctrl.c | 3 +++ 3 files changed, 13 insertions(+), 9 deletions(-)
diff --git a/dlls/comctl32/button.c b/dlls/comctl32/button.c index a0eb41fec20..2e009aa222d 100644 --- a/dlls/comctl32/button.c +++ b/dlls/comctl32/button.c @@ -1960,7 +1960,7 @@ static void PB_Paint( const BUTTON_INFO *infoPtr, HDC hDC, UINT action ) } if ((cdrf & CDRF_SKIPPOSTPAINT) || dtFlags == (UINT)-1L) goto cleanup;
- if (action == ODA_FOCUS || (state & BST_FOCUS)) + if (COMCTL32_keyboard_cues_enabled && (action == ODA_FOCUS || (state & BST_FOCUS))) { InflateRect( &rc, -2, -2 ); DrawFocusRect( hDC, &rc ); @@ -2133,7 +2133,7 @@ static void CB_Paint( const BUTTON_INFO *infoPtr, HDC hDC, UINT action ) if ((cdrf & CDRF_SKIPPOSTPAINT) || dtFlags == (UINT)-1L) goto cleanup;
/* ... and focus */ - if (action == ODA_FOCUS || (state & BST_FOCUS)) + if (COMCTL32_keyboard_cues_enabled && (action == ODA_FOCUS || (state & BST_FOCUS))) { labelRect.left--; labelRect.right++; @@ -2274,7 +2274,7 @@ static void UB_Paint( const BUTTON_INFO *infoPtr, HDC hDC, UINT action ) SendMessageW(parent, WM_NOTIFY, nmcd.hdr.idFrom, (LPARAM)&nmcd); }
- if (!(cdrf & CDRF_SKIPPOSTPAINT)) + if (COMCTL32_keyboard_cues_enabled && !(cdrf & CDRF_SKIPPOSTPAINT)) DrawFocusRect( hDC, &rc ); }
@@ -2459,7 +2459,7 @@ static void SB_Paint( const BUTTON_INFO *infoPtr, HDC hDC, UINT action ) } if ((cdrf & CDRF_SKIPPOSTPAINT) || dtFlags == (UINT)-1L) goto cleanup;
- if (action == ODA_FOCUS || (state & BST_FOCUS)) + if (COMCTL32_keyboard_cues_enabled && (action == ODA_FOCUS || (state & BST_FOCUS))) DrawFocusRect(hDC, &push_rect);
cleanup: @@ -2732,7 +2732,7 @@ static void CL_Paint( const BUTTON_INFO *infoPtr, HDC hDC, UINT action ) } if (cdrf & CDRF_SKIPPOSTPAINT) goto cleanup;
- if (action == ODA_FOCUS || (state & BST_FOCUS)) + if (COMCTL32_keyboard_cues_enabled && (action == ODA_FOCUS || (state & BST_FOCUS))) { InflateRect(&rc, -2, -2); DrawFocusRect(hDC, &rc); @@ -2810,7 +2810,7 @@ static void PB_ThemedPaint(HTHEME theme, const BUTTON_INFO *infoPtr, HDC hDC, in } if (cdrf & CDRF_SKIPPOSTPAINT) return;
- if (focused) DrawFocusRect(hDC, &focusRect); + if (COMCTL32_keyboard_cues_enabled && focused) DrawFocusRect(hDC, &focusRect); }
static void CB_ThemedPaint(HTHEME theme, const BUTTON_INFO *infoPtr, HDC hDC, int state, UINT dtFlags, BOOL focused) @@ -2917,7 +2917,7 @@ static void CB_ThemedPaint(HTHEME theme, const BUTTON_INFO *infoPtr, HDC hDC, in } if ((cdrf & CDRF_SKIPPOSTPAINT) || dtFlags == (UINT)-1L) goto cleanup;
- if (focused) + if (COMCTL32_keyboard_cues_enabled && focused) { label_rect.left--; label_rect.right++; @@ -3108,7 +3108,7 @@ static void SB_ThemedPaint(HTHEME theme, const BUTTON_INFO *infoPtr, HDC hDC, in } if (cdrf & CDRF_SKIPPOSTPAINT) return;
- if (focused) DrawFocusRect(hDC, &focus_rect); + if (COMCTL32_keyboard_cues_enabled && focused) DrawFocusRect(hDC, &focus_rect); }
static void CL_ThemedPaint(HTHEME theme, const BUTTON_INFO *infoPtr, HDC hDC, int state, UINT dtFlags, BOOL focused) @@ -3217,7 +3217,7 @@ static void CL_ThemedPaint(HTHEME theme, const BUTTON_INFO *infoPtr, HDC hDC, in } if (cdrf & CDRF_SKIPPOSTPAINT) return;
- if (focused) + if (COMCTL32_keyboard_cues_enabled && focused) { MARGINS margins;
diff --git a/dlls/comctl32/comctl32.h b/dlls/comctl32/comctl32.h index a9a0b44fcd1..ae65fdcd656 100644 --- a/dlls/comctl32/comctl32.h +++ b/dlls/comctl32/comctl32.h @@ -37,6 +37,7 @@
extern HMODULE COMCTL32_hModule; extern HBRUSH COMCTL32_hPattern55AABrush; +extern BOOL COMCTL32_keyboard_cues_enabled;
/* Property sheet / Wizard */ #define IDD_PROPSHEET 1006 diff --git a/dlls/comctl32/commctrl.c b/dlls/comctl32/commctrl.c index 6097b1a9735..27e58eb7ae0 100644 --- a/dlls/comctl32/commctrl.c +++ b/dlls/comctl32/commctrl.c @@ -81,6 +81,7 @@ HMODULE COMCTL32_hModule = 0; static LANGID COMCTL32_uiLang = MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL); HBRUSH COMCTL32_hPattern55AABrush = NULL; COMCTL32_SysColor comctl32_color; +BOOL COMCTL32_keyboard_cues_enabled = FALSE;
static HBITMAP COMCTL32_hPattern55AABitmap = NULL;
@@ -171,6 +172,8 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
COMCTL32_hModule = hinstDLL;
+ SystemParametersInfoW (SPI_GETKEYBOARDCUES, 0, &COMCTL32_keyboard_cues_enabled, 0); + /* add global subclassing atom (used by 'tooltip' and 'updown') */ COMCTL32_wSubclass = (LPWSTR)(DWORD_PTR)GlobalAddAtomW (strCC32SubclassInfo); TRACE("Subclassing atom added: %p\n", COMCTL32_wSubclass);
From: Zhiyi Zhang zzhang@codeweavers.com
--- dlls/comctl32/combo.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dlls/comctl32/combo.c b/dlls/comctl32/combo.c index 1094132826b..a984adc0d5d 100644 --- a/dlls/comctl32/combo.c +++ b/dlls/comctl32/combo.c @@ -716,7 +716,7 @@ static void CBPaintText(HEADCOMBO *lphc, HDC hdc_paint) &rectEdit, pText ? pText : L"" , size, NULL );
- if(lphc->wState & CBF_FOCUSED && !(lphc->wState & CBF_DROPPED)) + if(COMCTL32_keyboard_cues_enabled && lphc->wState & CBF_FOCUSED && !(lphc->wState & CBF_DROPPED)) DrawFocusRect( hdc, &rectEdit ); }
From: Zhiyi Zhang zzhang@codeweavers.com
--- dlls/comctl32/listbox.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/dlls/comctl32/listbox.c b/dlls/comctl32/listbox.c index 3d55fc2ad83..fcdd4dc1792 100644 --- a/dlls/comctl32/listbox.c +++ b/dlls/comctl32/listbox.c @@ -624,7 +624,10 @@ static void LISTBOX_PaintItem( LB_DESCR *descr, HDC hdc, const RECT *rect, if (index >= descr->nb_items) { if (action == ODA_FOCUS) - DrawFocusRect( hdc, rect ); + { + if (COMCTL32_keyboard_cues_enabled) + DrawFocusRect( hdc, rect ); + } else ERR("called with an out of bounds index %d(%d) in owner draw, Not good.\n",index,descr->nb_items); return; @@ -664,7 +667,8 @@ static void LISTBOX_PaintItem( LB_DESCR *descr, HDC hdc, const RECT *rect,
if (action == ODA_FOCUS) { - DrawFocusRect( hdc, rect ); + if (COMCTL32_keyboard_cues_enabled) + DrawFocusRect( hdc, rect ); return; } if (selected) @@ -697,7 +701,7 @@ static void LISTBOX_PaintItem( LB_DESCR *descr, HDC hdc, const RECT *rect, SetBkColor( hdc, oldBk ); SetTextColor( hdc, oldText ); } - if (focused) + if (COMCTL32_keyboard_cues_enabled && focused) DrawFocusRect( hdc, rect ); } }
From: Zhiyi Zhang zzhang@codeweavers.com
--- dlls/comctl32/listview.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/dlls/comctl32/listview.c b/dlls/comctl32/listview.c index b2856db471c..925574d9f59 100644 --- a/dlls/comctl32/listview.c +++ b/dlls/comctl32/listview.c @@ -1738,7 +1738,7 @@ static inline BOOL LISTVIEW_GetItemW(const LISTVIEW_INFO *infoPtr, LPLVITEMW lpL /* used to handle collapse main item column case */ static inline BOOL LISTVIEW_DrawFocusRect(const LISTVIEW_INFO *infoPtr, HDC hdc) { - return (infoPtr->rcFocus.left < infoPtr->rcFocus.right) ? + return (COMCTL32_keyboard_cues_enabled && infoPtr->rcFocus.left < infoPtr->rcFocus.right) ? DrawFocusRect(hdc, &infoPtr->rcFocus) : FALSE; }
@@ -5310,7 +5310,7 @@ enddraw: LISTVIEW_RefreshReportGrid(infoPtr, hdc);
/* Draw marquee rectangle if appropriate */ - if (infoPtr->bMarqueeSelect) + if (COMCTL32_keyboard_cues_enabled && infoPtr->bMarqueeSelect) DrawFocusRect(hdc, &infoPtr->marqueeDrawRect);
if (cdmode & CDRF_NOTIFYPOSTPAINT)
It's more complicated than that. There are dedicated messages WM_QUERYUISTATE/WM_UPDATEUISTATE/WM_CHANGEUISTATE. So this state is dynamic, but probably changes only one way. On modern Windows you don't get focus rectangles by default on buttons, but you can get them by pressing "Alt", that's how it worked from when this UI change was introduced. It applies both for focus rectangles and keyboard accelerators.
What I imagine we need is to send WM_CHANGEUISTATE to a top level window, let default procedure propagate it using WM_UPDATEUISTATE, and then react accordingly in controls.
Button v6 (or maybe all controls in v6 variants) works differently - newly created button does not have a focus rectangle, until you press Alt. User32 button "inherits" this state probably, and creating new button shows focus rectangle right away.
Nikolay Sivov (@nsivov) commented about dlls/comctl32/listview.c:
LISTVIEW_RefreshReportGrid(infoPtr, hdc); /* Draw marquee rectangle if appropriate */
- if (infoPtr->bMarqueeSelect)
- if (COMCTL32_keyboard_cues_enabled && infoPtr->bMarqueeSelect) DrawFocusRect(hdc, &infoPtr->marqueeDrawRect);
I don't see this on Windows.
Nikolay Sivov (@nsivov) commented about dlls/comctl32/listview.c:
/* used to handle collapse main item column case */ static inline BOOL LISTVIEW_DrawFocusRect(const LISTVIEW_INFO *infoPtr, HDC hdc) {
- return (infoPtr->rcFocus.left < infoPtr->rcFocus.right) ?
- return (COMCTL32_keyboard_cues_enabled && infoPtr->rcFocus.left < infoPtr->rcFocus.right) ? DrawFocusRect(hdc, &infoPtr->rcFocus) : FALSE;
}
Same for this one. Afaict focus rectangle is always visible.
On Tue Aug 19 15:11:05 2025 +0000, Nikolay Sivov wrote:
It's more complicated than that. There are dedicated messages WM_QUERYUISTATE/WM_UPDATEUISTATE/WM_CHANGEUISTATE. So this state is dynamic, but probably changes only one way. On modern Windows you don't get focus rectangles by default on buttons, but you can get them by pressing "Alt", that's how it worked from when this UI change was introduced. It applies both for focus rectangles and keyboard accelerators. What I imagine we need is to send WM_CHANGEUISTATE to a top level window, let default procedure propagate it using WM_UPDATEUISTATE, and then react accordingly in controls. Button v6 (or maybe all controls in v6 variants) works differently - newly created button does not have a focus rectangle, until you press Alt. User32 button "inherits" this state probably, and creating new button shows focus rectangle right away.
I am aware of WM_UPDATEUISTATE, but I didn't know that pressing Alt could turn on the focus rectangles dynamically. And I thought since we don't support WM_QUERYUISTATE/WM_UPDATEUISTATE/WM_CHANGEUISTATE in Wine at the moment, we could just use the sysparam state.
On Windows 10 22H2, you call SystemParametersInfoW(SPI_SETKEYBOARDCUES, 0, 1, 0) to turn on keyboard cues. And then comctl32 v6 buttons will have a focus rectangle by default, without pressing Alt. This seems to contradict what you said about comctl32 v6 button not showing a focus rectangle until Alt is pressed.
Even with Alt being able to turn on focus rectangles dynamically, I am not sure it's worth it to add support for WM_QUERYUISTATE/WM_UPDATEUISTATE/WM_CHANGEUISTATE and Alt handling in DefWindowProc(). The focus state can be manifested by the theme part state now. So having a focus rectangle is actually redundant.
Anyway, what I am trying to say is that I would like to delay implementing WM_QUERYUISTATE/WM_UPDATEUISTATE/WM_CHANGEUISTATE and Alt handling until a real-world application needs it.
On Mon Aug 18 17:29:35 2025 +0000, Nikolay Sivov wrote:
Same for this one. Afaict focus rectangle is always visible.

I can see the focus rectangle being disabled on Win10 22H2. Which Windows version did you use?
On Tue Aug 19 15:11:05 2025 +0000, Zhiyi Zhang wrote:
I am aware of WM_UPDATEUISTATE, but I didn't know that pressing Alt could turn on the focus rectangles dynamically. And I thought since we don't support WM_QUERYUISTATE/WM_UPDATEUISTATE/WM_CHANGEUISTATE in Wine at the moment, we could just use the sysparam state. On Windows 10 22H2, you call SystemParametersInfoW(SPI_SETKEYBOARDCUES, 0, 1, 0) to turn on keyboard cues. And then comctl32 v6 buttons will have a focus rectangle by default, without pressing Alt. This seems to contradict what you said about comctl32 v6 button not showing a focus rectangle until Alt is pressed. Even with Alt being able to turn on focus rectangles dynamically, I am not sure it's worth it to add support for WM_QUERYUISTATE/WM_UPDATEUISTATE/WM_CHANGEUISTATE and Alt handling in DefWindowProc(). The focus state can be manifested by the theme part state now. So having a focus rectangle is actually redundant. Anyway, what I am trying to say is that I would like to delay implementing WM_QUERYUISTATE/WM_UPDATEUISTATE/WM_CHANGEUISTATE and Alt handling until a real-world application needs it.
I haven't tried with SPI_SETKEYBOARDCUES, I was talking about defaults.
Disabling cues unconditionally or by this parameter that users don't interact with, is wrong. This will make it impossible for people to use keyboard navigation in dialogs for example.
Not implementing this with correct messages will also potentially create inconsistent behavior between any custom controls, and system provided controls.
On Tue Aug 19 15:11:05 2025 +0000, Zhiyi Zhang wrote:
 I can see the focus rectangle being disabled on Win10 22H2. Which Windows version did you use?
I used Windows 11 21H2. Right from the start it won't show a focus rectangle, but moving to another item with keyboard shows it.
On Tue Aug 19 15:26:17 2025 +0000, Nikolay Sivov wrote:
I used Windows 11 21H2. Right from the start it won't show a focus rectangle, but moving to another item with keyboard shows it.
I see. Thanks.
On Tue Aug 19 15:23:30 2025 +0000, Nikolay Sivov wrote:
I haven't tried with SPI_SETKEYBOARDCUES, I was talking about defaults. Disabling cues unconditionally or by this parameter that users don't interact with, is wrong. This will make it impossible for people to use keyboard navigation in dialogs for example. Not implementing this with correct messages will also potentially create inconsistent behavior between any custom controls, and system provided controls.
Okay. I will see if the customer is willing to cover the cost.