WinFormsで「textBox1に03 02 01のようにスペース区切りで数値を入力したら、textBox2に01 02 03と自動で並べ替えて表示したい」という要件は、入力イベントの選び方とTryParseの使い方を間違えると簡単にハマります。本記事では、TextChangedを正しく使い、先頭ゼロを保持したまま安全にソート表示する実装をまとめます。
C# / WinFormsの要件:textBox1の入力を自動ソートしてtextBox2へ表示
この手のWindowsフォーム(WinForms)アプリでは、ユーザーが入力した値をリアルタイムに加工して別コントロールへ反映するケースがよくあります。今回のゴールは次のとおりです。
- textBox1にスペース区切りで複数の数値を入力する(例:03 02 01)
- 入力されたトークンを昇順ソートし、textBox2に自動表示する(例:01 02 03)
処理の流れを固定する:分割→検証→ソート→結合
やっている処理はシンプルなので、実装を迷わせないためにも手順を先に固定しておくのがおすすめです。
- textBox1の文字列を受け取る
- スペース(または空白全般)で分割して配列にする
- 各要素が数値として解釈できるかを検証する(
int.TryParse) - 昇順に並べ替える(
OrderBy) - スペース区切りで結合してtextBox2へ出力する(
string.Join)
ポイントは「どのイベントで処理を走らせるか」と「ソート時に先頭ゼロをどう扱うか」です。ここを外すと、想定と違う並びになったり、イベントが意図せず再発火して挙動が不安定になったりします。
なぜ意図どおりに動かないのか:典型的な3つのズレ
質問でよく見かける「動かない理由」は、次の3点に集約できます。コードを直す前に、まずズレの位置を特定しましょう。
| 観点 | よくある誤り | 起きること | 正しい方向性 |
|---|---|---|---|
| イベント | textBox2_TextChangedに処理を書く | 入力側(textBox1)の変化で処理が走らない/textBox2.Textを変更すると再発火して混乱しやすい | 入力元のtextBox1_TextChangedをトリガーにする |
| TryParse | int.TryParse(文字列)のようにout引数なしで使う | コンパイルエラー、または判定が正しく書けない | int.TryParse(n, out var value)(またはout _)で真偽を判定 |
| ソート対象 | 入力値ではなく0〜count-1の連番を生成してしまう | 入力した数値が反映されず、期待とズレる | Splitしたトークン(入力値そのもの)を並べ替える |
特に「textBox2のTextChangedでtextBox2.Textを書き換える」は、再発火の入口になりやすい設計です。値が変わるたびにイベントが呼ばれ、イベント内でTextを書き換え、またイベントが呼ばれ……という流れが発生し得ます。今回の要件は入力の変化に応じて結果を出すのが本質なので、処理はtextBox1に寄せるのが素直です。
最小構成の解決策:textBox1_TextChangedで完結させる
まずは「03 02 01」のように桁数が揃っている入力を想定し、先頭ゼロを保持したまま昇順表示する最小実装です。LINQのAll/OrderByを使うため、ファイル先頭にusing System.Linq;を追加してください。
using System;
using System.Linq;
private void textBox1_TextChanged(object sender, EventArgs e)
{
// textBox1 の入力をスペース区切りで分割
string input = textBox1.Text.Trim();
string[] numbers = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
// 全要素が数値かチェック(out _ は C# 7.0 以降)
if (numbers.All(n => int.TryParse(n, out _)))
{
// 文字列として並べ替え(先頭ゼロを保持したまま出力しやすい)
// 文字列比較の揺れを避けたい場合は Ordinal を明示
var sortedNumbers = numbers
.OrderBy(n => n, StringComparer.Ordinal)
.ToArray();
textBox2.Text = string.Join(" ", sortedNumbers);
}
else
{
textBox2.Text = "Invalid input";
}
}
もしC#のバージョンが古くout _が使えない場合は、次のようにダミー変数で受けるだけでOKです。
int tmp;
if (numbers.All(n => int.TryParse(n, out tmp)))
{
// ...
}
この最小実装が刺さるケースは、例えば常に2桁(01〜99)のように入力形式が揃っている場合です。桁数が揃っているなら、文字列ソートでも数値ソートでも結果が一致し、しかも先頭ゼロが自然に保持できます。
文字列ソートと数値ソートの違い:桁数がバラバラだと並びが変わる
注意点として、上の最小実装は「文字列として」並べ替えています。これは先頭ゼロを保持しやすい反面、桁数がバラバラの入力では数値としての昇順とズレる場合があります。
| 入力(textBox1) | 文字列ソートの出力 | 数値ソートの出力 | コメント |
|---|---|---|---|
| 03 02 01 | 01 02 03 | 01 02 03 | 桁数が揃っているためどちらでも同じ |
| 2 10 1 | 1 10 2 | 1 2 10 | 桁数が揃っていないと文字列ソートは直感とズレやすい |
| 001 01 1 | 001 01 1 | 001 01 1(同値は入力順のまま) | すべて数値としては同じ1。重複の扱いは要件次第 |
「入力は2桁固定」「必ずゼロ埋めする」といった前提がない場合は、並べ替えキーだけ数値化して数値昇順にし、表示は元の文字列(Raw)を使うのが安全です。
実務向けの堅牢版:数値でソートしつつ、表示は元文字列で先頭ゼロを保持
次は実務寄りの実装です。ポイントは次の通りです。
- 入力を分割したトークンを1回だけTryParseし、数値(Value)と元文字列(Raw)をペアで持つ
- ソートはValueで行う(正しい数値昇順)
- 出力はRawを使う(入力の見た目=先頭ゼロを保持)
using System;
using System.Collections.Generic;
using System.Linq;
private void textBox1_TextChanged(object sender, EventArgs e)
{
string input = NormalizeInput(textBox1.Text);
// 空入力は空で返す(エラー表示したい場合は方針に合わせて変更)
if (string.IsNullOrWhiteSpace(input))
{
textBox2.Text = string.Empty;
return;
}
// 半角スペースだけでなく、タブや改行もまとめて区切りにしたい場合は
// Split の区切りを「null(=空白全般)」にするのが便利。
// null だけだとオーバーロードが曖昧になることがあるため、(char[])null を明示。
string[] tokens = input.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
var items = new List<(int Value, string Raw)>(tokens.Length);
foreach (var t in tokens)
{
if (!int.TryParse(t, out var v))
{
textBox2.Text = "Invalid input";
return;
}
items.Add((v, t));
}
var result = items
.OrderBy(x => x.Value) // 数値で昇順ソート
.Select(x => x.Raw); // 表示は元の文字列(先頭ゼロ保持)
textBox2.Text = string.Join(" ", result);
}
private static string NormalizeInput(string s)
{
// 全角スペースが混ざるケースを想定して半角に寄せる
return (s ?? string.Empty).Replace(' ', ' ');
}
この形にしておくと、入力が「2 10 1」でも正しく「1 2 10」になります。さらに「03 2 001」のように先頭ゼロが混在しても、表示は入力の形を維持したまま並べ替えできます。
なお、タプル((int Value, string Raw))が使えない環境(古い.NET Framework+古いC#)では、簡単なクラスを用意して同じことができます。
private class SortItem
{
public int Value;
public string Raw;
}
// items.Add(new SortItem { Value = v, Raw = t }); のように保持する
「Invalid input」をよりユーザーに優しくする工夫
学習用サンプルとしてはInvalid inputでも十分ですが、実際のアプリでは「どこがダメなのか」が分かると入力ミスの修正が速くなります。WinFormsでは、次のような改善が取り入れやすいです。
| 改善案 | メリット | 実装の方向性 |
|---|---|---|
| textBox2を読み取り専用にする | ユーザーが誤って結果欄を編集しない | textBox2.ReadOnly = true;を設定(デザイナーでも可) |
| 無効トークンを強調表示する | どの入力が原因かが分かる | ErrorProviderやラベルでエラー表示、または該当トークンを抽出して表示 |
| 空欄のときは空欄を返す | 入力途中のストレスが減る | string.IsNullOrWhiteSpaceで分岐し、textBox2.Textを空にする |
| 入力中はエラーを出さず、確定時に検証する | タイピング途中の「一瞬の不正」を許容できる | TextChangedではなくValidating/Leaveでチェック、またはタイマーでデバウンス |
「自動表示」は便利ですが、TextChangedはキー入力のたびに発火します。入力途中の「-」など、一時的に数値として成立しない文字が混ざる可能性があるなら、エラー表示のタイミング(即時か確定時か)も含めて設計すると完成度が上がります。
イベントの再発火を避ける:将来の仕様追加に備えた安全策
今回の方針(textBox1の変更でtextBox2を書き換える)なら、基本的に無限ループは起きません。ただし、次のような構成にすると途端に再発火が問題になります。
- textBox2_TextChangedにロジックがあり、その中でtextBox2.Textを更新している
- textBox1とtextBox2の両方でTextChangedを使い、互いのTextを書き換えている
- 整形のためにtextBox1側のTextも書き換える(例:全角→半角、重複スペース削除など)
もし入力欄自体も整形したい場合は、「イベント中は再入しない」ためのガードフラグを用意しておくと安定します。
private bool _updating;
private void textBox1_TextChanged(object sender, EventArgs e)
{
if (_updating) return;
try
{
_updating = true;
// ここで textBox1.Text を整形して書き戻す場合も安全
// textBox1.Text = NormalizeInput(textBox1.Text);
// あとは分割→検証→ソート→textBox2へ
}
finally
{
_updating = false;
}
}
「今は不要」に見えても、後で仕様追加(自動整形、入力補助、複数フォームでの共通化など)が入ると再発火問題は表面化しやすいので、早めに知っておくと役に立ちます。
先頭ゼロをどう扱うか:保持・統一・固定の3パターン
「先頭ゼロを保持したい」という言葉は、要件として掘り下げる価値があります。なぜなら、アプリによって「保持」の意味が変わるからです。
| 要件のイメージ | 例(入力) | 例(出力) | 向いている実装 |
|---|---|---|---|
| 入力どおりの見た目を維持したい | 003 2 10 | 2 003 10(数値昇順で並べ替えた上でRawを表示) | Raw文字列を保持して表示する(堅牢版) |
| 最大桁に合わせてゼロ埋めしたい | 3 12 1 | 01 03 12 | 最大桁を計算してPadLeftで統一 |
| 常に固定桁(例:2桁)で表示したい | 3 12 1 | 01 03 12 | ToString(“D2”)で固定フォーマット |
最大桁に合わせてゼロ埋めして表示を統一したい場合の例です。
var items = tokens
.Select(t => (Raw: t, Ok: int.TryParse(t, out var v), Value: v))
.ToList();
if (items.Any(x => !x.Ok))
{
textBox2.Text = "Invalid input";
return;
}
int width = items.Max(x => x.Raw.Length);
var result = items
.OrderBy(x => x.Value)
.Select(x => x.Value.ToString().PadLeft(width, '0'));
textBox2.Text = string.Join(" ", result);
このように「表示フォーマットまで整える」場合は、表示はRawではなくValueを使ってフォーマットします。要件が「保持」なのか「統一」なのかで、最適解が変わる点は押さえておきましょう。
入力仕様を決めると実装がブレなくなる
スペース区切り入力は自由度が高いぶん、仕様が曖昧だと「これはOK?NG?」が後から増えます。最低限、次のようなルールを決めておくと実装が安定します。
- 区切りは半角スペースのみか、タブ・改行も許容するか
- 全角スペースを許容するか(許容するなら半角に寄せる)
- 負の数(-1)を許容するか
- 重複(1 1 2)をどう扱うか(そのまま残す/重複排除する)
- intの範囲を超える値をどうするか(longにする/エラーにする)
例えば「重複を取り除きたい」なら、ソート前にDistinctを入れるだけで対応できます(ただし、同じ数値でも見た目が違うRawが混在するケースは要件次第です)。
// 数値として重複排除したい例(ValueでDistinct)
var result = items
.GroupBy(x => x.Value)
.OrderBy(g => g.Key)
.Select(g => g.First().Raw);
動作確認に使える入力例(コピペ用)
最後に、実装の正しさを確認しやすい入力例をまとめます。textBox1に貼り付けて、textBox2の出力が想定どおりか確認してください。
| 入力例 | 期待する挙動 | チェックポイント |
|---|---|---|
| 03 02 01 | 01 02 03 | 先頭ゼロが保持されるか |
| 2 10 1 | 1 2 10 | 数値ソートになっているか(文字列ソートだと1 10 2) |
| 5 3 4 | 3 4 5 | 余分なスペースが無視されるか |
| 1 a 2 | Invalid input(またはエラー表示) | 数値以外を検出できるか |
| 1<改行>2<改行>3 | 1 2 3 | 改行を区切りとして扱えるか(堅牢版のSplitならOK) |
まとめ:TextChangedは入力側に寄せ、TryParseとソートキーを正しく選ぶ
C# WinFormsで「スペース区切りの数値を自動ソートして別のTextBoxに表示」するなら、入力元であるtextBox1のTextChangedで処理を行い、Split→TryParseで検証→OrderByで並べ替え→Joinで出力、という流れにするとシンプルで壊れにくくなります。さらに、先頭ゼロを保持したい場合は「表示に使う文字列」と「ソートに使う数値」を分離して考えると、桁数がバラバラな入力でも安全に期待どおりの結果が得られます。

コメント