DataGridView:Excel と相互コピー&ペーストする方法
📋 目次(クリックで展開)
Excel で作った一覧をそのまま DataGridView に貼り付けたい——業務アプリでは頻出の要望です。
ところが DataGridView には、コピーは標準で用意されているのに、貼り付けは存在しません。Ctrl + C は何もしなくても動くのに、Ctrl + V は無反応。この非対称性が、実装者を最初に戸惑わせます。
この記事では、まずコピー側の仕組みを整理したうえで、貼り付け機能を自前で実装します。単に動くコードを示すだけでなく、実務で必ず問題になる ReadOnly 列の扱い・行の自動追加・型変換・検証のバイパスまで踏み込みます。
結論:コピーは設定のみ、貼り付けは自作
| 操作 | 標準サポート | 必要な作業 |
|---|---|---|
| コピー(Ctrl + C) | あり | ClipboardCopyMode の設定のみ |
| 貼り付け(Ctrl + V) | なし | ProcessCmdKey の捕捉から自前で実装 |
貼り付けが標準にないのは、DataGridView が「どの列に何を入れてよいか」を判断できないためです。ReadOnly 列、非表示列、型の不一致、行数の過不足——これらの方針はアプリケーション側でしか決められません。裏を返せば、方針さえ決めれば実装は難しくありません。
コピー:ClipboardCopyMode を理解する
4つの値の違い
ClipboardCopyMode は既定で EnableWithAutoHeaderText です。つまり何も設定しなくても Ctrl + C は動作します。
| 値 | 動作 |
|---|---|
Disable |
コピーを禁止する |
EnableWithAutoHeaderText |
既定値。行ヘッダー・列ヘッダーが選択に含まれるときだけヘッダー文字列を含める |
EnableWithAlwaysIncludeHeaderText |
選択範囲にかかわらず常にヘッダーを含める |
EnableWithoutHeaderText |
ヘッダーを含めない |
Excel へ貼り付ける前提なら、用途によって使い分けます。
// 見出し付きで Excel に貼りたい場合dataGridView1.ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithAlwaysIncludeHeaderText;
// 値だけを貼りたい場合dataGridView1.ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText;SelectionMode でコピー範囲が変わる
見落としやすい点として、コピーされる範囲は SelectionMode に左右されます。
| SelectionMode | Ctrl + C でコピーされるもの |
|---|---|
CellSelect(既定) |
選択したセルのみ |
FullRowSelect |
選択行の全セル |
FullColumnSelect |
選択列の全セル |
「1セルだけ選んだのに行全体がコピーされる」という場合は、FullRowSelect になっていないか確認してください。
GetClipboardContent で中身を確認する
DataGridView が実際にクリップボードへ何を載せているかは、GetClipboardContent() で取得できます。
private void CheckClipboardContent(){ var data = dataGridView1.GetClipboardContent();
if (data == null) { // 選択セルが無い場合は null が返る return; }
foreach (var format in data.GetFormats()) { Debug.WriteLine(format); }}出力される形式は次の4つです。
| 形式 | 内容 | 主な貼り付け先 |
|---|---|---|
Text / UnicodeText |
タブ区切り(TSV) | Excel、テキストエディタ |
HTML Format |
<table> 形式 |
Excel(書式付き)、Word |
Csv |
カンマ区切り | 一部のアプリ |
Excel は HTML Format を優先して解釈するため、貼り付けると背景色やフォントまで再現されます。値だけを渡したい場合は「形式を選択して貼り付け → テキスト」を案内するか、後述の自前コピーで Text のみを載せてください。
コピー処理を自作する
「選択に関係なく全件コピー」「特定列を除外」といった要件では、GetClipboardContent() を使って自前で組み立てます。
private void CopyAllRows(){ // 現在の選択状態を退避 var savedCell = dataGridView1.CurrentCell; var savedMode = dataGridView1.ClipboardCopyMode;
try { dataGridView1.ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithAlwaysIncludeHeaderText;
dataGridView1.SelectAll();
var data = dataGridView1.GetClipboardContent();
if (data != null) { // TSV のみをクリップボードへ載せる(HTML 形式を除外) var tsv = data.GetData(DataFormats.UnicodeText) as string; Clipboard.SetText(tsv ?? string.Empty, TextDataFormat.UnicodeText); } } finally { dataGridView1.ClipboardCopyMode = savedMode; dataGridView1.ClearSelection();
if (savedCell != null) { dataGridView1.CurrentCell = savedCell; } }}SelectAll() の直後は現在セルの位置が変わるため、finally で復元しています。この復元を怠ると、続く処理で BindingSource.Current を参照した際に例外が出ることがあります。
貼り付け:Ctrl + V を捕捉する
ProcessCmdKey でキーを拾う
DataGridView の KeyDown は、セルが編集モードに入ると編集用コントロールに奪われます。確実に捕捉するには、フォームの ProcessCmdKey をオーバーライドします。
protected override bool ProcessCmdKey(ref Message msg, Keys keyData){ if (keyData == (Keys.Control | Keys.V)) { // 編集中は通常のテキスト貼り付けを優先する if (dataGridView1.Focused && !dataGridView1.IsCurrentCellInEditMode) { PasteFromClipboard(); return true; // 処理済みとしてイベントを止める } }
return base.ProcessCmdKey(ref msg, keyData);}IsCurrentCellInEditMode の判定が重要です。これを省くと、セル編集中に Ctrl + V を押したときにグリッド全体への貼り付けが走り、ユーザーの意図と食い違います。
Excel のクリップボード形式を解析する
Excel がクリップボードに載せるテキストは、次の規則に従います。
| 要素 | 区切り |
|---|---|
| 列 | タブ \t |
| 行 | 改行 \r\n |
| 末尾 | 最終行の後にも \r\n が付く |
厄介なのは、セル内に改行やタブを含む場合です。その場合 Excel は値全体をダブルクォートで囲み、内部のダブルクォートを2つに増やします。
商品A 100 通常品商品B 200 "1行目2行目"商品C 300 """特価"""単純に Split('\t') と Split('\n') で分解すると、この形式で破綻します。状態を持った1文字ずつの走査が必要です。
using System.Collections.Generic;using System.Text;
public static class ClipboardTextParser{ /// <summary> /// Excel 形式のタブ区切りテキストを行×列に分解します。 /// ダブルクォートで囲まれたセル内の改行・タブに対応します。 /// </summary> public static List<List<string>> Parse(string text) { var rows = new List<List<string>>(); var currentRow = new List<string>(); var buffer = new StringBuilder();
bool inQuotes = false; int i = 0;
while (i < text.Length) { char c = text[i];
if (inQuotes) { if (c == '"') { // 連続するダブルクォートはエスケープされた 1 文字 if (i + 1 < text.Length && text[i + 1] == '"') { buffer.Append('"'); i += 2; continue; }
inQuotes = false; i++; continue; }
buffer.Append(c); i++; continue; }
// セル先頭のダブルクォートは引用の開始 if (c == '"' && buffer.Length == 0) { inQuotes = true; i++; continue; }
if (c == '\t') { currentRow.Add(buffer.ToString()); buffer.Clear(); i++; continue; }
if (c == '\r' || c == '\n') { currentRow.Add(buffer.ToString()); buffer.Clear(); rows.Add(currentRow); currentRow = new List<string>();
// CRLF は 2 文字で 1 つの改行 i += (c == '\r' && i + 1 < text.Length && text[i + 1] == '\n') ? 2 : 1; continue; }
buffer.Append(c); i++; }
// 末尾に改行が無い場合の取りこぼしを回収 if (buffer.Length > 0 || currentRow.Count > 0) { currentRow.Add(buffer.ToString()); rows.Add(currentRow); }
return rows; }}貼り付け本体
現在セルを起点として、解析した2次元データを流し込みます。
private void PasteFromClipboard(){ if (!Clipboard.ContainsText()) { return; }
// 日本語を含む場合は UnicodeText を指定する var text = Clipboard.GetText(TextDataFormat.UnicodeText);
if (string.IsNullOrEmpty(text)) { return; }
var table = ClipboardTextParser.Parse(text);
if (table.Count == 0) { return; }
var anchor = dataGridView1.CurrentCell;
if (anchor == null) { MessageBox.Show("貼り付け位置のセルを選択してください。"); return; }
// 貼り付け対象は「表示中かつ編集可能」な列に限定する var targetColumns = dataGridView1.Columns .Cast<DataGridViewColumn>() .Where(c => c.Visible && !c.ReadOnly) .OrderBy(c => c.DisplayIndex) .ToList();
int startColumnIndex = targetColumns.FindIndex(c => c.Index == anchor.ColumnIndex);
if (startColumnIndex < 0) { MessageBox.Show("この列には貼り付けできません。"); return; }
dataGridView1.SuspendLayout();
try { PasteCore(table, anchor.RowIndex, startColumnIndex, targetColumns); } finally { dataGridView1.ResumeLayout(); }}中核の書き込み処理です。
private void PasteCore( List<List<string>> table, int startRowIndex, int startColumnIndex, List<DataGridViewColumn> targetColumns){ int skippedRows = 0;
for (int r = 0; r < table.Count; r++) { int rowIndex = startRowIndex + r;
// グリッドの行数が足りない場合は追加する if (rowIndex >= RowCountExcludingNewRow()) { if (!TryAppendRow()) { skippedRows = table.Count - r; break; } }
var sourceRow = table[r];
for (int c = 0; c < sourceRow.Count; c++) { int columnPosition = startColumnIndex + c;
// 右端を超えた分は捨てる(Excel と同じ挙動) if (columnPosition >= targetColumns.Count) { break; }
var column = targetColumns[columnPosition]; var cell = dataGridView1.Rows[rowIndex].Cells[column.Index];
// セル単位の ReadOnly も尊重する if (cell.ReadOnly) { continue; }
try { cell.Value = ConvertValue(sourceRow[c], cell.ValueType); } catch (Exception ex) when (ex is FormatException or InvalidCastException) { cell.ErrorText = $"変換できません: {sourceRow[c]}"; } } }
if (skippedRows > 0) { MessageBox.Show($"{skippedRows} 行は行数の上限により貼り付けられませんでした。"); }}
/// <summary>/// 新規入力行を除いた実データの行数を返します。/// </summary>private int RowCountExcludingNewRow(){ return dataGridView1.AllowUserToAddRows ? dataGridView1.Rows.Count - 1 : dataGridView1.Rows.Count;}行を追加する
行の追加方法は、バインドの有無で分岐します。バインド中に Rows.Add() を呼ぶと例外になるためです。
private bool TryAppendRow(){ if (dataGridView1.DataSource == null) { // 非バインド:グリッドに直接追加 dataGridView1.Rows.Add(); return true; }
if (bindingSource1.DataSource != null && bindingSource1.AllowNew) { // バインド:データソース側に追加する bindingSource1.AddNew(); bindingSource1.EndEdit(); return true; }
return false;}Rows.Add() を直接呼んで例外が出るケースについては、こちらで詳しく扱っています。
型変換
貼り付け元は常に文字列です。ValueType に合わせた変換が必要になります。
private static object ConvertValue(string text, Type targetType){ if (targetType == null || targetType == typeof(string)) { return text; }
// Nullable<T> は基底の型で判定する var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType; bool nullable = underlying != targetType || !targetType.IsValueType;
if (string.IsNullOrWhiteSpace(text)) { return nullable ? null : Activator.CreateInstance(underlying); }
var converter = TypeDescriptor.GetConverter(underlying);
if (converter != null && converter.CanConvertFrom(typeof(string))) { // IsValid で事前判定し、例外コストを避ける if (!converter.IsValid(text)) { throw new FormatException($"'{text}' を {underlying.Name} に変換できません。"); }
return converter.ConvertFromString(text); }
return Convert.ChangeType(text, underlying, CultureInfo.CurrentCulture);}DataTable にバインドしている場合、空文字は null ではなく DBNull.Value を設定してください。列の AllowDBNull が false なら、そもそも空欄を許容しない設計になっているはずです。
実装後にはまる落とし穴
貼り付けは入力検証をすり抜ける
最も重要な注意点です。
CellValidating や CellValidated は、ユーザーがセルを編集して確定したときにのみ発火します。コードから cell.Value に代入しても呼ばれません。
つまり、手入力では弾いていた不正値が、貼り付けでは素通りします。
// このハンドラは貼り付け時には呼ばれないprivate void DataGridView1_CellValidating( object sender, DataGridViewCellValidatingEventArgs e){ // 手入力のみを検証している状態}対策は、検証ロジックを独立したメソッドに切り出し、貼り付け後にも明示的に呼ぶことです。
private void PasteCompleted(){ foreach (DataGridViewRow row in dataGridView1.Rows) { if (row.IsNewRow) { continue; }
ValidateRow(row); // CellValidating と共通のロジック }}検証ロジックを2箇所に重複させないことが要点です。片方だけ修正して不整合が生じるのは、この種のバグの典型的な発生経路になります。
非表示列はスキップすべきか
上のコードでは Visible == false の列を除外しています。これは「画面で見えているとおりに貼り付く」という直感に合う挙動です。
一方で、ID 列を隠して内部的に保持しているような設計では、除外が正しいとは限りません。列の役割によって方針を決めていきます。判断基準は次のとおりです。
| 非表示列の性質 | 推奨 |
|---|---|
| 内部 ID・外部キー | 除外する(貼り付けさせない) |
| 一時的に隠している表示用データ | 含めるか、貼り付け前に表示に戻す |
元に戻せない
DataGridView に Undo 機能はありません。数百セルを一括で上書きしたあと「元に戻したい」と言われても手段がない、というのは実務で起こりがちな事象です。
簡易的な対策として、貼り付け前にスナップショットを取っておく方法があります。
private DataTable _snapshot;
private void PasteFromClipboard(){ // DataTable バインドなら Copy() で退避できる if (dataGridView1.DataSource is BindingSource bs && bs.DataSource is DataTable dt) { _snapshot = dt.Copy(); }
// 以降、貼り付け処理}
private void UndoPaste(){ if (_snapshot == null) { return; }
bindingSource1.DataSource = _snapshot.Copy();}厳密な Undo スタックを組むより、 「直前の1回だけ戻せる」 で要件を満たせることがほとんどです。
大量貼り付けが遅い
数千セルを超えると、1セルごとの ListChanged 通知と再描画が支配的になります。
// DataTable バインドの場合dataTable.BeginLoadData();bindingSource1.RaiseListChangedEvents = false;
try{ PasteCore(/* ... */);}finally{ bindingSource1.RaiseListChangedEvents = true; dataTable.EndLoadData(); bindingSource1.ResetBindings(false);}それでも重い場合は、グリッド側の描画がボトルネックになっています。
右クリックメニューを追加する
キーボードショートカットだけでは気付かれないため、ContextMenuStrip を併設するのが親切です。
private void SetupContextMenu(){ var menu = new ContextMenuStrip();
var copyItem = new ToolStripMenuItem("コピー(&C)", null, (s, e) => { var data = dataGridView1.GetClipboardContent();
if (data != null) { Clipboard.SetDataObject(data); } }) { ShortcutKeyDisplayString = "Ctrl+C", };
var pasteItem = new ToolStripMenuItem("貼り付け(&P)", null, (s, e) => PasteFromClipboard()) { ShortcutKeyDisplayString = "Ctrl+V", };
// 開くたびに有効・無効を切り替える menu.Opening += (s, e) => { copyItem.Enabled = dataGridView1.SelectedCells.Count > 0; pasteItem.Enabled = Clipboard.ContainsText() && dataGridView1.CurrentCell != null && !dataGridView1.ReadOnly; };
menu.Items.AddRange(new ToolStripItem[] { copyItem, pasteItem }); dataGridView1.ContextMenuStrip = menu;}ShortcutKeys ではなく ShortcutKeyDisplayString を使っているのは、実際のキー処理を ProcessCmdKey 側に一本化するためです。両方に登録すると二重発火します。
Excel 連携方法の使い分け
クリップボード方式が常に最適とは限りません。
| 方式 | 実装コスト | 外部ライブラリ | 向いているケース |
|---|---|---|---|
| クリップボード(本記事) | 中 | 不要 | 少量データの手作業。ファイルを介さない |
| ファイル入出力 | 高 | 必要 | 大量データ、書式や複数シートを扱う |
「Excel の一部をコピーして貼りたい」だけならクリップボードで十分です。ファイル全体を扱うなら、専用ライブラリを使うほうが確実です。
まとめ
- コピーは
ClipboardCopyModeの設定だけで動く。既定値はEnableWithAutoHeaderText - クリップボードには TSV・HTML・CSV の3形式が載る。Excel は HTML を優先するため書式まで再現される
- 貼り付けは標準機能に存在しない。
ProcessCmdKeyでCtrl + Vを捕捉して自作する - Excel の TSV はセル内改行がダブルクォートで囲まれる。単純な
Splitでは壊れる - 貼り付け先は「表示中かつ編集可能な列」に限定し、セル単位の
ReadOnlyも尊重する - 行の追加はバインドの有無で分岐する。バインド中の
Rows.Add()は例外になる CellValidatingは貼り付け時に発火しない。検証ロジックを共通化して明示的に呼ぶ- Undo は存在しない。貼り付け前のスナップショットで簡易的に代替する
関連記事
- DataGridView:Excel データをインポート(読込)する(サンプルあり)
- DataGridView:データを Excel に出力する(サンプルあり)
- DataGridView グリッド全体・行・セルの編集制御(サンプルあり)
- DataGridView:セル編集時の入力制限(サンプルあり)
- DataGridView で行追加できない・反映されない原因と解決方法(サンプルあり)
- DataGridView 現在セルの取得・変更(サンプルあり)
💬 コメント