Windowsでは任意のショートカットキーを登録できることはご存じでしょうか?
Ctrl+Alt+(任意のキー)の組み合わせに対して予め登録しておいたファイル、フォルダ、アプリケーションなどを一発で呼び出せるというものです。
ただ、反応が著しく悪いという問題があります。呼び出しに数秒程度を要する場合もあるため結構イライラしますし、はっきり言って使い物になりません。以下のサイトでもショートカットキーの話は紹介されていますが、この機能が致命的に遅いという問題については触れられていません。今回はC#でキーボードのグローバルフックを利用してこの問題を解決する方法を紹介します。
Windowsのショートカットキー問題
Windowsでは、目的とするファイル、フォルダ、アプリケーションのショートカットを作成したうえで、そのショートカットに任意のショートカットキー(正確にはCtl+Alt+割り当てキーの組み合わせ)を割り当てるという機能が標準で搭載されています。
登録の手順等は上記で紹介したサイト等で紹介されていますので、ここでは詳細については触れません。本記事で問題としているのはこの機能の挙動が安定しないということです。
- 該当のショートカットキーをデスクトップに置いておかないと反応しない(?)
- ショートカットキーを押してから反応するまでに数秒かかる場合がある
などなどです。ショートカットキーなのでさっと呼び出せないと意味がないわけです。というわけでダメなら自分で作ればいいじゃんというのが今回の記事の趣旨です。
C#でグローバルフックを
ここでいうフック、というのはキーやマウスの入力を横取りしてしまうことです。通常、C#で作ったプログラム内でキーボード・マウス入力のフックをかけるのは難しくありません。これをローカルフック、と言います。
一方、今回実現したい処理(Windowsでいろいろな作業をしているときにキー入力でファイルを開きたい)ような場合にはWindowsが受け取るキーボードやマウスのイベントをすべて監視して、必要に応じて横取りする必要があります。これをグローバルフックと呼びます。
グローバルフックはあまりお行儀のよい素行とは言えない(ユーザーに隠れてキーボード入力を盗んでしまうことも可能)なのでC#では簡単には使わせてくれません。グローバルフックを実現するためにはWindowsAPIを呼び出す必要がありますのでひと手間かかります。
もう一つの問題
もともと、この問題に取り組もうと思ったきっかけはマクロパッドを自作したことにあります。3×3のボタンを配置した自作のPCB基板を作って、よく利用するフォルダを一発で開けるようにしたかったわけです。
そして、上記のグローバルフックの問題を解決したC#プログラムを作成したのですが実際に使ってみると、呼び出されたフォルダが最前面に表示されないという問題があることがわかりました。
例えばWebブラウザを開いてWordpressの記事を書いているときに、素材のフォルダを開こうとマクロパッドを押すとフォルダは開かれるのですが、タスクバーが点滅するだけで画面の最前面に表示されないんですね。
この挙動についてはWEBを漁ったものの、明確な理由はわかりませんでした。ともかく、このままでは使いにくいのでこれについてもパワーで解決しました。
ショートカットキーを制御するプログラムが最前面にいれば正しく動く(開きたいファイルが最前面に表示される)ため、ユーザーからは見えない透明なウィンドウを作って、ショートカットキーが呼ばれるたびにこの透明なウィンドウを最小化->通常化することで強制的に前面に表示させてから目的のフォルダが開かれるようにしています。ちなみに、この透明なウィンドウをActivateするだけではダメなんです。しかし、一度タスクバーに出し入れすると正しく動きます。凄まじい脳筋プレイですがこれで動くのでヨシとしています。
解決方法
今回はVisualStudio2017Express C#を利用しました。2022でも2008でもどちらでも挙動は確認していますので、適当なバージョンを使用してください。新規プロジェクトでWindowsフォームアプリケーションを利用しました。
なお、今回は簡単のためにCtrl+Alt+Aキーの組み合わせでC:¥を開くだけのプログラムを作成します。
また、本ブログの趣旨として”要素技術の紹介”を掲げております。細かなUIなどについては各自のアイデアと好みで実装をしてください
グローバルフック用のクラスを作成
グローバルフック関連の処理を行うクラスを作成します。ここではghook.csとでもしておきます。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Diagnostics; using System.Runtime.InteropServices; using System.Windows.Forms; namespace winshortcut { class ghook { //常に最前面で表示させるためのダミーフォーム public Form dummyFrm; delegate int delegateHookCallback(int nCode, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] static extern IntPtr SetWindowsHookEx(int idHook, delegateHookCallback lpfn, IntPtr hMod, uint dwThreadId); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] static extern bool UnhookWindowsHookEx(IntPtr hhk); [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] static extern IntPtr GetModuleHandle(string lpModuleName); IntPtr hookPtr = IntPtr.Zero; bool ALT = false; bool CRL = false; bool TGT = false; public ghook() { } ~ghook() { this.HookEnd(); } public void Hook() { using (Process curProcess = Process.GetCurrentProcess()) using (ProcessModule curModule = curProcess.MainModule) { hookPtr = SetWindowsHookEx( 13, HookCallback, GetModuleHandle(curModule.ModuleName), 0 ); } } int HookCallback(int nCode, IntPtr wParam, IntPtr lParam) { // フックしたキー switch ((int)wParam) { case 257://キーを離した if ((Keys)(short)Marshal.ReadInt32(lParam) == Keys.LMenu) { this.ALT = false; } else if ((Keys)(short)Marshal.ReadInt32(lParam) == Keys.LControlKey) { this.CRL = false; } else if ((Keys)(short)Marshal.ReadInt32(lParam) == Keys.A) { this.TGT = false; } break; case 256://キーを押した if ((Keys)(short)Marshal.ReadInt32(lParam) == Keys.LMenu) { this.ALT = true; } else if ((Keys)(short)Marshal.ReadInt32(lParam) == Keys.LControlKey) { this.CRL = true; } else if ((Keys)(short)Marshal.ReadInt32(lParam) == Keys.A) { this.TGT = true; } break; } if (ALT && CRL && TGT) { OpenFile(); // 1を戻すとフックしたキーが捨てられます this.ALT = false; this.CRL = false; this.TGT = false; return 1; } else { return 0; } } private void OpenFile() { dummyFrm.WindowState = FormWindowState.Minimized; dummyFrm.WindowState = FormWindowState.Normal; dummyFrm.Activate(); //指定された番号から開くファイル名を決める。 string Target = @"c:\"; //指定したファイルを開く System.Diagnostics.Process proc = new Process(); proc.StartInfo.FileName = Target; proc.StartInfo.UseShellExecute = true; try { proc.Start(); } catch { } } public void HookEnd() { UnhookWindowsHookEx(hookPtr); hookPtr = IntPtr.Zero; } } }
メインフォーム側の設定
メインフォーム側は次のような処理にしてダミーフォームやghookのインスタンスの生成等を行います。
ポイントとしては、ghook myHook;は必ずフィールドで宣言してください。 Form1_Load()内などで宣言するとガベージコレクションでいつの間にか消されて動かなくなります。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace winshortcut { public partial class Form1 : Form { ghook myHook; public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { this.ShowInTaskbar = false; this.Hide(); //グローバルフック myHook = new ghook(); myHook.Hook(); myHook.dummyFrm = new Form(); myHook.dummyFrm.ShowInTaskbar = false; myHook.dummyFrm.FormBorderStyle = FormBorderStyle.None; myHook.dummyFrm.Opacity = 0; myHook.dummyFrm.Left = 0; myHook.dummyFrm.Top = 0; myHook.dummyFrm.Width = 100; myHook.dummyFrm.Height = 100; myHook.dummyFrm.Show(); } } }
まとめ
いかがだったでしょうか。ここで作成したアプリケーションを常駐させておくと、ショートカットキーの押下によって瞬時に目的とするファイルやフォルダを開いてくれるため、非常に便利です。
ある程度UIまで整えたアプリケーションを本サイトにて配布予定です。プログラムを触れない方はもうしばらくお待ちください。
参考にさせていただいたサイト
フォームを一度しまってから出す、という2度手間脳筋手法(誉め言葉)を参考にさせていただきました。感謝!
コメント