<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Roman Tykhonenko, .NET developer, Kyiv]]></title><description><![CDATA[Practical notes on native Windows development in .NET — Win32 interop, global hotkeys, tray utilities, MSIX packaging, and shipping desktop apps solo. Real code]]></description><link>https://romantykhonenko.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a312773bddc842aa949eed7/54f4088a-7dc9-4671-8e3d-7f5246f1d219.jpg</url><title>Roman Tykhonenko, .NET developer, Kyiv</title><link>https://romantykhonenko.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 13 Oct 2026 10:21:13 GMT</lastBuildDate><atom:link href="https://romantykhonenko.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to publish a .NET app to the Microsoft Store for $0: a step-by-step guide with the gotchas]]></title><description><![CDATA[In 2026 the Microsoft Store reaches 240 markets and ships inside Windows 10/11 by default. It's the single largest legitimate distribution channel for Windows desktop apps — and surprisingly few indie]]></description><link>https://romantykhonenko.hashnode.dev/how-to-publish-a-net-app-to-the-microsoft-store-for-0-a-step-by-step-guide-with-the-gotchas</link><guid isPermaLink="true">https://romantykhonenko.hashnode.dev/how-to-publish-a-net-app-to-the-microsoft-store-for-0-a-step-by-step-guide-with-the-gotchas</guid><category><![CDATA[dotnet]]></category><category><![CDATA[Windows]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[desktop apps]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Роман Тихоненко]]></dc:creator><pubDate>Tue, 16 Jun 2026 11:31:57 GMT</pubDate><content:encoded><![CDATA[<p>In 2026 the Microsoft Store reaches 240 markets and ships inside Windows 10/11 by default. It's the single largest legitimate distribution channel for Windows desktop apps — and surprisingly few indie developers use it.</p>
<p>The reason is mundane: an EV code-signing certificate costs <strong>€170–300 per year</strong>, and that feels like a mandatory barrier. It isn't. There's an official path to publish <strong>without your own certificate</strong> — through the MSIX format, which the Microsoft Store signs with its own signature. I walked this path from scratch to certification in a week, and below is the full guide with every gotcha that cost me three extra days.</p>
<p>The guide is practical: it applies to .NET WPF, .NET WinUI 3, and Win32 apps. Electron and React Native support the MSIX route too, with their own quirks — covered briefly at the end.</p>
<h2>Why the Microsoft Store, not just your own site + GitHub Releases</h2>
<p>Before spending a week on packaging, understand the trade-off.</p>
<p><strong>Your own site + GitHub Releases:</strong></p>
<ul>
<li><p>Full control over the landing page, tracking, and updates.</p>
</li>
<li><p>Needs an EV cert (€170–300/yr) or SmartScreen blocks downloads.</p>
</li>
<li><p>Distribution = your own traffic.</p>
</li>
</ul>
<p><strong>Microsoft Store:</strong></p>
<ul>
<li><p>Free (for individual developers).</p>
</li>
<li><p>No need to sign the installer — the Store does it for you.</p>
</li>
<li><p>Automatic updates through Windows Update.</p>
</li>
<li><p>Distribution to 240 markets via a built-in Windows feature.</p>
</li>
<li><p>Limited control: pricing, listing, and updates go through review.</p>
</li>
</ul>
<p>For most indie devs it's worth having <strong>both channels in parallel</strong>. The Store is the channel for new users and trust; your site is the main one for your active base and marketing.</p>
<h2>The barrier: why indie devs fear the certificate</h2>
<p>A plain <code>.exe</code> installer without an EV cert triggers a SmartScreen warning: <em>"Windows protected your PC. Microsoft Defender SmartScreen prevented an unrecognized app from starting."</em> The user has to click "More info → Run anyway." That step loses 30–50% of conversions.</p>
<p>The options:</p>
<ol>
<li><p><strong>EV Code Signing Certificate</strong> — €200–300/yr. Bought through DigiCert, Sectigo, Certum. Issued only after identity verification (Ukraine is supported by Certum via a foreign passport).</p>
</li>
<li><p><strong>Standard Code Signing Certificate</strong> — €170–200/yr, no EV reputation, needs trust accumulation. Certum (PL) works with Ukraine and issues via SimplySign (cloud HSM, no USB token).</p>
</li>
<li><p><strong>Microsoft Store MSIX route</strong> — <strong>€0</strong>, but through Store review.</p>
</li>
</ol>
<p>Each path is justified in its own context. For a first release, when you don't yet know whether the product will take off, option #3 is objectively the cheapest.</p>
<h2>Step 1. Register a dev account through the new free flow</h2>
<p>In 2025 Microsoft moved individual-developer registration to a new portal: <strong>storedeveloper.microsoft.com</strong>. The old <strong>partner.microsoft.com</strong> returns <strong>"Access restricted"</strong> for new indie accounts — that's not a bug, it's a deliberate redesign.</p>
<p>The process:</p>
<ol>
<li><p>Go to <code>storedeveloper.microsoft.com</code> with your Microsoft account.</p>
</li>
<li><p>Choose <strong>Individual developer</strong> (not Company).</p>
</li>
<li><p>Fill in the Publisher Display Name. This is the public name shown in the Store.</p>
</li>
<li><p>Pay nothing — for indies the account is free in 2025–26 (it used to be a one-time $19).</p>
</li>
</ol>
<p>Processing time: <strong>instant</strong> in most cases.</p>
<p><strong>Gotcha #1:</strong> The Identity Name (<code>CN=...</code> in the manifest) is generated by Microsoft automatically and bound to the account. If you ever started a registration under "Your Name," it may be "reserved" by a stalled session and unavailable for the new one. In my case I had to use "CapyBro" as the Publisher Display Name instead of my own name.</p>
<h2>Step 2. Build MSIX instead of NSIS</h2>
<p>If you already have an NSIS installer (<code>.exe</code>), it won't work for the Store. You need the <strong>MSIX</strong> format — the Microsoft Store supports only that.</p>
<p>The tool: <code>makeappx.exe</code>, part of the Windows SDK. Its typical location on Windows 11:</p>
<pre><code class="language-plaintext">C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\makeappx.exe
</code></pre>
<p>The basic pipeline:</p>
<ol>
<li><strong>Publish the .NET project to a self-contained folder:</strong></li>
</ol>
<pre><code class="language-plaintext">dotnet publish -c Release -r win-x64 --self-contained
</code></pre>
<ol>
<li><p><strong>Create</strong> <code>AppxManifest.xml</code> — it describes the app (name, version, capabilities, entry points).</p>
</li>
<li><p><strong>Pack it:</strong></p>
</li>
</ol>
<pre><code class="language-plaintext">makeappx pack /d &lt;folder with all files&gt; /p MyApp.msix
</code></pre>
<ol>
<li><strong>Verify on your local machine:</strong> double-click the <code>.msix</code> — Windows offers to install it.</li>
</ol>
<p><strong>Gotcha #2:</strong> <code>makeappx</code> breaks with no clear message if there's a <strong>double dash</strong> <code>--</code> inside the XML comments of <code>AppxManifest.xml</code>. Don't write <code>&lt;!-- HACK -- this is a workaround --&gt;</code>, write <code>&lt;!-- HACK: this is a workaround --&gt;</code>.</p>
<p><strong>Gotcha #3:</strong> The project's TargetFramework must include Windows SDK targeting — for example <code>net8.0-windows10.0.17763.0</code>. Without it, the WinRT APIs aren't available, and some Store-route features (like the native autostart below) won't build.</p>
<h2>Step 3. Five gotchas that cost me three days</h2>
<p>This is the part worth saving the guide for.</p>
<h3>3.1. Autostart via the HKCU Run key no longer works</h3>
<p>The classic Windows autostart path is to write a shortcut into <code>HKCU\Software\Microsoft\Windows\CurrentVersion\Run</code>. In an MSIX environment this is <strong>virtualized</strong>: the write lands in an isolated container and doesn't affect the real registry.</p>
<p>The fix is to use a <strong>StartupTask</strong> through the WinRT API:</p>
<pre><code class="language-xml">&lt;!-- AppxManifest.xml --&gt;
&lt;Extensions&gt;
  &lt;desktop:Extension Category="windows.startupTask" Executable="MyApp.exe" EntryPoint="Windows.FullTrustApplication"&gt;
    &lt;desktop:StartupTask TaskId="MyAppStartup" Enabled="true" DisplayName="My App" /&gt;
  &lt;/desktop:Extension&gt;
&lt;/Extensions&gt;
</code></pre>
<p>In code, check the state via <code>Windows.ApplicationModel.StartupTask.GetAsync("MyAppStartup")</code>. The user can disable autostart in Settings → Apps → Startup, like any Store app.</p>
<h3>3.2. Parameters for a silent launch</h3>
<p>If your app shows a splash window or onboarding on first launch but should start silently in the tray on autostart, pass an argument via <code>uap10:Parameters</code>:</p>
<pre><code class="language-xml">&lt;desktop:StartupTask TaskId="MyAppStartup" Enabled="true" DisplayName="My App"&gt;
  &lt;uap10:Parameters&gt;--silent&lt;/uap10:Parameters&gt;
&lt;/desktop:StartupTask&gt;
</code></pre>
<p>In code just read <code>Environment.GetCommandLineArgs()</code> and pick the scenario.</p>
<h3>3.3. Config files in the user profile are not virtualized</h3>
<p>MSIX has a VFS (Virtual File System) concept — part of filesystem writes get isolated. That only applies to folders like <code>C:\Program Files</code>, <code>C:\Windows</code>. <strong>Files you write to</strong> <code>%USERPROFILE%</code> <strong>(e.g.</strong> <code>~/.myapp-config</code><strong>) are not touched by the VFS</strong> — they live in the real user profile and are accessible exactly as from the <code>.exe</code> version. This makes a shared config between the Store version and the <code>.exe</code> version of the same app possible.</p>
<h3>3.4. PublisherDisplayName ≠ Publisher Identity</h3>
<p><code>AppxManifest.xml</code> has two different "publisher" fields:</p>
<pre><code class="language-xml">&lt;Identity Name="MyApp.MyApp" Publisher="CN=5763E11C-3BC8-..." Version="1.0.0.0" /&gt;
&lt;Properties&gt;
  &lt;DisplayName&gt;My App&lt;/DisplayName&gt;
  &lt;PublisherDisplayName&gt;My Company&lt;/PublisherDisplayName&gt;
  ...
&lt;/Properties&gt;
</code></pre>
<ul>
<li><p><code>Publisher</code> <strong>in</strong> <code>&lt;Identity&gt;</code> — the Subject DN of your dev certificate (issued by Microsoft automatically). This string must exactly match the one in Partner Center.</p>
</li>
<li><p><code>PublisherDisplayName</code> — the <strong>public name</strong> on the Store page. It's arbitrary and you set it yourself.</p>
</li>
</ul>
<p>Don't mix them up — a mismatch on the first one gets your submission rejected with no explanation.</p>
<h3>3.5. The "Incomplete" label on Submission options is not a blocker</h3>
<p>When you first submit in Partner Center, the <strong>Submission options</strong> tab shows an <strong>Incomplete</strong> label demanding a justification for the <code>runFullTrust</code> capability. Don't panic — it doesn't block the submit. Microsoft approves runFullTrust <strong>during certification itself</strong>, based on the app and the test notes. In my case the label stayed red and the submission passed cleanly in 24 hours.</p>
<h2>Step 4. Fill in the listing in Partner Center</h2>
<p>This is the longest step. Set aside 2–3 hours.</p>
<p><strong>What to prepare in advance:</strong></p>
<ol>
<li><p><strong>App description</strong> in three or four languages. EN is mandatory, then your target markets. You'll have to type the text <strong>by hand from the keyboard</strong>: Partner Center has a bug that doesn't save synthetic (programmatic) input from automation.</p>
</li>
<li><p><strong>8 key features</strong> as short bullet points. It's a separate field in the listing.</p>
</li>
<li><p><strong>Screenshots</strong> — at least 4, in 1920×1080 PNG. Include at least one of the main UI, one of settings, one of a real-world use.</p>
</li>
<li><p><strong>Hero image 2400×1200</strong> (optional but recommended — the Store shows it at the top of the page).</p>
</li>
<li><p><strong>Teaser video</strong> (optional) — 30 seconds, MP4, up to 1 GB.</p>
</li>
<li><p><strong>Privacy URL</strong> — a link to your privacy policy. Without it, review fails.</p>
</li>
</ol>
<p><strong>Property declarations:</strong></p>
<ul>
<li><p>If you have In-App Purchases (via Gumroad/Paddle/your own backend), declare "In-App Purchases."</p>
</li>
<li><p>If the app uses generative AI, declare it in the relevant item.</p>
</li>
<li><p>Category: for most indie utilities — <strong>Productivity</strong>.</p>
</li>
</ul>
<p><strong>Age rating</strong> is generated through the IARC questionnaire — a separate form; answer honestly (for a text-rewriting tool you get PEGI 3 / Everyone).</p>
<h2>Step 5. Certification: what they test and how to prepare</h2>
<p>Certification takes <strong>1–3 days</strong>. Humans test it, not bots — that matters for writing the test notes.</p>
<p><strong>What to write in Notes for certification:</strong></p>
<ol>
<li><p>Which features to check (describe the main scenario in 5 lines).</p>
</li>
<li><p>If the app needs third-party credentials (API keys, logins), <strong>provide temporary ones</strong> in the test notes. For example, if your AI app works through OpenRouter, create a test key, add it to the notes — <strong>and don't forget to revoke it right after successful certification.</strong></p>
</li>
<li><p>Describe special scenarios (autostart, system-hotkey integration, offline mode).</p>
</li>
</ol>
<p>The certification type for the first submission is the regular one (no Mandatory Update flag). If your app uses <strong>runFullTrust</strong> (most WPF apps), justify it in two or three sentences in a separate field: why you need full access and which system APIs you use.</p>
<p><strong>Gotcha #6 (bonus):</strong> If you need to provide <strong>dev-only credentials</strong> in the test notes, register them on your personal email <strong>outside your brand domain</strong>. The Microsoft tester will see the credentials and send reset emails on error — you don't want those landing on support@yourdomain.</p>
<p>After passing certification the listing moves to the <strong>In Microsoft Store</strong> status and becomes publicly available within 1–2 hours. Google and Bing indexing kick in automatically.</p>
<h2>What the result looks like</h2>
<p>In my case I walked this route for the open-source app <a href="https://capybro.app">CapyBro</a> — its listing is live here: <code>apps.microsoft.com/detail/9N73FHJTW3M7</code>. The first submission passed certification in 24 hours, the second (an update to 2.0.1 plus a native ARM64 build) in 1 day.</p>
<p>The signature Microsoft puts on your MSIX is visible in the file properties: <code>Issued by: Microsoft Code Signing PCA</code>. SmartScreen doesn't block it, because the install comes through the trusted Store channel.</p>
<h2>If you're not on .NET</h2>
<p>The MSIX format isn't tied to .NET. It works with any Windows app.</p>
<ul>
<li><p><strong>Electron</strong> — supports MSIX via <code>electron-builder</code> (<code>target: 'msix'</code>). Similar gotchas: HKCU autostart doesn't work, you need <code>StartupTask</code>.</p>
</li>
<li><p><strong>React Native Windows</strong> — generates MSIX out of the box via the CLI.</p>
</li>
<li><p><strong>Win32 C++</strong> — packaged the same way via <code>makeappx.exe</code>, you need a hand-written <code>AppxManifest.xml</code>.</p>
</li>
<li><p><strong>PWA / Java / Python</strong> — each has runFullTrust quirks; check Microsoft's official docs for your specific stack.</p>
</li>
</ul>
<h2>Conclusion and checklist</h2>
<p>The Microsoft Store via the MSIX route is the cheapest way to publish into the Windows ecosystem legitimately, with a signature and distribution to 240 markets. The €170–300/yr for an EV certificate can (for now) be deferred to the stage when the product has found its audience and your own web downloads have become a meaningful share of the metric.</p>
<p><strong>Checklist before your first submission:</strong></p>
<ul>
<li><p>[ ] Dev account on <code>storedeveloper.microsoft.com</code>, Publisher Display Name set.</p>
</li>
<li><p>[ ] csproj targets <code>net8.0-windows10.0.17763.0</code> (or higher).</p>
</li>
<li><p>[ ] <code>dotnet publish -c Release -r win-x64 --self-contained</code> works.</p>
</li>
<li><p>[ ] <code>AppxManifest.xml</code> is valid, no <code>--</code> in XML comments.</p>
</li>
<li><p>[ ] Autostart via <code>StartupTask</code>, not the HKCU Run key.</p>
</li>
<li><p>[ ] Privacy URL ready on your site.</p>
</li>
<li><p>[ ] 4+ screenshots 1920×1080, description in 2–3 languages <strong>typed by hand</strong>.</p>
</li>
<li><p>[ ] Age rating obtained via IARC.</p>
</li>
<li><p>[ ] Test notes written, temporary credentials created.</p>
</li>
<li><p>[ ] <code>makeappx pack</code> produced the <code>.msix</code>, local install works.</p>
</li>
</ul>
<p><strong>Bonus step after publishing:</strong> add a Microsoft Store badge to your site and the project <code>README.md</code>. It's a trust signal for visitors and an extra backlink for indexing.</p>
<p>The free core of the app I built along this route is open-source (MIT): <a href="https://github.com/phantasmat2018/capy-bro">github.com/phantasmat2018/capy-bro</a>.</p>
<p>If you have a question about a specific gotcha — drop a comment. This route is still poorly documented publicly, and I'm happy to clarify on concrete cases.</p>
<p><em>Roman Tykhonenko, .NET developer, Kyiv.</em></p>
]]></content:encoded></item><item><title><![CDATA[Global hotkeys and pasting text into any app: a Windows system utility in .NET]]></title><description><![CDATA[There is a whole class of desktop apps that have no main window and operate "on top" of the entire system: clipboard managers, quick launchers, text utilities. The user presses a global key combinatio]]></description><link>https://romantykhonenko.hashnode.dev/global-hotkeys-and-pasting-text-into-any-app-a-windows-system-utility-in-net</link><guid isPermaLink="true">https://romantykhonenko.hashnode.dev/global-hotkeys-and-pasting-text-into-any-app-a-windows-system-utility-in-net</guid><category><![CDATA[dotnet]]></category><category><![CDATA[C#]]></category><category><![CDATA[Windows]]></category><category><![CDATA[winapi]]></category><category><![CDATA[desktop apps]]></category><dc:creator><![CDATA[Роман Тихоненко]]></dc:creator><pubDate>Tue, 16 Jun 2026 11:08:13 GMT</pubDate><content:encoded><![CDATA[<p>There is a whole class of desktop apps that have no main window and operate "on top" of the entire system: clipboard managers, quick launchers, text utilities. The user presses a global key combination anywhere in Windows, and the app does something with whatever is under the cursor or in the selection — without stealing focus from the active program.</p>
<p>Sounds simple, but plain .NET / WPF can't do it out of the box. The two technical pillars of such a utility are a <strong>global hotkey</strong> (works even when your window is inactive) and <strong>pasting the result into another program</strong> (Word, a browser, an IDE — anywhere). Both come down to the Win32 API through P/Invoke.</p>
<p>Below is the full working path on .NET 8 + WPF with real code and the gotchas that only show up in production. I walked this path building CapyBro — a utility that rewrites selected text with AI on a hotkey; the examples are based on its real architecture.</p>
<hr />
<h2>Part 1. Global hotkeys via RegisterHotKey</h2>
<h3>Why not WPF KeyBinding</h3>
<p>The first thing that comes to mind is <code>InputBinding</code> / <code>KeyBinding</code> in WPF. The problem: they only fire <strong>when your window has focus</strong>. A system utility lives in the tray with no active window, and the hotkey has to be caught while the user is working in Word or Chrome. So you need an OS-level hotkey, not an app-level one.</p>
<p>For that there's the Win32 API <code>RegisterHotKey</code>. It registers the combination globally: as soon as it's pressed anywhere, your process receives a <code>WM_HOTKEY</code> window message.</p>
<h3>P/Invoke declarations</h3>
<p>In .NET 8 it's better to use source-generated marshalling via <code>[LibraryImport]</code> instead of the old <code>[DllImport]</code> — less runtime overhead and fewer marshalling mistakes:</p>
<pre><code class="language-csharp">internal static partial class NativeMethods
{
    [LibraryImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static partial bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);

    [LibraryImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static partial bool UnregisterHotKey(IntPtr hWnd, int id);

    internal const uint MOD_ALT      = 0x0001;
    internal const uint MOD_CONTROL  = 0x0002;
    internal const uint MOD_SHIFT    = 0x0004;
    internal const uint MOD_WIN      = 0x0008;
    internal const uint MOD_NOREPEAT = 0x4000;

    internal const int WM_HOTKEY = 0x0312;
}
</code></pre>
<p><code>vk</code> is the virtual-key code of the key (for example <code>0x45</code> for <code>E</code>). <code>fsModifiers</code> is a bitmask of modifiers. Note <code>MOD_NOREPEAT</code> — more on that below; it's an important gotcha.</p>
<h3>Where WM_HOTKEY arrives if there is no window</h3>
<p><code>RegisterHotKey</code> requires an <code>HWND</code> to send the message to. A tray app may have no main window at all. The solution is a <strong>message-only window</strong>: an invisible window that exists purely to receive messages. In WPF it's convenient to create one via <code>HwndSource</code>:</p>
<pre><code class="language-csharp">public sealed class HotkeyManager : IDisposable
{
    private readonly HwndSource _source;
    private int _nextId = 1;
    private readonly Dictionary&lt;int, Action&gt; _callbacks = new();

    public HotkeyManager()
    {
        var parameters = new HwndSourceParameters("CapyBroHotkeyWindow")
        {
            // HWND_MESSAGE = -3: the window becomes message-only,
            // never appears on screen or in Alt+Tab.
            ParentWindow = new IntPtr(-3),
        };
        _source = new HwndSource(parameters);
        _source.AddHook(WndProc);
    }

    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam,
        ref bool handled)
    {
        if (msg == NativeMethods.WM_HOTKEY)
        {
            int id = wParam.ToInt32();
            if (_callbacks.TryGetValue(id, out var action))
            {
                action();           // your hotkey handler
                handled = true;
            }
        }
        return IntPtr.Zero;
    }
}
</code></pre>
<h3>Registration and conflict detection</h3>
<p><code>RegisterHotKey</code> returns <code>false</code> if the combination is already taken by another program. That's a free conflict-detection mechanism — always surface it to the user, because "nothing happens when I press it" is the worst possible UX:</p>
<pre><code class="language-csharp">public bool TryRegister(uint modifiers, uint vk, Action callback)
{
    int id = _nextId++;
    bool ok = NativeMethods.RegisterHotKey(
        _source.Handle, id, modifiers | NativeMethods.MOD_NOREPEAT, vk);

    if (!ok)
    {
        // Usually ERROR_HOTKEY_ALREADY_REGISTERED (1409).
        int err = Marshal.GetLastWin32Error();
        return false;   // → highlight the hotkey field red in the UI
    }

    _callbacks[id] = callback;
    return true;
}

public void Dispose()
{
    foreach (var id in _callbacks.Keys)
        NativeMethods.UnregisterHotKey(_source.Handle, id);
    _source.Dispose();
}
</code></pre>
<h3>Gotcha: without MOD_NOREPEAT the hotkey "chatters"</h3>
<p>If you don't pass <code>MOD_NOREPEAT</code>, then while the user holds the combination down, Windows will send <code>WM_HOTKEY</code> dozens of times per second (auto-repeat). For a utility that kicks off a heavy action (a network request, text processing), that means a flood of parallel calls from a single press. <code>MOD_NOREPEAT</code> guarantees exactly one message per physical key press.</p>
<hr />
<h2>Part 2. Read the selection and paste the result back</h2>
<p>Now the interesting part. The hotkey fired — you need to take the text the user selected in <em>another</em> program, process it, and put it back in the same place.</p>
<h3>You can't read the selected text directly</h3>
<p>Windows doesn't give you a "give me the active window's selected text" API for arbitrary apps. The universal trick is to <strong>simulate</strong> <code>Ctrl+C</code> and read what landed in the clipboard. Likewise, pasting the result is <strong>simulating</strong> <code>Ctrl+V</code> after you put the new text on the clipboard.</p>
<p>The sequence:</p>
<ul>
<li><p>save the current clipboard contents (to restore them politely afterward);</p>
</li>
<li><p>simulate <code>Ctrl+C</code> → wait for the clipboard to update;</p>
</li>
<li><p>read the selected text;</p>
</li>
<li><p>process it (your logic / an AI call);</p>
</li>
<li><p>put the result on the clipboard;</p>
</li>
<li><p>simulate <code>Ctrl+V</code> → the text gets pasted into the still-active target program;</p>
</li>
<li><p>restore the original clipboard.</p>
</li>
</ul>
<h3>Simulating keys via SendInput</h3>
<p>The modern way to send synthetic keystrokes is <code>SendInput</code> (legacy <code>keybd_event</code> still works, but <code>SendInput</code> is more atomic and handled more correctly by the system):</p>
<pre><code class="language-csharp">[StructLayout(LayoutKind.Sequential)]
internal struct INPUT
{
    public uint type;          // 1 = INPUT_KEYBOARD
    public KEYBDINPUT ki;
    // (for keyboard the union can be simplified to KEYBDINPUT;
    //  for full mouse/hardware compatibility, add padding)
}

[StructLayout(LayoutKind.Sequential)]
internal struct KEYBDINPUT
{
    public ushort wVk;
    public ushort wScan;
    public uint dwFlags;       // 0 = keydown, 2 = KEYEVENTF_KEYUP
    public uint time;
    public IntPtr dwExtraInfo;
}

[LibraryImport("user32.dll", SetLastError = true)]
internal static partial uint SendInput(uint nInputs,
    [In] INPUT[] pInputs, int cbSize);
</code></pre>
<p>To send <code>Ctrl+V</code>, the four-event sequence is: Ctrl down → V down → V up → Ctrl up. The keyup order matters — release the letter first, then the modifier, otherwise the target may receive a "bare" V.</p>
<h3>Gotcha #1: another program is holding the clipboard</h3>
<p>The Win32 clipboard is a global resource under a lock. Clipboard managers (Ditto, Punto), RDP channels, some antivirus software periodically open it for themselves. At that moment your <code>OpenClipboard</code> / read fails with <code>CLIPBRD_E_CANT_OPEN</code>.</p>
<p>The fix is a retry loop with small pauses. <strong>Critically:</strong> the pauses must be asynchronous, otherwise you freeze the UI thread for the duration of the wait:</p>
<pre><code class="language-csharp">private static async Task&lt;string?&gt; ReadClipboardTextAsync(
    Dispatcher dispatcher, CancellationToken ct)
{
    const int maxAttempts = 10;
    for (int attempt = 0; attempt &lt; maxAttempts; attempt++)
    {
        try
        {
            // Clipboard access must be on the UI thread (STA).
            return await dispatcher.InvokeAsync(() =&gt;
                Clipboard.ContainsText() ? Clipboard.GetText() : null);
        }
        catch (COMException)   // CLIPBRD_E_CANT_OPEN
        {
            await Task.Delay(50, ct);   // NOT Thread.Sleep — don't block the UI
        }
    }
    return null;
}
</code></pre>
<p>Apply the same retry pattern to writing the result to the clipboard. The rookie mistake here is <code>Thread.Sleep(50)</code> inside the UI thread: the window "freezes" for half a second and the user sees a hang. Only <code>await Task.Delay</code>.</p>
<h3>Gotcha #2: focus and the "focused child"</h3>
<p>Between <code>Ctrl+C</code> and <code>Ctrl+V</code>, focus must stay on the target program. If you show your own window in between (for example, a result preview) and don't return focus — <code>Ctrl+V</code> goes "nowhere" or into your own window.</p>
<p>Returning focus via <code>SetForegroundWindow</code> on the target's top-level window is necessary, but <strong>sometimes not enough</strong>. Some editors (the classic example is Notepad++ built on Scintilla) have an internal focused child window that actually receives input. <code>SetForegroundWindow</code> activates the top-level, but the caret stays in the child control, and the paste doesn't work. Then you have to find the focused element via <code>GetGUIThreadInfo</code> and call <code>SetFocus</code> on it specifically:</p>
<pre><code class="language-csharp">[StructLayout(LayoutKind.Sequential)]
internal struct GUITHREADINFO
{
    public int cbSize;
    public uint flags;
    public IntPtr hwndActive;
    public IntPtr hwndFocus;   // here it is — the real input receiver
    public IntPtr hwndCapture;
    public IntPtr hwndMenuOwner;
    public IntPtr hwndMoveSize;
    public IntPtr hwndCaret;
    public RECT rcCaret;
}

// 1. GetForegroundWindow → 2. GetWindowThreadProcessId →
// 3. GetGUIThreadInfo(threadId, ...) → hwndFocus →
// 4. AttachThreadInput + SetFocus(hwndFocus)
</code></pre>
<p><code>AttachThreadInput</code> is needed because <code>SetFocus</code> only works within its own thread's input state — to set focus in another thread's window, you temporarily attach the input queues.</p>
<h3>Gotcha #3: sandboxes block synthetic input</h3>
<p>UWP apps, Windows Subsystem for Android (WSA), and any process running <strong>with higher privileges</strong> than yours reject synthetic keystrokes via UIPI (User Interface Privilege Isolation). Your <code>SendInput</code> is simply ignored, and the paste silently doesn't happen.</p>
<p>The honest UX here is not to pretend everything worked, but to <strong>detect the failure and tell the truth</strong>: "The result is already in your clipboard — paste it manually with Ctrl+V." The user doesn't lose their work, and you don't leave them wondering why nothing pasted.</p>
<hr />
<h2>Part 3. The consolidated gotcha checklist</h2>
<ul>
<li><p><strong>WPF KeyBinding is not global</strong> — for a hotkey on top of the system you need <code>RegisterHotKey</code> + a message-only window via <code>HwndSource</code>.</p>
</li>
<li><p><strong>MOD_NOREPEAT</strong> — without it, holding the key fires the action dozens of times.</p>
</li>
<li><p><strong>Conflict detection</strong> — <code>RegisterHotKey == false</code> means "taken"; show it in the UI, don't swallow it silently.</p>
</li>
<li><p><strong>The selection is read by simulating Ctrl+C</strong>, the paste by simulating Ctrl+V; there's no direct API.</p>
</li>
<li><p><strong>The clipboard is under a lock</strong> — a retry loop with <code>await Task.Delay</code>, never <code>Thread.Sleep</code> on the UI thread.</p>
</li>
<li><p><strong>Restore the original clipboard</strong> after the operation — the user doesn't expect your utility to wipe the text they copied earlier.</p>
</li>
<li><p><strong>Focused child</strong> — for Scintilla-like controls, return focus via <code>GetGUIThreadInfo.hwndFocus</code> + <code>AttachThreadInput</code> + <code>SetFocus</code>, not just <code>SetForegroundWindow</code>.</p>
</li>
<li><p><strong>UIPI blocks synthetic input</strong> in UWP/elevated windows — detect it and offer manual paste as a fallback.</p>
</li>
<li><p><strong>Timing</strong> — a short pause is needed between Ctrl+C and reading the clipboard; for Ctrl+V the keyup order is letter first, then modifier.</p>
</li>
</ul>
<hr />
<h2>What it looks like in a real app</h2>
<p>I assembled all of these mechanisms in <a href="https://capybro.app">CapyBro</a> — a Windows utility that, on a global hotkey, rewrites the selected text with AI and pastes the result back, in any program. Hotkeys there are registered through the <code>RegisterHotKey</code> route described above with <code>MOD_NOREPEAT</code> and conflict detection, and paste-back runs the full "save clipboard → Ctrl+C → process → Ctrl+V → restore clipboard" cycle with a retry loop and a manual-paste fallback for sandboxes.</p>
<p>The free core is open-source (MIT): <a href="https://github.com/phantasmat2018/capy-bro">github.com/phantasmat2018/capy-bro</a>.</p>
<p>If you're building a similar utility and you're stuck on a specific gotcha — drop a comment. This path is still poorly documented publicly, and I'm happy to go into detail.</p>
<p><em>Roman Tykhonenko, .NET developer, Kyiv.</em></p>
]]></content:encoded></item></channel></rss>