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 시리즈를 마칩니다. 🎉