엑셀 시트의 셀 너비와 높이를 아주 작게 줄이면 셀 하나가 점 하나처럼 보입니다. 여기에 배경색을 칠하면 시트가 그대로 도화지가 되죠. 이 예제는 사용자 정의 폼의 이미지 컨트롤에 사진을 띄워 놓고, 화면에 그려진 점의 색을 Windows API GetPixel로 하나씩 읽어서 같은 위치의 셀에 칠합니다. 실무용이라기보다는 API와 셀 서식을 같이 다뤄 보는 재미용 예제입니다.

사용자 정의 폼의 이미지를 엑셀 시트 셀 색으로 옮겨 그린 결과 화면

코드

폼에 img라는 이름의 이미지 컨트롤과 CommandButton1 버튼을 올려 두고, 아래 코드를 폼 모듈에 넣습니다. 통합 문서와 같은 폴더에 img.jpg가 있어야 합니다.

Private Declare Function GetPixel Lib "gdi32" (ByVal hDC As Long, ByVal X As Long, ByVal Y As Long) As Long
'Private Declare Function GetCursorPos Lib "user32" (lpPoint As POINT) As Long
'Private Declare Function GetWindowDC Lib "user32" (ByVal hWnd As Long) As Long

Private Declare Function GetDC Lib "user32" (ByVal hWnd As Long) As Long
Private Declare Function GetDeviceCaps Lib "gdi32" (ByVal hDC As Long, ByVal nIndex As Long) As Long
Private Declare Function ReleaseDC Lib "user32" (ByVal hWnd As Long, ByVal hDC As Long) As Long

Private Const POINTS_PER_INCH As Long = 72
Private Const LOGPIXELSX = 88 ' tell GetDeviceCaps to return horiz

Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long

'Private Type POINT
'    X As Long
'    Y As Long
'End Type

Private Sub CommandButton1_Click()

    img.Picture = LoadPicture(ThisWorkbook.Path & "\img.jpg")   '// 현재 파일과 같은 경로의 img.jpg 파일을 불러옴
    img.PictureSizeMode = fmPictureSizeModeClip
    img.AutoSize = True: DoEvents   '// 이미지 크기에 맞게 컨트롤을 바꾸고 이벤트를 시스템에 넘김

    Application.ScreenUpdating = False  '// 이제부터 화면 갱신을 멈춤
    Columns.ColumnWidth = 0.38  '// 모든 셀의 너비를
    Rows.RowHeight = 3.75   '// 모든 셀의 높이를
    Cells.Interior.Pattern = xlNone '// 채우기 초기화

    Dim hDC As Long

    hDC = GetDC(FindWindow(vbNullString, Me.Caption))   '// 폼의 핸들값

    Dim Y As Integer, X As Integer
    For X = 1 To GetGraphicswidth   '// 이미지의 너비만큼
        For Y = 1 To GetGraphicsHeight  '// 이미지의 높이만큼
            Cells(Y, X).Interior.Color = GetPixel(hDC, X + (PixelsPerPoint * img.Left) - 1, Y + (PixelsPerPoint * img.Top) - 1) '// 셀 색상 변경
        Next
    Next
    hDC = ReleaseDC(0, hDC)
    Application.ScreenUpdating = True
End Sub

Public Function PixelsPerPoint() As Double
    Dim deviceContextHandle As Long
    Dim DotsPerInch As Long
    deviceContextHandle = GetDC(0)
    DotsPerInch = GetDeviceCaps(deviceContextHandle, LOGPIXELSX)
    PixelsPerPoint = DotsPerInch / POINTS_PER_INCH
    ReleaseDC 0, deviceContextHandle
End Function

Public Function GetGraphicswidth()
    GetGraphicswidth = PixelsPerPoint * img.Width
End Function

Public Function GetGraphicsHeight()
    GetGraphicsHeight = PixelsPerPoint * img.Height
End Function

포인트와 픽셀을 맞추는 부분

폼 컨트롤의 Left, Top, Width, Height는 포인트 단위이고, GetPixel은 화면 픽셀 좌표를 받습니다. 1인치가 72포인트이니, 화면의 가로 DPI(GetDeviceCaps에 LOGPIXELSX = 88)를 72로 나누면 "1포인트가 몇 픽셀인지"가 나옵니다. PixelsPerPoint가 이 값을 계산하고, 이미지 크기와 위치에 곱해서 픽셀 기준으로 바꿉니다. 일반적인 96DPI 화면이면 1.333… 정도가 나옵니다.

GetDC(FindWindow(vbNullString, Me.Caption))는 폼 제목으로 창 핸들을 찾아 그 창의 그리기 영역(DC)을 얻는 부분입니다. 그 DC에서 이미지 컨트롤이 있는 자리의 점 색을 읽어 Cells(Y, X).Interior.Color에 넣는데, GetPixel이 돌려주는 색상 값 형식이 엑셀 Color 속성과 같아서 변환 없이 그대로 넣을 수 있습니다. 다 쓴 DC는 ReleaseDC로 돌려줍니다.

돌려 보면 막히는 곳

읽어 오는 위치가 조금씩 어긋날 수 있습니다. 폼 안의 좌표와 실제 창의 그리기 영역 기준이 정확히 일치하지 않아서인데, 저도 이 부분은 더 다듬지 않고 간단히 마무리했습니다. 가장자리에 다른 색 줄이 섞이면 이 오차 때문입니다. 그리고 GetPixel은 화면에 실제로 보이는 점을 읽으므로, 실행 중에 폼 위로 다른 창이 덮이면 그 창의 색이 시트에 찍힙니다.

셀 하나에 색을 하나씩 칠하는 구조라 이미지가 크면 꽤 오래 걸립니다. 가로세로 200픽셀이면 셀 4만 개를 건드리니, 처음에는 작은 이미지로 시험해 보세요. ScreenUpdating = False를 켜 둔 것도 그나마 속도를 벌기 위해서입니다. Integer 변수라 32767을 넘는 크기는 다루지 못하지만, 엑셀 열 개수 한도에 먼저 걸리니 실제로 문제 될 일은 거의 없습니다.

Declare 문은 32비트 오피스 기준으로 작성되어 있습니다. 64비트 오피스에서는 Private Declare PtrSafe Function ... 처럼 PtrSafe를 붙이고 핸들 형식을 LongPtr로 바꿔야 컴파일됩니다.