Skip to content

Instantly share code, notes, and snippets.

@ji6czd
Created July 8, 2026 07:03
Show Gist options
  • Select an option

  • Save ji6czd/d398fc63ac418e30cb93fc2cc2f73884 to your computer and use it in GitHub Desktop.

Select an option

Save ji6czd/d398fc63ac418e30cb93fc2cc2f73884 to your computer and use it in GitHub Desktop.
dotnet10.0 with screen reader test
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<!-- Multi-targeted on purpose: JAWS tracks MenuStrip menu activation
correctly on the net8.0 build but fails on the net10.0 build of the
exact same source (NVDA works on both). Keep both targets so the
A/B comparison binaries stay in sync. -->
<TargetFrameworks>net8.0-windows;net10.0-windows</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
</Project>
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