[C#] Windows標準のタッチキーボードを起動する

Windowsにはtabtip.exe ("C:/Program Files/Common Files/Microsoft Shared/ink/tabtip.exe")というタッチキーボードの実行ファイルがあり、これを実行することでタッチキーボードを使用する事が出来ます。

osk.exe というスクリーンキーボードもありますが、今回は扱いません。
（UIがタッチ向けじゃない＆ScrLkとか押されたら困るボタンも含まれてたりするので、あまり使わせたくない）

## コード
(最終更新 2023/10/11)
Toggleメソッドで、例外の有無に関係なくProcess.Startが実行されていたので修正。

```c#
using System;
using System.Diagnostics;
using System.Threading;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

/// <summary>
/// Windows標準タッチキーボード操作クラス。
/// </summary>
public class WinTouchKeyboard
{
    [ComImport, Guid("D5120AA3-46BA-44C5-822D-CA8092C1FC72")]
    private class FrameworkInputPane
    {
    }

    [ComImport, System.Security.SuppressUnmanagedCodeSecurity,
    InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
    Guid("5752238B-24F0-495A-82F1-2FD593056796")]
    private interface IFrameworkInputPane
    {
        [PreserveSig]
        int Advise(
            [MarshalAs(UnmanagedType.IUnknown)] object pWindow,
            [MarshalAs(UnmanagedType.IUnknown)] object pHandler,
            out int pdwCookie
            );

        [PreserveSig]
        int AdviseWithHWND(
            IntPtr hwnd,
            [MarshalAs(UnmanagedType.IUnknown)] object pHandler,
            out int pdwCookie
            );

        [PreserveSig]
        int Unadvise(
            int pdwCookie
            );

        [PreserveSig]
        int Location(
            out Rectangle prcInputPaneScreenLocation
            );
    }

    [ComImport, Guid("4ce576fa-83dc-4F88-951c-9d0782b4e376")]
    private class UIHostNoLaunch
    {
    }

    [ComImport, Guid("37c994e7-432b-4834-a2f7-dce1f13b834b")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface ITipInvocation
    {
        void Toggle(IntPtr hwnd);
    }

    [DllImport("user32.dll", SetLastError = false)]
    private static extern IntPtr GetDesktopWindow();

    /// <summary>
    /// TabTip.exe のパス
    /// </summary>
    public static string TabTipPath { get; set; } = "C:/Program Files/Common Files/Microsoft Shared/ink/tabtip.exe";

    /// <summary>
    /// タッチキーボードを開く。
    /// タッチキーボードが表示されている場合は何もしない。
    /// </summary>
    /// <param name="delayMsec">タッチキーボード表示までの待ち時間。 
    /// TABキーに反応してタッチキーボードが即座に閉じられてしまう場合、待ち時間を調整する。（200～300msec程度）</param>
    public static void Open(int delayMsec)
    {
        if (IsVisible())
        {
            return;
        }

        Task.Run(() =>
        {
            if (delayMsec > 0)
            {
                Thread.Sleep(delayMsec);
            }

            //タッチキーボードのON/OFF切替
            Toggle();
        });
    }

    /// <summary>
    /// タッチキーボードを閉じる。
    /// タッチキーボードが表示されていない場合は何もしない。
    /// </summary>
    public static void Close()
    {
        if (!IsVisible())
        {
            return;
        }

        //タッチキーボードのON/OFF切替
        Toggle();
    }

    /// <summary>
    /// タッチキーボードのON/OFF切り替え
    /// </summary>
    public static void Toggle()
    {
        //UIHostNoLaunch.Toggle で表示切り替え
        try
        {
            UIHostNoLaunch uiHostNoLaunch = null;
            try
            {
                uiHostNoLaunch = new UIHostNoLaunch();
                var tipInvocation = (ITipInvocation)uiHostNoLaunch;
                tipInvocation.Toggle(GetDesktopWindow());
            }
            finally
            {
                if (uiHostNoLaunch != null)
                {
                    Marshal.ReleaseComObject(uiHostNoLaunch);
                }
            }
        }
        catch (COMException)
        {
            //tabtip.exe が実行されていない場合、new UIHostNoLaunch() でCOMExceptionが発生する。
            //Process.GetProcessesByName で事前にチェックするべきかとも考えたが、
            //COMException無視でもそこまで有意な速度差が無いので、チェックしない。

            //例外が発生した場合、tabtip.exe が起動していないので起動する
            var pi = new ProcessStartInfo();
            pi.FileName = TabTipPath;
            pi.UseShellExecute = true;
            Process.Start(pi);
        }
    }

    /// <summary>
    /// タッチキーボードの表示状態を取得する。
    /// </summary>
    /// <returns>true:表示 false:非表示</returns>
    public static bool IsVisible()
    {
        //タッチキーボードの位置・サイズを取得し、幅0なら非表示とみなす
        Rectangle bounds = GetBounds();
        return (bounds.Width != 0);
    }

    /// <summary>
    /// タッチキーボードの位置・サイズを取得する。
    /// </summary>
    /// <returns>タッチキーボードの位置・サイズ</returns>
    public static Rectangle GetBounds()
    {
        IFrameworkInputPane inputPane = null;
        Rectangle rect;
        try
        {
            //タッチキーボードの位置・サイズを取得し、幅0なら非表示とみなす
            inputPane = (IFrameworkInputPane)new FrameworkInputPane();
            inputPane.Location(out rect);
        }
        finally
        {
            if (inputPane != null)
            {
                Marshal.ReleaseComObject(inputPane);
            }
        }

        return rect;
    }
}
```

Stackoverflow の投稿を参考にしつつ、クラス化しました。

[Launching the touch keyboard (Tabtip.exe) from non-admin account on Windows 10](https://stackoverflow.com/questions/42988795/launching-the-touch-keyboard-tabtip-exe-from-non-admin-account-on-windows-10)
[Show touch keyboard (TabTip.exe) in Windows 10 Anniversary edition](https://stackoverflow.com/questions/38774139/show-touch-keyboard-tabtip-exe-in-windows-10-anniversary-edition)
[Determine if Windows 10 Touch Keyboard is Visible or Hidden](https://stackoverflow.com/questions/47187216/determine-if-windows-10-touch-keyboard-is-visible-or-hidden)

Win10Pro 1903 で動作するのを確認しています。他バージョンで動作するかは不明です。
（Win10以外で動いたら教えてください）
ITipInvocation、UIHostNoLaunch はMS公式でドキュメント化されていないCOMのため、使って良いのか何とも言えないところですが、とりあえず動きます。

タッチキーボードを閉じる際、tabtip.exeのプロセスを強制的にKillしたり、SendMessage で WM_SYSCOMMAND＋SC_CLOSE を送信してウィンドウを閉じている例が見受けられましたが、

- tabtip.exe が終了してしまう（tabtip.exe を都度再起動しなければならない）
- "IPTIP_Main_Window" のウィンドウが無くなってもタッチキーボードが残る場合がある

という問題が発生したため、これらの方法で閉じるのはやめた方がよいかと思われます。