DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

DataGridViewで選択されたセルをクリップボードにコピーできるようにする

注意:DataGridViewコントロールは、.NET Framework 2.0で新しく追加されました。

DataGridView.ClipboardCopyModeプロパティがDataGridViewClipboardCopyMode.Disable以外のときは、「Ctrl + C」キーを押すことにより、選択されたセルがクリップボードにコピーされるようになります。形式は、Text、UnicodeText、Html、CommaSeparatedValue(CSV)の4種類です。TextとUnicodeTextは、タブ区切り(TSV)形式のデータです。

ClipboardCopyModeによって、ヘッダーをコピーするかどうかを決めることができます。EnableAlwaysIncludeHeaderTextではヘッダーもコピーされ、EnableWithoutHeaderTextではコピーされません。既定値であるEnableWithAutoHeaderTextでは、ヘッダーが選択されている場合にコピーされます。

VB.NET
コードを隠すコードを選択
'ヘッダーをコピーしないようにする
DataGridView1.ClipboardCopyMode = _
    DataGridViewClipboardCopyMode.EnableWithoutHeaderText
C#
コードを隠すコードを選択
//ヘッダーをコピーしないようにする
DataGridView1.ClipboardCopyMode =
    DataGridViewClipboardCopyMode.EnableWithoutHeaderText;

プログラムにより、コピーする

ボタンをクリックした時や、メニューで選択した時にクリップボードにコピーするには、DataGridView.GetClipboardContentメソッドでDataObjectオブジェクトを取得して、これをClipboard.SetDataObjectメソッドに渡します。

以下に具体的なコードを示します。

VB.NET
コードを隠すコードを選択
'選択されたセルをクリップボードにコピーする
Clipboard.SetDataObject(DataGridView1.GetClipboardContent())
C#
コードを隠すコードを選択
//選択されたセルをクリップボードにコピーする
Clipboard.SetDataObject(DataGridView1.GetClipboardContent());

Excelで正常に貼り付けできない問題と解決法

上記のようにコピーすると、実はHTMLとCSVのデータに問題が発生します。HTML形式のデータは本来UTF8でなければなりませんが、ANSIになってしまいます。逆にCSVはANSIではなく、UTF8になってしまいます。私が試した限りでは、HTMLがUTF8にならない不具合は.NET Framework 4.0で修正されたようですが、CSVの方は直っていません。

上記の方法でコピーしたデータをExcelに貼り付けると、日本語が表示されなかったり、文字化けしたりします。これは、普通にExcelに貼り付けると、HTML形式のデータが使用されるためです。「形式を選択して貼り付け」で「Unicodeテキスト」か「テキスト」を選択すれば、正しく貼り付けることができます(「Csv」で貼り付けても、文字化けします)。.NET Framework 4.0からは、普通に貼り付けても文字化けしなくなったようです。

補足:HTML形式のデータは、文字コードの問題だけでなく、「What is this rogue version 1.0 of the HTML clipboard format? - MSDN Blogs」で指摘されているような、ヘッダが正しくない(バイト数で数えていない)という問題もあるようです。Excelの場合は、ヘッダが間違えていても文字コードがUTF8ならば正常に貼り付けることができるようですが、それ以外のアプリケーションではうまく行かない可能性もあります。HTML形式のデータを作成する方法については、「クリップボードにHTML形式のデータをコピーする」をご覧ください。
補足:「(VB.Net)DataObject.GetText()の引数にCSV(TextDataFormat.CommaSeparatedValu)指定時のバグ!? : 3流プログラマのメモ書き」によると、TSV(つまり、TextとUnicodeText)とCSVのデータは、フィールドに改行文字が含まれていてもダブルクォートで囲まないため、不正なデータになってしまうということです。その点では、HTML形式のデータは、改行が<br>に変換されるため、正常です。しかし、<br>で改行されたデータは、Excelに貼り付けると、セルが分割されてしまいます。それを防ぐためには、「br { mso-data-placement: same-cell; }」というスタイルが必要になります。

この問題の解決法としては、これらのデータを正しい文字コードでクリップボードにコピーすればよいということになります。その方法は、「クリップボードにHTML形式のデータをコピーする」と「クリップボードにCSV形式のデータをコピーする」で説明しています。

また、「DataGridView から Excel へコピーすると文字化けする | Moonmile Solutions Blog」によると、DataGridView.GetClipboardContentメソッドをオーバーライドして正しいデータを返すようにすれば、「Ctrl+C」キーでも正しくコピーできるようになるということです。

以下に、以上の問題を修正したDataGridViewの派生クラスの例を示します。DataGridViewの代わりに使用してください(詳しくは、「「○○○クラスの代わりに派生クラスを使用します」の意味は?」)。なお、文字コード以外の問題は、修正していません。

VB.NET
コードを隠すコードを選択
Imports System.Windows.Forms
Imports System.IO
Imports System.Text

''' <summary>
''' GetClipboardContentメソッドを修正したDataGridViewコントロール
''' </summary>
Public Class DataGridViewEx
    Inherits DataGridView
    Public Overrides Function GetClipboardContent() As DataObject
        '元のDataObjectを取得する
        Dim oldData As DataObject = MyBase.GetClipboardContent()

        '新しいDataObjectを作成する
        Dim newData As New DataObject()

        'テキスト形式のデータをセットする(UnicodeTextもセットされる)
        newData.SetData(DataFormats.Text, oldData.GetData(DataFormats.Text))

        'HTML形式のデータを取得する
        Dim htmlObj As Object = oldData.GetData(DataFormats.Html)
        Dim htmlStrm As MemoryStream = Nothing
        If TypeOf htmlObj Is String Then
            'String型の時は、MemoryStreamに変換する
            htmlStrm = New MemoryStream( _
                Encoding.UTF8.GetBytes(DirectCast(htmlObj, String)))
        ElseIf TypeOf htmlObj Is MemoryStream Then
            '.NET Framework 4.0以降では、MemoryStreamとなる
            htmlStrm = DirectCast(htmlObj, MemoryStream)
        End If
        If htmlStrm IsNot Nothing Then
            'HTML形式のデータをセットする
            newData.SetData(DataFormats.Html, htmlStrm)
        End If

        'CSV形式のデータを取得する
        Dim csvObj As Object = _
            oldData.GetData(DataFormats.CommaSeparatedValue)
        Dim csvStrm As MemoryStream = Nothing
        If TypeOf csvObj Is String Then
            'MemoryStreamに変換する
            csvStrm = New MemoryStream( _
                Encoding.Default.GetBytes(DirectCast(csvObj, String)))
        ElseIf TypeOf csvObj Is MemoryStream Then
            '今のところこうなることはないが、将来を見据えて...
            csvStrm = DirectCast(csvObj, MemoryStream)
        End If
        If csvStrm IsNot Nothing Then
            'CSV形式のデータをセットする
            newData.SetData(DataFormats.CommaSeparatedValue, csvStrm)
        End If

        Return newData
    End Function
End Class
C#
コードを隠すコードを選択
using System;
using System.Windows.Forms;
using System.IO;
using System.Text;

/// <summary>
/// GetClipboardContentメソッドを修正したDataGridViewコントロール
/// </summary>
public class DataGridViewEx : DataGridView
{
    public override DataObject GetClipboardContent()
    {
        //元のDataObjectを取得する
        DataObject oldData = base.GetClipboardContent();

        //新しいDataObjectを作成する
        DataObject newData = new DataObject();

        //テキスト形式のデータをセットする(UnicodeTextもセットされる)
        newData.SetData(DataFormats.Text, oldData.GetData(DataFormats.Text));

        //HTML形式のデータを取得する
        object htmlObj = oldData.GetData(DataFormats.Html);
        MemoryStream htmlStrm = null;
        if (htmlObj is string)
        {
            //String型の時は、MemoryStreamに変換する
            htmlStrm = new MemoryStream(
                Encoding.UTF8.GetBytes((string)htmlObj));
        }
        else if (htmlObj is MemoryStream)
        {
            //.NET Framework 4.0以降では、MemoryStreamとなる
            htmlStrm = (MemoryStream)htmlObj;
        }
        if (htmlStrm != null)
        {
            //HTML形式のデータをセットする
            newData.SetData(DataFormats.Html, htmlStrm);
        }

        //CSV形式のデータを取得する
        object csvObj = oldData.GetData(DataFormats.CommaSeparatedValue);
        MemoryStream csvStrm = null;
        if (csvObj is string)
        {
            //MemoryStreamに変換する
            csvStrm = new MemoryStream(
                Encoding.Default.GetBytes((string)csvObj));
        }
        else if (csvObj is MemoryStream)
        {
            //今のところこうなることはないが、将来を見据えて...
            csvStrm = (MemoryStream)csvObj;
        }
        if (csvStrm != null)
        {
            //CSV形式のデータをセットする
            newData.SetData(DataFormats.CommaSeparatedValue, csvStrm);
        }

        return newData;
    }
}

DataGridViewにペーストする

このようにクリップボードにコピーするのは簡単ですが、DataGridViewにペーストする(貼り付ける)のは大変です。「Ctrl + V」キーでペーストすることもできませんし、このような機能のメソッドが用意されている訳でもありません。つまり、自分でそのようなコードを書くしかありません。

以下に、ペーストを行うごく簡単な例を示します。この例では、コピーした行を現在のセルのある行以降にペーストするものです。行を挿入するのではなく、現在のセルの値を上書きしています(データがバインドされていると挿入できないため)。また、クリップボードにあるデータには列と行ヘッダーが含まれていると決め付けていますし、データ型も全く調べていません。

VB.NET
コードを隠すコードを選択
'現在のセルのある行から下にペーストする
If DataGridView1.CurrentCell Is Nothing Then
    Return
End If
Dim insertRowIndex As Integer = DataGridView1.CurrentCell.RowIndex

'クリップボードの内容を取得して、行で分ける
Dim pasteText As String = Clipboard.GetText()
If String.IsNullOrEmpty(pasteText) Then
    Return
End If
pasteText = pasteText.Replace(vbCrLf, vbLf)
pasteText = pasteText.Replace(vbCr, vbLf)
pasteText = pasteText.TrimEnd(New Char() {vbLf})
Dim lines As String() = pasteText.Split(vbLf)

Dim isHeader As Boolean = True
For Each line As String In lines
    '列ヘッダーならば飛ばす
    If isHeader Then
        isHeader = False
    Else
        'タブで分割
        Dim vals As String() = line.Split(ControlChars.Tab)
        '列数が合っているか調べる
        If vals.Length - 1 <> DataGridView1.ColumnCount Then
            Throw New ApplicationException("列数が違います。")
        End If
        Dim row As DataGridViewRow = DataGridView1.Rows(insertRowIndex)
        'ヘッダーを設定
        row.HeaderCell.Value = vals(0)
        '各セルの値を設定
        Dim i As Integer
        For i = 0 To row.Cells.Count - 1
            row.Cells(i).Value = vals((i + 1))
        Next i

        '次の行へ
        insertRowIndex += 1
    End If
Next line
C#
コードを隠すコードを選択
//現在のセルのある行から下にペーストする
if (DataGridView1.CurrentCell == null)
    return;
int insertRowIndex = DataGridView1.CurrentCell.RowIndex;

//クリップボードの内容を取得して、行で分ける
string pasteText = Clipboard.GetText();
if (string.IsNullOrEmpty(pasteText))
    return;
pasteText = pasteText.Replace("\r\n", "\n");
pasteText = pasteText.Replace('\r', '\n');
pasteText = pasteText.TrimEnd(new char[] { '\n' });
string[] lines = pasteText.Split('\n');

bool isHeader = true;
foreach (string line in lines)
{
    //列ヘッダーならば飛ばす
    if (isHeader)
    {
        isHeader = false;
        continue;
    }

    //タブで分割
    string[] vals = line.Split('\t');
    //列数が合っているか調べる
    if (vals.Length - 1 != DataGridView1.ColumnCount)
        throw new ApplicationException("列数が違います。");
    DataGridViewRow row = DataGridView1.Rows[insertRowIndex];
    //ヘッダーを設定
    row.HeaderCell.Value = vals[0];
    //各セルの値を設定
    for (int i = 0; i < row.Cells.Count; i++)
    {
        row.Cells[i].Value = vals[i + 1];
    }

    //次の行へ
    insertRowIndex++;
}
  • 履歴:
  • 2010/10/5 コードを修正。(コメントでご指摘いただきました。)
  • 2016/5/26 「Excelで正常に貼り付けできない問題と解決法」を追加。(コメントでご指摘いただきました。)

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • .NET Tipsをご利用いただく際は、注意事項をお守りください。