Настраиваемый кадр окна с помощью DWM
В этом разделе показано, как использовать API диспетчера окон рабочего стола (DWM) для создания пользовательских кадров окон для приложения.
- введение
- Клиентский кадр: расширение
- удаление стандартного кадра
- Рисование в окне расширенной рамки
- включение тестирования на попадание для пользовательской рамки
- Приложение А. Пример процедуры окна
- Приложение B: Оформление заголовка подписи
- приложение C : функция HitTestNCA
- Связанные темы
Знакомство
В Windows Vista и более поздних версиях внешний вид не клиентских областей окон приложений (строка заголовка, значок, граница окна и кнопки заголовка) управляется DWM. Используя API DWM, вы можете изменить способ отрисовки кадра окна в DWM.
Одной из функций API DWM является возможность расширения кадра приложения в клиентской области. Это позволяет интегрировать компонент пользовательского интерфейса клиента (например, панель инструментов) в структуру, обеспечивая элементам управления пользовательского интерфейса более заметное место в интерфейсе приложения. Например, Windows Internet Explorer 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. Это гарантирует, что расширение фрейма обрабатывается правильно, когда окно находится в своем обычном размере и когда оно развернуто на весь экран.
На следующем рисунке показан стандартный фрейм окна (слева) и та же рамка окна, расширенная (справа). Кадр расширяется с помощью предыдущего примера кода и фона WNDCLASS /WNDCLASSEX по умолчанию (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. Чтобы просмотреть код, который рисует заголовок, см. в приложении B: Рисование заголовка.
Заметка
При рисовании в собственной рамке будьте осторожны при размещении элементов управления пользовательского интерфейса. Поскольку всё окно — это область клиента, необходимо отрегулировать расположение элементов управления интерфейса в зависимости от ширины кадра, если вы не хотите, чтобы они отображались на или в расширенном кадре.
Включение тестирования на попадание для пользовательской рамки
Побочным эффектом удаления стандартного кадра является потеря размера и перемещения по умолчанию. Чтобы приложение правильно эмулировало стандартное поведение окна, необходимо реализовать логику для обработки нажатия кнопки заголовка и изменения размера кадра.
Для проверки нажатия кнопки заголовка 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;
}
}
Приложение A: Пример процедуры окна
В следующем примере кода показана процедура окна и вспомогательные рабочие функции, используемые для создания пользовательского приложения фрейма.
//
// 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;
}
Приложение B. Рисование заголовка текста
В следующем коде показано, как зарисовать заголовок на расширенном кадре. Эта функция должна вызываться в вызовах 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);
}
}
Приложение C. Функция HitTestNCA
В следующем коде показана функция 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];
}
Связанные разделы