Cadre de fenêtre personnalisé à l’aide de DWM
Cette rubrique montre comment utiliser les API Desktop Window Manager (DWM) pour créer des cadres de fenêtre personnalisés pour votre application.
- Introduction
- Extension de l’image cliente
- Suppression du cadre standard
- Dessin dans la fenêtre Cadre étendu
- Activation du test d’accès pour l’image personnalisée
- Annexe A : Exemple de procédure de fenêtre
- Annexe B : Peinture du titre de la légende
- Annexe C : Fonction HitTestNCA
- Rubriques connexes
Dans Windows Vista et versions ultérieures, l’apparence des zones non clientes des fenêtres d’application (barre de titre, icône, bordure de fenêtre et boutons légende) est contrôlée par le DWM. À l’aide des API DWM, vous pouvez modifier la façon dont le DWM affiche le cadre d’une fenêtre.
L’une des fonctionnalités des API DWM est la possibilité d’étendre le frame d’application dans la zone cliente. Cela vous permet d’intégrer un élément d’interface utilisateur client, tel qu’une barre d’outils, dans le cadre, ce qui donne aux contrôles d’interface utilisateur une place plus importante dans l’interface utilisateur de l’application. Par exemple, Windows Internet Explorer 7 sur Windows Vista intègre la barre de navigation dans le cadre de la fenêtre en étendant le haut du cadre, comme illustré dans la capture d’écran suivante.
La possibilité d’étendre le cadre de fenêtre vous permet également de créer des images personnalisées tout en conservant l’apparence de la fenêtre. Par exemple, Microsoft Office Word 2007 dessine le bouton Office et la barre d’outils Accès rapide à l’intérieur du cadre personnalisé tout en fournissant les boutons standard Réduire, Agrandir et Fermer légende, comme illustré dans la capture d’écran suivante.
La fonctionnalité permettant d’étendre le frame dans la zone cliente est exposée par la fonction DwmExtendFrameIntoClientArea . Pour étendre l’image, transmettez le handle de la fenêtre cible avec les valeurs de marge incrustés à DwmExtendFrameIntoClientArea. Les valeurs de l’incruste de marge déterminent la distance à laquelle étendre le cadre sur les quatre côtés de la fenêtre.
Le code suivant illustre l’utilisation de DwmExtendFrameIntoClientArea pour étendre le frame.
// 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;
}
Notez que l’extension de cadre est effectuée dans le message WM_ACTIVATE plutôt que dans le message WM_CREATE . Cela garantit que l’extension de frame est gérée correctement lorsque la fenêtre est à sa taille par défaut et lorsqu’elle est agrandie.
L’image suivante montre un cadre de fenêtre standard (à gauche) et le même cadre de fenêtre étendu (à droite). Le cadre est étendu à l’aide de l’exemple de code précédent et de l’arrière-plan WNDCLASS/WNDCLASSEX Microsoft Visual Studio par défaut (COLOR_WINDOW +1).
La différence visuelle entre ces deux fenêtres est très subtile. La seule différence entre les deux est que la bordure de ligne noire mince de la région cliente dans la fenêtre de gauche est manquante dans la fenêtre à droite. La raison de cette bordure manquante est qu’elle est incorporée dans le cadre étendu, mais pas le reste de la zone cliente. Pour que les images étendues soient visibles, les régions sous-jacentes de chaque côté de l’image étendue doivent avoir des données de pixels avec une valeur alpha de 0. La bordure noire autour de la région cliente contient des données de pixels dans lesquelles toutes les valeurs de couleur (rouge, vert, bleu et alpha) sont définies sur 0. Le reste de l’arrière-plan n’ayant pas la valeur alpha définie sur 0, le reste de l’image étendue n’est pas visible.
Le moyen le plus simple de s’assurer que les cadres étendus sont visibles consiste à peindre l’ensemble de la région cliente en noir. Pour ce faire, initialisez le membre hbrBackground de votre structure WNDCLASS ou WNDCLASSEX sur le handle du stock BLACK_BRUSH. L’image suivante montre les mêmes images standard (à gauche) et étendues (à droite) précédemment. Cette fois,cependant, hbrBackground est défini sur le BLACK_BRUSH handle obtenu à partir de la fonction GetStockObject .
Après avoir étendu le cadre de votre application et l’avoir rendu visible, vous pouvez supprimer le cadre standard. La suppression du cadre standard vous permet de contrôler la largeur de chaque côté du cadre plutôt que d’étendre simplement le cadre standard.
Pour supprimer le cadre de fenêtre standard, vous devez gérer le message WM_NCCALCSIZE , en particulier lorsque sa valeur wParam est TRUE et que la valeur de retour est 0. Ce faisant, votre application utilise l’ensemble de la région de la fenêtre comme zone cliente, en supprimant le cadre standard.
Les résultats de la gestion du message WM_NCCALCSIZE ne sont pas visibles tant que la région cliente n’a pas besoin d’être redimensionnée. Jusqu’à cette date, la vue initiale de la fenêtre s’affiche avec le cadre standard et les bordures étendues. Pour résoudre ce problème, vous devez redimensionner votre fenêtre ou effectuer une action qui lance un message WM_NCCALCSIZE au moment de la création de la fenêtre. Pour ce faire, utilisez la fonction SetWindowPos pour déplacer votre fenêtre et la redimensionner. Le code suivant illustre un appel à SetWindowPos qui force l’envoi d’un message WM_NCCALCSIZE à l’aide des attributs rectangles de fenêtre actuels et de l’indicateur 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;
}
L’image suivante montre le cadre standard (à gauche) et l’image nouvellement étendue sans le cadre standard (à droite).
En supprimant le cadre standard, vous perdez le dessin automatique de l’icône et du titre de l’application. Pour les ajouter à votre application, vous devez les dessiner vous-même. Pour ce faire, examinez d’abord la modification qui s’est produite à votre zone cliente.
Avec la suppression du cadre standard, votre zone client se compose désormais de la fenêtre entière, y compris le cadre étendu. Cela inclut la région où les boutons légende sont dessinés. Dans la comparaison côte à côte suivante, la zone cliente de l’image standard et de l’image étendue personnalisée est mise en surbrillance en rouge. La zone cliente de la fenêtre d’image standard (à gauche) est la région noire. Dans la fenêtre d’image étendue (à droite), la zone cliente correspond à la fenêtre entière.
Étant donné que la fenêtre entière est votre zone cliente, vous pouvez simplement dessiner ce que vous voulez dans le cadre étendu. Pour ajouter un titre à votre application, il vous suffit de dessiner du texte dans la région appropriée. L’image suivante montre le texte de thème dessiné sur le cadre de légende personnalisé. Le titre est dessiné à l’aide de la fonction DrawThemeTextEx . Pour afficher le code qui peint le titre, consultez Annexe B : Peindre le titre de la légende.
Notes
Lorsque vous dessinez dans votre cadre personnalisé, soyez prudent lorsque vous placez des contrôles d’interface utilisateur. Étant donné que la fenêtre entière est votre région cliente, vous devez ajuster l’emplacement de votre contrôle d’interface utilisateur pour chaque largeur de trame si vous ne souhaitez pas qu’elles apparaissent sur ou dans le cadre étendu.
Un effet secondaire de la suppression de l’image standard est la perte du comportement de redimensionnement et de déplacement par défaut. Pour que votre application émule correctement le comportement de la fenêtre standard, vous devez implémenter la logique pour gérer légende test d’accès au bouton et le redimensionnement/déplacement des images.
Pour légende test d’accès au bouton, DWM fournit la fonction DwmDefWindowProc. Pour tester correctement les boutons légende dans les scénarios de trame personnalisée, les messages doivent d’abord être passés à DwmDefWindowProc à des fins de gestion. DwmDefWindowProc retourne TRUE si un message est géré et FALSE s’il ne l’est pas. Si le message n’est pas géré par DwmDefWindowProc, votre application doit gérer le message elle-même ou le transmettre à DefWindowProc.
Pour le redimensionnement et le déplacement des images, votre application doit fournir la logique de test d’accès et gérer les messages de test d’accès aux images. Les messages de test d’accès à l’image vous sont envoyés via le message WM_NCHITTEST , même si votre application crée une image personnalisée sans le cadre standard. Le code suivant illustre la gestion du message WM_NCHITTEST lorsque DwmDefWindowProc ne le gère pas. Pour voir le code de la fonction appelée HitTestNCA
, consultez Annexe C : Fonction 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;
}
}
L’exemple de code suivant illustre une procédure de fenêtre et ses fonctions worker de prise en charge utilisées pour créer une application de frame personnalisée.
//
// 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;
}
Le code suivant montre comment peindre un titre légende sur le cadre étendu. Cette fonction doit être appelée à partir des appels BeginPaint et 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);
}
}
Le code suivant montre la HitTestNCA
fonction utilisée dans Activation du test d’accès pour l’image personnalisée. Cette fonction gère la logique de test d’accès pour le WM_NCHITTEST lorsque DwmDefWindowProc ne gère pas le message.
// 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];
}