walklang / uilib

A simply and powerful ui script framework library. via http://www.uilib.cn
Apache License 2.0
45 stars 7 forks source link

DWM: Desktop Window Manager #13

Open suhao opened 2 years ago

suhao commented 2 years ago

从Windows Vista开始,Aero Glass效果被应用在了Home Premium以上的系统中(Home Basic不具有该效果)。这种效果是由DWM(Desktop Window Manager)来控制的。对于一般的程序,缺省将在窗口边框应用这种效果。但如果我们想要更多的控制,比如让客户区的一部分也呈现这种效果,那也非常的简单。不需要我们在程序里做任何复杂的算法,我们只需要调API,交给DWM去做就可以了。

DWM相关操作的MSDN说明:Desktop Window Manager (DWM) APIs.

http://msdn.microsoft.com/en-us/library/aa969527(v=VS.85).aspx

Header Dwmapi.h Library Dwmapi.lib DLL Dwmapi.dll

一、Composition(窗口合成) and Non-client Rendering(非客户区渲染)

非客户区通常包括窗口标题栏和窗口边框。缺省状态下,非客户区会被渲染成毛玻璃效果,这也称为Compostion。有几个函数可以控制系统和当前窗口的渲染方式。同时也有Windows消息用于接受渲染模式的改变。

   1.检测系统是否开启Aero Glass。使用函数DwmIsCompositionEnabled检测系统当前是否开启了Aero Glass特效。它接受一个BOOL参数,并将当前状态存储到其中。函数原型:HRESULT DwmIsCompositionEnabled(BOOL *pfEnabled);

   2.开启/关闭Aero Glass。使用函数DwmEnableComposition开启或关闭系统Aero Glass效果,传入DWM_EC_ENABLECOMPOSITION开启,传入DWM_EC_DISABLECOMPOSITION关闭。

   3.开启/关闭当前窗口的非客户区渲染。函数DwmSetWindowAttribute用于设置窗口属性,属性DWMWA_NCRENDERING_POLICY控制当前窗口是否使用非客户区渲染。DWMNCRP_ENABLED开启,DWMNCRP_DISABLED关闭。当系统的Aero Glass关闭时,设置无效。与之对应,使用函数DwmGetWindowAttribute可以检测当前窗口属性。

   4.响应系统Aero Glass的开启或关闭。当Aero Glass被开启或关闭时,Windows会发送消息WM_DWMCOMPOSITIONCHANGED,使用函数DwmIsCompositionEnabled检测状态。

   5.响应窗口非客户区渲染的开启或关闭。当前窗口的非客户区渲染开启或关闭时,Windows会发送消息WM_DWMNCRENDERINGCHANGED,wParam指示当前状态。

二、Transition(窗口动画) and ColorizationColor(主题颜色)

Transition控制是否以动画方式显示窗口的最小化和还原。通过使用函数DwmSetWindowAttribute,设置属性DWMWA_TRANSITIONS_FORCEDISABLED,开启或关闭窗口动画。该设置只对当前窗口有效。

   当用户通过控制面板修改主题颜色时,Windows将发送消息WM_DWMCOLORIZATIONCOLORCHANGED,程序中通过函数DwmGetColorizationColor取得当前主题颜色,以及是否透明。通过响应颜色的变更,可以让程序的颜色风格随主题风格而变化。

三、开启客户区域Aero Glass效果

函数DwmEnableBlurBehindWindow开启客户区的Aero Glass效果,第一个参数为窗口句柄,第二个参数为一个DWM_BLURBEHIND结构。其中fEnable设置是否开启客户区Glass效果。hRgnBlur设置Glass效果的区域,该项设置为NULL将使整个客户区呈现Glass效果,设置为一个正确的区域后,该区域将呈现Glass效果, 而区域以外为完全透明。要呈现透明效果需要客户区原始的颜色为黑色,可以在WM_PAINT消息中绘制客户区,下面的代码使用GDI+,在Aero Glass开启时将整个窗口绘制为黑色,Aero Glass关闭时绘制为灰色:

    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hDC = BeginPaint(hWnd, &ps);
            //不要直接使用窗口句柄创建Graphics,会导致闪烁
            Graphics graph(hDC);
            //清除客户区域
            RECT rcClient;
            GetClientRect(hWnd, &rcClient);
            BOOL bCompEnabled;
            DwmIsCompositionEnabled(&bCompEnabled);
            SolidBrush br(bCompEnabled? Color::Black : Color::DarkGray);
            graph.FillRectangle(&br, Rect(rcClient.left, rcClient.top, 
                rcClient.right, rcClient.bottom));
            EndPaint(hWnd, &ps);
        }
        break;

GDI+的初始化和关闭仍然是必须的:

    //初始化GDI+
    ULONG_PTR token;
    GdiplusStartupInput input;
    GdiplusStartup(&token, &input, NULL);
    //*********************************
    //关闭GDI+
    GdiplusShutdown(token);

下面代码将整个客户区设置为Glass效果:

    DWM_BLURBEHIND bb = {0};
    bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
    bb.fEnable = true;
    bb.hRgnBlur = NULL;
    DwmEnableBlurBehindWindow(hWnd, &bb);

image

下面代码将客户区中心一个椭圆的区域设置为Glass效果:

    RECT rect;
    GetWindowRect(hWnd, &rect);
    int width = 300, height = 200;
    //居中椭圆形
    HRGN hRgn = CreateEllipticRgn((rect.right - rect.left)/2 - width/2, 
        (rect.bottom - rect.top)/2 - height/2, (rect.right - rect.left)/2 + width/2, 
        (rect.bottom - rect.top)/2 + height/2);
    DWM_BLURBEHIND bb = {0};
    bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
    bb.fEnable = true;
    bb.hRgnBlur = hRgn;
    DwmEnableBlurBehindWindow(hWnd, &bb);

image

四、窗口边框向客户区扩展

上面的方式中,非客户区和客户区之间仍然有界限。如何增大Glass效果的范围,并且消除界限呢?那就是使窗口边框向客户区扩展,利用函数DwmExtendFrameIntoClientArea实现。函数接受一个窗口句柄和一个MARGINS类型的参数。MARGINS指定了在上下左右4个方向上扩展的范围。如果4个值均为-1,则扩展到整个客户区。

                MARGINS margins = {50, 50, 50, 50};
                DwmExtendFrameIntoClientArea(hWnd, &margins);

image

                MARGINS margins2 = {-1};    //将扩展到整个客户区
                DwmExtendFrameIntoClientArea(hWnd, &margins2);

image

五、在窗口上绘制图形

PNG图片带有alpha通道,可以与Aero Glass很好的配合。利用GDI+显示PNG图片非常方便,下面的代码将一张PNG图片加载到内存中:

Bitmap bmp  = Bitmap::FromFile(L"Ferrari.png", false);

在WM_PAINT消息处理中,将整个客户区绘制为黑色以后,利用GDI+将图片绘制到窗口客户区:

            //绘制图形
            int width = bmp->GetWidth();
            int height = bmp->GetHeight();
            Rect rc(30, 30, width, height);
            graph.DrawImage(bmp, rc, 0, 0, width, height, UnitPixel);

image

六、文本的绘制

当窗口大范围的透明之后,窗口上的文字的阅读成了一个问题。Windows的解决办法是为文字加上发光效果(Glowing),标题栏的文本使用的就是这种方式。我们在自己的程序中可以使用DrawThemeTextEx函数来绘制发光的文字。该函数的原型定义如下:

HRESULT DrawThemeTextEx(          HTHEME hTheme,
    HDC hdc,
    int iPartId,
    int iStateId,
    LPCWSTR pszText,
    int iCharCount,
    DWORD dwFlags,
    LPRECT pRect,
    const DTTOPTS *pOptions
);

hTheme是一个主题句柄,可以使用OpenThemeData获得,OpenThemeData函数接受一个窗口句柄,和主题类的名称。iPartId和iStateId分别代表主题类中的Part和State,所有可用的主题类、Part和state在SDK的帮助文档中可以查看到。pszText是要绘制的文本。iCharCount为文字个数,-1代表绘制全部文本。dwFlags指定文本格式。pRect为文本绘制区域。pOptions中可以设定文本的发光、阴影等效果。HDC是一个设备上下文句柄,为了实现类似于标题栏中文本的发光效果,这里不能使用由BeginPaint得到的句柄,而是要使用CreateCompatibleDC创建一个内存中的句柄,并且要创建一张位图,通过内存句柄将文本绘制到位图上。然后再将位图转移到窗口上。下面的函数封装了绘制发光文本的过程:

//绘制发光文字
void DrawGlowingText(HDC hDC, LPWSTR szText, RECT &rcArea, 
    DWORD dwTextFlags = DT_LEFT | DT_VCENTER | DT_SINGLELINE, int iGlowSize = 10)
{
    //获取主题句柄
    HTHEME hThm = OpenThemeData(GetDesktopWindow(), L"TextStyle");
    //创建DIB
    HDC hMemDC = CreateCompatibleDC(hDC);
    BITMAPINFO bmpinfo = {0};
    bmpinfo.bmiHeader.biSize = sizeof(bmpinfo.bmiHeader);
    bmpinfo.bmiHeader.biBitCount = 32;
    bmpinfo.bmiHeader.biCompression = BI_RGB;
    bmpinfo.bmiHeader.biPlanes = 1;
    bmpinfo.bmiHeader.biWidth = rcArea.right - rcArea.left;
    bmpinfo.bmiHeader.biHeight = -(rcArea.bottom - rcArea.top);
    HBITMAP hBmp = CreateDIBSection(hMemDC, &bmpinfo, DIB_RGB_COLORS, 0, NULL, 0);
    if (hBmp == NULL) return;
    HGDIOBJ hBmpOld = SelectObject(hMemDC, hBmp);
    //绘制选项
    DTTOPTS dttopts = {0};
    dttopts.dwSize = sizeof(DTTOPTS);
    dttopts.dwFlags = DTT_GLOWSIZE | DTT_COMPOSITED;
    dttopts.iGlowSize = iGlowSize;  //发光的范围大小
    //绘制文本
    RECT rc = {0, 0, rcArea.right - rcArea.left, rcArea.bottom - rcArea.top};
    HRESULT hr = DrawThemeTextEx(hThm, hMemDC, TEXT_LABEL, 0, szText, -1, dwTextFlags , &rc, &dttopts);
    if(FAILED(hr)) return;
    BitBlt(hDC, rcArea.left, rcArea.top, rcArea.right - rcArea.left, 
        rcArea.bottom - rcArea.top, hMemDC, 0, 0, SRCCOPY | CAPTUREBLT);
    //Clear
    SelectObject(hMemDC, hBmpOld);
    DeleteObject(hBmp);
    DeleteDC(hMemDC);
    CloseThemeData(hThm);
}

在绘制了图形后,加入下面代码绘制一段文本:

            //绘制文本
            RECT rcText = {10, 10, 300, 40};
            DrawGlowingText(hDC, L"  一点点中文 and some english", rcText);

因为字体发光的缘故,在文本左侧留下一个空格看起来会舒服一些。效果如下: image

七、缩略图关联

DWM API中还有一个功能,即缩略图关联。它允许我们将一个窗口的缩略图显示到自己窗口的客户区。缩略图不同于截图,它是实时更新的。下面的代码将在窗口客户区显示QQ影音播放器的缩略图:

    HRESULT hr = S_OK;
    HTHUMBNAIL thumbnail = NULL;
    HWND hWndSrc = FindWindow(_T("QQPlayer Window"), NULL);
    hr = DwmRegisterThumbnail(hWnd, hWndSrc, &thumbnail);
    if (SUCCEEDED(hr))
    {
        RECT rc;
        GetClientRect(hWnd, &rc);
        DWM_THUMBNAIL_PROPERTIES dskThumbProps;
        dskThumbProps.dwFlags = DWM_TNP_RECTDESTINATION | DWM_TNP_VISIBLE | DWM_TNP_OPACITY ;
        dskThumbProps.fVisible = TRUE;
        dskThumbProps.opacity = 200;
        dskThumbProps.rcDestination = rc;
        hr = DwmUpdateThumbnailProperties(thumbnail,&dskThumbProps);
    }

首先通过窗口标题查找到源窗口句柄,然后使用DwmRegisterThumbnail注册缩略图关联,注册成功后,通过DwmUpdateThumbnailProperties更新缩略图属性,其中设定了是否可视、透明度以及目标绘制区域。得到下面的效果:

image

suhao commented 2 years ago

窗口玻璃特效,半透明窗口,使用DWM实现Aero Glass效果

从Windows Vista开始,Aero Glass效果被应用在了Home Premium以上的系统中(Home Basic不具有该效果)。这种效果是由DWM(Desktop Window Manager)来控制的。对于一般的程序,缺省将在窗口边框应用这种效果。但如果我们想要更多的控制,比如让客户区的一部分也呈现这种效果,那也非常的简单。不需要我们在程序里做任何复杂的算法,我们只需要调API,交给DWM去做就可以了。

一、Composition(窗口合成) and Non-client Rendering(非客户区渲染)

非客户区通常包括窗口标题栏和窗口边框。缺省状态下,非客户区会被渲染成毛玻璃效果,这也称为Compostion。有几个函数可以控制系统和当前窗口的渲染方式。同时也有Windows消息用于接受渲染模式的改变。

   1.检测系统是否开启Aero Glass。使用 函数 DwmIsCompositionEnabled 检测系统当前是否开启了Aero Glass特效。它接受一个BOOL参数,并将当前状态存储到其中。函数原型:HRESULT DwmIsCompositionEnabled(BOOL *pfEnabled );

   2.开启/关闭Aero Glass。使用函数DwmEnableComposition 开启或关闭系统Aero Glass效果,传入DWM_EC_ENABLECOMPOSITION 开启,传入DWM_EC_DISABLECOMPOSITION 关闭。

   3.开启/关闭当前窗口的非客户区渲染。函数DwmSetWindowAttribute 用于设置窗口属性,属性DWMWA_NCRENDERING_POLICY 控制当前窗口是否使用非客户区渲染。DWMNCRP_ENABLED 开启,DWMNCRP_DISABLED 关闭。当系统的Aero Glass关闭时,设置无效。与之对应,使用函数DwmGetWindowAttribute 可以检测当前窗口属性。

   4.响应系统Aero Glass的开启或关闭。当Aero Glass被开启或关闭时,Windows会发送消息WM_DWMCOMPOSITIONCHANGED , 使用 函数 DwmIsCompositionEnabled 检测状态。

   5.响应窗口非客户区渲染的开启或关闭。当前窗口的非客户区渲染开启或关闭时,Windows会发送消息WM_DWMNCRENDERINGCHANGED ,wParam 指示当前状态。

二、Transition(窗口动画) and ColorizationColor(主题颜色)

Transition控制是否以动画方式显示窗口的最小化和还原。通过使用函数DwmSetWindowAttribute ,设置属性DWMWA_TRANSITIONS_FORCEDISABLED ,开启或关闭窗口动画。该设置只对当前窗口有效。

   当用户通过控制面板修改主题颜色时,Windows将发送消息WM_DWMCOLORIZATIONCOLORCHANGED ,程序中通过函数DwmGetColorizationColor 取得当前主题颜色,以及是否透明。通过响应颜色的变更,可以让程序的颜色风格随主题风格而变化。

三、开启客户区域Aero Glass效果

函数DwmEnableBlurBehindWindow 开启客户区的Aero Glass效果,第一个参数为窗口句柄,第二个参数为一个DWM_BLURBEHIND 结构。其中fEnable 设置是否开启客户区Glass效果。hRgnBlur 设置Glass效果的区域,该项设置为NULL将使整个客户区呈现Glass效果,设置为一个正确的区域后,该区域将呈现Glass效果, 而区域以外为完全透明。要呈现透明效果需要客户区原始的颜色为黑色,可以在WM_PAINT 消息中绘制客户区,下面的代码使用GDI+,在Aero Glass开启时将整个窗口绘制为黑色,Aero Glass关闭时绘制为灰色:

case WM_PAINT:  
    {  
        PAINTSTRUCT ps;  
        HDC hDC = BeginPaint(hWnd, &ps);  
        //不要直接使用窗口句柄创建Graphics,会导致闪烁  
        Graphics graph(hDC);  
        //清除客户区域  
        RECT rcClient;  
        GetClientRect(hWnd, &rcClient);  
        BOOL bCompEnabled;  
        DwmIsCompositionEnabled(&bCompEnabled);  
        SolidBrush br(bCompEnabled? Color::Black : Color::DarkGray);  
        graph.FillRectangle(&br, Rect(rcClient.left, rcClient.top,   
            rcClient.right, rcClient.bottom));  
        EndPaint(hWnd, &ps);  
    }  
    break;  

GDI+的初始化和关闭仍然是必须的:

//初始化GDI+  
ULONG_PTR token;  
GdiplusStartupInput input;  
GdiplusStartup(&token, &input, NULL);  
//*********************************  
//关闭GDI+  
GdiplusShutdown(token);  

下面代码将整个客户区设置为Glass效果:

DWM_BLURBEHIND bb = {0};  
bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;  
bb.fEnable = true;  
bb.hRgnBlur = NULL;  
DwmEnableBlurBehindWindow(hWnd, &bb);  

下面代码将客户区中心一个椭圆的区域设置为Glass效果:

RECT rect;  
GetWindowRect(hWnd, &rect);  
int width = 300, height = 200;  
//居中椭圆形  
HRGN hRgn = CreateEllipticRgn((rect.right - rect.left)/2 - width/2,   
    (rect.bottom - rect.top)/2 - height/2, (rect.right - rect.left)/2 + width/2,   
    (rect.bottom - rect.top)/2 + height/2);  
DWM_BLURBEHIND bb = {0};  
bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;  
bb.fEnable = true;  
bb.hRgnBlur = hRgn;  
DwmEnableBlurBehindWindow(hWnd, &bb);  

四、窗口边框向客户区扩展

上面的方式中,非客户区和客户区之间仍然有界限。如何增大Glass效果的范围,并且消除界限呢?那就是使窗口边框向客户区扩展,利用函数DwmExtendFrameIntoClientArea 实现。函数接受一个窗口句柄和一个MARGINS 类型的参数。MARGINS指定了在上下左右4个方向上扩展的范围。如果4个值均为-1,则扩展到整个客户区

MARGINS margins = {50, 50, 50, 50};  
DwmExtendFrameIntoClientArea(hWnd, &margins);  

MARGINS margins2 = {-1};    //将扩展到整个客户区  
DwmExtendFrameIntoClientArea(hWnd, &margins2);  

五、在窗口上绘制图形

PNG图片带有alpha通道,可以与Aero Glass很好的配合。利用GDI+显示PNG图片非常方便,下面的代码将一张PNG图片加载到内存中:

Bitmap bmp  = Bitmap::FromFile(L"Ferrari.png", false);  

在WM_PAINT消息处理中,将整个客户区绘制为黑色以后,利用GDI+将图片绘制到窗口客户区:

//绘制图形  
int width = bmp->GetWidth();  
int height = bmp->GetHeight();  
Rect rc(30, 30, width, height);  
graph.DrawImage(bmp, rc, 0, 0, width, height, UnitPixel);  

六、文本的绘制

当窗口大范围的透明之后,窗口上的文字的阅读成了一个问题。Windows的解决办法是为文字加上发光效果(Glowing),标题栏的文本使用的就是这种方式。我们在自己的程序中可以使用DrawThemeTextEx 函数来绘制发光的文字。该函数的原型定义如下:

HRESULT DrawThemeTextEx(          HTHEME hTheme,  
    HDC hdc,  
    int iPartId,  
    int iStateId,  
    LPCWSTR pszText,  
    int iCharCount,  
    DWORD dwFlags,  
    LPRECT pRect,  
    const DTTOPTS *pOptions  
);  

hTheme是一个主题句柄,可以使用OpenThemeData 获得, OpenThemeData 函数接受一个窗口句柄,和主题类的名称。iPartId和iStateId分别代表主题类中的Part和State,所有可用的主题类、Part和state在SDK的帮助文档中可以查看到。pszText是要绘制的文本。iCharCount为文字个数,-1代表绘制全部文本。dwFlags指定文本格式。pRect为文本绘制区域。pOptions中可以设定文本的发光、阴影等效果。HDC是一个设备上下文句柄,为了实现类似于标题栏中文本的发光效果,这里不能使用由BeginPaint 得到的句柄,而是要使用CreateCompatibleDC 创建一个内存中的句柄,并且要创建一张位图,通过内存句柄将文本绘制到位图上。然后再将位图转移到窗口上。下面的函数封装了绘制发光文本的过程:

//绘制发光文字  
void DrawGlowingText(HDC hDC, LPWSTR szText, RECT &rcArea,   
    DWORD dwTextFlags = DT_LEFT | DT_VCENTER | DT_SINGLELINE, int iGlowSize = 10)  
{  
    //获取主题句柄  
    HTHEME hThm = OpenThemeData(GetDesktopWindow(), L"TextStyle");  
    //创建DIB  
    HDC hMemDC = CreateCompatibleDC(hDC);  
    BITMAPINFO bmpinfo = {0};  
    bmpinfo.bmiHeader.biSize = sizeof(bmpinfo.bmiHeader);  
    bmpinfo.bmiHeader.biBitCount = 32;  
    bmpinfo.bmiHeader.biCompression = BI_RGB;  
    bmpinfo.bmiHeader.biPlanes = 1;  
    bmpinfo.bmiHeader.biWidth = rcArea.right - rcArea.left;  
    bmpinfo.bmiHeader.biHeight = -(rcArea.bottom - rcArea.top);  
    HBITMAP hBmp = CreateDIBSection(hMemDC, &bmpinfo, DIB_RGB_COLORS, 0, NULL, 0);  
    if (hBmp == NULL) return;  
    HGDIOBJ hBmpOld = SelectObject(hMemDC, hBmp);  
    //绘制选项  
    DTTOPTS dttopts = {0};  
    dttopts.dwSize = sizeof(DTTOPTS);  
    dttopts.dwFlags = DTT_GLOWSIZE | DTT_COMPOSITED;  
    dttopts.iGlowSize = iGlowSize;  //发光的范围大小  
    //绘制文本  
    RECT rc = {0, 0, rcArea.right - rcArea.left, rcArea.bottom - rcArea.top};  
    HRESULT hr = DrawThemeTextEx(hThm, hMemDC, TEXT_LABEL, 0, szText, -1, dwTextFlags , &rc, &dttopts);  
    if(FAILED(hr)) return;  
    BitBlt(hDC, rcArea.left, rcArea.top, rcArea.right - rcArea.left,   
        rcArea.bottom - rcArea.top, hMemDC, 0, 0, SRCCOPY | CAPTUREBLT);  
    //Clear  
    SelectObject(hMemDC, hBmpOld);  
    DeleteObject(hBmp);  
    DeleteDC(hMemDC);  
    CloseThemeData(hThm);  
}  

在绘制了图形后,加入下面代码绘制一段文本:

//绘制文本  
RECT rcText = {10, 10, 300, 40};  
DrawGlowingText(hDC, L"  一点点中文 and some english", rcText);  

因为字体发光的缘故,在文本左侧留下一个空格看起来会舒服一些。

七、缩略图关联

DWM API中还有一个功能,即缩略图关联。它允许我们将一个窗口的缩略图显示到自己窗口的客户区。缩略图不同于截图,它是实时更新的。下面的代码将在窗口客户区显示QQ影音播放器的缩略图:

HRESULT hr = S_OK;  
HTHUMBNAIL thumbnail = NULL;  
HWND hWndSrc = FindWindow(_T("QQPlayer Window"), NULL);  
hr = DwmRegisterThumbnail(hWnd, hWndSrc, &thumbnail);  
if (SUCCEEDED(hr))  
{  
    RECT rc;  
    GetClientRect(hWnd, &rc);  
    DWM_THUMBNAIL_PROPERTIES dskThumbProps;  
    dskThumbProps.dwFlags = DWM_TNP_RECTDESTINATION | DWM_TNP_VISIBLE | DWM_TNP_OPACITY ;  
    dskThumbProps.fVisible = TRUE;  
    dskThumbProps.opacity = 200;  
    dskThumbProps.rcDestination = rc;  
    hr = DwmUpdateThumbnailProperties(thumbnail,&dskThumbProps);  
} 

首先通过窗口标题查找到源窗口句柄,然后使用DwmRegisterThumbnail 注册缩略图关联,注册成功后,通过DwmUpdateThumbnailProperties 更新缩略图属性,其中设定了是否可视、透明度以及目标绘制区域。得到下面的效果:

image