※本ページには広告・PR が含まれます。

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 でキーを拾う

DataGridViewKeyDown は、セルが編集モードに入ると編集用コントロールに奪われます。確実に捕捉するには、フォームの 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文字ずつの走査が必要です。

ClipboardTextParser.cs
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 を設定してください。列の AllowDBNullfalse なら、そもそも空欄を許容しない設計になっているはずです。

実装後にはまる落とし穴

貼り付けは入力検証をすり抜ける

最も重要な注意点です。

CellValidatingCellValidated は、ユーザーがセルを編集して確定したときにのみ発火します。コードから 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 を優先するため書式まで再現される
  • 貼り付けは標準機能に存在しない。ProcessCmdKeyCtrl + V を捕捉して自作する
  • Excel の TSV はセル内改行がダブルクォートで囲まれる。単純な Split では壊れる
  • 貼り付け先は「表示中かつ編集可能な列」に限定し、セル単位の ReadOnly も尊重する
  • 行の追加はバインドの有無で分岐する。バインド中の Rows.Add() は例外になる
  • CellValidating は貼り付け時に発火しない。検証ロジックを共通化して明示的に呼ぶ
  • Undo は存在しない。貼り付け前のスナップショットで簡易的に代替する

関連記事


💬 コメント