Desktop Window Manager
本文翻译自 MSDN
从 Vista 开始引进的 Desktop composition feature 从根本上改变了程序在屏幕上绘制像素的方式。当打开 desktop composition 时,单个窗口不再像以前那样直接绘制到屏幕上,而是先将绘制操作重定向到 video memory 的off-screen suface 上,然后再渲染到桌面屏幕上。
Desktop composition 由 Desktop Window Manager(DWM) 执行。通过 desktop composition,DWM 能够实现多种界面效果,如 窗口玻璃效果、3D 窗口动画、窗口 3D 翻转、高 DPI 支持等。
Desktop Window Manager 以 Windows 服务的方式运行。它可以通过控制面板中的服务管理器来启用或禁用。
程序可以通过 DWM API 来控制许多 DWM 特性。以下章节描述了 DWM 特性及其对应的 API。
- DWM Overviews
- DWM Sample Code
- DWM Reference
DWM Overviews
本章节分为以下几个小节:
- Enable and Control DWM Composition DWM 提供了几组函数来设置和查询 DWM 的基本信息。这些 API 可以让你查询和修改 composition 状态。另外,你还可以对不同的 DWM 窗口属性设定和查询对应的 渲染策略。
- DWM Blur Behind Overview DWM 的一个特性是给窗口的非客户区设定半透明和模糊效果。DWM API 能够让程序将这些效果设定到顶层窗口的客户区上。
- DWM Thumbnail Overview DWM 能够显示程序的缩略图。这个缩略图不是窗口的一个静态截图,而是动态的,它能够将缩略图窗口和目标窗口连接起来,渲染出一个即时的缩略图。这可以实现鼠标 hover 到任务栏或者使用 ALT-TAB 进行切换的时候,显示速览图的功能。
- Accessing and Controlling DWM Frame Data 讨论用于调度和多媒体呈现的 DWM API。
- Performance Considerations and Best Practices 关于 DWM API 的最佳实践。
- Custom Window Frame Using DWM 说明如何使用 DWM API 来创建 custom window frame。
Enable and Control DWM Composition
DWM 提供了几组函数来设置和查询 DWM 的基本信息。这些 API 可以让你查询和修改 composition 状态。另外,你还可以对不同的 DWM 窗口属性设定和查询对应的 渲染策略。
本章节分为以下几个小节:
- Disabling DWM Composition 禁用 DWM Composition
- Retrieving the Colorization Information 获取 Colorization 信息
- Controlling Non-Client Region Rendering 控制非客户区渲染
- Messaging 消息
Disabling DWM Composition
注意 在 Windows8 以后,本章节的内容不再有效。 DWM 不再能够被程序禁用,程序也无法直接在主屏幕上进行绘制操作。以下信息仅适用于 Window7 或者更久的版本。
因为 DWM 需要用到 GPU 来运行,某些程序可能会因为兼容性的问题而必须禁用 DWM。某些程序,例如运行在全屏模式的游戏程序,需要判断 DWM 是否打开,并禁用它。要完成这种功能,可以使用 DwmIsCompositionEnabled 和 DwmEnableComposition 函数。
调用 DwmEnableComposition 时给 fEnable 传入 DWM_EC_DISABLECOMPOSITION 就能禁用 DWM,直到程序退出或者重新调用 DwmEnableComposition 并传入 DWM_EC_ENABLECOMPOSITION 时 DWM 才会再次启用。
注意 当程序试图直接在主屏幕上进行绘制时,DWM 将会自动禁用。程序解除对主屏幕的占用后,DWM 将自动恢复。
Retrieving the Colorization Information
非客户区的颜色由系统颜色主体来控制。程序可以通过 DWM API 拿到色彩值,从而让程序适配系统颜色主题。
要访问色彩值并监听色彩变化,可以使用 DwmGetColorizationColor 函数和 WM_DWMCOLORIZATIONCOLORCHANGED 消息。
以下代码示例展示了如何监听色彩变化并访问最新色彩:
...
case WM_DWMCOLORIZATIONCOLORCHANGED:
{
g_currColor = (DWORD)wParam;
g_opacityblend = (BOOL)lParam;
}
break;
...
Controlling Non-Client Region Rendering
DWM 提供了两个可视效果,非客户区的半透明效果 和 过渡效果。一些程序可能会因为兼容性的原因而需要去禁用或重启这些效果。以下两个函数可以管理透明度和过度效果:
- DwmGetWindowAttribute
- DwmSetWindowAttribute
要获取当前程序窗口的非客户区渲染状态,可以给 DwmGetWindowAttribute 传入 DWMWA_NCRENDERING_ENABLED 标记。以下代码展示了这种用法:
BOOL enabled = FALSE;
HRESULT hr = DwmGetWindowAttribute(hwnd, DWMWA_NCRENDERING_ENABLED, &enabled, sizeof(enabled));
注意 每个 DWMWINDOWATTRIBUTE 都关联了一个隐含类型,请参考这些类型来了解更多细节。
DwmSetWindowAttribute 能够让程序设置非客户区的渲染策略。这也决定了程序如何处理 DWM 过渡效果。
以下代码展示了如何禁用非客户区渲染:
HRESULT DisableNCRendering(HWND hwnd)
{
HRESULT hr = S_OK;
DWMNCRENDERINGPOLICY ncrp = DWMNCRP_DISABLED;
// Disable non-client area rendering on the window.
hr = DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, &ncrp, sizeof(ncrp));
if (SUCCEEDED(hr))
{
// ...
}
return hr;
}
除了控制非客户区渲染策略,DwmSetWindowAttribute 也可以直接控制 DWM 过渡效果。只要给 dwAttribute 参数传入 DWMWA_TRANSITIONS_FORCEDISABLED 即可禁用。
Messaging
以下消息用于 DWM 事件的通知。这些之间可以用于监听 DWM 开启状态、系统颜色主题变化。
- WM_DWMCOLORIZATIONCOLORCHANGED
- WM_DWMCOMPOSITIONCHANGED
- WM_DWMNCRENDERINGCHANGED
- WM_DWMWINDOWMAXIMIZEDCHANGE
DWM Blur Behind Overview
DWM 的一个特性是给窗口的非客户区设定半透明和模糊效果。DWM API 能够让程序将这些效果设定到顶层窗口的客户区上。
注意 Windows Vista Home Basic 版本不支持透明玻璃效果,那些本应该被渲染为透明玻璃效果的区域,将被渲染为不透明。
本章节讨论了如下几个 DWM 支持模糊效果的场景
- Adding Blur to a Specific Region of the Client Area
- Extending the Window Frame into the Client Area
- Related topics
Adding Blur to a Specific Region of the Client Area
程序可以将模糊效果应用到整个客户区的后面或者是某块指定的子区域,这使得程序可以设定别具一格的路径栏和状态栏风格。
这种场景下可以使用 DwmEnableBlurBehindWindow 函数,这个函数需要 DWM Blur Behind Constants 和 DWM_BLURBEHIND 结构体。
以下代码示例说明了如何将模糊效果设置到整个窗口上。
HRESULT EnableBlurBehind(HWND hwnd)
{
HRESULT hr = S_OK;
// Create and populate the blur-behind structure.
DWM_BLURBEHIND bb = {0};
// Specify blur-behind and blur region.
bb.dwFlags = DWM_BB_ENABLE;
bb.fEnable = true;
bb.hRgnBlur = NULL;
// Enable blur-behind.
hr = DwmEnableBlurBehindWindow(hwnd, &bb);
if (SUCCEEDED(hr))
{
// ...
}
return hr;
}
注意上述代码中给 hRgnBlur 参数传入了 NULL,这将使得 DWM 将模糊效果应用到整个窗口。
以下图片展示了上述代码的执行效果:
要将模糊效果应用到一个子区域,你需要传入一个 HRGN 类型的值给 hRgnBlur 成员,并将 dwFlags 参数设定为 DWM_BB_BLURREGION。
当你将窗口的子区域设定为模糊效果时,窗口的 alpha 通道将用到窗口的非模糊区中。这可能造成窗口的非模糊区透明度不正常。因此,在窗口子区域设定模糊效果要谨慎。
Extending the Window Frame into the Client Area
程序可以将 window frame 的 blur 效果扩展到客户区。当你的 blur 窗口有一个工具栏或者你希望自己的程序与其它程序有所区别的时候,这个技巧可能会派上用场。
你可以给 DwmExtendFrameIntoClientArea 传入 MARGINS 结构体来控制如何扩展 blur 效果。在以下示例代码中,blur 效果将在客户区底部被扩展。
HRESULT ExtendIntoClientBottom(HWND hwnd)
{
HRESULT hr = S_OK;
// Set the margins, extending the bottom margin.
MARGINS margins = {0,0,0,25};
// Extend the frame on the bottom of the client area.
hr = DwmExtendFrameIntoClientArea(hwnd,&margins);
if (SUCCEEDED(hr))
{
// ...
}
return hr;
}
下图展示了上述代码的执行效果:
DwmExtendFrameIntoClientArea 还支持"sheet of glass"(玻璃铺满?)效果,这种效果可以让 blur 作用到窗口的整个 surface 上,使得窗口没有 window border。下面的代码示例能够实现这种效果:
HRESULT ExtendIntoClientAll(HWND hwnd)
{
HRESULT hr = S_OK;
// Negative margins have special meaning to DwmExtendFrameIntoClientArea.
// Negative margins create the "sheet of glass" effect, where the client
// area is rendered as a solid surface without a window border.
MARGINS margins = {-1};
// Extend the frame across the whole window.
hr = DwmExtendFrameIntoClientArea(hwnd,&margins);
if (SUCCEEDED(hr))
{
// ...
}
return hr;
}
下图展示了 "sheet of glass" 风格的窗口
DWM Thumbnail Overview
DWM 能够显示程序的缩略图。这个缩略图不是窗口的一个静态截图,而是动态的,它能够将缩略图窗口和目标窗口连接起来,渲染出一个即时的缩略图。这可以实现鼠标 hover 到任务栏或者使用 ALT-TAB 进行切换的时候,显示速览图的功能。
下图展示了鼠标 hover 在任务栏上时的缩略图效果:
下图展示了使用 ALT-TAB 切换时的缩略图效果:
注意 DWM thumbnails 并不能让开发者在自己的程序里实现类似于 Windows Vista Flip3D (WINKEY-TAB) 效果。因为缩略图是以 2D 的形式直接渲染到目标窗口的。
DWM Thumbnail Relationships
要在你的程序里展现缩略图,你得现在源窗口和目标窗口间建立关系。这可以通过调用 DwmRegisterThumbnail 函数来实现。
用 DwmRegisterThumbnail 建立起关系并拿到缩略图句柄后,并不会立即将缩略图渲染到目标装口上。必须先调用 DwmUpdateThumbnailProperties 函数并传入 DWM_THUMBNAIL_PROPERTIES 标记,渲染才会开始。随后对 DwmUpdateThumbnailProperties 的调用将用新的一组属性来更新缩略图。DWM 还提供了 DwmQueryThumbnailSourceSize 函数从缩略图句柄中获取源窗口的尺寸。
要终止缩略图关系,可调用 DwmUnregisterThumbnail 函数。
以下代码示例说明了如何建立与 Windows 桌面的关系,并将其显示到程序中:
HRESULT hr = S_OK;
HTHUMBNAIL thumbnail = NULL;
// Register the thumbnail
hr = DwmRegisterThumbnail(hwnd, FindWindow(_T("Progman"), NULL), &thumbnail);
if (SUCCEEDED(hr))
{
// Specify the destination rectangle size
RECT dest = {0,50,100,150};
// Set the thumbnail properties for use
DWM_THUMBNAIL_PROPERTIES dskThumbProps;
dskThumbProps.dwFlags = DWM_TNP_SOURCECLIENTAREAONLY | DWM_TNP_VISIBLE | DWM_TNP_OPACITY | DWM_TNP_RECTDESTINATION;
dskThumbProps.fSourceClientAreaOnly = FALSE;
dskThumbProps.fVisible = TRUE;
dskThumbProps.opacity = (255 * 70)/100;
dskThumbProps.rcDestination = dest;
// Display the thumbnail
hr = DwmUpdateThumbnailProperties(thumbnail,&dskThumbProps);
if (SUCCEEDED(hr))
{
// ...
}
}
return hr;
Accessing and Controlling DWM Frame Data
这一节讨论用于调度和多媒体呈现的 DWM API。
DWM Frame Timing API
Scheduling and media presentation APIs 能够控制桌面图像在构建和展示时的细节。这种功能常用在媒体或视频回放程序中,DWM 的调度和它们的展示调度是异步的,如果控制不好的话可能会出现采样失真的问题。
与此相关的 API 有下面这些:
- DwmEnableMMCSS Notifies the DWM to enable Multimedia Class Schedule Service (MMCSS) scheduling while the calling process is alive.
- DwmGetCompositionTimingInfo Retrieves the current composition timing information.
- DwmModifyPreviousDxFrameDuration Changes the number of refreshes during which the previous frame will be displayed.
- DwmSetDxFrameDuration Sets the number of refreshes in which to display the presented frame.
- DwmSetPresentParameters Sets the present parameters for frame composition.
Performance Considerations and Best Practices
本章主要讨论使用 DWM API 的最佳实践。
本章包含以下章节:
- Application Practices for DWM
- Drawing Practices for DWM
- DWM Blur-Behind Client Region
Application Practices for DWM
如果你的程序已经处理了 DPI 缩放。你可以将程序声明为 dpi-aware 来阻止系统的 dpi 自动缩放。声明 dpi-aware 可以在程序 manifest 里配置,也可以在程序初始化的时候调用 SetProcessDPIAware 来设置。
当 DWM 服务被开启时,被遮盖的程序不再能够收到 WM_PAINT 消息并不再被要求重绘。
顶层 WS_EX_TRANSPARENT 风格窗口应该同时设定 WS_EX_LAYERED 风格以支持 hit test。WS_EX_TRANSPARENT 在典型的场景下不会重定向,对子窗口有用,但不适用于顶层窗口。(没懂啥意思,原文:WS_EX_TRANSPARENT in the classic sense, without redirection, is useful for child windows in a hierarchy of windows that belong to the same thread, but is not intended for top-level windows.)
使用 regions 或 layering 来创建异形或混合窗口时,要注意在 Vista 或更久版本之前,(接下来的也没看懂:custom drawing only part of a top-level window will not provide the desired stale content in undrawn regions.)
GetDCOrgEx 之类的 API 可以用来判断某些实际值。如果你已经有了某个重定向窗口的 设备上下文(DC), GetDCOrgEx 返回的原点将会不符合窗口在屏幕上的实际原点。原点将编程 back-buffer surface 上的窗口原点:(0,0)
如果有其他错误发生,可以调用 DwmSetWindowAttribute 来禁用 DWM 渲染。
Drawing Practices for DWM
避免直接在主显示屏幕上绘制,如果这么做的话将强制关闭 DWM 直到你的程序释放主屏幕。
评估你的程序是否需要双缓冲,DWM 支持双缓冲。
避免从 display DC 中读取或写入。尽管 DWM 支持这么做,但并不推荐,因为会降低效率。
避免混合使用 GDI 和 DirectX 除非他俩完全互不干涉。如果一定要混合使用,要么把 GDI 内容绘制到 DirectX surface 上,并且在组装到桌面时把它俩合在一起, 要么在各自独立的窗口上进行绘制。
避免在客户区进行绘制操作,尽管程序支持,并且也有对应的 Win32 API。但这么做可能会导致窗口失去 玻璃边框效果。
Use BitBlt or StretchBlt function instead of Windows GDI+ to present your drawing for rendering. GDI+ renders one scan line at a time with software rendering. This can cause flickering in your applications.
DWM Blur-Behind Client Region
渲染 blur 对 CPU 和 GPU 来说都是一个耗资源的操作。开发者在客户区使用 blur 效果时一定要谨慎考虑,避免消耗过多资源。尤其要注意一下几种场景:
当 blur 区域很大的时候,即使 blur 区域本身没有更新。每当 blur 区域底下的部分改变的时候,blur 区域也得更新,这会造成 CPU 和 GPU 的消耗。另外,窗口操作(移动、改变大小、转换)都会导致更多的消耗。
当你需要在 blur 区域显式更新的时候,每次更新都会导致 blur 区域的重绘并导致资源开销。
如果 blur 区域要覆盖一个区域,并且这个区域也在被更新,我们强烈建议不要对这块区域应用 blur 效果。
Custom Window Frame Using DWM
本章说明了如何利用 DWM API 来为你的程序创建自定义窗口边框。
- Introduction
- Extending the Client Frame
- Removing the Standard Frame
- Drawing in the Extended Frame Window
- Enabling Hit Testing for the Custom Frame
- Appendix A: Sample Window Procedure
- Appendix B: Painting the Caption Title
- Appendix C: HitTestNCA Function
- Related topics
Introduction
从 Vista 版本以后,窗口非客户区的外观由 DWM 控制,包括标题栏、icon、窗口边框、标题菜单等。通过 DWM API 你可以改变 DWM 绘制窗口边框的方式。
DWM API 的特性之一是可以将程序的边框扩展到客户区。这个特性可以让你将客户区的元素,例如工具栏,整合到边框中。给某些 UI 控件更突出的位置。例如,IE7 以后的版本将边框往下扩展,显示出了工具栏:
这种特性也可以让你在边框未扩展的情况下对边框进行自定义。例如下图的 office 程序中,office 按钮和快捷按钮被放到了边框上:
Extending the Client Frame
调用 DwmExtendFrameIntoClientArea 可以将边框扩展到客户区。要使用这个函数,需要填充一个 MARGINS 结构体,这个结构体指明了要扩展的尺寸。
以下代码展示了 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 中,这是为了确保边框扩展能够在它的默认尺寸和最大化尺寸下被正常处理。
以下图片展示了标准窗口和经过上述代码处理后的窗口:
两者看起来没什么区别,唯一的区别是右边经过扩展的窗口里没有了边框内侧的细线。细线之所以会消失是因为边框已经被扩展到了内部,但客户区的其它区域没有被扩展。(下面这段没懂)For the extended frames to be visible, the regions underlying each of the extended frame's sides must have pixel data with an alpha value of 0. The black border around the client region has pixel data in which all color values (red, green, blue, and alpha) are set to 0. The rest of the background does not have the alpha value set to 0, so the rest of the extended frame is not visible.
要保证扩展边框正确显示的最简单方法是将整个客户区绘制为黑色。要做到这一点,将 WMDCLASS 或 WNDCLASSEX 的 hbrBackground 成员初始化为 BLACK_BRUSH。以下图片展示了这么做的效果:
Removing the Standard Frame
扩展边框并将其设为可见后,你可以移除标准边框。移除标准边框可以让你控制边框每一边的宽度而不是简单地扩展标准边框。
要移除标准边框,你必须处理 WM_NCCALCSIZE 消息,尤其当 wParam 为 TRUE 并切返回值是 0 的时候。这么做的话,你的程序可以将标准边框移除,把整个窗口区域当作客户区。
WM_NCCALCSIZE 消息需要在窗口创建时发出。你可以使用 SetWindowPos 函数来触发这个事件。以下代码展示了在 WM_CREATE 中调用 SetWindowPos 发出 WM_NCCALCSIZE 事件的过程:
// 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;
}
以下图片展示了标准窗口和没有标准边框的窗口:
Drawing in the Extended Frame Window
移除标准边框后,系统就不会为你自动绘制 icon 和标题了。要把这些加回来,你得自己绘制。要这么干的话,首先看看你的客户区上发生了什么。
移除标准边框后,窗口的客户区就变成了整个窗口,包括扩展边框的部分,也包括标题按钮被重绘的区域。在下面的示意图中,客户区用红色框标识。左图的是标准边框,客户区是黑色区域;右图是扩展边框,客户区是整个窗口:
因为整个窗口都变成了客户区,你可以直接在扩展边框上绘制你想绘制的内容。要添加标题,就在合适的区域绘制文本就可以了。下图展示了绘制标题的效果,通过调用 DrawThemeTextEx 实现。要看这段代码,可以参考接下来的 Appendix B: Painting the Caption Title 章节。
注意 在自定义边框上绘制时,要小心放置 UI 控件。因为整个窗口都变成了客户区,如果你不想让控件出现在边框上,你得小心调整空间宽度。
Enabling Hit Testing for the Custom Frame
移除标准边框的一个副作用是你的窗口失去了默认的 resize 和 move 功能。要让你的自定义窗口模拟原有标准窗口的表现,你得自己实现标题按钮和 resize, remove 的逻辑。
为了标题按钮的 hit test,DWM 提供了 DwmDefWindowProc 函数。为了在标题按钮中进行 hit test,消息首先被发送给 DwmDefWindowProc 来处理。DwmDefWindowProc 处理了消息的话,会返回 TRUE 否则 FALSE。如果 DwmDefWindowProc 没有处理消息,你的程序应该自己处理消息或者把消息传给 DefWindowProc。
为了边框的 resize, remove 功能,你的程序得提供 hit test 逻辑并处理边框的 hit test 消息。边框的 hit test 消息会通过 WM_NCHITTEST 消息 send 给你,即使你创建的窗口移除了标准边框。以下代码展示了当 DwmDefWindowProc 没有处理 hit test 的时候,你应该如何处理 WM_NCHITTEST 消息。要了解其中 HitTestNCA 的代码,可以参考接下来的 Appendix C: HitTestNCA Function。
// 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;
}
}
Appendix A: Sample Window Procedure
以下代码展示了一个 window procture。它能够用来创建自定义边框程序。
//
// 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;
{
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;
}
Appendix B: Painting the Caption Title
下面的代码示例说明了如何在扩展边框上绘制标题。这个函数必须在 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);
}
}
Appendix C: HitTestNCA Function
以下代码展示了 HitTestNCA 函数。这个函数处理了 DwmDefWindowProc 没有处理的时候 WM_NCHITTEST 消息中的 hit test 逻辑。
/ 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];
}