Created
July 8, 2026 07:03
-
-
Save ji6czd/d398fc63ac418e30cb93fc2cc2f73884 to your computer and use it in GitHub Desktop.
dotnet10.0 with screen reader test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Runtime.InteropServices; | |
| namespace AltMenuTest; | |
| static class Program | |
| { | |
| [STAThread] | |
| static void Main(string[] args) | |
| { | |
| Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); | |
| Application.EnableVisualStyles(); | |
| Application.SetCompatibleTextRenderingDefault(false); | |
| Application.Run(new MainForm(args.Contains("--auto"))); | |
| } | |
| } | |
| /// <summary> | |
| /// Minimal form for screen reader testing: a WinForms MenuStrip plus a TextBox, | |
| /// with no keyboard handling customization at all (KeyPreview stays at the | |
| /// default false). Key messages delivered to the TextBox, WM_SYSCOMMAND | |
| /// SC_KEYMENU delivered to the form, and MenuStrip activation events are | |
| /// logged to the bottom pane and to a log file (alt_test_log.txt next to | |
| /// the executable). | |
| /// The "Classic" menu opens a comparison window that uses a classic Win32 | |
| /// HMENU menu bar instead of a MenuStrip. | |
| /// With the --auto switch, the app synthesizes real Alt / F10 key input, | |
| /// records the results, and exits automatically. | |
| /// </summary> | |
| public class MainForm : Form | |
| { | |
| private readonly bool _auto; | |
| private readonly MenuStrip _menu = new(); | |
| private readonly LoggingTextBox _txt = new() { Multiline = true, Dock = DockStyle.Fill, Text = "TextBox" }; | |
| private readonly TextBox _log = new() | |
| { | |
| Multiline = true, ReadOnly = true, Dock = DockStyle.Bottom, | |
| Height = 280, ScrollBars = ScrollBars.Vertical, TabStop = false, | |
| }; | |
| private readonly string _logPath; | |
| private bool _menuActivated; | |
| [DllImport("user32.dll")] | |
| private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, nuint dwExtraInfo); | |
| [DllImport("user32.dll")] | |
| private static extern nint GetFocus(); | |
| [DllImport("user32.dll")] | |
| private static extern nint GetForegroundWindow(); | |
| private const byte VK_MENU = 0x12, VK_F10 = 0x79, VK_ESCAPE = 0x1B; | |
| private const uint KEYEVENTF_KEYUP = 0x02; | |
| private string FocusName() | |
| { | |
| nint h = GetFocus(); | |
| if (h == 0) return "(none / other thread)"; | |
| if (h == _txt.Handle) return "TextBox"; | |
| if (h == _menu.Handle) return "MenuStrip"; | |
| if (h == Handle) return "Form"; | |
| return $"0x{h:X}"; | |
| } | |
| public MainForm(bool auto) | |
| { | |
| _auto = auto; | |
| Text = "AltMenuTest — MenuStrip + TextBox"; | |
| Width = 800; Height = 650; | |
| _logPath = Path.Combine(AppContext.BaseDirectory, "alt_test_log.txt"); | |
| File.WriteAllText(_logPath, ""); | |
| var file = new ToolStripMenuItem("&File"); | |
| file.DropDownItems.Add(new ToolStripMenuItem("&Dummy")); | |
| file.DropDownItems.Add(new ToolStripMenuItem("E&xit", null, (_, _) => Close())); | |
| var edit = new ToolStripMenuItem("&Edit"); | |
| edit.DropDownItems.Add(new ToolStripMenuItem("Dummy 2")); | |
| var classic = new ToolStripMenuItem("&Classic"); | |
| classic.DropDownItems.Add(new ToolStripMenuItem("&Open HMENU window", null, | |
| (_, _) => new ClassicMenuForm(Log).Show(this))); | |
| _menu.Items.Add(file); | |
| _menu.Items.Add(edit); | |
| _menu.Items.Add(classic); | |
| MainMenuStrip = _menu; | |
| Controls.Add(_txt); | |
| Controls.Add(_log); | |
| Controls.Add(_menu); | |
| _menu.MenuActivate += (_, _) => { _menuActivated = true; Log("MenuStrip.MenuActivate raised"); }; | |
| _menu.MenuDeactivate += (_, _) => Log("MenuStrip.MenuDeactivate raised"); | |
| _txt.MsgLog += Log; | |
| _txt.Enter += (_, _) => Log("focus -> TextBox"); | |
| _menu.Enter += (_, _) => Log("focus -> MenuStrip"); | |
| } | |
| protected override void WndProc(ref Message m) | |
| { | |
| if (m.Msg == 0x0112 && ((int)m.WParam & 0xFFF0) == 0xF100) // WM_SYSCOMMAND / SC_KEYMENU | |
| Log($"Form WM_SYSCOMMAND SC_KEYMENU lParam=0x{(long)m.LParam:X}"); | |
| base.WndProc(ref m); | |
| } | |
| private void Log(string s) | |
| { | |
| var line = $"{DateTime.Now:HH:mm:ss.fff} {s}"; | |
| File.AppendAllText(_logPath, line + Environment.NewLine); | |
| if (_log.IsHandleCreated) _log.AppendText(line + Environment.NewLine); | |
| } | |
| protected override async void OnShown(EventArgs e) | |
| { | |
| base.OnShown(e); | |
| _txt.Focus(); | |
| if (!_auto) return; | |
| try | |
| { | |
| Activate(); | |
| await Task.Delay(600); | |
| await RunScenario("Alt @ TextBox (1st)", VK_MENU, 0x38); | |
| await RunScenario("Alt @ TextBox (2nd)", VK_MENU, 0x38); | |
| await RunScenario("F10 @ TextBox", VK_F10, 0x44); | |
| Log("DONE"); | |
| } | |
| catch (Exception ex) | |
| { | |
| Log("ERROR " + ex); | |
| } | |
| await Task.Delay(200); | |
| Close(); | |
| } | |
| private async Task RunScenario(string name, byte vk, byte scan) | |
| { | |
| Log($"---- {name} ----"); | |
| _menuActivated = false; | |
| _txt.Focus(); | |
| await Task.Delay(250); | |
| // Note: Control.ContainsFocus is not sufficient here — it only checks | |
| // this thread's focus window, which is set even when another process | |
| // is in the foreground. Injected keys would then leak to that process. | |
| if (GetForegroundWindow() != Handle) | |
| { | |
| Log("SKIP: form is not the foreground window; not sending keys"); | |
| return; | |
| } | |
| keybd_event(vk, scan, 0, 0); | |
| await Task.Delay(60); | |
| keybd_event(vk, scan, KEYEVENTF_KEYUP, 0); | |
| await Task.Delay(400); | |
| Log($"Result: MenuActivate={_menuActivated}, menuFocused={_menu.ContainsFocus}, " + | |
| $"Items[0].Selected={_menu.Items[0].Selected}, Win32Focus={FocusName()}"); | |
| if (_menuActivated || _menu.ContainsFocus) | |
| { | |
| // Dismiss menu mode before the next scenario. | |
| keybd_event(VK_ESCAPE, 0x01, 0, 0); | |
| keybd_event(VK_ESCAPE, 0x01, KEYEVENTF_KEYUP, 0); | |
| await Task.Delay(200); | |
| } | |
| } | |
| } | |
| /// <summary> | |
| /// A TextBox that logs the key messages it receives. | |
| /// prev: lParam bit 30 (previous key state; 1 = key was already down) | |
| /// altctx: lParam bit 29 (context code; 1 = Alt was held down) | |
| /// </summary> | |
| public class LoggingTextBox : TextBox | |
| { | |
| public event Action<string>? MsgLog; | |
| protected override void WndProc(ref Message m) | |
| { | |
| if (m.Msg is 0x0100 or 0x0101 or 0x0104 or 0x0105 or 0x0106) | |
| MsgLog?.Invoke($"{Tag as string ?? "TXT "} {MsgName(m.Msg)} vk=0x{(long)m.WParam:X2}" + | |
| $" prev={((long)m.LParam >> 30) & 1} altctx={((long)m.LParam >> 29) & 1}"); | |
| base.WndProc(ref m); | |
| } | |
| internal static string MsgName(int m) => m switch | |
| { | |
| 0x0100 => "WM_KEYDOWN ", | |
| 0x0101 => "WM_KEYUP ", | |
| 0x0104 => "WM_SYSKEYDOWN", | |
| 0x0105 => "WM_SYSKEYUP ", | |
| 0x0106 => "WM_SYSCHAR ", | |
| _ => $"0x{m:X4}", | |
| }; | |
| } | |
| /// <summary> | |
| /// Comparison window with a classic Win32 menu bar (HMENU). | |
| /// It does not use a WinForms MenuStrip; Alt/F10 handling is left entirely | |
| /// to the standard menu processing in DefWindowProc. This serves as the | |
| /// "true OS standard" specimen. Menu mode entry/exit is observed via | |
| /// WM_ENTERMENULOOP / WM_EXITMENULOOP. | |
| /// </summary> | |
| public class ClassicMenuForm : Form | |
| { | |
| [DllImport("user32.dll")] | |
| private static extern nint CreateMenu(); | |
| [DllImport("user32.dll")] | |
| private static extern nint CreatePopupMenu(); | |
| [DllImport("user32.dll", CharSet = CharSet.Unicode)] | |
| private static extern bool AppendMenu(nint hMenu, uint uFlags, nuint uIDNewItem, string lpNewItem); | |
| [DllImport("user32.dll")] | |
| private static extern bool SetMenu(nint hWnd, nint hMenu); | |
| private const uint MF_STRING = 0x0, MF_POPUP = 0x10; | |
| private const int CMD_CLOSE = 1002; | |
| private readonly Action<string> _log; | |
| private readonly LoggingTextBox _txt = new() | |
| { Multiline = true, Dock = DockStyle.Fill, Text = "TextBox (classic-menu window)", Tag = "CTXT" }; | |
| public ClassicMenuForm(Action<string> log) | |
| { | |
| _log = log; | |
| Text = "AltMenuTest — Classic HMENU"; | |
| Width = 640; Height = 420; | |
| Controls.Add(_txt); | |
| _txt.MsgLog += s => _log("[classic] " + s); | |
| } | |
| protected override void OnHandleCreated(EventArgs e) | |
| { | |
| base.OnHandleCreated(e); | |
| nint bar = CreateMenu(); | |
| nint file = CreatePopupMenu(); | |
| AppendMenu(file, MF_STRING, 1001, "&Dummy"); | |
| AppendMenu(file, MF_STRING, CMD_CLOSE, "&Close"); | |
| AppendMenu(bar, MF_POPUP, (nuint)file, "&File"); | |
| nint edit = CreatePopupMenu(); | |
| AppendMenu(edit, MF_STRING, 1003, "Dummy 2"); | |
| AppendMenu(bar, MF_POPUP, (nuint)edit, "&Edit"); | |
| SetMenu(Handle, bar); | |
| } | |
| protected override void WndProc(ref Message m) | |
| { | |
| switch (m.Msg) | |
| { | |
| case 0x0112 when ((int)m.WParam & 0xFFF0) == 0xF100: // WM_SYSCOMMAND / SC_KEYMENU | |
| _log($"[classic] Form WM_SYSCOMMAND SC_KEYMENU lParam=0x{(long)m.LParam:X}"); | |
| break; | |
| case 0x0211: // WM_ENTERMENULOOP | |
| _log("[classic] WM_ENTERMENULOOP (menu mode started)"); | |
| break; | |
| case 0x0212: // WM_EXITMENULOOP | |
| _log("[classic] WM_EXITMENULOOP (menu mode ended)"); | |
| break; | |
| case 0x0111 when (long)m.WParam == CMD_CLOSE: // WM_COMMAND | |
| Close(); | |
| return; | |
| } | |
| base.WndProc(ref m); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment