본문으로 건너뛰기
Previous
Next
MFC 이미지 뷰어 만들기 | GDI+ 실전 프로젝트

MFC 이미지 뷰어 만들기 | GDI+ 실전 프로젝트

MFC 이미지 뷰어 만들기 | GDI+ 실전 프로젝트

이 글의 핵심

MFC와 GDI+로 이미지 뷰어를 만듭니다. CImage로 JPG/PNG/BMP 로드, Matrix로 확대/축소/회전, 더블 버퍼링으로 부드러운 화면 갱신, CListCtrl로 썸네일 목록을 구현합니다.

들어가며

이 글에서는 MFC와 GDI+로 완전한 이미지 뷰어를 만듭니다. Windows 사진 뷰어와 유사한 기능을 구현하며, 이미지 처리, 변환, UI 구성 기술을 종합합니다.

구현 기능

  • 이미지 열기 (JPG, PNG, BMP, GIF)
  • 확대/축소 (휠, 버튼)
  • 회전 (90도씩, 자유 회전)
  • 이미지 정보 표시 (크기, 포맷, EXIF)
  • 썸네일 목록 (폴더 내 이미지)
  • 슬라이드쇼
  • 간단한 편집 (밝기, 대비, 그레이스케일)

1. 프로젝트 구조

ImageViewer/
├─ ImageViewerApp.h/cpp    // CWinApp
├─ MainFrm.h/cpp            // CFrameWnd
├─ ImageView.h/cpp          // CScrollView (이미지 표시)
├─ ThumbnailView.h/cpp      // CListCtrl (썸네일)
├─ ImageProcessor.h/cpp     // 이미지 처리 유틸리티
└─ resource.h/rc            // 리소스

2. ImageView 클래스 (이미지 표시)

// ImageView.h
#pragma once
#include <gdiplus.h>
using namespace Gdiplus;

class CImageView : public CScrollView
{
    DECLARE_DYNCREATE(CImageView)
    
protected:
    Image* m_pImage;
    float m_fZoom;
    float m_fRotation;
    CPoint m_ptOffset;
    
    BOOL m_bDragging;
    CPoint m_ptDragStart;
    
public:
    CImageView();
    virtual ~CImageView();
    
    BOOL LoadImage(const CString& filePath);
    void ZoomIn();
    void ZoomOut();
    void ZoomFit();
    void Rotate(float angle);
    void ResetView();
    
    Image* GetImage() const { return m_pImage; }
    
protected:
    virtual void OnDraw(CDC* pDC);
    virtual void OnInitialUpdate();
    
    DECLARE_MESSAGE_MAP()
    
    afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
    afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
    afx_msg void OnMouseMove(UINT nFlags, CPoint point);
    afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt);
};

// ImageView.cpp
#include "ImageView.h"

IMPLEMENT_DYNCREATE(CImageView, CScrollView)

CImageView::CImageView()
    : m_pImage(NULL)
    , m_fZoom(1.0f)
    , m_fRotation(0.0f)
    , m_bDragging(FALSE)
{
}

CImageView::~CImageView()
{
    if (m_pImage) {
        delete m_pImage;
    }
}

BEGIN_MESSAGE_MAP(CImageView, CScrollView)
    ON_WM_LBUTTONDOWN()
    ON_WM_LBUTTONUP()
    ON_WM_MOUSEMOVE()
    ON_WM_MOUSEWHEEL()
END_MESSAGE_MAP()

void CImageView::OnInitialUpdate()
{
    CScrollView::OnInitialUpdate();
    
    SetScrollSizes(MM_TEXT, CSize(0, 0));
}

BOOL CImageView::LoadImage(const CString& filePath)
{
    if (m_pImage) {
        delete m_pImage;
        m_pImage = NULL;
    }
    
    m_pImage = Image::FromFile(filePath);
    
    if (m_pImage == NULL || m_pImage->GetLastStatus() != Ok) {
        AfxMessageBox(_T("이미지 로드 실패!"));
        return FALSE;
    }
    
    ResetView();
    ZoomFit();
    
    Invalidate();
    
    return TRUE;
}

void CImageView::OnDraw(CDC* pDC)
{
    if (m_pImage == NULL) {
        return;
    }
    
    CRect rect;
    GetClientRect(&rect);
    
    // 더블 버퍼링
    CDC memDC;
    memDC.CreateCompatibleDC(pDC);
    
    CBitmap bitmap;
    bitmap.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height());
    CBitmap* pOldBitmap = memDC.SelectObject(&bitmap);
    
    // 배경 (회색)
    memDC.FillSolidRect(&rect, RGB(64, 64, 64));
    
    // GDI+ 그래픽 객체
    Graphics graphics(memDC.m_hDC);
    graphics.SetInterpolationMode(InterpolationModeHighQualityBicubic);
    graphics.SetSmoothingMode(SmoothingModeAntiAlias);
    
    // 이미지 크기
    int imgWidth = m_pImage->GetWidth();
    int imgHeight = m_pImage->GetHeight();
    
    // 줌 적용 크기
    int scaledWidth = (int)(imgWidth * m_fZoom);
    int scaledHeight = (int)(imgHeight * m_fZoom);
    
    // 중앙 정렬 위치
    int x = (rect.Width() - scaledWidth) / 2 + m_ptOffset.x;
    int y = (rect.Height() - scaledHeight) / 2 + m_ptOffset.y;
    
    // 회전 변환
    if (m_fRotation != 0.0f) {
        PointF center(rect.Width() / 2.0f, rect.Height() / 2.0f);
        graphics.TranslateTransform(center.X, center.Y);
        graphics.RotateTransform(m_fRotation);
        graphics.TranslateTransform(-center.X, -center.Y);
    }
    
    // 이미지 그리기
    graphics.DrawImage(m_pImage, x, y, scaledWidth, scaledHeight);
    
    // 화면에 복사
    pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &memDC, 0, 0, SRCCOPY);
    
    memDC.SelectObject(pOldBitmap);
}

void CImageView::ZoomIn()
{
    m_fZoom *= 1.2f;
    if (m_fZoom > 10.0f) m_fZoom = 10.0f;
    Invalidate();
}

void CImageView::ZoomOut()
{
    m_fZoom /= 1.2f;
    if (m_fZoom < 0.1f) m_fZoom = 0.1f;
    Invalidate();
}

void CImageView::ZoomFit()
{
    if (m_pImage == NULL) return;
    
    CRect rect;
    GetClientRect(&rect);
    
    int imgWidth = m_pImage->GetWidth();
    int imgHeight = m_pImage->GetHeight();
    
    float zoomX = (float)rect.Width() / imgWidth * 0.9f;
    float zoomY = (float)rect.Height() / imgHeight * 0.9f;
    
    m_fZoom = min(zoomX, zoomY);
    m_ptOffset = CPoint(0, 0);
    
    Invalidate();
}

void CImageView::Rotate(float angle)
{
    m_fRotation += angle;
    while (m_fRotation >= 360.0f) m_fRotation -= 360.0f;
    while (m_fRotation < 0.0f) m_fRotation += 360.0f;
    Invalidate();
}

void CImageView::ResetView()
{
    m_fZoom = 1.0f;
    m_fRotation = 0.0f;
    m_ptOffset = CPoint(0, 0);
}

BOOL CImageView::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
    if (zDelta > 0) {
        ZoomIn();
    } else {
        ZoomOut();
    }
    
    return CScrollView::OnMouseWheel(nFlags, zDelta, pt);
}

void CImageView::OnLButtonDown(UINT nFlags, CPoint point)
{
    m_bDragging = TRUE;
    m_ptDragStart = point;
    SetCapture();
    
    CScrollView::OnLButtonDown(nFlags, point);
}

void CImageView::OnLButtonUp(UINT nFlags, CPoint point)
{
    m_bDragging = FALSE;
    ReleaseCapture();
    
    CScrollView::OnLButtonUp(nFlags, point);
}

void CImageView::OnMouseMove(UINT nFlags, CPoint point)
{
    if (m_bDragging) {
        CPoint delta = point - m_ptDragStart;
        m_ptOffset += delta;
        m_ptDragStart = point;
        Invalidate();
    }
    
    CScrollView::OnMouseMove(nFlags, point);
}

3. ThumbnailView 클래스 (썸네일 목록)

// ThumbnailView.h
class CThumbnailView : public CListCtrl
{
private:
    CImageList m_imageList;
    CString m_strCurrentFolder;
    
public:
    CThumbnailView();
    
    void LoadFolder(const CString& folderPath);
    CString GetSelectedImagePath();
    
protected:
    DECLARE_MESSAGE_MAP()
    
    afx_msg void OnNMDblclk(NMHDR* pNMHDR, LRESULT* pResult);
};

// ThumbnailView.cpp
CThumbnailView::CThumbnailView()
{
}

void CThumbnailView::LoadFolder(const CString& folderPath)
{
    m_strCurrentFolder = folderPath;
    
    DeleteAllItems();
    
    if (m_imageList.GetSafeHandle()) {
        m_imageList.DeleteImageList();
    }
    
    m_imageList.Create(128, 128, ILC_COLOR32 | ILC_MASK, 0, 10);
    SetImageList(&m_imageList, LVSIL_NORMAL);
    
    // 이미지 파일 검색
    CFileFind finder;
    CString searchPath = folderPath + _T("\\*.*");
    
    BOOL found = finder.FindFile(searchPath);
    
    int index = 0;
    while (found) {
        found = finder.FindNextFile();
        
        if (finder.IsDots() || finder.IsDirectory()) {
            continue;
        }
        
        CString ext = finder.GetFileName().Right(4);
        ext.MakeLower();
        
        if (ext == _T(".jpg") || ext == _T(".jpeg") || 
            ext == _T(".png") || ext == _T(".bmp") || ext == _T(".gif")) {
            
            CString filePath = finder.GetFilePath();
            
            // 썸네일 생성
            Image* pImage = Image::FromFile(filePath);
            if (pImage && pImage->GetLastStatus() == Ok) {
                Image* pThumb = pImage->GetThumbnailImage(128, 128);
                
                if (pThumb) {
                    HBITMAP hBitmap;
                    pThumb->GetHBITMAP(Color(255, 255, 255), &hBitmap);
                    
                    CBitmap bitmap;
                    bitmap.Attach(hBitmap);
                    
                    m_imageList.Add(&bitmap, RGB(255, 255, 255));
                    
                    delete pThumb;
                }
                
                delete pImage;
            }
            
            // 리스트 항목 추가
            InsertItem(index, finder.GetFileName(), index);
            SetItemData(index, (DWORD_PTR)new CString(filePath));
            
            index++;
        }
    }
    
    finder.Close();
}

CString CThumbnailView::GetSelectedImagePath()
{
    POSITION pos = GetFirstSelectedItemPosition();
    if (pos == NULL) {
        return _T("");
    }
    
    int index = GetNextSelectedItem(pos);
    CString* pPath = (CString*)GetItemData(index);
    
    return *pPath;
}

BEGIN_MESSAGE_MAP(CThumbnailView, CListCtrl)
    ON_NOTIFY_REFLECT(NM_DBLCLK, &CThumbnailView::OnNMDblclk)
END_MESSAGE_MAP()

void CThumbnailView::OnNMDblclk(NMHDR* pNMHDR, LRESULT* pResult)
{
    CString imagePath = GetSelectedImagePath();
    if (!imagePath.IsEmpty()) {
        // 메인 프레임에 알림
        GetParent()->SendMessage(WM_COMMAND, ID_VIEW_IMAGE, (LPARAM)(LPCTSTR)imagePath);
    }
    
    *pResult = 0;
}

4. ImageProcessor (이미지 처리)

// ImageProcessor.h
class CImageProcessor
{
public:
    static Image* AdjustBrightness(Image* pImage, int delta);
    static Image* AdjustContrast(Image* pImage, float factor);
    static Image* ToGrayscale(Image* pImage);
    static Image* Rotate90(Image* pImage);
    static Image* FlipHorizontal(Image* pImage);
};

// ImageProcessor.cpp
Image* CImageProcessor::AdjustBrightness(Image* pImage, int delta)
{
    if (pImage == NULL) return NULL;
    
    int width = pImage->GetWidth();
    int height = pImage->GetHeight();
    
    Bitmap* pBitmap = new Bitmap(width, height, PixelFormat32bppARGB);
    Graphics graphics(pBitmap);
    
    ColorMatrix colorMatrix = {
        1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
        0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f, 0.0f,
        delta / 255.0f, delta / 255.0f, delta / 255.0f, 0.0f, 1.0f
    };
    
    ImageAttributes imageAttr;
    imageAttr.SetColorMatrix(&colorMatrix);
    
    graphics.DrawImage(pImage, 
        Rect(0, 0, width, height),
        0, 0, width, height,
        UnitPixel, &imageAttr);
    
    return pBitmap;
}

Image* CImageProcessor::ToGrayscale(Image* pImage)
{
    if (pImage == NULL) return NULL;
    
    int width = pImage->GetWidth();
    int height = pImage->GetHeight();
    
    Bitmap* pBitmap = new Bitmap(width, height, PixelFormat32bppARGB);
    Graphics graphics(pBitmap);
    
    ColorMatrix colorMatrix = {
        0.299f, 0.299f, 0.299f, 0.0f, 0.0f,
        0.587f, 0.587f, 0.587f, 0.0f, 0.0f,
        0.114f, 0.114f, 0.114f, 0.0f, 0.0f,
        0.0f,   0.0f,   0.0f,   1.0f, 0.0f,
        0.0f,   0.0f,   0.0f,   0.0f, 1.0f
    };
    
    ImageAttributes imageAttr;
    imageAttr.SetColorMatrix(&colorMatrix);
    
    graphics.DrawImage(pImage,
        Rect(0, 0, width, height),
        0, 0, width, height,
        UnitPixel, &imageAttr);
    
    return pBitmap;
}

Image* CImageProcessor::Rotate90(Image* pImage)
{
    if (pImage == NULL) return NULL;
    
    int width = pImage->GetWidth();
    int height = pImage->GetHeight();
    
    Bitmap* pBitmap = new Bitmap(height, width, PixelFormat32bppARGB);
    Graphics graphics(pBitmap);
    
    graphics.TranslateTransform(height / 2.0f, width / 2.0f);
    graphics.RotateTransform(90.0f);
    graphics.TranslateTransform(-width / 2.0f, -height / 2.0f);
    
    graphics.DrawImage(pImage, 0, 0, width, height);
    
    return pBitmap;
}

5. MainFrm (메인 프레임)

// MainFrm.h
class CMainFrame : public CFrameWnd
{
protected:
    CSplitterWnd m_wndSplitter;
    CImageView* m_pImageView;
    CThumbnailView* m_pThumbnailView;
    CToolBar m_wndToolBar;
    CStatusBar m_wndStatusBar;
    
public:
    CMainFrame();
    
protected:
    virtual BOOL OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext);
    
    DECLARE_MESSAGE_MAP()
    
    afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
    afx_msg void OnFileOpen();
    afx_msg void OnViewZoomIn();
    afx_msg void OnViewZoomOut();
    afx_msg void OnViewZoomFit();
    afx_msg void OnViewRotate();
    afx_msg void OnEditBrightness();
    afx_msg void OnEditGrayscale();
};

// MainFrm.cpp
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
    // 스플리터 생성 (좌우 분할)
    if (!m_wndSplitter.CreateStatic(this, 1, 2)) {
        return FALSE;
    }
    
    // 왼쪽: 썸네일
    if (!m_wndSplitter.CreateView(0, 0, RUNTIME_CLASS(CThumbnailView),
        CSize(200, 0), pContext)) {
        return FALSE;
    }
    
    // 오른쪽: 이미지 뷰
    if (!m_wndSplitter.CreateView(0, 1, RUNTIME_CLASS(CImageView),
        CSize(0, 0), pContext)) {
        return FALSE;
    }
    
    m_pThumbnailView = (CThumbnailView*)m_wndSplitter.GetPane(0, 0);
    m_pImageView = (CImageView*)m_wndSplitter.GetPane(0, 1);
    
    return TRUE;
}

void CMainFrame::OnFileOpen()
{
    CFileDialog dlg(TRUE, NULL, NULL, 
        OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST,
        _T("Image Files (*.jpg;*.png;*.bmp;*.gif)|*.jpg;*.jpeg;*.png;*.bmp;*.gif|All Files (*.*)|*.*||"));
    
    if (dlg.DoModal() == IDOK) {
        CString filePath = dlg.GetPathName();
        m_pImageView->LoadImage(filePath);
        
        // 폴더 썸네일 로드
        CString folder = filePath.Left(filePath.ReverseFind(_T('\\')));
        m_pThumbnailView->LoadFolder(folder);
    }
}

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • MFC GDI+ | CDC·CBitmap·그래픽 처리 완벽 가이드
  • MFC 기초 | Microsoft Foundation Class 시작 가이드
  • C++ 메모리 관리 완벽 가이드 | RAII·스마트 포인터

이 글이 도움이 되셨나요? MFC와 GDI+로 완전한 이미지 뷰어를 만드는 데 도움이 되었기를 바랍니다!

Windows API와 MFC 시리즈를 마칩니다. 🎉