データグリッドビューをエクセルに出力したいです

タグの編集
投稿者 初心者  (社会人) 投稿日時 2017/8/9 09:02:07
VB.NETであるプログラムを作成しています。
DBと接続してデータグリッドビューにテーブルの情報を表示しています。
そこで、出力ボタンを押下した際にデータグリッドビューに表示しているデータをExcelに入力できるようにしたいです。
Excelにはあらかじめカラム名だけ書いていて後はそこにデータを入力していきたいです。 
  '出力ボタン
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Dim app As Excel.Application = Nothing
        Dim book As Excel.Workbook = Nothing
        Dim sheet As Excel.Worksheet = Nothing

        Try
            app = New Excel.Application()

            app = CreateObject("Excel.Application")
            book = app.Workbooks.Open("C:\form3\Book1.xlsx")
            sheet = book.Worksheets(1)

         'For i As Integer = 0 To DataGridView1.Rows.Count - 1               
     'For x As Integer = 0 To DataGridView1.ColumnCount - 1

                    sheet.Range("A2").Value = "aaa"
                    sheet.Range("B2").Value = "ab"
                    sheet.Range("C2").Value = "abc"
                    sheet.Range("D2").Value = "a"
                    sheet.Range("E2").Value = "a"
                'Next
            'Next
                app.Visible = True

                System.Threading.Thread.Sleep(5000)

                book.Save()
                app.Quit()

        Catch ex As Exception
            Throw ex

        Finally
            Marshal.ReleaseComObject(sheet)
            Marshal.ReleaseComObject(book)
            Marshal.ReleaseComObject(app)

        End Try


    End Sub


一応今はこれでExcelにベタ書きで文字を入力することはできています。
お願いします。 
for文で回して出力していくと思うのですが書き方がわかっていません。。。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2017/8/9 10:22:36
クリップボードを使っても構わないのであれば、
 .ClipboardCopyMode プロパティを指定して
 .SelectAll メソッドで全選択して
 .GetClipboardContent メソッドで得たデータを
 Clipboard オブジェクトに(加工してから)渡す
という手順を採ることで、データを一括転送できます。
http://dobon.net/vb/dotnet/datagridview/clipboardcopy.html

貼り付けには、Worksheet オブジェクトあるいは Range オブジェクトの
PasteSpecial メソッドを使うことができるでしょう。大抵の場合、
セル単位でひとつずつ貼り付けるよりも高速になると思います。


クリップボードを汚したくない場合には、ActiveX Data Object の PIA を
参照設定して、切断型 Recordset に DataGridView の内容を転写してから、
Range オブジェクトの CopyFromRecordset メソッドに渡すという手もあります。



> book = app.Workbooks.Open("C:\form3\Book1.xlsx")
> sheet = book.Worksheets(1)
ReleaseComObject のために、
 books = app.Workbooks
 book = books.Open("C:\form3\Book1.xlsx")
 sheets = book.Worksheets
 sheet = DirectCast(sheets(1), Excel.Worksheet)
のようにするべきです。Workbooks や Sheets も解放対象です。


> sheet.Range("A2").Value = "aaa"
こちらも同様に、
 rng = sheet.Range("A2")
 rng.Value = "aaa"
 Marshal.ReleaseComObject(rng)
のようにします。


> System.Threading.Thread.Sleep(5000)
UI スレッドから Sleep メソッドを呼び出してはいけません。
Excel オートメーションの利用中ならば尚の事。
投稿者 (削除されました)  () 投稿日時 2017/8/9 10:24:08
(削除されました)
投稿者 初心者  (社会人) 投稿日時 2017/8/9 11:21:20
回答ありがとうございます。
ご指摘いただいた箇所は後で修正します。


 For i As Integer = 0 To DataGridView1.Rows.Count - 1
                For j As Integer = 2 To DataGridView1.Rows.Count - 1
                    sheet.Range("A2").Value = DataGridView1.Rows(i).Cells(1).Value
                    sheet.Range("B2").Value = DataGridView1.Rows(i).Cells(4).Value
                    sheet.Range("C2").Value = DataGridView1.Rows(i).Cells(5).Value
                    sheet.Range("D2").Value = DataGridView1.Rows(i).Cells(6).Value
                    sheet.Range("E2").Value = DataGridView1.Rows(i).Cells(7).Value
                Next
            Next

これでデータグリッドの一行だけはExcelに出力できるようになりました。
jをExcelのセルの値に持ってきたいのですが、HRESULT からの例外:0x800A03ECとエラーが出てしまいます。どうしたらいいでしょうか??
投稿者 YuO  (社会人) 投稿日時 2017/8/9 11:42:46
とりあえず,DataGridViewの行とExcelの行が1対1の対応をするのであれば,二重ループはいりません。
DataGridViewの行番号であるiでループを回して,Excelの行番号は(i + 2)を使えば良いです。
つまり,
"A" & (i + 2).ToString()
のように書けば"A2", "A3", ...となります。
2015以降,
$"A{i + 2}"
と書いても同じようになります。


0x800A03ECのCOMExceptionですが,状況に依存して色々な理由で出るようです。
とりあえず,魔界の仮面弁士さんの書かれているようにCOMの解放をきっちりやると起きなくなることもあるようです。

根本的には,Excelのオートメーションを使わなければCOMExceptionは発生しません。
EPPlusとかNPOIとかClosedXMLとかのライブラリが存在します。
使い方もExcelオートメーションとそれほど変わらないようですし,こちらに切り替えてみるのも良いでしょう。
# 選択基準はnuget.orgでExcelを検索して出てくる,オートメーションを使っておらず,書き込み可能な上位3つ。
投稿者 初心者  (社会人) 投稿日時 2017/8/9 11:59:32
Yuoさん
回答ありがとうございます。
            For i As Integer = 0 To DataGridView1.Rows.Count - 1 
                sheet.Range("A" & i + 2 & "").Value = DataGridView1.Rows(i).Cells(1).Value
                sheet.Range("B" & i + 2 & "").Value = DataGridView1.Rows(i).Cells(4).Value
                sheet.Range("C" & i + 2 & "").Value = DataGridView1.Rows(i).Cells(5).Value
                sheet.Range("D" & i + 2 & "").Value = DataGridView1.Rows(i).Cells(6).Value
                sheet.Range("E" & i + 2 & "").Value = DataGridView1.Rows(i).Cells(7).Value
            Next
これでできるようになりました!!

後は、細かい修正をしていきたいと思います。ありがとうございました。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2017/8/9 12:44:01
> DataGridView1.Rows(i).Cells(1).Value
実はこれも無駄で、
 DataGridView1(xIndex, yIndex).Value
で十分です。

「DataGridView1.Rows(i).Cells(1).Value」は
 .Rows  → DataGridViewRowCollection 型
 (i)    → DataGridViewRow 型
 .Cells → DataGridViewCellCollection 型
 (1)    → DataGridViewCell 型
 .Value → Object 型
という段階を経てオブジェクトが参照されますが、
「DataGridView1(xIndex, yIndex).Value」を使えば、
 (xIndex, yIndex) → DataGridViewCell 型
 .Value           → Object 型
だけで済みます。
投稿者 初心者  (社会人) 投稿日時 2017/8/9 13:46:42
返信ありがとうございます。そして追加の情報ありがとうございます。

>  sheets = book.Worksheets
修正していたところ上記の箇所で
「型 'System.__ComObject' の COM オブジェクトをインターフェイス型 'Microsoft.Office.Interop.Excel.Worksheets' にキャストできません。」
エラーがでてしまいました。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2017/8/9 20:11:06
Worksheets オブジェクトは、As Worksheets ではなく As Sheets で受けてください。
投稿者 (削除されました)  () 投稿日時 2017/8/10 09:22:43
(削除されました)
投稿者 初心者  (社会人) 投稿日時 2017/8/10 09:23:57
返信ありがとうございます。
 一応正常に動くことは確認できたのですが、COMの解放がちゃんとできていないせいかタスクが残ってしまいます。
for文の箇所をコメントアウトして動かしたらタスクは残らなかったのでfor文がおかしいと思うのですがどこがおかしいのでしょうか、、、お願いします。
  '出力ボタン
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        
         Dim app As Excel.Application = Nothing
         Dim books As Excel.Workbooks = Nothing
         Dim book As Excel.Workbook = Nothing
         Dim sheets As Excel.Sheets = Nothing
         Dim sheet As Excel.Worksheet = Nothing
         Dim range As Excel.Range = Nothing

         Try
             'Excelを開く
            app = New Excel.Application()

             'Bookを開く
            books = app.Workbooks
             book = books.Open("C:\form3\Book1.xlsx")

             'シートの指定
            sheets = book.Worksheets
             sheet = DirectCast(sheets(1), Excel.Worksheet)

             range = sheet.Range("B2")
             range.Value = LoginForm1.username & "さん"

             'Excelに出力する
            For i As Integer = 0 To DataGridView1.Rows.Count - 1

                 range = sheet.Range("B" & i + 5 & "")
                 range.Value = DataGridView1.Rows(i).Cells(1).Value

                 range = sheet.Range("C" & i + 5 & "")
                 range.Value = DataGridView1.Rows(i).Cells(4).Value

                 range = sheet.Range("D" & i + 5 & "")
                 range.Value = DataGridView1.Rows(i).Cells(5).Value

                 range = sheet.Range("E" & i + 5 & "")
                 range.Value = DataGridView1.Rows(i).Cells(6).Value

                 range = sheet.Range("F" & i + 5 & "")
                 range.Value = DataGridView1.Rows(i).Cells(7).Value

                 'range = sheet.Range("B" & i + 5 & "")
                 'range.Value = DataGridView1(i, 1).Value

                 'range = sheet.Range("C" & i + 5 & "")
                 'range.Value = DataGridView1(i, 4).Value

                 'range = sheet.Range("D" & i + 5 & "")
                 'range.Value = DataGridView1(i, 5).Value

                 'range = sheet.Range("E" & i + 5 & "")
                 'range.Value = DataGridView1(i, 6).Value

                 'range = sheet.Range("F" & i + 5 & "")
                 'range.Value = DataGridView1(i, 7).Value

             Next

             'Excelの表示
            app.Visible = True

             ''5秒だけ表示する
            System.Threading.Thread.Sleep(1000)

             '保存する
            book.Save()

         Catch ex As Exception
             Throw ex

         Finally
             Marshal.ReleaseComObject(range)
             Marshal.ReleaseComObject(sheet)
             Marshal.ReleaseComObject(sheets)
             Marshal.ReleaseComObject(book)
             Marshal.ReleaseComObject(books)
             '閉じる
            app.Quit()
             Marshal.ReleaseComObject(app)

         End Try

     End Sub 
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2017/8/10 10:54:27
> range = sheet.Range("B" & i + 5 & "")
range オブジェクトには、「B5セル」を現す Range オブジェクトが格納されています。

それを解放しないまま、
> range = sheet.Range("C" & i + 5 & "")
にて、その参照先を「C5セル」のインスタンスに差し替えています。これが原因ですね。

解放処理は「変数」ではなく、「変数が指し示すインスタンス」に対して行われますので、
別のインスタンスに差し替えるのであれば、差し替える前のインスタンスも ReleaseComObject せねばなりません。


なお、連続した領域であるならば、Range("B5:F5") などの範囲で取得し、
Value プロパティに「配列」で渡す形にするのがお奨めです。
通信回数を減らすことができるため、処理速度がUPしますし、
COM 解放の手間も減らすことができます。



> ''5秒だけ表示する
> System.Threading.Thread.Sleep(1000)
これでは 5 秒になっていませんよね。

そもそも先にも述べたように、この場所での Sleep の利用は厳禁です。
(メッセージループを阻害するため、Windows Forms の UI スレッドから利用すべきでは無い)

どうしても待ち合わせをしたいなら、Timer コンポーネントを利用するか、
もしくは、『Await task.Delay(5000)』などを使うようにします。


> Catch ex As Exception
>  Throw ex
これは悪手です。このような例外処理を書いてはいけません。

これでは例外のスタックトレースが握りつぶされますので、
どこで発生した問題なのかが分からなくなってしまいます。

別の例外に変換するのであればさておき、例外をそのまま再スローしたいだけであれば、
「Throw ex」ではなく「Throw」とするべきです。

また、今回のようにそもそも例外処理を行わないのであれば、
Catch 句無しの「Try~Finally~End Try」を使うのが望ましいです。



> Finally
>   Marshal.ReleaseComObject(range)
>   Marshal.ReleaseComObject(sheet)
>   Marshal.ReleaseComObject(sheets)
>   Marshal.ReleaseComObject(book)
>   Marshal.ReleaseComObject(books)
>   '閉じる
>  app.Quit()
>   Marshal.ReleaseComObject(app)
>
> End Try

これも非常にまずいです。

たとえばこの処理だと、Book1.xlsx を開けずに処理が中断した場合、
app と books のみが設定され、それ以外は Nothing の状態に陥ります。

しかし ReleaseComObject にNothing を渡すことはできませんので、
その時点で、NullReferenceException の例外が発生して
Finally 句が中断されます。結果、COM オブジェクトは一切解放されません。

ということはつまり、Finally 句としては何の役目も果たしていない、ということです。

しかも Catch 句でスタックトレースが潰されているので、処理としては
Try ブロックを使わずに書いたケースよりも、むしろ悪化しているほどです。


Finally 句で対処するのであれば、
 Marshal.ReleaseComObject(sheets)
ではなく、それぞれを
 If sheets IsNot Nothing AndAlso Marshal.IsComObject(sheets) Then
  Marshal.ReleaseComObject(sheets)
 End If
と書くべきです。これなら、Finally ブロック内での例外の発生を抑えられます。

上記で IsComObject は呼ばなくても構いませんが、COM オブジェクトではなく
マネージオブジェクトを返すような修正が入った場合には、ReleaseComObject に
渡すわけには行かなくなるので、念のための保険です。


とはいえ、If 文を毎回書くのは面倒だと思いますので、解放処理を行うために
http://hanatyan.sakura.ne.jp/dotnet/Excel01.htm
の末尾にある MRComObject メソッドを用意しておくのも良いでしょう。
そうすれば、下記のように簡潔に書けます。
Finally
    MRComObject(range)
    MRComObject(sheet)
    MRComObject(sheets)
    MRComObject(book)
    MRComObject(books)
    If app IsNot Nothing Then
        app.Quit()
    End If
    MRComObject(app)
End Type
投稿者 初心者  (社会人) 投稿日時 2017/8/18 11:25:56
返信ありがとうございます。
ご指摘いただいた箇所は修正して完成させることができました。
あとひとつ質問なのですが、セルを初期化したくて
sheet.Rows("5:50").Delete()
というコードを追加したらCOMが正常に解放されなくなりました。
どうしてでしょうか??
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2017/8/18 13:05:06
> あとひとつ質問なのですが、セルを初期化したくて
初期化なら Clear メソッドなのでは。


> sheet.Rows("5:50").Delete()
先の指摘と同じ理由により NG です。(^_^;)


> ご指摘いただいた箇所は修正して完成させることができました。
「指摘した箇所」だけではなく、
「指摘した内容」を見直しましょう。


'Worksheet の Rows プロパティは引数の無いプロパティであり、
'戻り値として、Excel.Range オブジェクトを返します。
oRows1 = sheet.Rows

'Range のインデクサは、別の Range オブジェクトを返します。
'これは ._Default("5:50") というプロパティ呼び出しと同義です。
oRows2 = oRows("5:50")

'Range の Delete メソッドは、削除に成功すると True を返します。
'この戻り値は COM オブジェクトではないため、
'変数に受けて解放する必要はありません。
oRows.Delete()


あとは、上記の oRows1 と oRows2 を処分すれば OK です。