Настраиваемая рамка окна с использованием DWM
В этом разделе показано, как использовать API диспетчера окон рабочего стола (DWM) для создания пользовательских оконных кадров для приложения.
- Введение
- Расширение клиентского кадра
- Удаление стандартного кадра
- Рисование в окне расширенной рамки
- Включение проверки попадания для пользовательского кадра
- Приложение А. Пример процедуры окна
- Приложение Б. Рисование заголовка
- Приложение В. Функция HitTestNCA
- Связанные темы
В Windows Vista и более поздних версиях внешний вид неклиентских областей окон приложений (заголовок окна, значок, граница окна и кнопки подпись) управляется dwm. С помощью API DWM можно изменить способ отображения кадра окна в DWM.
Одной из функций API DWM является возможность расширения фрейма приложения в клиентную область. Это позволяет интегрировать в фрейм элемент пользовательского интерфейса клиента, например панель инструментов, чтобы элементы управления пользовательского интерфейса занимали более видное место в пользовательском интерфейсе приложения. Например, Windows Internet Обозреватель 7 в Windows Vista интегрирует панель навигации в рамку окна, расширяя верхнюю часть кадра, как показано на следующем снимке экрана.
Возможность расширения рамки окна также позволяет создавать пользовательские фреймы, сохраняя внешний вид окна. Например, Microsoft Office Word 2007 рисует кнопку Office и панель быстрого доступа внутри настраиваемого фрейма, предоставляя стандартные кнопки Свернуть, Развернуть и Закрыть подпись, как показано на следующем снимке экрана.
Функциональные возможности расширения фрейма в клиентской области предоставляются функцией DwmExtendFrameIntoClientArea . Чтобы расширить кадр, передайте дескриптор целевого окна вместе со значениями вставки полей в DwmExtendFrameIntoClientArea. Значения полей определяют, насколько далеко нужно расширить рамку на четырех сторонах окна.
В следующем коде показано использование 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_ACTIVATE сообщении, а не в сообщении WM_CREATE . Это гарантирует правильную обработку расширения кадра, когда окно имеет размер по умолчанию и его развернуто.
На следующем рисунке показана стандартная рамка окна (слева) и та же расширенная рамка окна (справа). Фрейм расширяется с помощью предыдущего примера кода и фона WNDCLASSEX/ microsoft Visual Studio по умолчанию (COLOR_WINDOW +1).
Визуальное различие между этими двумя окнами очень незначительно. Единственное различие между ними заключается в том, что в окне справа отсутствует тонкая черная граница клиентской области в окне слева. Причина отсутствия границы заключается в том, что она включена в расширенный кадр, а остальная часть клиентской области — нет. Чтобы расширенные кадры были видимыми, области, лежащие в основе каждой из сторон расширенного кадра, должны иметь пиксельные данные с альфа-значением 0. Черная граница вокруг клиентской области содержит пиксельные данные, в которых все значения цвета (красный, зеленый, синий и альфа) имеют значение 0. В остальной части фона не задано альфа-значение 0, поэтому остальная часть расширенного кадра не видна.
Самый простой способ обеспечить видимость расширенных кадров — закрасить весь клиентский регион в черный цвет. Для этого инициализируйте элемент hbrBackground структуры WNDCLASS или WNDCLASSEX дескриптором BLACK_BRUSH акций. На следующем рисунке показана та же стандартная рамка (слева) и расширенная рамка (справа), показанная ранее. Однако на этот раз для hbrBackground задается дескриптор BLACK_BRUSH, полученный из функции GetStockObject .
После того как вы расширили кадр приложения и сделали его видимым, можно удалить стандартный кадр. Удаление стандартного кадра позволяет управлять шириной каждой стороны рамки, а не просто расширять стандартный кадр.
Чтобы удалить стандартный фрейм окна, необходимо обработать сообщение WM_NCCALCSIZE , в частности, если его значение wParam имеет значение TRUE , а возвращаемое значение равно 0. Таким образом приложение использует всю область окна в качестве клиентской области, удаляя стандартный кадр.
Результаты обработки сообщения WM_NCCALCSIZE не видны, пока не потребуется изменить размер клиентского региона. До этого времени отображается начальное представление окна со стандартной рамкой и расширенными границами. Чтобы устранить эту проблему, необходимо либо изменить размер окна, либо выполнить действие, которое инициирует WM_NCCALCSIZE сообщение во время создания окна. Это можно сделать с помощью функции SetWindowPos , чтобы переместить окно и изменить его размер. В следующем коде показан вызов Метода SetWindowPos , который принудительно отправляет сообщение WM_NCCALCSIZE с использованием атрибутов текущего прямоугольника окна и флага SWP_FRAMECHANGED.
// 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 . Чтобы просмотреть код, который закрашивает заголовок, см. приложение Б. Рисование заголовка заголовка.
Примечание
При рисовании в пользовательском фрейме будьте внимательны при размещении элементов управления пользовательского интерфейса. Так как все окно является вашей клиентской областью, необходимо настроить размещение элемента управления пользовательского интерфейса для каждой ширины фрейма, если вы не хотите, чтобы они отображались в расширенном кадре или в ней.
Побочным эффектом удаления стандартного кадра является потеря поведения изменения размера и перемещения по умолчанию. Чтобы приложение правильно эмулировало стандартное поведение окна, необходимо реализовать логику для обработки подпись проверки нажатия кнопки и изменения размера или перемещения кадра.
Для проверки нажатия кнопки подпись DWM предоставляет функцию DwmDefWindowProc. Для правильного нажатия кнопки подпись в пользовательских сценариях кадров сообщения должны быть сначала переданы в DwmDefWindowProc для обработки. DwmDefWindowProc возвращает значение TRUE , если сообщение обработано, и FALSE , если сообщение не обработано. Если сообщение не обрабатывается DwmDefWindowProc, приложение должно обработать само сообщение или передать его в DefWindowProc.
Для изменения размера и перемещения кадра приложение должно предоставлять логику проверки попадания и обрабатывать сообщения проверки попадания в кадр. Сообщения проверки попадания в кадр отправляются через WM_NCHITTEST сообщение, даже если приложение создает пользовательский кадр без стандартного кадра. Следующий код демонстрирует обработку сообщения WM_NCHITTEST , если DwmDefWindowProc не обрабатывает его. Код вызываемой 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
при включении проверки попадания для пользовательского кадра. Эта функция обрабатывает логику проверки попадания для WM_NCHITTEST , если DwmDefWindowProc не обрабатывает сообщение.
// 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];
}