Marco de ventana personalizado mediante DWM
En este tema se muestra cómo usar las API del Administrador de ventanas de escritorio (DWM) para crear marcos de ventana personalizados para la aplicación.
- Introducción
- Extensión del marco de cliente
- Quitar el marco estándar
- Dibujar en la ventana Marco extendido
- Habilitación de pruebas de posicionamiento para el marco personalizado
- Apéndice A: Procedimiento de ventana de ejemplo
- Apéndice B: Pintar el título del título
- Apéndice C: Función HitTestNCA
- Temas relacionados
Introducción
En Windows Vista y versiones posteriores, la apariencia de las áreas que no son cliente de las ventanas de la aplicación (la barra de título, el icono, el borde de la ventana y los botones de subtítulo) se controla mediante DWM. Con las API de DWM, puede cambiar la forma en que DWM representa el marco de una ventana.
Una característica de las API de DWM es la capacidad de ampliar el marco de la aplicación al área cliente. Esto le permite integrar un elemento de interfaz de usuario cliente (como una barra de herramientas) en el marco, lo que proporciona a los controles de interfaz de usuario un lugar más destacado en la interfaz de usuario de la aplicación. Por ejemplo, Windows Internet Explorer 7 en Windows Vista integra la barra de navegación en el marco de ventana extendiendo la parte superior del marco, como se muestra en la siguiente captura de pantalla.
La capacidad de ampliar el marco de ventana también le permite crear marcos personalizados mientras mantiene la apariencia y la apariencia de la ventana. Por ejemplo, Microsoft Office Word 2007 dibuja el botón de Office y la barra de herramientas acceso rápido dentro del marco personalizado al proporcionar los botones estándar Minimizar, Maximizar y Cerrar subtítulo, como se muestra en la siguiente captura de pantalla.
Extensión del marco de cliente
La función DwmExtendFrameIntoClientArea expone la funcionalidad para extender el marco al área cliente. Para extender el marco, pase el identificador de la ventana de destino junto con los valores de conjunto de margen a DwmExtendFrameIntoClientArea. Los valores de conjunto de margen determinan hasta dónde extender el marco en los cuatro lados de la ventana.
En el código siguiente se muestra el uso de DwmExtendFrameIntoClientArea para extender el marco.
// 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;
}
Tenga en cuenta que la extensión de marco se realiza dentro del mensaje WM_ACTIVATE en lugar del mensaje WM_CREATE . Esto garantiza que la extensión de marco se controla correctamente cuando la ventana tiene su tamaño predeterminado y cuando se maximiza.
En la imagen siguiente se muestra un marco de ventana estándar (a la izquierda) y el mismo marco de ventana extendido (a la derecha). El marco se extiende mediante el ejemplo de código anterior y el fondo WNDCLASS WNDCLASSEX/ predeterminado de Microsoft Visual Studio (COLOR_WINDOW +1).
La diferencia visual entre estas dos ventanas es muy sutil. La única diferencia entre los dos es que falta el borde de línea negra fina de la región de cliente en la ventana de la izquierda de la ventana de la derecha. El motivo de esta falta de borde es que se incorpora al marco extendido, pero el resto del área de cliente no lo es. Para que los fotogramas extendidos sean visibles, las regiones subyacentes a cada uno de los lados del marco extendido deben tener datos de píxeles con un valor alfa de 0. El borde negro alrededor de la región del cliente tiene datos de píxeles en los que todos los valores de color (rojo, verde, azul y alfa) se establecen en 0. El resto del fondo no tiene el valor alfa establecido en 0, por lo que el resto del marco extendido no es visible.
La manera más fácil de asegurarse de que los fotogramas extendidos son visibles es pintar toda la región cliente en negro. Para ello, inicialice el miembro hbrBackground de la estructura WNDCLASS o WNDCLASSEX para el identificador de la BLACK_BRUSH de existencias. En la imagen siguiente se muestra el mismo marco estándar (izquierda) y marco extendido (derecha) mostrado anteriormente. Sin embargo, esta vez hbrBackground se establece en el identificador de BLACK_BRUSH obtenido de la función GetStockObject .
Quitar el marco estándar
Después de extender el marco de la aplicación y hacerlo visible, puede quitar el marco estándar. Quitar el marco estándar le permite controlar el ancho de cada lado del marco en lugar de simplemente extender el marco estándar.
Para quitar el marco de ventana estándar, debe controlar el mensaje de WM_NCCALCSIZE , específicamente cuando su valor wParam es TRUE y el valor devuelto es 0. Al hacerlo, la aplicación usa toda la región de ventana como área cliente, quitando el marco estándar.
Los resultados de controlar el mensaje de WM_NCCALCSIZE no son visibles hasta que se debe cambiar el tamaño de la región del cliente. Hasta ese momento, la vista inicial de la ventana aparece con el marco estándar y los bordes extendidos. Para superar esto, debe cambiar el tamaño de la ventana o realizar una acción que inicie un mensaje de WM_NCCALCSIZE en el momento de la creación de la ventana. Esto se puede lograr mediante la función SetWindowPos para mover la ventana y cambiar su tamaño. En el código siguiente se muestra una llamada a SetWindowPos que obliga a enviar un mensaje de WM_NCCALCSIZE mediante los atributos de rectángulo de ventana actuales y la marca 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;
}
En la imagen siguiente se muestra el marco estándar (izquierda) y el marco recién extendido sin el marco estándar (derecha).
Dibujar en la ventana Marco extendido
Al quitar el marco estándar, se pierde el dibujo automático del icono y el título de la aplicación. Para volver a agregarlas a la aplicación, debe dibujarlas usted mismo. Para ello, examine primero el cambio que se ha producido en el área de cliente.
Con la eliminación del marco estándar, el área de cliente ahora consta de toda la ventana, incluido el marco extendido. Esto incluye la región donde se dibujan los botones de subtítulo. En la siguiente comparación en paralelo, el área de cliente para el marco estándar y el marco extendido personalizado se resaltan en rojo. El área cliente de la ventana de marco estándar (izquierda) es la región negra. En la ventana de marco extendida (derecha), el área cliente es toda la ventana.
Dado que toda la ventana es el área de cliente, simplemente puede dibujar lo que desea en el marco extendido. Para agregar un título a la aplicación, basta con dibujar texto en la región adecuada. En la imagen siguiente se muestra el texto con temas dibujado en el marco de subtítulo personalizado. El título se dibuja mediante la función DrawThemeTextEx . Para ver el código que pinta el título, vea apéndice B: Pintar el título del título.
Nota
Al dibujar en el marco personalizado, tenga cuidado al colocar controles de interfaz de usuario. Dado que toda la ventana es la región de cliente, debe ajustar la ubicación del control de la interfaz de usuario para cada ancho de marco si no desea que aparezcan en o en el marco extendido.
Habilitación de pruebas de posicionamiento para el marco personalizado
Un efecto secundario de quitar el marco estándar es la pérdida del cambio de tamaño predeterminado y el comportamiento de movimiento. Para que la aplicación emule correctamente el comportamiento estándar de la ventana, deberá implementar lógica para controlar subtítulo pruebas de posicionamiento de botones y cambio de tamaño o movimiento de fotogramas.
Para subtítulo botón de prueba de posicionamiento, DWM proporciona la función DwmDefWindowProc. Para realizar correctamente la prueba de posicionamiento de los botones de subtítulo en escenarios de fotogramas personalizados, los mensajes deben pasarse primero a DwmDefWindowProc para su control. DwmDefWindowProc devuelve TRUE si se controla un mensaje y FALSE si no lo está. Si DwmDefWindowProc no controla el mensaje, la aplicación debe controlar el propio mensaje o pasar el mensaje a DefWindowProc.
Para cambiar y mover fotogramas, la aplicación debe proporcionar la lógica de prueba de posicionamiento y controlar los mensajes de prueba de posicionamiento de fotogramas. Los mensajes de prueba de posicionamiento de fotogramas se envían a través del mensaje de WM_NCHITTEST , incluso si la aplicación crea un marco personalizado sin el marco estándar. En el código siguiente se muestra cómo controlar el mensaje de WM_NCHITTEST cuando DwmDefWindowProc no lo controla. Para ver el código de la función llamada HitTestNCA
, vea Apéndice C: Función 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;
}
}
Apéndice A: Procedimiento de ventana de ejemplo
En el ejemplo de código siguiente se muestra un procedimiento de ventana y sus funciones de trabajo auxiliares que se usan para crear una aplicación de marco personalizada.
//
// 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;
}
Apéndice B: Pintar el título del título
En el código siguiente se muestra cómo pintar un título de subtítulo en el marco extendido. Se debe llamar a esta función desde las llamadas BeginPaint y 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);
}
}
Apéndice C: Función HitTestNCA
En el código siguiente se muestra la HitTestNCA
función usada en Habilitar pruebas de posicionamiento para el marco personalizado. Esta función controla la lógica de prueba de posicionamiento de la WM_NCHITTEST cuando DwmDefWindowProc no controla el mensaje.
// 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];
}
Temas relacionados
-
Desktop Window Manager Overview (Administrador de ventanas de escritorio)