前置き
本プログラムはWin10想定です。
また、新しい言語を触る際の勉強の忘備録を兼ねています。従って、すでに知っている部分は読み飛ばしていただいて大丈夫です。
なれない言語でプログラムを書く際には、横で資料やチュートリアルをみながらプログラミングすることが多いと思います。
皆さんはその際にこのような経験はないでしょうか?
せっかくエディタをキーボードだけで操作できるようにこだわっても、資料を触る際にはどうしてもキーボードから手が離れてしまいます。
blenderとかを使用する際も、チュートリアル動画を止めたりうごかしたりするのが意外とめんどくさかったりします。
エディタのカーソルの位置などは保ったまま、別ウィンドウにキーボードショートカットだけを送ることができればいいのになぁ と思うことが多々あります。
それができれば、スクロールしたり動画を止めたり戻したりと、できることの幅が広がりそうですよね。
[TAB+ALT] でこの問題は解決するのですが、いちいち操作が多くてめんどくさいのでかえって時間がかかってしまいます。
そこで、今回はこの問題を解決するプログラムを書いていきたいと思います。
要件
今回作るプログラムの要件は、以下の通りです。
- ショートカットキーで別のウィンドウをアクティブにできる
- 何らかの操作ですぐに元のウィンドウをアクティブにできる
- バックグラウンドで動く
- 前もってどのウィンドウをアクティブにするかは決めておく
イメージ
動作イメージはこんな感じです。
①プロセス一覧から切り替えたいウィンドウAを選ぶ
②最小化で待機
③メインで操作したいウィンドウBで作業する
④特定のキー入力でウィンドウAをアクティブにする
⑤ウィンドウAにキーボード入力を送る
⑥特定のキー入力でウィンドウBをアクティブに
C#の勉強
C#経験者は実装まで読み飛ばし推奨
私はC#を扱うのが初めてなので、まずは情報収集から入ります。
VisualStudioが必要らしいので、入れてみる。
本当に知らないのでとりあえず調べて出てきたこのサイトで勉強
https://docs.microsoft.com/ja-jp/visualstudio/ide/create-csharp-winform-visual-studio?view=vs-2022
https://csharp.sevendays-study.com/day1.html
大体普通の言語と似た感じ。
deligateとかがよくわからないけどなんとかなるでしょう
詰まりポイント
プロジェクト作成の際にちゃんと(.NET Framework)と書いてあるテンプレートを選びましょう。うまいこといかなくなりました。
上のサイトを一通りやってみてウィンドウアプリを作る感覚がつかめたので、プログラムに必要な知識にわけて情報収集します。
必要な知識
- ウィンドウ持ちのプロセス一覧を取得できる(①)
- アクティブじゃなくてもキー入力を受け取る(②、④、⑥)
- アクティブウィンドウを取得、設定できる(④、⑥)
①プロセス一覧から切り替えたいウィンドウAを選ぶ
②最小化で待機
③メインで操作したいウィンドウBで作業する
④特定のキー入力でウィンドウAをアクティブにする
⑤ウィンドウAにキーボード入力を送る
⑥特定のキー入力でウィンドウBをアクティブに
ウィンドウ持ちのプロセス一覧を取得できる
C#.NET プロセス 一覧 で検索してみる。
これらの情報を使えば実装できそう。
「グラフィカルインターフェイスがあるプロセスを探す」で紹介した方法をそのまま使います。つまり、Process.GetProcessesメソッドで得られるすべてのプロセスから、メインウィンドウを持っているものを探すという方法です。この方法ではプロセスのメインウィンドウしか探せませんので、同じプロセスが複数のウィンドウを表示している場合は、その内1つしか取得できません。
dobon.netより引用
Process.GetProcessメソッドでプロセス一覧を取得し、その中からメインウィンドウをもつプロセスのみのリストを取得することでウィンドウありのプロセスの一覧ができあがりそうです。
アクティブじゃなくてもキー入力を受け取る
アクティブ状態でキー入力を受け取るのは大体の言語にあるけど、アクティブじゃない状態で受け取るためにはシステムからなにかインポートしないとダメそう。
調べてみるとグローバルフックというものを使うと良いらしい。
「鳩でもわかるC#」というサイトが非常に参考になりました。
参考
アクティブウィンドウを取得、設定できる
Win32 APIのSetForegroundWIndowを利用すれば良いみたいです。
参考
https://dobon.net/vb/dotnet/process/appactivate.html
実装
大体イメージがついたので実装。
完成コードはこちらとなります。
https://github.com/chikuwakun/ChangeWindow/tree/master
コードについて見ていきます。
全文
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using static ChangeWindow.KeyboardHook;
namespace ChangeWindow
{
public partial class Form1 : Form
{
// 諸々の読み込み
[DllImport("USER32.DLL")]
private static extern IntPtr GetForegroundWindow();
[DllImport("USER32.DLL")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetForegroundWindow(IntPtr hWnd);
private static string searchWindowText = null;
private static string searchClassName = null;
private static ArrayList foundProcessIds = null;
private static ArrayList foundProcesses = null;
private delegate bool EnumWindowsDelegate(IntPtr hWnd, IntPtr lparam);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private extern static bool EnumWindows(EnumWindowsDelegate lpEnumFunc,
IntPtr lparam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetWindowText(IntPtr hWnd,
StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetWindowTextLength(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetClassName(IntPtr hWnd,
StringBuilder lpClassName, int nMaxCount);
[DllImport("user32.dll", SetLastError = true)]
private static extern int GetWindowThreadProcessId(
IntPtr hWnd, out int lpdwProcessId);
public static Process[] GetProcessesByWindow(
string windowText, string className)
{
//検索の準備をする
foundProcesses = new ArrayList();
foundProcessIds = new ArrayList();
searchWindowText = windowText;
searchClassName = className;
//ウィンドウを列挙して、対象のプロセスを探す
EnumWindows(new EnumWindowsDelegate(EnumWindowCallBack), IntPtr.Zero);
//結果を返す
return (Process[])foundProcesses.ToArray(typeof(Process));
}
public static List<Process> GetProcessesOnlyWindow()
{
List<Process> foundProcesses = new List<Process>();
//ウィンドウのタイトルに「」を含むプロセスをすべて取得する
Process[] ps = GetProcessesByWindow("", null);
//windowの名前があるプロセスだけをリストに入れる
foreach (Process p in ps)
{
if (p.MainWindowTitle.Length > 1)
{
foundProcesses.Add(p);
}
}
return foundProcesses;
}
private static bool EnumWindowCallBack(IntPtr hWnd, IntPtr lparam)
{
if (searchWindowText != null)
{
//ウィンドウのタイトルの長さを取得する
int textLen = GetWindowTextLength(hWnd);
if (textLen == 0)
{
//次のウィンドウを検索
return true;
}
//ウィンドウのタイトルを取得する
StringBuilder tsb = new StringBuilder(textLen + 1);
GetWindowText(hWnd, tsb, tsb.Capacity);
//タイトルに指定された文字列を含むか
if (tsb.ToString().IndexOf(searchWindowText) < 0)
{
//含んでいない時は、次のウィンドウを検索
return true;
}
}
if (searchClassName != null)
{
//ウィンドウのクラス名を取得する
StringBuilder csb = new StringBuilder(256);
GetClassName(hWnd, csb, csb.Capacity);
//クラス名に指定された文字列を含むか
if (csb.ToString().IndexOf(searchClassName) < 0)
{
//含んでいない時は、次のウィンドウを検索
return true;
}
}
//プロセスのIDを取得する
int processId;
GetWindowThreadProcessId(hWnd, out processId);
//今まで見つかったプロセスでは無いことを確認する
if (!foundProcessIds.Contains(processId))
{
foundProcessIds.Add(processId);
//プロセスIDをからProcessオブジェクトを作成する
foundProcesses.Add(Process.GetProcessById(processId));
}
//次のウィンドウを検索
return true;
}
//keyboard Hook
KeyboardHook keyboardHook = new KeyboardHook();
private List<int> plessedKeys = new List<int>();
protected override void OnLoad(EventArgs e)
{
keyboardHook.KeyDownEvent += KeyboardHook_KeyDownEvent;
keyboardHook.KeyUpEvent += KeyboardHook_KeyUpEvent;
keyboardHook.Hook();
}
//shift
public int key1 = 32;
//space
public int key2 = 160;
//Esc
public int keyEsc = 27;
public bool flagOn;
IntPtr latestWindow;
private void KeyboardHook_KeyDownEvent(object sender, KeyEventArg e)
{
plessedKeys.Add(e.KeyCode);
if (plessedKeys.Contains(key1) && plessedKeys.Contains(key2))
{
//同時おししたときにやりたい
latestWindow = GetForegroundWindow();
SetForegroundWindow(selectedProcess.MainWindowHandle);
plessedKeys.Remove(key1);
plessedKeys.Remove(key2);
flagOn = true;
}
if(flagOn&&e.KeyCode == keyEsc)
{
flagOn = false;
SetForegroundWindow(latestWindow);
}
}
//ここからメインの処理
//選択中のプロセス
public Process selectedProcess = null;
public Form1()
{
InitializeComponent();
//コンボボックス初期設定
List<Process> comboBoxMyItems = GetProcessesOnlyWindow();
//comboBoxの表示を指定
comboBox1.DisplayMember = "MainWindowTitle";
comboBox1.ValueMember = "Id";
comboBox1.DataSource = comboBoxMyItems;
}
private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
Process selected = (Process)comboBox1.SelectedItem;
string selectedItem = selected.MainWindowTitle;
label1.Text = selectedItem + "が選択されています。";
selectedProcess = (Process)comboBox1.SelectedItem;
}
//タスクバーのアイコンダブルクリックでフォームが表示されるようにする
private void notifyIcon1_MouseDoubleClick(object sender, MouseEventArgs e)
{
this.Visible = true; //フォームの表示
if (this.WindowState == FormWindowState.Minimized)
{
this.WindowState = FormWindowState.Normal;
}
this.Activate();
}
//非表示ボタンを押すと非表示
private void button3_Click(object sender, EventArgs e)
{
this.Visible = false;
}
//更新ボタンを押すとプロセスを再取得する
private void button1_Click(object sender, EventArgs e)
{
comboBox1.DataSource = GetProcessesOnlyWindow();
}
}
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)
{
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))
{
var kb = (KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));
var vkCode = (int)kb.vkCode;
OnKeyDownEvent(vkCode);
}
else if (nCode >= 0 && (wParam == (IntPtr)WM_KEYUP || wParam == (IntPtr)WM_SYSKEYUP))
{
var kb = (KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));
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 void OnKeyDownEvent(int keyCode)
{
KeyDownEvent?.Invoke(this, new KeyEventArg(keyCode));
}
protected void OnKeyUpEvent(int keyCode)
{
KeyUpEvent?.Invoke(this, new KeyEventArg(keyCode));
}
public class KeyEventArg : EventArgs
{
public int KeyCode { get; }
public KeyEventArg(int keyCode)
{
KeyCode = keyCode;
}
}
}
}
詳細
20~142行目はdobon.net様のこちらの記事を参考、引用してかかせていただいています。
詳しい説明等は以下のサイトで読んでいただけると幸いです。
以下の部分がFormの処理部分です。
//選択中のプロセス
public Process selectedProcess = null;
public Form1()
{
InitializeComponent();
//コンボボックス初期設定
List<Process> comboBoxMyItems = GetProcessesOnlyWindow();
//comboBoxの表示を指定
comboBox1.DisplayMember = "MainWindowTitle";
comboBox1.ValueMember = "Id";
comboBox1.DataSource = comboBoxMyItems;
}
private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
Process selected = (Process)comboBox1.SelectedItem;
string selectedItem = selected.MainWindowTitle;
label1.Text = selectedItem + "が選択されています。";
selectedProcess = (Process)comboBox1.SelectedItem;
}
//タスクバーのアイコンダブルクリックでフォームが表示されるようにする
private void notifyIcon1_MouseDoubleClick(object sender, MouseEventArgs e)
{
this.Visible = true; //フォームの表示
if (this.WindowState == FormWindowState.Minimized)
{
this.WindowState = FormWindowState.Normal;
}
this.Activate();
}
//非表示ボタンを押すと非表示
private void button3_Click(object sender, EventArgs e)
{
this.Visible = false;
}
//更新ボタンを押すとプロセスを再取得する
private void button1_Click(object sender, EventArgs e)
{
comboBox1.DataSource = GetProcessesOnlyWindow();
}
}
Formの初期設定時にGetProcessOnlyWindow()を呼び出すことで、ウィンドゥのあるプロセスの一覧をプロセスのリストとして取得し、それをcomboboxのDataSourceとして設定します。こうすることで初回起動時にプロセスの一覧をコンボボックスが持つことができます。
また、コンボボックスはデフォルトでは各要素のtoString()が呼び出されますが、DisplayMemberにフィールドを指定することで表示名を変えることができます。
更に、コンボボックスの値はValueMemberによりフィールドを指定することができます。
InitializeComponent();
//コンボボックス初期設定
List<Process> comboBoxMyItems = GetProcessesOnlyWindow();
//comboBoxの表示を指定
comboBox1.DisplayMember = "MainWindowTitle";
comboBox1.ValueMember = "Id";
comboBox1.DataSource = comboBoxMyItems;
KeyHook クラスに関しては以下のサイトを参考にさせていただいており、まずはそちらをよんでいただくことをおすすめします。
https://lets-csharp.com/keyboard-hook/
KeyboardHook keyboardHook = new KeyboardHook();
private List<int> plessedKeys = new List<int>();
protected override void OnLoad(EventArgs e)
{
keyboardHook.KeyDownEvent += KeyboardHook_KeyDownEvent;
keyboardHook.KeyUpEvent += KeyboardHook_KeyUpEvent;
keyboardHook.Hook();
}
//shift
public int key1 = 32;
//space
public int key2 = 160;
//Esc
public int keyEsc = 27;
public bool flagOn;
IntPtr latestWindow;
private void KeyboardHook_KeyDownEvent(object sender, KeyEventArg e)
{
plessedKeys.Add(e.KeyCode);
if (plessedKeys.Contains(key1) && plessedKeys.Contains(key2))
{
//同時おししたときにやりたい
latestWindow = GetForegroundWindow();
SetForegroundWindow(selectedProcess.MainWindowHandle);
plessedKeys.Remove(key1);
plessedKeys.Remove(key2);
flagOn = true;
}
if(flagOn&&e.KeyCode == keyEsc)
{
flagOn = false;
SetForegroundWindow(latestWindow);
}
}
今回はアクティブウィンドウ切り替えのためのキーとしてshift+space
を、もとのウィンドウをアクティブにするキーとしてEsc
を採用しています。
使用する環境によってはキーコードが違いますので、それぞれの環境にあったキーコードを設定してください。
//shift
public int key1 = 32;
//space
public int key2 = 160;
//Esc
public int keyEsc = 27;
同時押しを実装するためにflagOn
という変数を用意しています。
キー入力があった場合、plessedKeys
にそのキーコードが保存されます。
その後plessedKeys
内に key1とkey2に対応したコードが入っていた場合は現在のアクティブウィンドウをlatestWindowに残しておき、アクティブウィンドウをコンボボックスで選んだselectedProcess
にしてkeys1と2を削除します。
if (plessedKeys.Contains(key1) && plessedKeys.Contains(key2))
{
//同時おししたときにやりたい
latestWindow = GetForegroundWindow();
SetForegroundWindow(selectedProcess.MainWindowHandle);
plessedKeys.Remove(key1);
plessedKeys.Remove(key2);
flagOn = true;
}
また、Escキーが押された場合はlatestWindow
をアクティブウィンドウにします。
if(flagOn&&e.KeyCode == keyEsc)
{
flagOn = false;
SetForegroundWindow(latestWindow);
}
実演
VSとblenderで実際に利用してみました。
ちゃんと動作していますね。
今後
現在、切り替えの際にキーがご動作する場合があるのでその修正を行っています。
また、ショートカットキーをビルドの際にキーコードをコーディングするのではなく、フォームから入力して設定できるようにしようと考えています。
この度初めてC#でプログラムを書いたため至らぬ点も多く存在すると思います。
もし何か見つけましたらコメントしていただけると幸いです。