DWM을 사용하는 사용자 지정 창 프레임
이 항목에서는 DWM(데스크톱 창 관리자) API를 사용하여 애플리케이션에 대한 사용자 지정 창 프레임을 만드는 방법을 보여 줍니다.
- 소개
- 클라이언트 프레임 확장
- 표준 프레임 제거
- 확장 프레임 창에서 그리기
- 사용자 지정 프레임에 적중 테스트 사용
- 부록 A: 샘플 창 프로시저
- 부록 B: 캡션 제목 그리기
- 부록 C: HitTestNCA 함수
- 관련 항목
Windows Vista 이상에서는 애플리케이션 창의 비 클라이언트 영역(제목 표시줄, 아이콘, 창 테두리 및 캡션 단추)의 모양이 DWM에 의해 제어됩니다. DWM API를 사용하여 DWM이 창의 프레임을 렌더링하는 방식을 변경할 수 있습니다.
DWM API의 한 가지 기능은 애플리케이션 프레임을 클라이언트 영역으로 확장하는 기능입니다. 이렇게 하면 도구 모음과 같은 클라이언트 UI 요소를 프레임에 통합하여 UI 컨트롤이 애플리케이션 UI에서 더 두드러진 위치를 제공할 수 있습니다. 예를 들어 Windows Vista의 Windows Internet Explorer 7은 다음 스크린샷과 같이 프레임의 위쪽을 확장하여 탐색 모음을 창 프레임에 통합합니다.
창 프레임을 확장하는 기능을 사용하면 창의 모양과 느낌을 유지하면서 사용자 지정 프레임을 만들 수도 있습니다. 예를 들어 Microsoft Office Word 2007은 다음 스크린샷과 같이 표준 최소화, 최대화 및 캡션 닫기 단추를 제공하면서 사용자 지정 프레임 내에 Office 단추와 빠른 실행 도구 모음을 그립니다.
프레임을 클라이언트 영역으로 확장하는 기능은 DwmExtendFrameIntoClientArea 함수에 의해 노출됩니다. 프레임을 확장하려면 여백 인셋 값과 함께 대상 창의 핸들을 DwmExtendFrameIntoClientArea에 전달합니다. 여백 삽입 값은 창의 4면에서 프레임을 확장할 범위를 결정합니다.
다음 코드에서는 DwmExtendFrameIntoClientArea 를 사용하여 프레임을 확장하는 방법을 보여 줍니다.
// Handle the window activation.
if (message == WM_ACTIVATE)
{
// Extend the frame into the client area.
MARGINS margins;
margins.cxLeftWidth = LEFTEXTENDWIDTH; // 8
margins.cxRightWidth = RIGHTEXTENDWIDTH; // 8
margins.cyBottomHeight = BOTTOMEXTENDWIDTH; // 20
margins.cyTopHeight = TOPEXTENDWIDTH; // 27
hr = DwmExtendFrameIntoClientArea(hWnd, &margins);
if (!SUCCEEDED(hr))
{
// Handle the error.
}
fCallDWP = true;
lRet = 0;
}
프레임 확장은 WM_CREATE 메시지가 아닌 WM_ACTIVATE메시지 내에서 수행됩니다. 이렇게 하면 창이 기본 크기이고 최대화될 때 프레임 확장이 제대로 처리됩니다.
다음 이미지는 표준 창 프레임(왼쪽)과 동일한 창 프레임 확장(오른쪽)을 보여 줍니다. 프레임은 이전 코드 예제와 기본 Microsoft Visual Studio WNDCLASS WNDCLASSEX/ 배경(COLOR_WINDOW +1)을 사용하여 확장됩니다.
이 두 창의 시각적 차이는 매우 미묘합니다. 둘 사이의 유일한 차이점은 왼쪽 창에 있는 클라이언트 영역의 얇은 검은색 선 테두리가 오른쪽 창에서 누락되었다는 것입니다. 이 테두리가 누락된 이유는 확장된 프레임에 통합되지만 나머지 클라이언트 영역은 통합되지 않기 때문입니다. 확장된 프레임이 표시되려면 각 확장 프레임 측면의 기본 영역에 알파 값이 0인 픽셀 데이터가 있어야 합니다. 클라이언트 영역 주변의 검은색 테두리에는 모든 색 값(빨강, 녹색, 파랑 및 알파)이 0으로 설정된 픽셀 데이터가 있습니다. 나머지 배경에는 알파 값이 0으로 설정되어 있지 않으므로 확장된 프레임의 나머지 부분을 볼 수 없습니다.
확장된 프레임이 표시되도록 하는 가장 쉬운 방법은 전체 클라이언트 영역을 검은색으로 그리는 것입니다. 이렇게 하려면 WNDCLASS 또는 WNDCLASSEX 구조체의 hbrBackground 멤버 를 주식 BLACK_BRUSH 핸들로 초기화합니다. 다음 이미지는 이전에 표시된 것과 동일한 표준 프레임(왼쪽) 및 확장된 프레임(오른쪽)을 보여 줍니다. 그러나 이번에는 hbrBackground 가 GetStockObject 함수에서 가져온 BLACK_BRUSH 핸들로 설정됩니다.
애플리케이션의 프레임을 확장하고 표시한 후에는 표준 프레임을 제거할 수 있습니다. 표준 프레임을 제거하면 단순히 표준 프레임을 확장하는 대신 프레임의 각 면 너비를 제어할 수 있습니다.
표준 창 프레임을 제거하려면 특히 wParam 값이 TRUE이고 반환 값이 0인 경우 WM_NCCALCSIZE 메시지를 처리해야 합니다. 이렇게 하면 애플리케이션은 전체 창 영역을 클라이언트 영역으로 사용하여 표준 프레임을 제거합니다.
WM_NCCALCSIZE 메시지 처리 결과는 클라이언트 영역의 크기를 조정해야 할 때까지 표시되지 않습니다. 그때까지 창의 초기 보기는 표준 프레임과 확장된 테두리로 표시됩니다. 이를 극복하려면 창 크기를 조정하거나 창 생성 시 WM_NCCALCSIZE 메시지를 시작하는 작업을 수행해야 합니다. SetWindowPos 함수를 사용하여 창을 이동하고 크기를 조정하여 이 작업을 수행할 수 있습니다. 다음 코드에서는 현재 창 사각형 특성 및 SWP_FRAMECHANGED 플래그를 사용하여 WM_NCCALCSIZE 메시지를 강제로 보내는 SetWindowPos 호출을 보여 줍니다.
// Handle window creation.
if (message == WM_CREATE)
{
RECT rcClient;
GetWindowRect(hWnd, &rcClient);
// Inform the application of the frame change.
SetWindowPos(hWnd,
NULL,
rcClient.left, rcClient.top,
RECTWIDTH(rcClient), RECTHEIGHT(rcClient),
SWP_FRAMECHANGED);
fCallDWP = true;
lRet = 0;
}
다음 이미지는 표준 프레임(왼쪽)과 표준 프레임(오른쪽)이 없는 새로 확장된 프레임을 보여 줍니다.
표준 프레임을 제거하면 애플리케이션 아이콘과 제목의 자동 그리기를 잃게 됩니다. 애플리케이션에 다시 추가하려면 직접 그려야 합니다. 이렇게 하려면 먼저 클라이언트 영역에 발생한 변경 내용을 확인합니다.
표준 프레임을 제거하면 이제 클라이언트 영역이 확장된 프레임을 포함한 전체 창으로 구성됩니다. 여기에는 캡션 단추가 그려지는 영역이 포함됩니다. 다음 병렬 비교에서는 표준 프레임과 사용자 지정 확장 프레임 모두에 대한 클라이언트 영역이 빨간색으로 강조 표시됩니다. 표준 프레임 창의 클라이언트 영역(왼쪽)은 검은색 영역입니다. 확장된 프레임 창(오른쪽)에서 클라이언트 영역은 전체 창입니다.
전체 창이 클라이언트 영역이므로 확장된 프레임에서 원하는 내용을 그릴 수 있습니다. 애플리케이션에 제목을 추가하려면 해당 지역에 텍스트를 그리기만 하면 됩니다. 다음 이미지는 사용자 지정 캡션 프레임에 그려진 테마 텍스트를 보여 줍니다. DrawThemeTextEx 함수를 사용하여 제목을 그립니다. 제목을 그리는 코드를 보려면 부록 B: 캡션 제목 그리기를 참조하세요.
참고
사용자 지정 프레임에 그릴 때는 UI 컨트롤을 배치할 때 주의해야 합니다. 전체 창이 클라이언트 영역이므로 확장된 프레임에 표시하지 않으려면 각 프레임 너비에 대한 UI 컨트롤 배치를 조정해야 합니다.
표준 프레임을 제거하는 부작용은 기본 크기 조정 및 이동 동작의 손실입니다. 애플리케이션이 표준 창 동작을 제대로 에뮬레이트하려면 캡션 단추 적중 테스트 및 프레임 크기 조정/이동을 처리하는 논리를 구현해야 합니다.
캡션 단추 적중 테스트를 위해 DWM은 DwmDefWindowProc 함수를 제공합니다. 사용자 지정 프레임 시나리오에서 캡션 단추를 제대로 적중하려면 먼저 처리를 위해 메시지를 DwmDefWindowProc에 전달해야 합니다. DwmDefWindowProc 은 메시지가 처리되면 TRUE 를 반환하고, 그렇지 않으면 FALSE 를 반환합니다. 메시지가 DwmDefWindowProc에서 처리되지 않는 경우 애플리케이션은 메시지 자체를 처리하거나 메시지를 DefWindowProc에 전달해야 합니다.
프레임 크기 조정 및 이동의 경우 애플리케이션은 적중 테스트 논리를 제공하고 프레임 적중 테스트 메시지를 처리해야 합니다. 프레임 적중 테스트 메시지는 애플리케이션이 표준 프레임 없이 사용자 지정 프레임을 만드는 경우에도 WM_NCHITTEST 메시지를 통해 전송됩니다. 다음 코드는 DwmDefWindowProc에서 처리하지 않는 경우 WM_NCHITTEST 메시지를 처리하는 방법을 보여 줍니다. 호출 HitTestNCA
된 함수의 코드를 보려면 부록 C: HitTestNCA 함수를 참조하세요.
// Handle hit testing in the NCA if not handled by DwmDefWindowProc.
if ((message == WM_NCHITTEST) && (lRet == 0))
{
lRet = HitTestNCA(hWnd, wParam, lParam);
if (lRet != HTNOWHERE)
{
fCallDWP = false;
}
}
다음 코드 샘플에서는 사용자 지정 프레임 애플리케이션을 만드는 데 사용되는 창 프로시저 및 지원 작업자 함수를 보여 줍니다.
//
// Main WinProc.
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
bool fCallDWP = true;
BOOL fDwmEnabled = FALSE;
LRESULT lRet = 0;
HRESULT hr = S_OK;
// Winproc worker for custom frame issues.
hr = DwmIsCompositionEnabled(&fDwmEnabled);
if (SUCCEEDED(hr))
{
lRet = CustomCaptionProc(hWnd, message, wParam, lParam, &fCallDWP);
}
// Winproc worker for the rest of the application.
if (fCallDWP)
{
lRet = AppWinProc(hWnd, message, wParam, lParam);
}
return lRet;
}
//
// Message handler for handling the custom caption messages.
//
LRESULT CustomCaptionProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, bool* pfCallDWP)
{
LRESULT lRet = 0;
HRESULT hr = S_OK;
bool fCallDWP = true; // Pass on to DefWindowProc?
fCallDWP = !DwmDefWindowProc(hWnd, message, wParam, lParam, &lRet);
// Handle window creation.
if (message == WM_CREATE)
{
RECT rcClient;
GetWindowRect(hWnd, &rcClient);
// Inform application of the frame change.
SetWindowPos(hWnd,
NULL,
rcClient.left, rcClient.top,
RECTWIDTH(rcClient), RECTHEIGHT(rcClient),
SWP_FRAMECHANGED);
fCallDWP = true;
lRet = 0;
}
// Handle window activation.
if (message == WM_ACTIVATE)
{
// Extend the frame into the client area.
MARGINS margins;
margins.cxLeftWidth = LEFTEXTENDWIDTH; // 8
margins.cxRightWidth = RIGHTEXTENDWIDTH; // 8
margins.cyBottomHeight = BOTTOMEXTENDWIDTH; // 20
margins.cyTopHeight = TOPEXTENDWIDTH; // 27
hr = DwmExtendFrameIntoClientArea(hWnd, &margins);
if (!SUCCEEDED(hr))
{
// Handle error.
}
fCallDWP = true;
lRet = 0;
}
if (message == WM_PAINT)
{
HDC hdc;
{
PAINTSTRUCT ps;
hdc = BeginPaint(hWnd, &ps);
PaintCustomCaption(hWnd, hdc);
EndPaint(hWnd, &ps);
}
fCallDWP = true;
lRet = 0;
}
// Handle the non-client size message.
if ((message == WM_NCCALCSIZE) && (wParam == TRUE))
{
// Calculate new NCCALCSIZE_PARAMS based on custom NCA inset.
NCCALCSIZE_PARAMS *pncsp = reinterpret_cast<NCCALCSIZE_PARAMS*>(lParam);
pncsp->rgrc[0].left = pncsp->rgrc[0].left + 0;
pncsp->rgrc[0].top = pncsp->rgrc[0].top + 0;
pncsp->rgrc[0].right = pncsp->rgrc[0].right - 0;
pncsp->rgrc[0].bottom = pncsp->rgrc[0].bottom - 0;
lRet = 0;
// No need to pass the message on to the DefWindowProc.
fCallDWP = false;
}
// Handle hit testing in the NCA if not handled by DwmDefWindowProc.
if ((message == WM_NCHITTEST) && (lRet == 0))
{
lRet = HitTestNCA(hWnd, wParam, lParam);
if (lRet != HTNOWHERE)
{
fCallDWP = false;
}
}
*pfCallDWP = fCallDWP;
return lRet;
}
//
// Message handler for the application.
//
LRESULT AppWinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
HRESULT hr;
LRESULT result = 0;
switch (message)
{
case WM_CREATE:
{}
break;
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
switch (wmId)
{
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_PAINT:
{
hdc = BeginPaint(hWnd, &ps);
PaintCustomCaption(hWnd, hdc);
// Add any drawing code here...
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
다음 코드에서는 확장된 프레임에 캡션 제목을 그리는 방법을 보여 줍니다. 이 함수는 BeginPaint 및 EndPaint 호출 내에서 호출해야 합니다.
// Paint the title on the custom frame.
void PaintCustomCaption(HWND hWnd, HDC hdc)
{
RECT rcClient;
GetClientRect(hWnd, &rcClient);
HTHEME hTheme = OpenThemeData(NULL, L"CompositedWindow::Window");
if (hTheme)
{
HDC hdcPaint = CreateCompatibleDC(hdc);
if (hdcPaint)
{
int cx = RECTWIDTH(rcClient);
int cy = RECTHEIGHT(rcClient);
// Define the BITMAPINFO structure used to draw text.
// Note that biHeight is negative. This is done because
// DrawThemeTextEx() needs the bitmap to be in top-to-bottom
// order.
BITMAPINFO dib = { 0 };
dib.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
dib.bmiHeader.biWidth = cx;
dib.bmiHeader.biHeight = -cy;
dib.bmiHeader.biPlanes = 1;
dib.bmiHeader.biBitCount = BIT_COUNT;
dib.bmiHeader.biCompression = BI_RGB;
HBITMAP hbm = CreateDIBSection(hdc, &dib, DIB_RGB_COLORS, NULL, NULL, 0);
if (hbm)
{
HBITMAP hbmOld = (HBITMAP)SelectObject(hdcPaint, hbm);
// Setup the theme drawing options.
DTTOPTS DttOpts = {sizeof(DTTOPTS)};
DttOpts.dwFlags = DTT_COMPOSITED | DTT_GLOWSIZE;
DttOpts.iGlowSize = 15;
// Select a font.
LOGFONT lgFont;
HFONT hFontOld = NULL;
if (SUCCEEDED(GetThemeSysFont(hTheme, TMT_CAPTIONFONT, &lgFont)))
{
HFONT hFont = CreateFontIndirect(&lgFont);
hFontOld = (HFONT) SelectObject(hdcPaint, hFont);
}
// Draw the title.
RECT rcPaint = rcClient;
rcPaint.top += 8;
rcPaint.right -= 125;
rcPaint.left += 8;
rcPaint.bottom = 50;
DrawThemeTextEx(hTheme,
hdcPaint,
0, 0,
szTitle,
-1,
DT_LEFT | DT_WORD_ELLIPSIS,
&rcPaint,
&DttOpts);
// Blit text to the frame.
BitBlt(hdc, 0, 0, cx, cy, hdcPaint, 0, 0, SRCCOPY);
SelectObject(hdcPaint, hbmOld);
if (hFontOld)
{
SelectObject(hdcPaint, hFontOld);
}
DeleteObject(hbm);
}
DeleteDC(hdcPaint);
}
CloseThemeData(hTheme);
}
}
다음 코드는 사용자 지정 프레임에 대한 적중 테스트 사용에서 사용되는 함수를 보여 HitTestNCA
줍니다. 이 함수는 DwmDefWindowProc에서 메시지를 처리하지 않는 경우 WM_NCHITTEST 대한 적중 테스트 논리를 처리합니다.
// Hit test the frame for resizing and moving.
LRESULT HitTestNCA(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
// Get the point coordinates for the hit test.
POINT ptMouse = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
// Get the window rectangle.
RECT rcWindow;
GetWindowRect(hWnd, &rcWindow);
// Get the frame rectangle, adjusted for the style without a caption.
RECT rcFrame = { 0 };
AdjustWindowRectEx(&rcFrame, WS_OVERLAPPEDWINDOW & ~WS_CAPTION, FALSE, NULL);
// Determine if the hit test is for resizing. Default middle (1,1).
USHORT uRow = 1;
USHORT uCol = 1;
bool fOnResizeBorder = false;
// Determine if the point is at the top or bottom of the window.
if (ptMouse.y >= rcWindow.top && ptMouse.y < rcWindow.top + TOPEXTENDWIDTH)
{
fOnResizeBorder = (ptMouse.y < (rcWindow.top - rcFrame.top));
uRow = 0;
}
else if (ptMouse.y < rcWindow.bottom && ptMouse.y >= rcWindow.bottom - BOTTOMEXTENDWIDTH)
{
uRow = 2;
}
// Determine if the point is at the left or right of the window.
if (ptMouse.x >= rcWindow.left && ptMouse.x < rcWindow.left + LEFTEXTENDWIDTH)
{
uCol = 0; // left side
}
else if (ptMouse.x < rcWindow.right && ptMouse.x >= rcWindow.right - RIGHTEXTENDWIDTH)
{
uCol = 2; // right side
}
// Hit test (HTTOPLEFT, ... HTBOTTOMRIGHT)
LRESULT hitTests[3][3] =
{
{ HTTOPLEFT, fOnResizeBorder ? HTTOP : HTCAPTION, HTTOPRIGHT },
{ HTLEFT, HTNOWHERE, HTRIGHT },
{ HTBOTTOMLEFT, HTBOTTOM, HTBOTTOMRIGHT },
};
return hitTests[uRow][uCol];
}