WinFormsでTableLayoutPanelに動的追加したComboBoxを後から更新しようとして、ControlsやGetControlFromPositionで取得したControlに対してItems.Addを呼ぶとCS1061で止まります。本記事では原因を型の観点から整理し、安全なキャスト方法と運用しやすい設計パターンを具体例で解説します。
症状:取得できているのにItems.Addが呼べない
TableLayoutPanel(例:tblRinks)に対して、フォーム起動時に行・列を増やしながらComboBoxを動的に追加する構成はよくあります。ところが、ユーザーの選択に応じて特定のComboBoxへ候補を追加しようとすると、次のようなコンパイルエラーに遭遇します。
| 状況 | やりたいこと | 起きること |
|---|---|---|
| tblRinks.Controls[r] でコントロールを取得 | 取得したComboBoxへ Items.Add で候補を追加 | CS1061: ‘Control’ に ‘Items’ の定義がない |
| GetControlFromPosition(col, row) でセルから取得 | セルのComboBoxを更新 | 戻り値がControlなので同じエラー/未解決 |
ポイントは「取得できていない」のではなく、「取得した変数の型が Control になっている」ことです。Control型の変数に対しては、ComboBox固有のプロパティであるItemsにアクセスできません。
原因:Control型にはItemsプロパティが存在しない
WinFormsのTableLayoutPanelは、子コントロールを Control.ControlCollection として管理します。つまり、tblRinks.Controls[r] の型は常に Control です。コンパイラは「ControlにItemsなんて無い」と判断するので、実行以前にCS1061で止まります。
| 型 | 代表的に持っているもの | 持っていないもの |
|---|---|---|
| Control | Name / Text / Enabled / BackColor / Dock など | ComboBox.Items / SelectedIndex / DataSource など |
| ComboBox | Items / SelectedIndex / SelectedItem / DropDownStyle など | (ComboBox固有のためControl側からは見えない) |
言い換えると、ランタイム的にはComboBoxが入っていても、コンパイル時の型がControlならItemsに触れない、という話です。解決はシンプルで、ComboBoxとして扱いたい瞬間にキャスト(型変換)を行います。
解決策:パターンマッチで安全にComboBoxへキャストする
最もおすすめなのは、C#のパターンマッチ(is)で「ComboBoxであること」を確認しつつ、同時に変数へ受ける方法です。失敗時に例外が出ないため、運用で事故りにくいのが利点です。
for (int r = 0; r < tblRinks.Controls.Count; r++)
{
Control control = tblRinks.Controls[r];
if (control is ComboBox comboBox && comboBox.Name == "cboRink2B")
{
// 既存候補が残ると重複しやすいので必要に応じてクリア
comboBox.Items.Clear();
foreach (string team in Globals.rList)
{
// senderはイベント発火元のコントロール。ここでは選択済みのチームを除外する例
if (team != ((Control)sender).Text && team != cboRink1A.Text)
{
comboBox.Items.Add(team);
}
}
comboBox.Enabled = true;
comboBox.BackColor = Color.LightGoldenrodYellow;
((Control)sender).BackColor = Color.White;
return;
}
}
この書き方の重要ポイントは次の通りです。
- control is ComboBox comboBox で型を確認し、成功時だけItemsに触る
- 目的のコントロールはNameなどの識別子で確実に特定する(インデックス依存は避ける)
- 候補の更新が「差分追加」なのか「作り直し」なのかに応じて Items.Clear() を使い分ける
- 候補を追加した後に Enabled / BackColor などのUI状態も同期する
GetControlFromPositionを使う場合の注意点
TableLayoutPanelのセル座標で狙い撃ちしたい場合は GetControlFromPosition(column, row) が便利です。ただし戻り値はやはりControlなので、同様にComboBoxへキャストします。また、セルにコントロールが無い場合はnullが返る点にも注意してください。
Control c = tblRinks.GetControlFromPosition(i, j);
if (c is ComboBox cb && cb.Name == "cboRink2A")
{
cb.Items.Add("...");
}
座標系は「列→行」の順番です。(row, col) のつもりで渡してしまうと、意図しないセルを参照して「見つからない」「nullになる」原因になります。
| 取得方法 | メリット | 注意点 |
|---|---|---|
| Controls[index] | 手軽。全体を走査しながら条件一致を探せる | 順序はレイアウト位置と一致しないことがある。Name等の条件が必須 |
| GetControlFromPosition(col, row) | セル単位でピンポイント取得できる | nullが返ることがある。列・行の順番を間違えやすい |
| Controls.Find(name, true) | Nameで探せる。ネストしていても拾える | 戻り値はControl配列。最終的にキャストは必要 |
「Handleをintにする」など寄り道しない
Items.Addができない問題は、あくまで型の問題です。たとえばコントロールのHandleをintに変換して何かする必要は基本的にありません。Handleはウィンドウハンドル(ネイティブ側の識別子)であり、Itemsの追加とは無関係です。
同じく、TableLayoutPanelの内部構造を無理に辿ろうとせず、まずはComboBoxとして扱える形で取得しているかを確認するのが最短ルートです。
ループ境界の落とし穴:< と <= の違い
TableLayoutPanelを行・列で走査する場合、次のような境界ミスが混入しがちです。
for (int col = 0; col <= tblRinks.ColumnCount; col++)
{
for (int row = 0; row <= tblRinks.RowCount; row++)
{
// ...
}
}
ColumnCount や RowCount は「個数」なので、有効な最大インデックスは Count - 1 です。通常は次のように < を使うのが読みやすく、安全です。
for (int col = 0; col < tblRinks.ColumnCount; col++)
{
for (int row = 0; row < tblRinks.RowCount; row++)
{
// ...
}
}
もちろんGetControlFromPositionは範囲外の値を渡すと例外になる可能性があるため、境界は明確に守る方がトラブルシュートが早くなります。
実務で効く改善:探し回るより「参照を持つ」
その場しのぎでControlsを総当たりしても動きますが、コントロール数が増えると可読性と保守性が落ちます。特に「ユーザー操作のたびに他のComboBoxを更新する」ようなUIはイベントが多く、探索コードがあちこちに散らばりがちです。
おすすめは、動的生成したComboBoxを辞書(Dictionary)などで保持し、Nameで直接引けるようにする設計です。これなら更新時に探索が不要になり、ミスも減ります。
// フィールド例
private readonly Dictionary<string, ComboBox> _comboMap = new();
// 動的生成する箇所の例
private void AddRinkCombo(string name, int col, int row)
{
var cb = new ComboBox
{
Name = name,
Dock = DockStyle.Fill,
DropDownStyle = ComboBoxStyle.DropDownList
};
cb.SelectedIndexChanged += Combo_SelectedIndexChanged;
tblRinks.Controls.Add(cb, col, row);
_comboMap[name] = cb;
}
更新側は次のように書けます。
private void UpdateComboItems(string targetName, IEnumerable<string> candidates)
{
if (!_comboMap.TryGetValue(targetName, out ComboBox cb))
{
return; // 見つからないなら何もしない
}
cb.BeginUpdate();
try
{
cb.Items.Clear();
foreach (var s in candidates)
{
cb.Items.Add(s);
}
}
finally
{
cb.EndUpdate();
}
cb.Enabled = cb.Items.Count > 0;
}
この方式の利点は、次のように「UIを安全に更新するための定型処理」をまとめられる点です。
- Itemsのクリアや重複対策、BeginUpdate/EndUpdateを共通化できる
- 「どのComboBoxを更新するか」を文字列Name(またはenum)で統一できる
- Controlsの並び順(Zオーダー)に依存しない
候補の追加ロジックを「仕様として固定」しておく
質問例のように「自分自身(sender)と、別のComboBoxの選択値は候補から除外する」といった要件は、後から微妙に変わりやすい部分です。場当たり的にif条件を増やすと、どこで何を除外しているのか分からなくなります。
除外条件はできるだけコレクションとして扱うと読みやすくなります。
private IEnumerable<string> BuildTeamCandidates(string selectedA, string selectedB)
{
// 除外リストを先に作っておく
var excluded = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
selectedA ?? string.Empty,
selectedB ?? string.Empty
};
foreach (var team in Globals.rList)
{
if (!excluded.Contains(team))
{
yield return team;
}
}
}
イベント側は「候補を組み立てて流し込む」だけにすると、UIロジックとビジネスロジックの分離が進み、テストや改修が楽になります。
Items.AddとDataSourceの使い分け
ComboBoxには Items.Add で追加する方法のほか、DataSource を設定してデータバインドする方法があります。要件次第で使い分けると、更新処理が簡単になります。
| 方式 | 向いているケース | 注意点 |
|---|---|---|
| Items.Add / Items.Clear | 候補数が少ない、都度組み立てる、シンプルに済ませたい | 重複やクリア漏れに注意。大量更新はBeginUpdate推奨 |
| DataSource(ListやBindingList) | 候補の差し替えが頻繁、表示名と値を分けたい、並べ替えを統一したい | ItemsとDataSourceを混在させない(運用中に混ぜると例外/挙動不審の原因) |
どちらを選んでも、Controlとして取得したらキャストが必要という点は変わりません。更新戦略だけが変わります。
よくあるハマりポイントと対策チェックリスト
| ハマりポイント | 症状 | 対策 |
|---|---|---|
| Controls[index]の順序を信じる | 意図したComboBoxではなく別のコントロールを触っている | Nameで特定する/辞書で参照を保持する |
| GetControlFromPositionの引数を逆にする | nullになって更新されない | (col, row)の順番を固定し、変数名もcol/rowにする |
| Items.Clearしないまま追加 | 操作のたびに候補が増殖する | 「差分追加」なのか「作り直し」なのか仕様を決め、明示的にClear |
| SelectedIndexChanged内でSelectedIndexを変更 | イベントが連鎖して二重更新・無限ループ気味になる | 必要ならフラグで抑制する/一時的にイベント解除する |
| senderをControl扱いのまま使う | Textは取れるがSelectedItemなどが使えず条件が複雑化 | senderもComboBoxとして受け、必要情報を明確にする |
デバッグのコツ:型とNameをまず出す
「見つからない」「キャストできない」「nullになる」といった問題が出たら、まずは取得できたControlの情報をログに出すのが早いです。Visual Studioの出力ウィンドウに流すだけでも、原因の切り分けが一気に進みます。
Control c = tblRinks.GetControlFromPosition(col, row);
if (c == null)
{
Debug.WriteLine($"[{col},{row}] は空です");
return;
}
Debug.WriteLine($"[{col},{row}] Name={c.Name}, Type={c.GetType().FullName}");
このログで「そもそも違うセルを見ていた」「Nameが想定と違う(生成時に付けていない)」「ComboBoxではなくLabelだった」などがすぐに分かります。
まとめ:CS1061は“取得失敗”ではなく“型の扱い方”
TableLayoutPanelから取得したコントロールは、API上どうしてもControlとして返ってきます。そのままではComboBox固有のItemsにアクセスできないため、CS1061が発生します。解決はComboBoxへ安全にキャストしてからItems.Addすることです。
加えて、実務では「都度探す」より「参照を持つ」方が堅牢です。Nameでの特定、辞書での保持、GetControlFromPositionの使い分け、境界条件の整理まで押さえておくと、動的UIでも安定して拡張できます。

コメント