午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

非アクティブ時にもキーの状態を取得してみたWindowsAPIのGetAsyncKeyState

f:id:gogowaten:20201111095318g:plain
非アクティブ時にShift+Aの回数をカウントしているところ
Timerで一定時間間隔ごとに GetAsyncKeyStateを実行してキーの状態を取得
2020WPF/20201110_WinApiでキーの状態取得 at master · gogowaten/2020WPF
github.com

環境
Visual Studio Community 2019
.NET Core 3.1、C#WPF

<Window x:Class="_20201110_WinApiでキーの状態取得.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_20201110_WinApiでキーの状態取得"
        mc:Ignorable="d"
        Title="MainWindow" Height="200" Width="400">
  <Grid>
    <Grid.Resources>
      <Style TargetType="TextBlock">
        <Setter Property="Margin" Value="0,2,0,2"/>
      </Style>
      <Style TargetType="StackPanel">
        <Setter Property="Margin" Value="8,2,0,2"/>
        <Setter Property="Width" Value="120"/>
      </Style>
    </Grid.Resources>
    
    <StackPanel Orientation="Horizontal" Width="auto">
      <StackPanel Orientation="Vertical">
        <TextBlock Text="Key1"/>
        <ComboBox x:Name="MyCombo1Key"/>
        <TextBlock x:Name="MyTextBlockKey1AsyncState" Text="asyncState1"/>
        <TextBlock x:Name="MyTextBlockKey1State" Text="State1"/>
      </StackPanel>
      <StackPanel Orientation="Vertical">
        <TextBlock Text="Key2"/>
        <ComboBox x:Name="MyCombo2Key"/>
        <TextBlock x:Name="MyTextBlockKey2AsyncState" Text="asyncState2"/>
        <TextBlock x:Name="MyTextBlockKey2State" Text="State2"/>
      </StackPanel>
      <StackPanel Width="100">
        <TextBlock Text="Key1 を押しながら Key2 を押した回数" TextWrapping="Wrap"/>
        <TextBlock x:Name="MyTextBlockCount" Text="count" FontSize="20" HorizontalAlignment="Right"/>
        <Button x:Name="MyButtonCountReset" Content="リセット" Click="MyButtonCountReset_Click"/>
      </StackPanel>
    </StackPanel>
  </Grid>
</Window>

f:id:gogowaten:20201111101324p:plain
MainWindow.xaml

MainWindow.xaml.cs

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;

namespace _20201110_WinApiでキーの状態取得
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        //必要なAPIはGetAsyncKeyStateだけなんだけど
        //似たようなGetKeyStateも使ってみた
        [DllImport("user32.dll")]
        private static extern short GetAsyncKeyState(int vKey);
        [DllImport("user32.dll")]
        private static extern short GetKeyState(int vKey);

        //状態を知りたいキーの仮想キーコードを渡す
        //戻り値の型はshortで、これを2進数にして最上位ビットと最下位ビットで判定する
        //GetAsyncKeyState
        //最上位ビットが1のとき、押されている状態を表す
        //最下位ビットが1のとき、前回取得時から今回までに押された形跡があることを表す
        //  2進数               10進数(最上位、最下位以外が0のとき)
        // 0xxx_xxxx_xxxx_xxx0       0  押されてない、押された形跡無し
        // 0xxx_xxxx_xxxx_xxx1       1  押されてない、押された形跡あり
        // 1xxx_xxxx_xxxx_xxx0  -32768  押されている、押された形跡無し
        // 1xxx_xxxx_xxxx_xxx1  -32767  押されている、押された形跡あり
        //↑のxのところは0か1か決まっていないようだけど、見ていた感じでは常に0だった

        //ビットの判定はビット演算のandを使う
        //最下位ビットの判定、(戻り値 & 1) これが1なら1
        //最上位ビットの判定、(戻り値 & 0x8000)してこれを右に15シフトして1なら1
        //あとは、short型で最上位ビットが1ならマイナスの値になるはず?なので、戻り値 < 0 なら1

        //GetKeyState
        //最上位ビットが1のとき、押されている状態
        //最下位ビットはトグル式のキー(CapsLockとか)用で1のとき、トグルオン状態

        //違い
        //GetAsyncKeyStateは押された形跡がわかる
        //GetKeyStateはトグル式のキーのトグル状態がわかる

        //イマイチ
        //GetAsyncKeyStateはアプリによっては取得できない(無反応?)
        //タスクマネージャー、エクセル、デスクトップ検索のEverythingなど

        private DispatcherTimer MyTimer;
        private int MyCount;
        public MainWindow()
        {
            InitializeComponent();

            //タイマー初期化
            MyTimer = new DispatcherTimer();
            MyTimer.Tick += MyTimer_Tick;
            //時間間隔、8ミリ秒にしてみた
            MyTimer.Interval = new TimeSpan(0, 0, 0, 0, 8);
            MyTimer.Start();


            //キー選択用のコンボボックスの初期化
            MyCombo1Key.Items.Add(Key.RightCtrl);
            MyCombo1Key.Items.Add(Key.RightShift);
            MyCombo1Key.Items.Add(Key.RightAlt);
            MyCombo1Key.Items.Add(Key.Right);

            MyCombo2Key.Items.Add(Key.RightCtrl);
            MyCombo2Key.Items.Add(Key.RightShift);
            MyCombo2Key.Items.Add(Key.Right);
            MyCombo2Key.Items.Add(Key.A);

            MyCombo1Key.SelectedIndex = 1;
            MyCombo2Key.SelectedIndex = 3;
        }

        //一定時間間隔でキーの状態を取得して表示
        private void MyTimer_Tick(object sender, EventArgs e)
        {
            //KeyをWindowsAPIで使う仮想キーコードに変換
            var vKey1 = KeyInterop.VirtualKeyFromKey((Key)MyCombo1Key.SelectedItem);
            var vKey2 = KeyInterop.VirtualKeyFromKey((Key)MyCombo2Key.SelectedItem);

            //GetAsyncKeyStateでキーの状態を取得して値を表示
            short key1AsyncState = GetAsyncKeyState(vKey1);
            short key2AsyncState = GetAsyncKeyState(vKey2);
            MyTextBlockKey1AsyncState.Text = "AsyncKey= " + key1AsyncState.ToString();
            MyTextBlockKey2AsyncState.Text = "AsyncKey= " + key2AsyncState.ToString();

            //GetKeyStateでキーの状態を取得して値を表示
            short key1State = GetKeyState(vKey1);
            short key2State = GetKeyState(vKey2);
            MyTextBlockKey1State.Text = "Key= " + key1State.ToString();
            MyTextBlockKey2State.Text = "Key= " + key2State.ToString();

            //Key1が押された状態で、Key2も押されていたらの判定
            // != 0 この判定の仕方は雑だけど問題なさそう
            if (key1AsyncState != 0 & (key2AsyncState & 1) == 1)
            {
                //カウントを増やして回数の表示を更新
                MyCount++;
                MyTextBlockCount.Text = MyCount.ToString() + "回";
            }

            //↑の雑じゃない判定版
            //if (((key1AsyncState & 0x8000) >> 15 == 1) & ((key2AsyncState & 1) == 1)) { }

            //GetKeyStateとGetAsyncKeyState版でもできた
            //if ((key1State & 0x8000) >> 15 == 1 & (key2AsyncState & 1)== 1)
            //{
            //    MyCount++;
            //    MyTextBlockCount.Text = MyCount.ToString() + "回";
            //}

            //GetkeyStateだけだとできなかった
            //両キーとも押しっぱなしのときしか判定されない
            //if ((key1State & 0x8000) >> 15 == 1 & (key2State & 0x80) >> 7 == 1)
            //{
            //    MyCount++;
            //    MyTextBlockCount.Text = MyCount.ToString() + "回";
            //}

            //GetAsynckeyStateだけ → できた
            //GetkeyStateとGetAsynckeyState → できた
            //GetkeyStateだけ → むり?

        }

        //カウントリセット
        private void MyButtonCountReset_Click(object sender, RoutedEventArgs e)
        {
            MyCount = 0;
            MyTextBlockCount.Text = MyCount.ToString();
        }
    }
}


GetAsyncKeyStateとGetKeyStateの戻り値

f:id:gogowaten:20201111105136p:plain
戻り値

戻り値の型はどちらもshort型のはず
押された状態のキーの戻り値は
GetAsyncKeyState:-32768か、-32767
GetKeyState:-128か、-127

押されていない状態では
GetAsyncKeyState:0
GetKeyState:1か、0

になった
これを2進数にして最上位ビットが1なら、押された状態
2進数にして最上位ビットをみると

f:id:gogowaten:20201111110511p:plain
Windowsの電卓アプリ
-32768と-32767をはshort型のWORDで確かに1になっている

-128と-127は
f:id:gogowaten:20201111112819p:plain
途中の関係ない桁が1になっているけど、最上位ビットも1になっている、へー


参照したところ
Keyboard and Mouse Input - Win32 apps | Microsoft Docs
docs.microsoft.com

Keyを仮想キーコードに変換
f:id:gogowaten:20201111102237p:plain
System.Windows.Input.KeyInteropクラスの、VirtualKeyFromKeyを使う
Aキーの場合は
VirtualKeyFromKey(Key.A);
大体はこれで事足りるけど、左右に2つあるctrlキーを一度に取得したいときは
KT Software - 仮想キーコード一覧
kts.sakaiweb.com
ここを見ると載っている

変数の型でWORDとかDWORDってなんなん?ってときは
データ型 - Windows API 入門
kaitei.net

docs.microsoft.com

docs.microsoft.com



だいたいできたけど動かない場合もあって
タスクマネージャーとデスクトップ検索のEverythingでは、キーによってGetAsyncKeyStateやGetKeyStateが無反応?でキーを押しても常に0を返してくる
f:id:gogowaten:20201111120238p:plain
タスクマネージャー上では
CtrlキーはGetKeyStateだけ返ってくる
カーソルキーの右キーはどちらも無反応
フックするとかいうのを使うとできそうなんだけど、かなり難しそうだった



関連記事
1ヶ月後
gogowaten.hatenablog.com こっちのほうがラクかな