C#でキー入力を取得する方法|Console.ReadKey・KeyDown・非同期処理まで初心者向けに解説

はじめに

C#でキー入力を取得する方法は、作成しているアプリケーションの種類によって大きく変わります。

コンソールアプリであれば Console.ReadKeyConsole.ReadLine を使うのが基本です。一方、Windows FormsやWPFのようなGUIアプリでは、KeyDown イベントや PreviewKeyDown イベントを使ってキー入力を受け取ります。

また、キー入力を待っている間に処理が止まってしまう問題を避けたい場合は、Console.KeyAvailableTask.Runasync/await などを組み合わせる必要があります。

この記事では、C#でキー入力を取得する基本から、矢印キー・Enterキー・Escキーの判定、Shift・Ctrl・Altとの組み合わせ、非同期処理、Windows Forms、WPFでの実装方法まで、初心者向けに順番に解説します。

1. C#でキー入力を取得する前に知っておきたい基本

1-1. 「C# キー入力」で検索する人が知りたいこと

「C# キー入力」で検索する人が知りたい内容は、主に次のようなものです。

コンソールアプリでキーボードから1文字入力したい、EnterキーやEscキーが押されたか判定したい、矢印キーでメニューを操作したい、キー入力を待っている間も別の処理を続けたい、Windows FormsやWPFでキー入力イベントを使いたい、といったケースです。

C#では、すべてのアプリで同じ方法を使うわけではありません。コンソールアプリでは Console.ReadKey がよく使われますが、Windows Formsでは KeyDown イベント、WPFでは KeyDownPreviewKeyDown イベントを使います。

そのため、まずは「どの種類のアプリでキー入力を取得したいのか」を明確にすることが大切です。

1-2. コンソールアプリ・Windows Forms・WPFでキー入力の取得方法が違う理由

C#のキー入力取得方法がアプリの種類によって違う理由は、入力を受け取る仕組みが異なるためです。

コンソールアプリは、コマンドプロンプトやターミナル上で動作します。そのため、標準入力から文字やキーを受け取る Console.ReadKeyConsole.ReadLine を使います。

Windows FormsやWPFは、ウィンドウ上のボタン、テキストボックス、フォームなどのコントロールに対してユーザーが操作を行います。そのため、キーが押されたタイミングで発生するイベントを使って処理します。

たとえば、コンソールアプリでは次のようにキー入力を取得します。

C#
ConsoleKeyInfo keyInfo = Console.ReadKey();

Windows Formsでは、次のようにイベントで受け取ります。

C#
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
MessageBox.Show("Enterキーが押されました");
}
}

WPFでは、次のように KeyDown イベントを使います。

C#
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
MessageBox.Show("Enterキーが押されました");
}
}

同じ「Enterキーを判定する」処理でも、書き方が異なる点に注意しましょう。

1-3. 目的別に使い分ける方法:Console.ReadKey・KeyDown・非同期処理

C#でキー入力を取得するときは、目的に応じて方法を選びます。

コンソールアプリで1つのキー入力を取得したい場合は、Console.ReadKey が最も簡単です。たとえば「何かキーを押したら終了する」「Escキーが押されたら終了する」といった処理に向いています。

Enterキーが押されるまで文字列を入力したい場合は、Console.ReadLine を使います。名前、メールアドレス、数値など、まとまった文字列を入力してもらう場合はこちらが適しています。

処理を止めずにキー入力を確認したい場合は、Console.KeyAvailable を使います。ゲームループやリアルタイム処理のように、キー入力を監視しながら画面更新や計算を続けたい場合に便利です。

Windows FormsやWPFでキー入力を取得したい場合は、KeyDown イベントを使います。GUIアプリでは、コンソール用の Console.ReadKey ではなく、イベントを使ってキー入力を処理するのが基本です。

1-4. この記事で扱うサンプルコードの前提環境

この記事のサンプルコードは、主に次の環境を想定しています。

C#の基本的な文法を理解していること、Visual StudioまたはVisual Studio CodeでC#を実行できること、コンソールアプリ・Windows Forms・WPFの違いをある程度知っていることを前提にしています。

コンソールアプリの例では、.NETのコンソールプロジェクトを想定します。

C#
using System;

class Program
{
static void Main()
{
Console.WriteLine("キーを押してください。");
Console.ReadKey();
}
}

Windows FormsやWPFの例では、イベントハンドラーにコードを記述します。コンソールアプリとはコードを書く場所が異なるため、サンプルをそのまま貼り付ける場所に注意してください。

2. コンソールアプリでキー入力を取得する基本:Console.ReadKey

2-1. Console.ReadKeyとは

Console.ReadKey は、コンソールアプリでキーボードから1つのキー入力を取得するためのメソッドです。

たとえば、ユーザーが何かキーを押すまで待機したい場合に使います。

C#
Console.WriteLine("何かキーを押してください...");
Console.ReadKey();

このコードを実行すると、画面にメッセージが表示され、ユーザーがキーを押すまでプログラムはそこで待機します。

Console.ReadKey は、押されたキーの情報を ConsoleKeyInfo として返します。これにより、押されたキーがEnterなのか、Escなのか、Aキーなのか、Shiftキーと一緒に押されたのかなどを判定できます。

2-2. 1文字のキー入力を取得する基本コード

1文字のキー入力を取得する基本コードは次のとおりです。

C#
using System;

class Program
{
static void Main()
{
Console.WriteLine("キーを1つ押してください。");

ConsoleKeyInfo keyInfo = Console.ReadKey();

Console.WriteLine();
Console.WriteLine($"押された文字: {keyInfo.KeyChar}");
}
}

このコードでは、Console.ReadKey() によってユーザーが押したキーを取得し、KeyChar プロパティで文字として表示しています。

たとえば、ユーザーが a を押した場合、押された文字: a のように表示されます。

ただし、矢印キーやEscキー、ファンクションキーのように文字として表せないキーの場合、KeyChar だけでは正しく扱えないことがあります。その場合は、Key プロパティを使います。

2-3. 押されたキーをConsoleKeyInfoで確認する方法

Console.ReadKey の戻り値である ConsoleKeyInfo には、押されたキーに関する情報が入っています。

C#
ConsoleKeyInfo keyInfo = Console.ReadKey();

Console.WriteLine();
Console.WriteLine($"Key: {keyInfo.Key}");
Console.WriteLine($"KeyChar: {keyInfo.KeyChar}");
Console.WriteLine($"Modifiers: {keyInfo.Modifiers}");

ConsoleKeyInfo を使うと、文字だけでなく、キーの種類や修飾キーの情報も確認できます。

たとえば A キーを押した場合、Key には AKeyChar には a または A が入ります。Shiftキーを押しながら入力した場合は、ModifiersShift が含まれます。

2-4. Key・KeyChar・Modifiersの違い

ConsoleKeyInfo でよく使うプロパティは、KeyKeyCharModifiers の3つです。

Key は、押されたキーの種類を表します。Enterキーなら ConsoleKey.Enter、Escキーなら ConsoleKey.Escape、上矢印キーなら ConsoleKey.UpArrow のように判定できます。

C#
if (keyInfo.Key == ConsoleKey.Enter)
{
Console.WriteLine("Enterキーが押されました");
}

KeyChar は、押されたキーを文字として取得します。a1 など、文字入力として扱いたい場合に使います。

C#
Console.WriteLine($"入力された文字: {keyInfo.KeyChar}");

Modifiers は、Shift、Ctrl、Altなどの修飾キーが同時に押されていたかを表します。

C#
if ((keyInfo.Modifiers & ConsoleModifiers.Control) != 0)
{
Console.WriteLine("Ctrlキーが押されています");
}

文字として扱いたい場合は KeyChar、キーの種類で判定したい場合は Key、ShiftやCtrlとの組み合わせを見たい場合は Modifiers を使うと覚えておくとよいでしょう。

2-5. Enterキー・Escキー・矢印キーを判定する方法

Enterキー、Escキー、矢印キーは、Key プロパティで判定します。

C#
Console.WriteLine("キーを押してください。");

ConsoleKeyInfo keyInfo = Console.ReadKey(true);

switch (keyInfo.Key)
{
case ConsoleKey.Enter:
Console.WriteLine("Enterキーが押されました");
break;

case ConsoleKey.Escape:
Console.WriteLine("Escキーが押されました");
break;

case ConsoleKey.UpArrow:
Console.WriteLine("上矢印キーが押されました");
break;

case ConsoleKey.DownArrow:
Console.WriteLine("下矢印キーが押されました");
break;

case ConsoleKey.LeftArrow:
Console.WriteLine("左矢印キーが押されました");
break;

case ConsoleKey.RightArrow:
Console.WriteLine("右矢印キーが押されました");
break;

default:
Console.WriteLine($"その他のキー: {keyInfo.Key}");
break;
}

矢印キーは文字ではないため、KeyChar ではなく Key を使って判定します。

メニュー操作やカーソル移動を実装する場合は、ConsoleKey.UpArrowConsoleKey.DownArrow を使うことが多いです。

2-6. Console.ReadKey(true)で入力文字を画面に表示しない方法

Console.ReadKey には、引数に true または false を指定できます。

C#
Console.ReadKey(true);

true を指定すると、押されたキーがコンソール画面に表示されません。

たとえば、ユーザーが a キーを押しても、画面には a が表示されません。

C#
Console.WriteLine("キーを押してください。入力文字は表示されません。");

ConsoleKeyInfo keyInfo = Console.ReadKey(true);

Console.WriteLine($"押されたキー: {keyInfo.Key}");

メニュー操作やショートカットキー判定のように、入力文字を画面に表示したくない場合は Console.ReadKey(true) を使うと便利です。

一方、ユーザーが入力した文字を画面に表示してもよい場合は、Console.ReadKey() または Console.ReadKey(false) を使います。

3. Console.ReadKeyを使った実践的なキー入力処理

3-1. 特定のキーが押されたら処理を実行する

特定のキーが押されたときだけ処理を実行したい場合は、if 文や switch 文で Key を判定します。

C#
Console.WriteLine("Sキーを押すと開始します。");

ConsoleKeyInfo keyInfo = Console.ReadKey(true);

if (keyInfo.Key == ConsoleKey.S)
{
Console.WriteLine("処理を開始します。");
}
else
{
Console.WriteLine("Sキー以外が押されました。");
}

大文字と小文字を区別せずにキーの種類で判定したい場合は、KeyChar ではなく Key を使うのがおすすめです。

ConsoleKey.S は、基本的にSキーそのものを表すため、Shiftキーを押しているかどうかに関係なく判定できます。

3-2. Escキーが押されるまでループする

Escキーが押されるまで処理を繰り返すには、while ループと Console.ReadKey を組み合わせます。

C#
Console.WriteLine("Escキーを押すと終了します。");

while (true)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

if (keyInfo.Key == ConsoleKey.Escape)
{
Console.WriteLine("終了します。");
break;
}

Console.WriteLine($"押されたキー: {keyInfo.Key}");
}

このコードでは、キーが押されるたびに Console.ReadKey(true) で入力を取得します。Escキーが押された場合は break でループを抜けます。

簡単なコンソールツールやミニゲームでは、このようなループ処理がよく使われます。

3-3. 矢印キーでメニューやカーソルを操作する

矢印キーを使うと、コンソール上で簡単なメニュー操作を実装できます。

C#
string[] menuItems = { "開始", "設定", "終了" };
int selectedIndex = 0;

while (true)
{
Console.Clear();

for (int i = 0; i < menuItems.Length; i++)
{
if (i == selectedIndex)
{
Console.WriteLine($"> {menuItems[i]}");
}
else
{
Console.WriteLine($" {menuItems[i]}");
}
}

ConsoleKeyInfo keyInfo = Console.ReadKey(true);

if (keyInfo.Key == ConsoleKey.UpArrow)
{
selectedIndex--;

if (selectedIndex < 0)
{
selectedIndex = menuItems.Length - 1;
}
}
else if (keyInfo.Key == ConsoleKey.DownArrow)
{
selectedIndex++;

if (selectedIndex >= menuItems.Length)
{
selectedIndex = 0;
}
}
else if (keyInfo.Key == ConsoleKey.Enter)
{
Console.Clear();
Console.WriteLine($"{menuItems[selectedIndex]} が選択されました。");
break;
}
}

このサンプルでは、上矢印キーで選択位置を上に移動し、下矢印キーで下に移動します。Enterキーを押すと、現在選択されている項目を確定します。

Console.Clear() を使って画面を再描画しているため、簡単なメニュー画面のように見せることができます。

3-4. Shift・Ctrl・Altキーとの組み合わせを判定する

Shift、Ctrl、Altキーとの組み合わせは、Modifiers プロパティを使って判定します。

C#
Console.WriteLine("Ctrl + S を押してください。");

ConsoleKeyInfo keyInfo = Console.ReadKey(true);

if (keyInfo.Key == ConsoleKey.S &&
(keyInfo.Modifiers & ConsoleModifiers.Control) != 0)
{
Console.WriteLine("Ctrl + S が押されました。保存します。");
}
else
{
Console.WriteLine("Ctrl + S ではありません。");
}

Modifiers はフラグとして扱われるため、& 演算子を使って特定の修飾キーが含まれているかを確認します。

Shiftキーを判定する場合は、次のように書きます。

C#
if ((keyInfo.Modifiers & ConsoleModifiers.Shift) != 0)
{
Console.WriteLine("Shiftキーが押されています。");
}

Altキーを判定する場合は、次のように書きます。

C#
if ((keyInfo.Modifiers & ConsoleModifiers.Alt) != 0)
{
Console.WriteLine("Altキーが押されています。");
}

ショートカットキーを作りたい場合は、KeyModifiers を組み合わせて判定しましょう。

3-5. 複数のキー入力に応じて処理を分岐する

複数のキー入力に応じて処理を分岐する場合は、switch 文を使うと読みやすくなります。

C#
Console.WriteLine("A: 追加 / D: 削除 / Q: 終了");

while (true)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

switch (keyInfo.Key)
{
case ConsoleKey.A:
Console.WriteLine("追加処理を実行します。");
break;

case ConsoleKey.D:
Console.WriteLine("削除処理を実行します。");
break;

case ConsoleKey.Q:
Console.WriteLine("終了します。");
return;

default:
Console.WriteLine("未対応のキーです。");
break;
}
}

このようにキーごとに処理を分けることで、メニュー操作やショートカット操作を実装しやすくなります。

キーの種類が増えてきた場合は、処理をメソッドに分けるとコードが整理しやすくなります。

3-6. ReadKeyとReadLineの違いと使い分け

Console.ReadKeyConsole.ReadLine は、どちらもコンソールから入力を受け取るためのメソッドですが、使い方が異なります。

Console.ReadKey は、1つのキーが押された時点で入力を取得します。Enterキーを押す必要はありません。

C#
ConsoleKeyInfo keyInfo = Console.ReadKey();

一方、Console.ReadLine は、Enterキーが押されるまでの文字列を取得します。

C#
string input = Console.ReadLine();

たとえば、「Yキーを押したら続行」「Escキーを押したら終了」のような処理では ReadKey が向いています。

C#
Console.WriteLine("続行するにはYキーを押してください。");
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

if (keyInfo.Key == ConsoleKey.Y)
{
Console.WriteLine("続行します。");
}

名前やメッセージなど、複数文字の入力を受け取りたい場合は ReadLine が向いています。

C#
Console.WriteLine("名前を入力してください。");
string name = Console.ReadLine();

Console.WriteLine($"こんにちは、{name}さん。");

1キーだけ取得したいなら ReadKey、文字列を入力してもらいたいなら ReadLine と覚えておくとよいでしょう。

4. キー入力待ちで処理が止まる問題と対処法

4-1. Console.ReadKeyが処理をブロックする理由

Console.ReadKey は、ユーザーがキーを押すまで処理を停止します。

たとえば、次のコードでは、キーが押されるまで次の行に進みません。

C#
Console.WriteLine("キーを押してください。");
Console.ReadKey();
Console.WriteLine("キーが押されました。");

このように、処理がそこで止まることを「ブロックする」といいます。

通常のコンソールアプリでは問題にならないことも多いですが、リアルタイムに画面を更新したい場合や、一定時間ごとに処理を続けたい場合には問題になります。

たとえば、ゲームループで Console.ReadKey をそのまま使うと、キーが押されるまで画面更新が止まってしまいます。

4-2. Console.KeyAvailableでキーが押されたか確認する

キー入力があるかどうかだけを確認したい場合は、Console.KeyAvailable を使います。

Console.KeyAvailable は、キー入力が待機中かどうかを true または false で返します。

C#
if (Console.KeyAvailable)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
Console.WriteLine($"押されたキー: {keyInfo.Key}");
}

Console.KeyAvailabletrue のときだけ Console.ReadKey を呼び出せば、キーが押されていないときに処理が止まるのを防げます。

次のように使うと、キー入力を確認しながら別の処理を続けられます。

C#
while (true)
{
if (Console.KeyAvailable)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

if (keyInfo.Key == ConsoleKey.Escape)
{
break;
}

Console.WriteLine($"入力: {keyInfo.Key}");
}

Console.WriteLine("処理中...");
Thread.Sleep(500);
}

このコードでは、Escキーが押されるまで「処理中...」と表示し続けます。

4-3. キー入力を待ちながら別の処理を続ける方法

キー入力を待ちながら別の処理を続けたい場合は、Console.KeyAvailable を使って定期的にキー入力を確認します。

C#
using System;
using System.Threading;

class Program
{
static void Main()
{
int count = 0;

Console.WriteLine("Escキーで終了します。");

while (true)
{
if (Console.KeyAvailable)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

if (keyInfo.Key == ConsoleKey.Escape)
{
Console.WriteLine("終了します。");
break;
}
}

Console.WriteLine($"カウント: {count}");
count++;

Thread.Sleep(1000);
}
}
}

このコードでは、1秒ごとにカウントを表示しながら、Escキーが押されたかどうかを確認しています。

Console.ReadKey を直接呼び出すとキー入力があるまで止まってしまいますが、Console.KeyAvailable で事前に確認することで、処理を続けながらキー入力を監視できます。

4-4. ゲームループやリアルタイム処理で注意するポイント

コンソールアプリで簡単なゲームやリアルタイム処理を作る場合は、Console.ReadKey の使い方に注意が必要です。

ゲームループの中でそのまま Console.ReadKey を呼び出すと、キー入力があるまでループが止まります。

避けたい書き方は次のようなコードです。

C#
while (true)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

// キーが押されるまでここで止まる
Console.WriteLine(keyInfo.Key);
}

リアルタイム処理では、次のように Console.KeyAvailable を使います。

C#
while (true)
{
if (Console.KeyAvailable)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

if (keyInfo.Key == ConsoleKey.LeftArrow)
{
Console.WriteLine("左へ移動");
}
else if (keyInfo.Key == ConsoleKey.RightArrow)
{
Console.WriteLine("右へ移動");
}
}

// ここに画面更新や状態更新を書く
Thread.Sleep(50);
}

ただし、コンソールアプリは本格的なゲーム開発に向いているわけではありません。学習用や簡単なミニゲームであれば十分ですが、本格的なリアルタイム入力が必要な場合は、ゲームエンジンやGUIフレームワークの利用も検討しましょう。

4-5. CPU使用率を上げすぎないための待機処理

while ループでキー入力を監視するときは、待機処理を入れないとCPU使用率が高くなることがあります。

避けたい例は次のようなコードです。

C#
while (true)
{
if (Console.KeyAvailable)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
Console.WriteLine(keyInfo.Key);
}
}

このコードでは、キーが押されていない間もループが高速に回り続けます。

CPU使用率を抑えるには、Thread.SleepTask.Delay を使って少し待機します。

C#
while (true)
{
if (Console.KeyAvailable)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

if (keyInfo.Key == ConsoleKey.Escape)
{
break;
}
}

Thread.Sleep(50);
}

50ミリ秒程度の待機を入れるだけでも、CPU使用率を抑えやすくなります。

非同期メソッド内であれば、Task.Delay を使うこともできます。

C#
await Task.Delay(50);

キー入力の反応速度とCPU使用率のバランスを考えて、適切な待機時間を設定しましょう。

5. C#でキー入力を非同期に扱う方法

5-1. 非同期処理が必要になるケース

C#でキー入力を非同期に扱いたいケースは、キー入力を待っている間も別の処理を続けたい場合です。

たとえば、一定間隔でログを表示しながらキー入力を待ちたい、バックグラウンド処理を実行しながらEscキーで終了できるようにしたい、コンソールアプリで簡単な監視ツールを作りたい、といったケースです。

ただし、Console.ReadKey 自体は非同期メソッドではありません。await Console.ReadKey() のようには書けません。

そのため、非同期処理と組み合わせる場合は、Task.Run で別スレッドに逃がす、または Console.KeyAvailableTask.Delay を組み合わせて定期的に確認する方法を使います。

5-2. Task.Runでキー入力処理を別スレッド化する

Console.ReadKey を別スレッドで実行したい場合は、Task.Run を使います。

C#
using System;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
Console.WriteLine("キー入力を待っています。");

Task<ConsoleKeyInfo> keyTask = Task.Run(() => Console.ReadKey(true));

Console.WriteLine("キー入力待ちの間も別の処理を実行できます。");

ConsoleKeyInfo keyInfo = await keyTask;

Console.WriteLine($"押されたキー: {keyInfo.Key}");
}
}

このコードでは、Console.ReadKey(true)Task.Run の中で実行しています。

メイン処理は await keyTask するまで他の処理を進めることができます。ただし、Console.ReadKey は内部的にはキー入力があるまで待機し続けるため、完全にキャンセルしやすい処理ではない点に注意が必要です。

5-3. async/awaitとConsole.ReadKeyを組み合わせる考え方

async/await は、時間のかかる処理を待っている間にスレッドを効率よく使うための仕組みです。

しかし、Console.ReadKey は標準では非同期版が用意されていません。そのため、Console.ReadKey を直接 await することはできません。

間違った書き方の例です。

C#
// これはできません
// ConsoleKeyInfo keyInfo = await Console.ReadKey();

非同期的に扱いたい場合は、次のように Task.Run で包みます。

C#
ConsoleKeyInfo keyInfo = await Task.Run(() => Console.ReadKey(true));

ただし、キー入力を監視しながら処理を続けるだけであれば、Console.KeyAvailableTask.Delay を使ったループのほうが扱いやすい場合もあります。

C#
while (true)
{
if (Console.KeyAvailable)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
Console.WriteLine(keyInfo.Key);
}

await Task.Delay(100);
}

この方法なら、一定間隔でキー入力を確認しつつ、非同期的に待機できます。

5-4. CancellationTokenでキー入力待ちをキャンセルする

非同期処理では、CancellationToken を使うことで処理のキャンセルを表現できます。

ただし、Console.ReadKey そのものを安全に強制キャンセルするのは簡単ではありません。そのため、キャンセル可能なキー入力監視を作りたい場合は、Console.KeyAvailableTask.Delay を組み合わせるのがおすすめです。

C#
using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
using CancellationTokenSource cts = new CancellationTokenSource();

Console.WriteLine("Escキーで終了します。");

while (!cts.Token.IsCancellationRequested)
{
if (Console.KeyAvailable)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

if (keyInfo.Key == ConsoleKey.Escape)
{
cts.Cancel();
break;
}

Console.WriteLine($"押されたキー: {keyInfo.Key}");
}

await Task.Delay(100, cts.Token).ContinueWith(_ => { });
}

Console.WriteLine("終了しました。");
}
}

このコードでは、Escキーが押されたら cts.Cancel() を呼び出してループを終了します。

よりシンプルに書くなら、CancellationToken を使わずに break だけで終了しても問題ありません。複数の非同期処理をまとめてキャンセルしたい場合に、CancellationToken が役立ちます。

5-5. 非同期処理で例外や終了処理を安全に扱う方法

非同期処理では、例外処理と終了処理を意識することが重要です。

特に Task.DelayCancellationToken を渡している場合、キャンセル時に TaskCanceledExceptionOperationCanceledException が発生することがあります。

安全に扱うには、try-catch-finally を使います。

C#
using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
using CancellationTokenSource cts = new CancellationTokenSource();

try
{
while (!cts.Token.IsCancellationRequested)
{
if (Console.KeyAvailable)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

if (keyInfo.Key == ConsoleKey.Escape)
{
cts.Cancel();
}
}

await Task.Delay(100, cts.Token);
}
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました。");
}
finally
{
Console.WriteLine("終了処理を実行します。");
}
}
}

finally には、ファイルを閉じる、ログを書き出す、接続を切るなど、必ず実行したい終了処理を書きます。

非同期キー入力では、プログラム終了時に中途半端な状態にならないように、終了条件と例外処理を明確にしておきましょう。

5-6. 非同期キー入力のサンプルコード

次のサンプルは、非同期でカウントを表示しながら、Escキーで終了するコンソールアプリです。

C#
using System;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
int count = 0;

Console.WriteLine("Escキーを押すと終了します。");

while (true)
{
if (Console.KeyAvailable)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

if (keyInfo.Key == ConsoleKey.Escape)
{
Console.WriteLine("終了します。");
break;
}

Console.WriteLine($"押されたキー: {keyInfo.Key}");
}

Console.WriteLine($"処理中... {count}");
count++;

await Task.Delay(1000);
}
}
}

このコードでは、1秒ごとに「処理中...」と表示しながら、キー入力があるかどうかを確認しています。

Console.ReadKey を直接呼び出していないため、キー入力がない状態でも処理が止まりません。

リアルタイム性が必要な処理では、Task.Delay(1000) の値を短くすると反応が速くなります。ただし、短くしすぎるとCPU使用率が高くなりやすいため注意しましょう。

6. Windows Formsでキー入力を取得する方法:KeyDownイベント

6-1. KeyDownイベントとは

Windows Formsでキー入力を取得する場合は、KeyDown イベントを使います。

KeyDown イベントは、キーボードのキーが押されたときに発生するイベントです。フォームやボタン、テキストボックスなどのコントロールに対して設定できます。

たとえば、フォーム上でEnterキーが押されたら処理を実行したい場合、フォームの KeyDown イベントにコードを書きます。

コンソールアプリのように Console.ReadKey を使うのではなく、ユーザーの操作に応じてイベントが呼び出される点が大きな違いです。

6-2. フォームでキー入力を受け取る基本コード

Windows Formsでフォームのキー入力を受け取る基本コードは次のとおりです。

C#
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
MessageBox.Show($"押されたキー: {e.KeyCode}");
}

Visual Studioでフォームを選択し、プロパティウィンドウから KeyDown イベントを設定すると、このようなイベントハンドラーを作成できます。

また、コードでイベントを登録することもできます。

C#
public Form1()
{
InitializeComponent();

this.KeyDown += Form1_KeyDown;
}

private void Form1_KeyDown(object sender, KeyEventArgs e)
{
MessageBox.Show($"押されたキー: {e.KeyCode}");
}

フォームでキー入力を受け取りたい場合は、フォームにフォーカスがあるかどうかも重要です。テキストボックスなど他のコントロールにフォーカスがある場合、フォーム側でキー入力を受け取れないことがあります。

その場合は、後述する KeyPreview を使います。

6-3. e.KeyCodeで押されたキーを判定する

Windows Formsでは、押されたキーを e.KeyCode で判定します。

C#
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
MessageBox.Show("Enterキーが押されました。");
}
}

Keys.EnterKeys.EscapeKeys.UpKeys.Down などを使って、特定のキーを判定できます。

複数のキーを分岐したい場合は、switch 文を使うと読みやすくなります。

C#
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
switch (e.KeyCode)
{
case Keys.Enter:
MessageBox.Show("Enterキーです。");
break;

case Keys.Escape:
this.Close();
break;

case Keys.Up:
MessageBox.Show("上キーです。");
break;

case Keys.Down:
MessageBox.Show("下キーです。");
break;
}
}

Windows Formsでは、コンソールの ConsoleKey ではなく、Keys 列挙型を使う点に注意しましょう。

6-4. Enter・Esc・矢印キーを判定する方法

Enterキー、Escキー、矢印キーの判定は、次のように書きます。

C#
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
MessageBox.Show("Enterキーが押されました。");
}
else if (e.KeyCode == Keys.Escape)
{
MessageBox.Show("Escキーが押されました。");
this.Close();
}
else if (e.KeyCode == Keys.Up)
{
MessageBox.Show("上矢印キーが押されました。");
}
else if (e.KeyCode == Keys.Down)
{
MessageBox.Show("下矢印キーが押されました。");
}
else if (e.KeyCode == Keys.Left)
{
MessageBox.Show("左矢印キーが押されました。");
}
else if (e.KeyCode == Keys.Right)
{
MessageBox.Show("右矢印キーが押されました。");
}
}

矢印キーを使って画面上の選択位置を移動したい場合は、現在位置を変数で管理し、キー入力に応じて座標や選択インデックスを変更します。

C#
private int x = 100;
private int y = 100;

private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Left)
{
x -= 10;
}
else if (e.KeyCode == Keys.Right)
{
x += 10;
}
else if (e.KeyCode == Keys.Up)
{
y -= 10;
}
else if (e.KeyCode == Keys.Down)
{
y += 10;
}

label1.Location = new Point(x, y);
}

このように、キー入力と画面上のコントロール操作を組み合わせることで、GUIアプリらしい操作を実装できます。

6-5. Shift・Ctrl・Altキーを組み合わせて判定する方法

Windows FormsでShift、Ctrl、Altキーとの組み合わせを判定するには、e.Shifte.Controle.Alt を使います。

C#
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.Control && e.KeyCode == Keys.S)
{
MessageBox.Show("Ctrl + S が押されました。保存します。");
}
}

Shiftキーとの組み合わせは次のように書きます。

C#
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.Shift && e.KeyCode == Keys.Enter)
{
MessageBox.Show("Shift + Enter が押されました。");
}
}

Altキーとの組み合わせは次のように書きます。

C#
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.Alt && e.KeyCode == Keys.F4)
{
MessageBox.Show("Alt + F4 が押されました。");
}
}

ショートカットキーを実装する場合は、e.Controle.Shifte.KeyCode を組み合わせて判定します。

なお、実際のアプリでは、標準的なショートカットキーと競合しないように注意しましょう。

6-6. KeyPreviewをtrueにしてフォーム全体でキー入力を受け取る方法

Windows Formsで「フォーム全体でキー入力を受け取りたい」のに KeyDown が発火しない場合は、KeyPreviewtrue にします。

C#
public Form1()
{
InitializeComponent();

this.KeyPreview = true;
this.KeyDown += Form1_KeyDown;
}

KeyPreviewtrue にすると、フォーム上のコントロールより先にフォームがキー入力を受け取れるようになります。

たとえば、フォーム上にTextBoxがある場合、通常はTextBoxがキー入力を受け取ります。そのため、フォームの KeyDown が期待通りに発火しないことがあります。

しかし、KeyPreview = true にしておけば、フォーム側でもキー入力を処理しやすくなります。

C#
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Escape)
{
this.Close();
}
}

画面全体でEscキーによるキャンセルや、Ctrl + Sによる保存を実装したい場合に便利です。

7. WPFでキー入力を取得する方法

7-1. WPFにおけるKeyDownイベントの基本

WPFでも、キー入力を取得するには KeyDown イベントを使います。

XAMLでイベントを指定する場合は、次のように書きます。

XML
<Window x:Class="SampleApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Sample" Height="300" Width="400"
KeyDown="Window_KeyDown">
</Window>

コードビハインドでは、次のようにイベントハンドラーを書きます。

C#
private void Window_KeyDown(object sender, KeyEventArgs e)
{
MessageBox.Show($"押されたキー: {e.Key}");
}

WPFでは、Windows Formsの Keys ではなく、System.Windows.Input.Key を使います。

7-2. e.Keyで押されたキーを判定する

WPFでは、押されたキーを e.Key で判定します。

C#
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
MessageBox.Show("Enterキーが押されました。");
}
}

Escキーを判定する場合は次のように書きます。

C#
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
Close();
}
}

矢印キーも同じように判定できます。

C#
private void Window_KeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Up:
MessageBox.Show("上キー");
break;

case Key.Down:
MessageBox.Show("下キー");
break;

case Key.Left:
MessageBox.Show("左キー");
break;

case Key.Right:
MessageBox.Show("右キー");
break;
}
}

WPFでは、Key.EnterKey.EscapeKey.Up のように書く点を覚えておきましょう。

7-3. TextBoxなどのコントロール上でキー入力を取得する

TextBoxなど特定のコントロール上でキー入力を取得したい場合は、そのコントロールに KeyDown イベントを設定します。

XML
<TextBox Width="200"
Height="30"
KeyDown="TextBox_KeyDown" />

コードビハインドでは次のように書きます。

C#
private void TextBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
MessageBox.Show("TextBoxでEnterキーが押されました。");
}
}

TextBoxでEnterキーを押したときに検索を実行したい場合などに使えます。

C#
private void TextBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
TextBox textBox = sender as TextBox;

if (textBox != null)
{
MessageBox.Show($"検索キーワード: {textBox.Text}");
}
}
}

WPFでは、どのコントロールにフォーカスがあるかによってイベントの発生場所が変わります。フォーム全体、つまりWindow全体でキー入力を受け取りたい場合は、Window側のイベントや PreviewKeyDown を使います。

7-4. PreviewKeyDownを使うべきケース

WPFには、KeyDown のほかに PreviewKeyDown があります。

PreviewKeyDown は、通常の KeyDown よりも前に発生するイベントです。WPFのイベントには「トンネル」と「バブル」という仕組みがあり、PreviewKeyDown は親要素から子要素へ向かって処理されます。

たとえば、TextBoxなどのコントロールがキー入力を先に処理してしまい、Window側の KeyDown が思ったように動かない場合があります。

そのようなときは、PreviewKeyDown を使うと、より早い段階でキー入力を取得できます。

XML
<Window x:Class="SampleApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Sample"
PreviewKeyDown="Window_PreviewKeyDown">
</Window>
C#
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
Close();
}
}

特に、画面全体でEscキーを受け取りたい場合や、特定のコントロールにフォーカスがあってもショートカットキーを処理したい場合に便利です。

7-5. Windows Formsとの違い

Windows FormsとWPFでは、キー入力の考え方は似ていますが、使うクラスやイベントの仕組みが異なります。

Windows Formsでは、KeyDown イベントの引数として KeyEventArgs を受け取り、e.KeyCode を使って Keys.Enter のように判定します。

C#
if (e.KeyCode == Keys.Enter)
{
MessageBox.Show("Enter");
}

WPFでは、e.Key を使って Key.Enter のように判定します。

C#
if (e.Key == Key.Enter)
{
MessageBox.Show("Enter");
}

また、Windows Formsでフォーム全体のキー入力を受け取りたい場合は KeyPreview = true を使います。

WPFでは、PreviewKeyDown を使うことで、子コントロールより前にキー入力を処理できます。

同じC#でも、Windows FormsとWPFではイベントの流れやプロパティ名が異なるため、サンプルコードを混同しないように注意しましょう。

8. よくあるエラー・つまずきポイント

8-1. コンソール画面がすぐ閉じる場合の対処法

C#のコンソールアプリを実行したとき、処理が終わるとコンソール画面がすぐ閉じてしまうことがあります。

この場合は、最後に Console.ReadKey を入れると、キー入力があるまで画面を開いたままにできます。

C#
Console.WriteLine("処理が完了しました。");
Console.WriteLine("何かキーを押すと終了します。");
Console.ReadKey();

Visual Studioからデバッグなしで実行する場合や、実行ファイルを直接開いた場合に起こりやすい現象です。

ただし、アプリ本体の処理としてキー入力が必要ない場合は、開発中の確認用として使う程度にしておきましょう。

8-2. Console.ReadKeyで文字ではなくキー名が返る理由

Console.ReadKey で取得した値を表示すると、文字ではなくキー名のように見えることがあります。

たとえば、次のように書いた場合です。

C#
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
Console.WriteLine(keyInfo.Key);

この場合、表示されるのは AEnter などのキー名です。

文字として取得したい場合は、KeyChar を使います。

C#
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
Console.WriteLine(keyInfo.KeyChar);

ただし、Enterキー、Escキー、矢印キーなどは通常の文字ではないため、KeyChar では扱いにくい場合があります。

キーの種類を判定したい場合は Key、入力文字を扱いたい場合は KeyChar を使い分けましょう。

8-3. 矢印キーやファンクションキーがうまく取得できない場合

矢印キーやファンクションキーを取得したい場合は、KeyChar ではなく Key を使います。

間違いやすい例です。

C#
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

if (keyInfo.KeyChar == '↑')
{
Console.WriteLine("上キー");
}

矢印キーは通常の文字入力ではないため、このような判定は適切ではありません。

正しくは、次のように書きます。

C#
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

if (keyInfo.Key == ConsoleKey.UpArrow)
{
Console.WriteLine("上キー");
}

ファンクションキーも同じです。

C#
if (keyInfo.Key == ConsoleKey.F1)
{
Console.WriteLine("F1キーが押されました。");
}

特殊キーを扱う場合は、ConsoleKey の値で判定するのが基本です。

8-4. KeyDownイベントが発火しない場合の確認ポイント

Windows FormsやWPFで KeyDown イベントが発火しない場合は、まずフォーカスを確認しましょう。

キー入力イベントは、基本的にフォーカスを持っているコントロールに対して発生します。フォームやWindowにイベントを書いていても、TextBoxやButtonにフォーカスがあると、期待通りにイベントが届かないことがあります。

Windows Formsの場合は、KeyPreviewtrue にします。

C#
this.KeyPreview = true;

WPFの場合は、PreviewKeyDown を使うと解決できることがあります。

XML
<Window PreviewKeyDown="Window_PreviewKeyDown">
</Window>

また、イベントハンドラーが正しく登録されているかも確認しましょう。

Windows Formsなら、コンストラクタで次のように登録できます。

C#
this.KeyDown += Form1_KeyDown;

WPFなら、XAMLのイベント名とコードビハインドのメソッド名が一致しているか確認します。

8-5. 日本語入力やIME使用時に注意すること

日本語入力やIMEを使用している場合、キー入力の扱いには注意が必要です。

KeyDown はキーが押されたタイミングで発生しますが、日本語入力では、キー入力がすぐに確定文字になるとは限りません。IMEで変換中の文字や未確定文字があるためです。

たとえば、TextBoxで日本語文字列を入力したい場合、KeyDown で1文字ずつ処理するよりも、TextBoxの TextChanged イベントや入力確定後の文字列を使うほうが適している場合があります。

コンソールアプリでも、Console.ReadKey は1キーごとの入力を扱うため、日本語の文章入力には向いていません。

日本語の文字列を入力してもらう場合は、Console.ReadLine を使うのが基本です。

C#
Console.WriteLine("名前を入力してください。");
string name = Console.ReadLine();

Console.WriteLine($"入力された名前: {name}");

キーそのものを判定したいのか、入力された文字列を扱いたいのかを分けて考えましょう。

8-6. 非同期処理でキー入力が反応しない場合の原因

非同期処理でキー入力が反応しない場合は、いくつかの原因が考えられます。

まず、Console.ReadKey がブロックしている可能性があります。Console.ReadKey はキーが押されるまで処理を止めるため、ループや非同期処理の設計によっては他の処理が進まなくなります。

次に、Console.KeyAvailable を確認せずに Console.ReadKey を呼び出している可能性があります。

C#
// キーが押されるまで止まる
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

処理を止めたくない場合は、次のように書きます。

C#
if (Console.KeyAvailable)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
}

また、Task.Delay の待機時間が長すぎると、キー入力への反応が遅く感じることがあります。

C#
await Task.Delay(1000);

リアルタイム性を高めたい場合は、待機時間を短くします。

C#
await Task.Delay(50);

ただし、待機時間を短くしすぎるとCPU使用率が上がりやすくなるため、用途に応じて調整しましょう。

9. 目的別:C#のキー入力取得方法の選び方

9-1. コンソールで1キーだけ取得したい場合

コンソールアプリで1キーだけ取得したい場合は、Console.ReadKey を使います。

C#
Console.WriteLine("何かキーを押してください。");
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

Console.WriteLine($"押されたキー: {keyInfo.Key}");

「何かキーを押したら続行」「Yキーなら処理を実行」「Escキーなら終了」のような処理に向いています。

文字を画面に表示したくない場合は、Console.ReadKey(true) を使いましょう。

9-2. Enterまでの文字列を入力したい場合

Enterキーが押されるまでの文字列を取得したい場合は、Console.ReadLine を使います。

C#
Console.WriteLine("名前を入力してください。");
string name = Console.ReadLine();

Console.WriteLine($"こんにちは、{name}さん。");

名前、住所、検索キーワード、数値など、複数文字の入力を受け取りたい場合に適しています。

Console.ReadKey は1キー単位、Console.ReadLine は1行単位と考えると分かりやすいです。

9-3. キー入力で処理を分岐したい場合

キー入力によって処理を分岐したい場合は、Console.ReadKeyswitch 文を組み合わせます。

C#
Console.WriteLine("1: 新規作成 / 2: 編集 / 3: 終了");

ConsoleKeyInfo keyInfo = Console.ReadKey(true);

switch (keyInfo.Key)
{
case ConsoleKey.D1:
case ConsoleKey.NumPad1:
Console.WriteLine("新規作成を選択しました。");
break;

case ConsoleKey.D2:
case ConsoleKey.NumPad2:
Console.WriteLine("編集を選択しました。");
break;

case ConsoleKey.D3:
case ConsoleKey.NumPad3:
Console.WriteLine("終了します。");
break;

default:
Console.WriteLine("無効な入力です。");
break;
}

数字キーを判定する場合、キーボード上部の数字キーは D1、テンキーの数字キーは NumPad1 のように分かれる点に注意しましょう。

9-4. 処理を止めずにキー入力を監視したい場合

処理を止めずにキー入力を監視したい場合は、Console.KeyAvailable を使います。

C#
while (true)
{
if (Console.KeyAvailable)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

if (keyInfo.Key == ConsoleKey.Escape)
{
break;
}
}

Console.WriteLine("処理を継続中...");
Thread.Sleep(500);
}

この方法なら、キーが押されていない間も処理を続けられます。

ゲームループ、監視処理、定期実行処理などでは、Console.ReadKey を直接呼び出すのではなく、Console.KeyAvailable と組み合わせるのが基本です。

9-5. Windows FormsやWPFで画面操作をしたい場合

Windows FormsやWPFでキー入力を取得したい場合は、イベントを使います。

Windows Formsでは KeyDown イベントと e.KeyCode を使います。

C#
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
MessageBox.Show("Enterキーが押されました。");
}
}

WPFでは KeyDown イベントと e.Key を使います。

C#
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
MessageBox.Show("Enterキーが押されました。");
}
}

フォーム全体やWindow全体でキー入力を受け取りたい場合、Windows Formsでは KeyPreview = true、WPFでは PreviewKeyDown を検討しましょう。

9-6. 初心者におすすめの学習順序

C#初心者がキー入力を学ぶなら、まずはコンソールアプリで Console.ReadLineConsole.ReadKey の違いを理解するのがおすすめです。

最初に、文字列入力として Console.ReadLine を使います。

C#
string input = Console.ReadLine();

次に、1キー入力として Console.ReadKey を使います。

C#
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

その後、keyInfo.Key を使ってEnterキーやEscキーを判定します。

C#
if (keyInfo.Key == ConsoleKey.Escape)
{
Console.WriteLine("終了します。");
}

慣れてきたら、Console.KeyAvailable を使って処理を止めないキー入力監視に進みます。

GUIアプリを作りたい場合は、Windows Formsの KeyDown イベント、またはWPFの KeyDownPreviewKeyDown を学びましょう。

順番としては、次の流れがおすすめです。

  1. Console.ReadLine で文字列入力を学ぶ

  2. Console.ReadKey で1キー入力を学ぶ

  3. ConsoleKeyInfoKeyKeyCharModifiers を理解する

  4. Console.KeyAvailable で処理を止めない入力を学ぶ

  5. Windows FormsやWPFの KeyDown イベントを学ぶ

  6. 必要に応じて非同期処理と組み合わせる

この順番で学ぶと、C#のキー入力処理を無理なく理解できます。

まとめ

C#でキー入力を取得する方法は、アプリケーションの種類と目的によって使い分けることが大切です。

コンソールアプリで1つのキーを取得したい場合は、Console.ReadKey を使います。押されたキーの種類は ConsoleKeyInfo.Key、入力文字は ConsoleKeyInfo.KeyChar、Shift・Ctrl・Altなどの修飾キーは ConsoleKeyInfo.Modifiers で確認できます。

Enterキー、Escキー、矢印キーのような特殊キーを判定する場合は、KeyChar ではなく Key を使うのが基本です。

キー入力を待っている間に処理を止めたくない場合は、Console.KeyAvailable を使って、キーが押されているときだけ Console.ReadKey を呼び出します。リアルタイム処理やゲームループでは、CPU使用率を上げすぎないように Thread.SleepTask.Delay を組み合わせることも重要です。

非同期処理と組み合わせたい場合は、Task.RunConsole.ReadKey を別スレッド化する方法や、Console.KeyAvailableTask.Delay を使って定期的にキー入力を確認する方法があります。ただし、Console.ReadKey 自体は非同期メソッドではないため、キャンセル処理には注意が必要です。

Windows Formsでは KeyDown イベントと e.KeyCode、WPFでは KeyDown イベントと e.Key を使います。フォームやWindow全体でキー入力を受け取りたい場合は、Windows Formsなら KeyPreview = true、WPFなら PreviewKeyDown を活用しましょう。

C#のキー入力は、最初は Console.ReadKey から学び、次に Console.KeyAvailable、Windows Forms、WPF、非同期処理へと進むと理解しやすくなります。