엑셀 VBA에서는 Application.ScreenUpdating = False 한 줄이면 작업하는 동안 화면이 깜빡이지 않습니다. VB.NET 폼에서 DataGridView에 행을 수천 개 넣거나 셀 값을 대량으로 바꿀 때도 같은 게 필요한데, 비슷해 보이는 메서드가 여러 개라 하나씩 시도해 봤습니다.
1) 안됨
'DataGridView1.BeginUpdate()
'DataGridView1.EndUpdate()
2) 안됨
'DataGridView1.SuspendDrawing()
'DataGridView1.ResumeDrawing()
3) 됨
DataGridView1.SuspendLayout()
DataGridView1.ResumeLayout()
1번, 2번이 안 되는 이유
BeginUpdate / EndUpdate 는 ListBox, ComboBox, ListView, TreeView 같은 목록형 컨트롤에 있는 메서드입니다. 이 컨트롤들에서는 항목을 추가하는 동안 다시 그리기를 멈춰 주는 정확히 원하는 기능인데, DataGridView에는 이 메서드가 없어서 컴파일부터 안 됩니다. 인터넷 예제에서 ListView 코드를 보고 그대로 옮기면 이렇게 됩니다.
SuspendDrawing / ResumeDrawing 은 .NET 기본 메서드가 아닙니다. 여러 블로그에서 확장 메서드로 직접 만들어 쓰는 이름이라, 그 확장 코드를 프로젝트에 넣지 않으면 역시 존재하지 않는 멤버 오류가 납니다.
3번 SuspendLayout
SuspendLayout / ResumeLayout 은 모든 컨트롤에 있는 메서드라 DataGridView에서도 바로 됩니다. 실제로 대량 작업 앞뒤로 감싸 두니 처리하는 동안의 버벅임이 줄었습니다.
DataGridView1.SuspendLayout()
Try
' 행 추가, 셀 값 변경 등 대량 작업
Finally
DataGridView1.ResumeLayout()
End Try
중간에 예외가 나도 레이아웃이 멈춘 채로 남지 않도록 Try ... Finally 로 감싸 두는 게 좋습니다.
다만 이 메서드가 멈추는 건 엄밀히 말하면 "레이아웃 계산"(크기·위치 재배치)입니다. 그리기 자체를 완전히 막는 기능은 아니어서, 작업 내용에 따라 여전히 깜빡임이 남을 수 있습니다. 그럴 땐 Windows 메시지 WM_SETREDRAW 로 컨트롤의 다시 그리기를 직접 껐다가 켜는 방법이 있습니다. 앞에서 말한 SuspendDrawing 확장 메서드들도 대부분 이 방식입니다.
<Runtime.InteropServices.DllImport("user32.dll")>
Private Shared Function SendMessage(hWnd As IntPtr, msg As Integer, wParam As Integer, lParam As Integer) As Integer
End Function
Private Const WM_SETREDRAW As Integer = &HB
SendMessage(DataGridView1.Handle, WM_SETREDRAW, 0, 0) ' 그리기 중지
' ... 대량 작업 ...
SendMessage(DataGridView1.Handle, WM_SETREDRAW, 1, 0) ' 그리기 재개
DataGridView1.Refresh()
다시 켠 뒤에 Refresh 를 꼭 불러야 그동안 바뀐 내용이 화면에 그려집니다.
그래도 느리다면
DataGridView가 느린 진짜 원인은 그리기보다 다른 데 있는 경우가 많습니다. AutoSizeColumnsMode 나 AutoSizeRowsMode 가 켜져 있으면 행이 하나 들어갈 때마다 전체 열 너비를 다시 계산하니, 작업 동안은 None 으로 두었다가 끝나고 한 번만 맞추는 게 효과가 큽니다. 행을 Rows.Add 로 하나씩 넣기보다 DataTable 을 다 채운 다음 DataSource 로 한 번에 바인딩하는 쪽도 훨씬 빠릅니다.
SuspendDrawing 을 직접 만들어 쓰려면?
2번에서 안 됐던 SuspendDrawing / ResumeDrawing 은 위의 WM_SETREDRAW 를 확장 메서드로 감싼 것입니다. 모듈 하나를 추가해서 아래 코드를 넣으면 2번 코드가 그대로 컴파일되고, DataGridView 뿐 아니라 모든 컨트롤에서 쓸 수 있습니다.
Imports System.Runtime.CompilerServices
Imports System.Runtime.InteropServices
Module ControlExtensions
<DllImport("user32.dll")>
Private Function SendMessage(hWnd As IntPtr, msg As Integer, wParam As Integer, lParam As Integer) As Integer
End Function
Private Const WM_SETREDRAW As Integer = &HB
<Extension()>
Public Sub SuspendDrawing(c As Control)
SendMessage(c.Handle, WM_SETREDRAW, 0, 0)
End Sub
<Extension()>
Public Sub ResumeDrawing(c As Control)
SendMessage(c.Handle, WM_SETREDRAW, 1, 0)
c.Refresh()
End Sub
End Module
WM_SETREDRAW 는 횟수를 세지 않습니다. 함수 안에서 SuspendDrawing 을 두 번 부르고 ResumeDrawing 을 한 번 부르면 그 한 번에 바로 그리기가 재개됩니다. 중첩해서 쓰는 구조라면 호출 횟수를 직접 세는 변수를 두는 게 안전합니다. 그리고 이 역시 Try ... Finally 로 감싸서, 중간에 예외가 나도 ResumeDrawing 이 반드시 불리게 해야 합니다. 안 그러면 그리드가 화면에서 굳은 채로 남습니다.
스크롤할 때 깜빡이는 건 다른 문제
대량 작업 때가 아니라 평소 스크롤하거나 창 크기를 바꿀 때 깜빡인다면 이중 버퍼링이 꺼져 있어서입니다. DataGridView 의 DoubleBuffered 속성은 Protected 라 바깥에서 바로 켤 수 없어서, 리플렉션으로 켜는 방법이 많이 쓰입니다.
GetType(DataGridView).GetProperty("DoubleBuffered",
Reflection.BindingFlags.Instance Or Reflection.BindingFlags.NonPublic).SetValue(DataGridView1, True, Nothing)
폼 Load 에서 한 번만 실행하면 됩니다. 행이 수십만 개라면 이것들로도 한계가 있고, 그때는 화면에 보이는 행만 그때그때 값을 채우는 VirtualMode 를 검토해야 합니다.