午後わてんのブログ

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

アプリのウィンドウが非アクティブ状態でも任意のキーの入力を感知、WPFでグローバルホットキーの登録

アプリのウィンドウが非アクティブ状態でもキー入力を感知したくて以前試したのはこれ
gogowaten.hatenablog.com でも、この方法ではタスクマネージャーなど、特定のアプリがアクティブウィンドウだと無反応だった
これを解決したのが今回
グローバルホットキーって言葉があって、WPF グローバルホットキーでググって
ここ
C#|グローバルホットキーを登録する – 貧脚レーサーのサボり日記
https://anis774.net/codevault/hotkey.html


WPFでホットキーの登録 - SourceChord
http://sourcechord.hatenablog.com/entry/2017/02/13/005456

キーをたくさん登録するときは、このあたりの方法が良さそう

数個でいいならここ
Nine Works WPFでHotKeyを設定する方法
http://nineworks2.blog.fc2.com/blog-entry-17.html
今回はここのをコピペ改変して

f:id:gogowaten:20201211105630p:plain
グローバルホットキー登録

動作確認
登録したキーが押されたらこのウィンドウを表示する

f:id:gogowaten:20201211110950p:plain
登録したキーが押されたら表示するウィンドウ

ホットキーの登録
修飾キーはチェックボックスにチェックで、キーはコンボボックスから選択する

f:id:gogowaten:20201211111203p:plain
コンボボックスから選択
もしくは
コンボボックス上で登録したいキーを押す
f:id:gogowaten:20201211111352p:plain
コンボボックス上でテンキーの5を押したところ
で、
f:id:gogowaten:20201211111749p:plain
登録ボタンで登録
ボタンを押すと
f:id:gogowaten:20201211111824p:plain
登録確認
必要ないけど確認ウィンドウ表示

Ctrl+テンキーの5を押すと

f:id:gogowaten:20201211110950p:plain
動作確認ウィンドウ
これが表示される

複数の修飾キーの組み合わせ

f:id:gogowaten:20201211112433p:plain
複数の修飾キー
Alt + Ctrl + F12とかもできた

タスクマネージャーのウィンドウ上で実行

f:id:gogowaten:20201211112849p:plain
タスクマネージャー
できた!

別のアプリで使われているホットキーは登録できない

f:id:gogowaten:20201211113609p:plain
登録に失敗
すでに他のアプリで登録されているキーの組み合わせを登録しようとしてもできない

スクリーンショット系は登録できない?

f:id:gogowaten:20201211120513p:plain
スクリーンショット系のキー
SnapshotってのはPrintScreenキーのことで
Win + PrintScreen
Win + Alt + PrintScreen
この2つはウィンドウズで使われているみたいで登録できない

ってことは、アクティブウィンドウのスクリーンショットクリップボードにコピーするAlt + PrintScreenもできなのかと思ったけど

f:id:gogowaten:20201211114741p:plain
Alt + PrintScreenの登録
これはできた
で、これを登録して押してみたら、ホットキーのほうが優先されてスクショはコピーされなかった


github.com

MainWindow.xaml

<Window x:Class="_20201210_グローバルホットキー3_2.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:_20201210_グローバルホットキー3_2"
        mc:Ignorable="d"
        Title="MainWindow" Height="200" Width="260">
  <Grid>
    <StackPanel>
      <GroupBox Header="ModKey修飾キー">
        <StackPanel Orientation="Horizontal" Margin="2">
          <CheckBox x:Name="MyChecAlt" Content="Alt" Margin="8"/>
          <CheckBox x:Name="MyChecCtrl" Content="Ctrl" Margin="8"/>
          <CheckBox x:Name="MyCheckShift" Content="Shift" Margin="8"/>
          <CheckBox x:Name="MyCheckWin" Content="Win" Margin="8"/>
        </StackPanel>
      </GroupBox>
      
      <TextBlock Text="+" HorizontalAlignment="Center"/>
      
      <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
        <TextBlock Text="key = "/>
        <ComboBox Name="MyComboBoxKey" IsEditable="True" Width="100"
                  PreviewKeyDown="MyComboBoxKey_PreviewKeyDown"
                  PreviewKeyUp="MyComboBoxKey_PreviewKeyUp"/>
      </StackPanel>
      <Button x:Name="MyButton" Content="登録" Click="MyButton_Click" Margin="10"/>
    </StackPanel>
  </Grid>
</Window>

f:id:gogowaten:20201211120951p:plain
コンボボックス上でキー入力したかったので、IsEditableをtrue指定
キーを押し下げたときと上げたときのイベントで、押されたキーの取得したかったので、PreviewKeyDownとPreviewKeyUpを使用


MainWindow.xaml.cs

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

//Nine Works WPFでHotKeyを設定する方法
//http://nineworks2.blog.fc2.com/blog-entry-17.html
//ここからのコピペ改変

//グローバルホットキーに任意のキーを登録
//修飾キーはチェックボックスで指定、普通のキーはコンボボックスから選択、もしくはコンボボックス上でキーを押す
//登録は登録ボタン
namespace _20201210_グローバルホットキー3_2
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        //登録用ID
        private const int HOTKEY_ID1 = 0x0001;
        private const int HOTKEY_ID2 = 0x0002;

        private const int WM_HOTKEY = 0x0312;

        private IntPtr MyWindowHandle;

        //必要なAPI参照
        [DllImport("user32.dll")]
        private static extern int RegisterHotKey(IntPtr hWnd, int id, int modKey, int vKey);
        [DllImport("user32.dll")]
        private static extern int UnregisterHotKey(IntPtr hWnd, int id);


        public MainWindow()
        {
            InitializeComponent();

            var host = new WindowInteropHelper(this);
            MyWindowHandle = host.Handle;

            //コンボボックス初期化
            MyComboBoxKey.ItemsSource = Enum.GetValues(typeof(Key));
            MyComboBoxKey.SelectedIndex = 0;

            ComponentDispatcher.ThreadPreprocessMessage += ComponentDispatcher_ThreadPreprocessMessage;

            this.Closed += MainWindow_Closed;
        }

        //アプリ終了時に登録解除
        private void MainWindow_Closed(object sender, EventArgs e)
        {
            UnregisterHotKey(MyWindowHandle, HOTKEY_ID1);
            UnregisterHotKey(MyWindowHandle, HOTKEY_ID2);
            ComponentDispatcher.ThreadPreprocessMessage -= ComponentDispatcher_ThreadPreprocessMessage;
        }



        //ホットキー登録
        private void MyButton_Click(object sender, RoutedEventArgs e)
        {
            int modifier = 0;
            string str = "";
            if (MyChecAlt.IsChecked == true) { str += $" + Alt"; modifier = (int)ModifierKeys.Alt; }
            if (MyChecCtrl.IsChecked == true) { str += $" + Ctrl"; modifier += (int)ModifierKeys.Control; }
            if (MyCheckShift.IsChecked == true) { str += $" + Shift"; modifier += (int)ModifierKeys.Shift; }
            if (MyCheckWin.IsChecked == true) { str += $" + Win"; modifier += (int)ModifierKeys.Windows; }

            var key = (Key)MyComboBoxKey.SelectedValue;
            UnregisterHotKey(MyWindowHandle, HOTKEY_ID1);
            if (RegisterHotKey(MyWindowHandle, HOTKEY_ID1, modifier, KeyInterop.VirtualKeyFromKey(key)) == 0)
            {
                MessageBox.Show("登録に失敗");
            }
            else
            {
                str += $" + {key}";
                str = str.Remove(0, 3);
                MessageBox.Show($"{str} を登録しました");
            }
        }


        //ホットキーが押されたときの動作
        private void ComponentDispatcher_ThreadPreprocessMessage(ref MSG msg, ref bool handled)
        {
            if (msg.message != WM_HOTKEY) return;

            switch (msg.wParam.ToInt32())
            {
                case HOTKEY_ID1:
                    MessageBox.Show("HotKey1");
                    break;
                case HOTKEY_ID2:
                    MessageBox.Show("HotKey2");
                    break;

                default:
                    break;
            }
        }


        //コンボボックス上でキーを押し下げたとき
        //入力されたキー文字は無視
        private void MyComboBoxKey_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            e.Handled = true;//キーイベント無視む~し
        }

        //コンボボックス上でキーが上げられたとき
        //修飾キー以外なら、そのキーと同じキーをコンボボックスで選択する
        //文字は無視
        private void MyComboBoxKey_PreviewKeyUp(object sender, KeyEventArgs e)
        {
            var key = e.Key;
            if ((key == Key.LeftAlt || key == Key.RightAlt ||
                key == Key.LeftCtrl || key == Key.RightCtrl ||
                key == Key.LeftShift || key == Key.RightShift ||
                key == Key.LWin || key == Key.RWin) == false)
            {
                MyComboBoxKey.SelectedValue = key;
            }

            e.Handled = true;
        }
    }
}


コンボボックスのitemSourceにKey列挙型をそのまま指定
f:id:gogowaten:20201211121845p:plain
ラクなんだけど
f:id:gogowaten:20201211121924p:plain
名前が微妙に違う、NextはPageDownキーのこと、SnapshotはPrintScreenキーだし、SelectやExecuteとかは何のキーかわからんw

他にも
f:id:gogowaten:20201211122355p:plain
ShiftキーやCtrlキーなんかの修飾キーは、表示しないほうがいいねえ

コンボボックス上でのキーイベント
f:id:gogowaten:20201211122832p:plain
PreviewKeyDownイベントだとPrintScreenキーだけ感知できなかったので、Upのほうで処理している
120行目の長いifは修飾キーを無視するためのもの



前回はタイマーを使って一定間隔でキーの状態を取得して判定していたけど、今回のRegisterHotKeyはタイマーが必要ないぶん簡単になった

アプリのウィンドウが非アクティブ状態でも
キーが押されたのを感知したいときはRegisterHotKeyを使う
キーの状態を取得するときはGetAsyncKeyStateやGetKeyStateを使う
ってかんじかなあ
あとGetAsyncKeyStateやGetKeyStateはめんどくさいけど、他のアプリで登録されているキーでも感知できるってのはいいねえ

関連記事
次回は18日後の2020/12/28、ようやくできた
gogowaten.hatenablog.com