9
10

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

Last updated at Posted at 2020-08-31

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

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

コード

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

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
Show touch keyboard (TabTip.exe) in Windows 10 Anniversary edition
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" のウィンドウが無くなってもタッチキーボードが残る場合がある

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

9
10
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
10