Skip to content

Move the menu bar and the image list to NativeControls - #69

Merged
Menelion merged 1 commit into
masterfrom
native-controls-migration
Sep 8, 2026
Merged

Menelion merged 1 commit into
masterfrom
native-controls-migration

Conversation

@Menelion

@Menelion Menelion commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #68

Summary

Both controls are now native rather than WinForms-drawn.

The menu was a MenuStrip — a panel drawing things that resemble menu items, with an accessibility tree assembled alongside. It is now a real HMENU attached with SetMenu, announced as a menu bar with real submenus.

The image list was a WinForms ListView, which is a SysListView32 but wears a window class no screen reader recognizes — both UI Automation and NVDA select their list handling by class name. It is now the real control, so every column is read rather than only the first.

Verified against Windows, not the source

HMENU on the form: 0x7C0FEF        <- a real native menu
top-level menu items: 4
  [0] '&File'  [1] '&Edit'  [2] '&Convert'  [3] '&Help'

list window class: 'SysListView32'  <- not WindowsForms10.SysListView32.app.0…
columns: File Name | Format | Dimensions | Size | Status

What the menu change removes

The menu is described as a spec in one place, instead of a designer block plus a separate Click wiring block. Enabled state lives in the spec and is read on every popup, so UpdateMenuState sets two flags rather than pushing enabled state into fifteen controls.

RefreshShortcutKeys is gone. It existed to make ToolStrip re-render accelerator text after a language change; a native menu takes its display string from the spec, so rebuilding the spec is the whole job.

Three mechanical translations

  • SelectedIndicesSelectedItems, which carries the rows themselves; row.Index where an index is still wanted.
  • AutoResizeColumnsAutoSizeListColumns, because an auto-size is a width you assign rather than a method you call.
  • ListViewItem + five SubItems.Add → one NativeListViewItem constructor, in BuildRow.

Two subtler ones, both easy to miss

imageListView.Focused is now false while the list has focus, because focus lives on the native child window. Measured: Focused = False, ContainsFocus = True. The Delete-key guard would have silently stopped working; it now asks ContainsFocus.

Column headers are no longer controls, so the GetText catalog walk in ApplyLocalization cannot reach them the way it reached ColumnHeader.Text. They are localized explicitly in LocalizeColumns. Every string was already in the catalogs, so no translation is lost — including Hebrew.

Worth a listen before merging

This is an accessibility change, so the real test is by ear: Alt to open the menu bar, arrow through it, and confirm the list reads all five columns rather than just the file name.

Also worth checking Program.cs:31, which swallows NullReferenceExceptions from "ListView.WndProc / ListView.Unhook races with accessibility". That workaround targets the WinForms accessibility layer this change removes, so it may now be unnecessary — the logs record TargetSite.Name on each suppression, which would confirm it.

https://claude.ai/code/session_01TJ8i7jHmkUjVCjVccLp6Hf

Both controls are now native rather than WinForms-drawn, which is what
screen readers need from them.

The menu was a MenuStrip: a panel that draws things resembling menu items,
with an accessibility tree assembled alongside. It is now a real HMENU
attached with SetMenu, announced as a menu bar with real submenus. The menu
is described as a spec in one place instead of a designer block plus a
separate wiring block, so the whole structure reads at once. Enabled state
lives in the spec and is read on every popup, which is why UpdateMenuState
no longer pushes flags into fifteen controls.

RefreshShortcutKeys is gone with it. It existed to make ToolStrip
re-render accelerator text after a language change; a native menu takes its
display string from the spec, so rebuilding the spec is the whole job.

The image list was a WinForms ListView, which is a SysListView32 wearing a
window class no screen reader recognizes - both UI Automation and NVDA pick
their list handling by class name. It is now the real control, so every
column is read rather than just the first.

Three things had no direct counterpart and were translated:

- SelectedIndices became SelectedItems, which carries the rows themselves;
  an index is row.Index where one is still wanted.
- AutoResizeColumns became AutoSizeListColumns, because an auto-size is a
  width you assign rather than a method you call.
- ListViewItem plus five SubItems.Add became one NativeListViewItem
  constructor, in BuildRow.

Two subtler ones, both easy to miss:

- imageListView.Focused is false while the list has focus, because focus
  lives on the native child window. The Delete-key guard now asks
  ContainsFocus, which is true.
- Column headers are no longer controls, so the catalog walk in
  ApplyLocalization cannot reach them. They are localized explicitly in
  LocalizeColumns. Every string was already in the catalogs, so no
  translation is lost.

Verified against Windows rather than the source: GetMenu returns a real
HMENU with four top-level items, and the list window's class name is
SysListView32.

Fixes #68

Claude-Session: https://claude.ai/code/session_01TJ8i7jHmkUjVCjVccLp6Hf
@Menelion
Menelion merged commit 1e6fcda into master Sep 8, 2026
1 check passed
@Menelion
Menelion deleted the native-controls-migration branch September 8, 2026 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Leverage NativeControls for menus and list view

1 participant