以前、クリップボードの履歴をとるアプリを作成しました。
ただ、これは履歴をとるだけでそこからワンタッチでそれらをペーストする機能はありませんでした。ここではその機能を追加します。実際にクリップボードの履歴をとり、そこからペーストをすることができるアプリは存在します。しかし履歴をとられては不味いものもあるはずです。このようなコントロールができないので自作することにしました。
前回グローバルキーフックをする方法を解説しました。
これを使えば他のアプリを使っていてもクリップボードの履歴をとったり、そこからテキストをペーストすることができそうです。
KeyboardHookクラスをそのまま使います。KeyboardHookクラスの内容は
を参照してください。
まずグローバルフックをするために
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class Form1 : Form { KeyboardHook keyboardHook = new KeyboardHook(); public Form1() { InitializeComponent(); keyboardHook.KeyDownEvent += KeyboardHook_KeyDownEvent; keyboardHook.Hook(); } } |
とやる必要があります。それからグローバルフックを解除するためにアプリケーションが終了するときに
1 2 3 4 5 6 7 |
public partial class Form1 : Form { protected override void OnFormClosing(FormClosingEventArgs e) { keyboardHook.UnHook(); } } |
という処理も必要です。
またクリップボードの変更されたことを知るために以下も追加します。
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 |
public partial class Form1 : Form { [DllImport("user32.dll", SetLastError = true)] private extern static void AddClipboardFormatListener(IntPtr hwnd); [DllImport("user32.dll", SetLastError = true)] private extern static void RemoveClipboardFormatListener(IntPtr hwnd); private const int WM_CLIPBOARDUPDATE = 0x31D; public Form1() { InitializeComponent(); keyboardHook.KeyDownEvent += KeyboardHook_KeyDownEvent; keyboardHook.Hook(); // これを追加 AddClipboardFormatListener(Handle); } protected override void OnFormClosing(FormClosingEventArgs e) { // アプリケーション終了時には忘れずもとに戻す keyboardHook.UnHook(); RemoveClipboardFormatListener(Handle); } protected override void WndProc(ref Message m) { if(m.Msg == WM_CLIPBOARDUPDATE) { OnClipboardUpdate(); m.Result = IntPtr.Zero; } else base.WndProc(ref m); } void OnClipboardUpdate() { // クリップボードのデータが変更されたときにやりたいことを書く } } |
ではキーボードが押されたときやクリップボード内のテキストデータが変更されたときはどうすればよいのでしょうか?
ここではクリップボードのデータが変更されたら、クリップボード内のデータがテキストなのかどうかを調べます。そしてテキストであれば、これをリストに保存します。
あとになってメニューを表示して、どれをペーストするのかを選択できるようにしておかないといけません。そこでペーストするための文字列を格納するリストを用意します(List<string> CopiedStrings)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public partial class Form1 : Form { List<string> CopiedStrings = new List<string>(); void OnClipboardUpdate() { if(isNotSave) return; // クリップボードのデータが変更された if(Clipboard.ContainsText()) { string str = Clipboard.GetText(); CopiedStrings.Insert(0, str); } } } |
Shiftキーを2回連続して押すとコピーされた文字列の履歴を表示されるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public partial class Form1 : Form { DateTime dtLastKeyDowm = DateTime.MinValue; private void KeyboardHook_KeyDownEvent(object sender, KeyEventArg e) { // e.KeyCode == 160 || e.KeyCode == 161 は左右のshiftキー if(e.KeyCode == 160 || e.KeyCode == 161) { // 1秒以内にもう一度Shiftキーが押されたらを2回連続して押されたと見なす bool b = DateTime.Now < dtLastKeyDowm.AddSeconds(1); dtLastKeyDowm = DateTime.Now; if(b) ShowMenu(); } } } |
コピーされた文字列は複数行で文字数も相当な文字数になることが予想されます。そこでメニューには先頭の一部のみを表示します。
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 |
public partial class Form1 : Form { ContextMenuStrip contextMenuStrip = new ContextMenuStrip(); void ShowMenu() { contextMenuStrip.Items.Clear(); int count = CopiedStrings.Count; if(count == 0) { contextMenuStrip.Items.Add("[キャンセル]クリップボードの履歴はありません"); Point pos = Cursor.Position; contextMenuStrip.Opacity = 0.8; contextMenuStrip.Show(pos); return; } for(int i = 0; i< count; i++) { string itemText = CopiedStrings[i]; // 改行は文字列"[改行]"に変換 itemText = itemText.Replace("\r\n", "[改行]"); // タブ文字は 半角スペースに変換 itemText = itemText.Replace("\t", " "); // 連続するスペースは1つの半角スペースに変換 itemText = itemText.Replace(" ", " "); string old = itemText; do { old = itemText; itemText = itemText.Replace(" ", " "); } while(old != itemText); if(itemText.Length > 100) itemText = itemText.Substring(0, 100); contextMenuStrip.Items.Add(itemText, null, OnEvent); } contextMenuStrip.Items.Add("[キャンセル]"); Point p = Cursor.Position; contextMenuStrip.Opacity = 0.8; contextMenuStrip.Show(p); } } |
メニューが選択されたらそれに対応するテキストがクリップボードにコピーされペーストされます。このときもクリップボード内のデータがかわってしまうのでメニューの項目が無意味に増えてしまいます。そこでそうならないようにペースト処理のときはクリップボード内のテキストデータが変更されても無視するようにしています。
ペースト処理をするときだけ isIgnoreUpdateClipboard フラグをセットして終わったらすぐに戻してしまう方法ではうまくいきません。そこでペースト処理がおわってから1秒待ってからisIgnoreUpdateClipboard フラグをクリアしています。
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 |
public partial class Form1 : Form { bool isIgnoreUpdateClipboard = false; protected void OnEvent(object sender, EventArgs e) { ToolStripMenuItem toolStrip = (ToolStripMenuItem)sender; int index = contextMenuStrip.Items.IndexOf(toolStrip); if(index == -1) return; string str = CopiedStrings[index]; isIgnoreUpdateClipboard = true; Clipboard.SetText(str); SendKeys.Send("^(v)"); Timer timer = new Timer(); timer.Interval = 1000; timer.Tick += Timer_Tick; timer.Start(); } private void Timer_Tick(object sender, EventArgs e) { Timer t = (Timer)sender; t.Stop(); t.Dispose(); isIgnoreUpdateClipboard = false; } void OnClipboardUpdate() { // フラグがセットされているときはクリップボードのデータが変更されても無視 if(isIgnoreUpdateClipboard) return; // クリップボードのデータが変更された if(Clipboard.ContainsText()) { string str = Clipboard.GetText(); CopiedStrings.Insert(0, str); } } } |
それからクリップボード内のテキストによっては履歴をとりたくないものもあるはずです。マスターパスワードだけ覚えておけばたくさんの異なるパスワードを管理することができるパスワード管理ソフト(KeePassなど)は便利ですが、これだとクリップボードに転送されたパスワードまで履歴に残ってしまいます。これは困りますね。
そこでパスワードらしきものがクリップボードに転送された場合は履歴に残らないようにします。パスワードには全角文字はないはずなので、すべて半角英数や記号である場合はパスワードかもしれないと見なして記録しないようにしています。
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 |
public partial class Form1 : Form { List<string> CopiedStrings = new List<string>(); void OnClipboardUpdate() { if(isIgnoreUpdateClipboard) return; // クリップボードのデータが変更された if(Clipboard.ContainsText()) { string str = Clipboard.GetText(); if(!IsPassWord(str)) CopiedStrings.Insert(0, str); } } bool IsPassWord(string str) { char[] vs = str.ToArray(); // 全角文字があればパスワードではない if (vs.Any(x => x > 0x7f)) return false; // 改行文字があればパスワードではない if(vs.Any(x => x == '\n')) return false; // タブ文字があればパスワードではない if(vs.Any(x => x == '\t')) return false; // 登録できないことを示す音を鳴らす Console.Beep(); return true; } } |