個人的なことで申し訳ありませんが、鳩でもわかるC#管理人はタイピングが遅いです。そこで決められた文字列を登録しておいてペースト処理をおこなえばあたかも人が1文字ずつ打ち込んでいるかのように見せるためのアプリをつくったのですが、結果的には失敗でした。
これがそのアプリです。
作ったのでこのブログでネタにします。どんなコードを書いたのかをみてみましょう。
グローバルキーフックをするクラスの定義
まず自分自身ではないアプリ(テキストエディタやVisualStudo)などにペーストをすることを考えているのでグローバルキーフックをします。
最初にそのためのクラスを定義します。このクラスはC#でグローバルキーフックをする方法とほとんど同じです。違う点はキーが押されたときに本来の動作をさせるかさせないかをコントロールできるようにさせていることです。
ペーストを実行するためにはCtrlキーを押しながらVキーをおします。このときに普通にペーストの処理をさせずに指定された文字列を1文字ずつペーストしていきます。Shiftキーを押しながらInsertキーを押してもペーストできますが、今回は無視させてください。
変更しているのはOnKeyDownEventメソッドの定義です。自分で定義したKeyDownEventイベントが発生したときにイベントハンドラにKeyEventArgオブジェクトと引数として渡しますが、処理が終わったらIsCancelプロパティを調べてその値を返しています。
これによってCallNextHookEx関数を呼び出したり呼び出さなかったりと制御できるので、キーフックして自分がやりたい処理を行なわせたあと、本来の動作をさせたりさせないことが可能になるのです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
using System.Diagnostics; using System.Runtime.InteropServices; class KeyboardHook { protected const int WH_KEYBOARD_LL = 0x000D; protected const int WM_KEYDOWN = 0x0100; protected const int WM_KEYUP = 0x0101; protected const int WM_SYSKEYDOWN = 0x0104; protected const int WM_SYSKEYUP = 0x0105; [StructLayout(LayoutKind.Sequential)] public class KBDLLHOOKSTRUCT { public uint vkCode; public uint scanCode; public KBDLLHOOKSTRUCTFlags flags; public uint time; public UIntPtr dwExtraInfo; } [Flags] public enum KBDLLHOOKSTRUCTFlags : uint { KEYEVENTF_EXTENDEDKEY = 0x0001, KEYEVENTF_KEYUP = 0x0002, KEYEVENTF_SCANCODE = 0x0008, KEYEVENTF_UNICODE = 0x0004, } [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr SetWindowsHookEx(int idHook, KeyboardProc lpfn, IntPtr hMod, uint dwThreadId); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool UnhookWindowsHookEx(IntPtr hhk); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr GetModuleHandle(string lpModuleName); private delegate IntPtr KeyboardProc(int nCode, IntPtr wParam, IntPtr lParam); private KeyboardProc proc; private IntPtr hookId = IntPtr.Zero; public void Hook() { if (hookId == IntPtr.Zero) { proc = HookProcedure; using (var curProcess = Process.GetCurrentProcess()) { using (ProcessModule? curModule = curProcess.MainModule) { if(curModule != null && curModule.ModuleName != null) hookId = SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0); } } } } public void UnHook() { UnhookWindowsHookEx(hookId); hookId = IntPtr.Zero; } public IntPtr HookProcedure(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0 && (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN)) { KBDLLHOOKSTRUCT? kb = (KBDLLHOOKSTRUCT?)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT)); if (kb != null) { // ここを変更している int vkCode = (int)kb.vkCode; if(OnKeyDownEvent(vkCode)) return IntPtr.MaxValue; } } else if (nCode >= 0 && (wParam == (IntPtr)WM_KEYUP || wParam == (IntPtr)WM_SYSKEYUP)) { KBDLLHOOKSTRUCT ? kb = (KBDLLHOOKSTRUCT?)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT)); if (kb != null) { var vkCode = (int)kb.vkCode; OnKeyUpEvent(vkCode); } } return CallNextHookEx(hookId, nCode, wParam, lParam); } public delegate void KeyEventHandler(object sender, KeyEventArg e); public event KeyEventHandler? KeyDownEvent; public event KeyEventHandler? KeyUpEvent; // ここを変更している protected bool OnKeyDownEvent(int keyCode) { KeyEventArg arg = new KeyEventArg(keyCode); KeyDownEvent?.Invoke(this, arg); return arg.IsCancel; } protected void OnKeyUpEvent(int keyCode) { KeyUpEvent?.Invoke(this, new KeyEventArg(keyCode)); } } public class KeyEventArg : EventArgs { public int KeyCode { get; } public bool IsCancel { set; get; } public KeyEventArg(int keyCode) { KeyCode = keyCode; IsCancel = false; } } |
Form1クラス
次にForm1クラスを考えます。最初の動画で最初にでてきたフォームにRichTextBoxを貼り付けただけの簡単な作りです。
コンストラクタ内でキー操作時のイベントハンドラを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public partial class Form1 : Form { KeyboardHook keyboardHook = new KeyboardHook(); public Form1() { InitializeComponent(); keyboardHook.KeyDownEvent += KeyboardHook_KeyDownEvent; ; keyboardHook.KeyUpEvent += KeyboardHook_KeyUpEvent; ; keyboardHook.Hook(); } } |
イベントハンドラの定義
キーが押されたときと離されたときのイベントハンドラを定義します。
ペーストの処理はCtrlキーを押しているときにVキーが押されたときなので、現在Ctrlキーが推されている状態かどうかを保存しておくフラグ(isCtrlKeyDown)を用意します。そのキーがCtrlキーかどうかはe.KeyCodeを調べればわかります。162か163の場合です。
KeysConverterを使えばe.KeyCodeを文字列に変換できます。isCtrlKeyDownがtrueでkc.ConvertToString(e.KeyCode) が “V”のときがペースト処理がおこわれるその時ということになります。このときは後述する自作メソッドのPasteCharacterByCharacterを呼び出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public partial class Form1 : Form { bool isCtrlKeyDown = false; KeysConverter kc = new KeysConverter(); private async void KeyboardHook_KeyDownEvent(object sender, KeyEventArg e) { if (kc.ConvertToString(e.KeyCode) == "V" && isCtrlKeyDown) await PasteCharacterByCharacter(e); if (e.KeyCode == 162 || e.KeyCode == 163) isCtrlKeyDown = true; } private void KeyboardHook_KeyUpEvent(object sender, KeyEventArg e) { if (e.KeyCode == 162 || e.KeyCode == 163) isCtrlKeyDown = false; } } |
一文字ずつペーストをする
一文字ずつペーストをする処理を示します。
isAllowフラグを用意していますが、文字列を文字に分解してクリップボードにコピー、そのあとSendKeys.SendWait(“^v”)を実行してCtrl+Vを押したのと同じ効果を得ようとしていますが、するとそのときにまたPasteCharacterByCharacterメソッドが実行されてしまいます。
すると無限ループと同じような現象がおきてフリーズしてしまいます。またタスクマネージャをつかって強制終了させたときキーが離されたときにおこなわれるはずの処理がおこなわれていないのでキーが押しっぱなしのままロックされた状態になります。
フリーズしたうえにキー操作したときにおかしな動作をする。最初はなにがおきたのかわからず慌ててしまいましたが、上記がその原因でした。キーフックするときは注意が必要です。マウスフックも同時にしてバグると完全に死にます。
一文字ずつクリップボードにコピーしてペーストをする処理を繰り返していますが、改行文字だけをひとつだけコピーするとペーストしてもなにもおきません(改行文字だけでも複数であるならペーストできる)。そこでforeach内で’\n’を取得してしまったときは文字列に追加してほかの文字を取得したときいっしょにペースト処理を実行しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
public partial class Form1 : Form { bool isAllow = true; async Task PasteCharacterByCharacter(KeyEventArg e) { if (!richTextBox1.Focused) { if (!isAllow) return; isAllow = false; { e.IsCancel = true; string str = richTextBox1.Text; str = str.Replace("\r\n", "\n"); Char[] chars = str.ToArray(); string pastText = ""; foreach (char c in chars) { await Task.Delay(100); { if (c.ToString() != "\n") { pastText += c.ToString(); Clipboard.SetText(pastText); SendKeys.SendWait("^v"); pastText = ""; var a = Task.Run(() => { PlaySound(); }); } else { pastText += "\r\n"; } } } } isAllow = true; } } } |
効果音の再生
入力時にキーを叩いたときの音がでるように効果音を鳴らします。ソリューションエクスプローラの[参照に追加]からC:\Windows\system32\wmp.dllを追加します。するとWMPLib.WindowsMediaPlayerをつかって効果音の再生ができるようになります。
ただWMPLib.WindowsMediaPlayerがひとつだけだと、同じ音を短い間隔で再生する場合、音がなりはじめる前につぎの音を鳴らす処理がはじまってしまうため、最後の音しか再生されないという問題がおきます。そこでWMPLib.WindowsMediaPlayerオブジェクトを複数生成して処理を分散させます。5つくらいあれば対応できることがわかりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { List<WMPLib.WindowsMediaPlayer> players = new List<WindowsMediaPlayer>(); int soundCount = 0; void PlaySound() { if (players.Count == 0) { for(int i = 0; i < 5; i++) players.Add(new WMPLib.WindowsMediaPlayer()); } soundCount++; int index = soundCount % 5; players[index].URL = System.Windows.Forms.Application.StartupPath + "\\type.mp3"; players[index].controls.play(); } } |
グローバルキーフックの解除
アプリケーションを終了するときにキーフックを解除します。この処理を忘れてしまうとどうなるのでしょうか? 特に問題は確認できませんでした(見えないところでトラブルが起きているかも)が、一応やっておきます。
1 2 3 4 5 6 7 |
public partial class Form1 : Form { protected override void OnFormClosing(FormClosingEventArgs e) { keyboardHook.UnHook(); } } |
これで実際に使い物になるかどうか試してみましたが、テキストエディタやたいていのワープロソフトであれば使えます。ところが肝心のVisualStudoではつかえません。VisualStudoでは自動的に入力された文字列を整形する機能(int i=0; をint i = 0;に変換するなど)があり、これが邪魔しているのかうまく文字列が出力できませんでした。
ということでジョークプログラム以上のものではなく、実際には使い物になりません。