MFC기반 화면 캡쳐앱 만들기

이번에 만든 주제는 '화면 캡쳐' (Screen Capture)프로그램 입니다.




물론 더 우수한 화면 캡쳐 프로그램도 이미 많이 있지만, 우리의 목적은 늘 그렇듯이 프로그래밍 공부이죠.

공부를 위한 예제로서, 복잡한 개념은 제외하고 캡쳐와 이미지 저장에 포커스를 맞춰 만들어 보았습니다.

MFC 대화상자 기반의 화면 캡쳐 프로그램의 기능은 아래와 같습니다.

1. 현재 스크린(화면전체) 캡쳐

2. 특정 윈도우만 캡쳐

3. 캡쳐된 이미지를 JPG, BMP 로 저장


그럼 그 설계 과정을 한번 살펴볼까요.

1. 대화상자 프로젝트를 만들어 컨트롤을 배치 합니다.

- 전체화면, 특정윈도우 선택 라디오 버튼
- 저장 옵션 JPG, BMP 라디오 버튼
- 전체화면 캡쳐 시 현재 캡쳐 스크린을 보여줄 픽쳐 컨트롤
- 특정 윈도우 선택 캡쳐 시 모든 윈도우를 표시할 리스트 박스


2. 캡쳐 대상 윈도우 DC 얻기

전체 화면(스크린 윈도우)을 캡쳐하기 위해 윈도우 DC 얻기는 쉽습니다.
GetDC()함수의 전달인자를 NULL로 넣어주면 전체 윈도우 DC를 얻을 수 있습니다.

 HDC hdc = ::GetDC(NULL);

나중에 이 hdc를 이용해 이미지를 복사해 저장만 하면 끝이죠.

특정 윈도우만 캡쳐하기는 이 보다 조금 복잡합니다.

먼저 현재 내 컴퓨터의 모든 윈도우를 가져와야 합니다. 왜냐하면 어떤 특정 윈도우를 캡쳐할지 알 수 없기 때문에 모든 윈도우를 다 표시하고 사용자가 선택하게 만들기 위함입니다.

바로 아래 API 함수를 호출하면 모든 윈도우의 핸들(HWND)을 가져올 수 있습니다.

BOOL EnumWindows(
  WNDENUMPROC lpEnumFunc,
  LPARAM      lParam
);

첫 전달인자는 콜백함수의 포인터 입니다.
두번째 전달인자는 콜백함수에 전달할 값입니다.

따라서 EnumWidnows 함수호출시 사용할 콜백함수가 하나 필요합니다.

아래와 GetWindowList라는 이름으로 함수 선언하였습니다.

static BOOL CALLBACK GetWindowList(HWND hWnd, LPARAM lParam);

static 함수라는 점에 유의할 필요가 있습니다. EnumWindows() 함수가 함수포인터를 통해 GetWindowsList() 함수를 호출 할때(콜백) , 그 함수가 특정 클래스의 객체인지를 알 수 없기 때문에 static 함수로 만들어 사용합니다.

즉, EnumWindows함수를 호출하고 첫 전달인자에 GetWindowList()함수의 포인터를 전달 해주면 모든 윈도우 목록을 얻어올 때까지 GetWindowsList()함수가 계속 호출 될 테고, 이 함수의 첫 전달인자의 HWND를 통해 윈도우 핸들이 들어 오게 됩니다.

이때 해당 윈도우의 핸들을 CWnd형 포인터로 변환해 DC를 얻는 다면, 특정 윈도우도 캡쳐가 가능합니다.

아래는 GetWindowsList()함수 내부에서 HWND를 이용해 CWnd* 형 포인터로 변환하는 부분입니다.

pWnd = CWnd::FromHandle(hWnd);

얻어진 윈도우의 CWnd 포인터는 몇개의 윈도우가 내 컴퓨터에서 생성되어 있을지 알 수 없기 때문에 가변 배열인 vector<CWnd*> pWnds 를 만들어 목록화 시켜 둡니다.

이젠 특정 윈도우의 DC로 얻었으므로, 캡쳐 과정은 전체 윈도우 DC를 얻은 경우와 마찬가지로 DC를 복사해 저장만 하면 끝이죠.

아래는 GetWindowsList() 콜백함수의 정의입니다.

BOOL CCaptureDlg::GetWindowList(HWND hWnd, LPARAM lParam)
{
CWnd* pWnd = NULL;
pWnd = CWnd::FromHandle(hWnd);

if (pWnd)
{
// 보이지 않는 윈도우 캡쳐 안함
if (::IsWindowVisible(hWnd) == FALSE)
return TRUE;
// 윈도우 캡션 문자열 없는 경우 캡쳐 안함
if (::GetWindowTextLength(hWnd) == 0)
return TRUE;
// 자식윈도우(부모가 있다면) 캡쳐 안함
if (::GetParent(hWnd) != 0)
return TRUE;

//벡터에 윈도우 리스트 저장
m_pWnds.push_back(pWnd);

TCHAR name[256] = { 0, };
TCHAR title[256] = { 0, };
::GetClassName(hWnd, name, 256);
::GetWindowText(hWnd, title, 256);

// 리스트 컨트롤에 표시 하기 위해
CString str;
int num = (int)m_strArray.GetCount() + 1;
str.Format(_T("%02d. [%s] [%s]"), num, name, title);

m_strArray.Add(str);
}
else
{
return FALSE;
}

return TRUE;
}

콜백함수의 개념과 윈도우 DC(Device Context) 를 이해하지 못한다면, 어렵게 느껴 질 수도 있습니다.

이제 전체 화면이든, 특정 윈도우든 윈도우 포인터(CWnd*)를 얻었다면 그 다음은 매우 쉽습니다.



3. 윈도우 DC 화면을 파일 저장하기

Gdiplus와 ATL::CImage 사용을 위해 아래 헤더와, 라이브러리 파일이 필요합니다.

#include <gdiplus.h>
#include <atlimage.h>
#pragma comment (lib, "Gdiplus.lib")

이제 대화상자의 파일저장 버튼 이벤트 처리기 함수에서 저장하고자 하는 파일의 경로를 얻고, 저장옵션(jpg, bmp)을 선택한 후 저장만 하면 끝입니다.

아래는 파일의 경로를 얻어오는 함수 코드 입니다. (레퍼런스 타입의 문자열 변수에 경로 저장)



BOOL CCaptureDlg::GetFilePath(CString& str)
{
// 저장 경로 대화상자 설정
CString strFilter, strDefault;
if (m_saveoption == 0)
{
strDefault = _T("jpg file(*.jpg)");
strFilter = _T("jpg file(*.jpg)|*.jpg|");
}
else
{
strDefault = _T("bmp file(*.bmp)");
strFilter = _T("bmp file(*bmp)|*.bmp|");
}

CFileDialog dlg(FALSE,
strDefault,
_T("제목없음"),
OFN_HIDEREADONLY | OFN_FILEMUSTEXIST,
strFilter,
NULL);


if (dlg.DoModal() == IDOK)
{
str = dlg.GetPathName();
}
else
{
return FALSE;
}

return TRUE;
}

이제 경로도 얻어왔다면 저장만 하면 끝입니다.

아래 코드는 ATL::CImage 클래스를 이용해 해당 hdc의 픽셀 비트 수를 구하고, 이미지의 크기(cx, cy)를 구한 후 CImage의 DC에 복사한 후, 해당 경로(strPath)에 캡쳐된 이미지 파일을  저장하는 코드입니다.

파일 저장 버튼 클릭시 수행되는 함수 내부 코드입니다.

// 이미지 클래스 객체 생성
CImage image;

// 현재 화면의 픽셀당 컬러 비트수 구하기
int color_depth = GetDeviceCaps(hdc, BITSPIXEL);

// 크기, 컬러비트 수를 이용한 이미지 생성
image.Create(cx, cy, color_depth, 0);

// 이미지 dc에 화면 dc의 내용을 복사
BitBlt(image.GetDC(), 0, 0, cx, cy, hdc, 0, 0, SRCCOPY);

// 이미지를 jpeg로 저장
if(m_saveoption == 0)
image.Save(strPath, Gdiplus::ImageFormatJPEG);
else
image.Save(strPath, Gdiplus::ImageFormatBMP);

// 윈도우 dc 해제
::ReleaseDC(NULL, hdc);
image.ReleaseDC();


만약 전체 화면 캡쳐라면 이미지의 크기는 모니터의 해상도를 얻으면 됩니다. (듀얼 모니터의 경우는 고려하지 않았습니다.)

int cx = GetSystemMetrics(SM_CXSCREEN);
int cy = GetSystemMetrics(SM_CYSCREEN);

특정 윈도우라면 아래와 같이 얻을 수 있습니다.

CRect rect;
특정윈도우포인터->GetWindowRect(&rect);
int cx = rect.Width();
int cy = rect.Height();


실제 캡쳐를 시도해 보면 일부 캡쳐되지 않는 윈도우가 존재하거나, 화면의 크기가 맞지 않는 윈도우가 존재합니다.

이는 DirectX (윈도우 DC를 거치지 않고 그래픽 카드에서 바로 그림) 또는, 윈도우즈의 버전에 따른 투명(Aero) 기능이 하드웨어 가속을 통해 처리 (DC를 거치지 않고) 되기 때문입니다.

윈도우 DC에 그려지지 않는 화면은 이 프로그램으로 캡쳐되지 않습니다.

다음에는 DirectX 화면(주로 게임화면) 을 캡쳐하는 프로그램도 한번 만들어 보겠습니다.

감사합니다.






  • 개발환경 : Microsoft Visual Studio 2015, Windows 10 Pro 64bit
  • 개발언어 : C++, MFC
  • 핵심 적용 기술, 환경 
  1. Device Context, GDI, GDIplus
  2. Dialog 기반
  3. 콜백함수 
  • 첨부파일 
  1. 설치파일 (Installshield로 제작된 설치파일, 클릭) 
  2. 소스코드 (C++ 소스코드, rar 압축파일형태, 클릭)
  3. 구글아이디로 로그인하면 다운로드 가능합니다.

댓글

  1. strPath가 선언되지 않는 식별자라고 하는데 GetFilePath에서 얻은 경로정보를 strPath에 어디서 적용시켜야 하나요?

    답글삭제
  2. 좀 더 질문이 구체적이면 좋겠습니다. 예를 들면 어떤 파일의 몇번 라인에서 이렇게 하니 오류가 발생 한다든지...

    모든 예제 게시물은 코드의 실행을 확인하고 업로드 하므로, 질문자의 코드 수정에 의해 오류가 발생한 것으로 추측되는데 그 상황을 모르면 답변이 어렵습니다.

    답글삭제

댓글 쓰기

이 블로그의 인기 게시물

Qt Designer 설치하기

PyQt5 기반 동영상 플레이어앱 만들기