[WPF]使用Desktop Window Manager(DWM) APIs擴展視窗的邊框區域

前陣子在使用Microsoft Ribbon套件時,很簡單的就把自己的程式的樣式,用的像Windows Live Writer 2011一樣,而且一點也不覺得很突兀,那時就很好奇它是如何在視窗的標題列上加入控制項的,如果我不用Microsoft Ribbon我要怎麼樣做到同樣的效果,搜尋後找到原來是使用Desktop Window Manager(簡稱DWM) APIs,DWN APIs可以讓我們的程式使用Windows內建的框架,不用自己處理關閉或放大縮小等事件,可以在現有的機制上擴充按鈕或其他控制項,除了可以大幅增加設計的空間外,有時候把全域的控制選項放在邊框上,還可以大大的增加使用的便利性。

前陣子在使用Microsoft Ribbon套件時,很簡單的就把自己的程式的樣式,用的像Windows Live Writer 2011一樣,而且一點也不覺得很突兀,那時就很好奇它是如何在視窗的標題列上加入控制項的,如果我不用Microsoft Ribbon我要怎麼樣做到同樣的效果,搜尋後找到原來是使用Desktop Window Manager(簡稱DWM) APIs,DWN APIs可以讓我們的程式使用Windows內建的框架,不用自己處理關閉或放大縮小等事件,可以在現有的機制上擴充按鈕或其他控制項,除了可以大幅增加設計的空間外,有時候把全域的控制選項放在邊框上,還可以大大的增加使用的便利性。

圖一

圖一 使用DWM,讓自己的程式也可以像Windows Live Writer 2011一樣,在視窗邊框上增加控制項。

NOTE:

DWM APIs是Vista之後才有的功能,DWM APIs除了可以擴展用戶區外,還有玻璃效果、縮圖等等功能,詳細內容可以參考相關文章。

 

相關文章

[MSDN]Desktop Window Manager - Topic

[MSDN]Custom Window Frame Using DWM – ★★★

[MSDN]將玻璃框架擴充至 WPF 應用程式中 – ★★

[Code Project]Vista Aero ToolStrip on Non-Client Area - ★★★★★

[Code Project]Drawing smooth text and pictures on the extended glass area of your WinForm in Windows Vista - ★★★

[Code Project]How to Support the Ribbon and a Menu in the Same Executable - C++

[CodePlex]DWM.NET Library

 

圖二

圖二 範例執行畫面

 

用戶區(client area)與非用戶區(non-client area)

用戶區指的我們程式可呈現內容的區域,相反的非用戶區就是指的不可呈現內容的區域,如視窗的邊框,用戶區加上非用戶區才是整個視窗的大小。

圖三

圖三 用戶區與非用戶區的示意圖

 

停用非用戶區

我有找到幾個停用非用戶區的方法,如UxTheme.dll中的SetWindowThemeAttribute,不過本篇使用的是攔截WinProc的方式處理,產生非用戶區的訊息是WM_NCCALCSIZE = 0x0083,收到訊息時回傳IntPtr.Zero停用非用戶區。

private IntPtr WndProc(IntPtr hwnd, int msgValue, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    Win32Messages msg = (Win32Messages)msgValue;
    
    if (msg == Win32Messages.WM_NCCALCSIZE && (int)wParam == 1)
    {
        handled = true;
        return IntPtr.Zero;
    }   
    
    //沒有handled = true回傳什麼都不會處理
    return IntPtr.Zero;
}

 

圖四

圖四 停用非用戶區後整個外框就不見了

NOTE:

WPF的WndProc處理跟WinForm不同,無法直接Override使用,必需WindowInteropHelper找出視窗的handler,然後用HwndSource來註冊一個委派。

IntPtr mainWindowPtr = new WindowInteropHelper(this).Handle;
HwndSource mainWindowSrc = HwndSource.FromHwnd(mainWindowPtr);
mainWindowSrc.AddHook(WndProc);

 

寫WPF雖然在Handler、訊息、游標等比WinForm麻煩些,但有一點比WinForm好,之前在WinForm上測試時,邊框的控制項在有玻璃效果時都要處理透明度,不然顏色會跑掉,不過在WPF中測試都沒有這個問題,省下不少功夫,透明度的問題可以參考Drawing smooth text and pictures on the extended glass area of your WinForm in Windows Vista

 

使用DwmExtendFrameIntoClientArea API為視窗加上邊框

使用DwmExtendFrameIntoClientArea所加上的邊框與原本的邊框最大的差異,是使用DwmExtendFrameIntoClientArea可以自訂邊距,如圖二上下的邊距比原本邊距大上許多,還有幾個要注意地方,如:

  • 背景一定要設定透明不然不會有效果。
  • 你可以想像使用DwmExtendFrameIntoClientArea是幫你的程式加上最底層的圖層,控制項是在邊框之上重疊在一起,會有搶滑鼠事件的問題。
  • Margin可以同設成-1效果滿特別的如圖五。
private void InitializeClientArea()
{
    IntPtr mainWindowPtr = new WindowInteropHelper(this).Handle;
    HwndSource mainWindowSrc = HwndSource.FromHwnd(mainWindowPtr);
    mainWindowSrc.AddHook(WndProc);
    //背景一定要透明不然沒有效果
    this.Background = System.Windows.Media.Brushes.Transparent; 
    mainWindowSrc.CompositionTarget.BackgroundColor = Colors.Transparent;

    System.Drawing.Graphics desktop = System.Drawing.Graphics.FromHwnd(mainWindowPtr);
    float dpiOffset = desktop.DpiX / 96;
    
    //this.ClientArea是Grid用來放用戶區控制項容器
    dwmMargins.cxLeftWidth = Convert.ToInt32(this.ClientArea.Margin.Left * dpiOffset);
    dwmMargins.cxRightWidth = Convert.ToInt32(this.ClientArea.Margin.Right * dpiOffset);
    dwmMargins.cyTopHeight = Convert.ToInt32(this.ClientArea.Margin.Top * dpiOffset);
    dwmMargins.cyBottomHeight = Convert.ToInt32(this.ClientArea.Margin.Bottom * dpiOffset); ;

    int hr = Dwm.DwmExtendFrameIntoClientArea(mainWindowSrc.Handle, ref dwmMargins);

    if (hr < 0)
    {
        throw new InvalidOperationException();
    }
}

 

圖五

圖五 Margin同設成-1,無內框的效果。

 

訊息處理與位址計算

除了最大、最小、關閉這三個按鈕有DWM有函式可以知道目前滑鼠是不是在這三個按鈕上外,其他的所有都要自己計算,如移動、放大、縮小等,這些還好處理,麻煩的是重壘的部份,在收到滑鼠的WndProc訊息時,不能攔截到重壘的控制項訊息,害它對滑鼠沒有反應。

private IntPtr HitTestNCA(IntPtr hwnd, IntPtr wparam, IntPtr lparam)
{
    int HTCLIENT = 1;
    int HTCAPTION = 2;
    int HTGROWBOX = 4;
    int HTSIZE = HTGROWBOX;
    int HTMINBUTTON = 8;
    int HTMAXBUTTON = 9;
    int HTLEFT = 10;
    int HTRIGHT = 11;
    int HTTOP = 12;
    int HTTOPLEFT = 13;
    int HTTOPRIGHT = 14;
    int HTBOTTOM = 15;
    int HTBOTTOMLEFT = 16;
    int HTBOTTOMRIGHT = 17;
    int HTREDUCE = HTMINBUTTON;
    int HTZOOM = HTMAXBUTTON;
    int HTSIZEFIRST = HTLEFT;
    int HTSIZELAST = HTBOTTOMRIGHT;

    RECT windowPosition;
    if (!WinApi.GetWindowRect(hwnd, out windowPosition))
    {
        throw new InvalidOperationException();
    }

    //電腦中的滑鼠位址要換成應用程式中的位址,然後計算游標是否在之上
    System.Drawing.Point mousePosScreen = new System.Drawing.Point(WinApi.LoWord((int)lparam), WinApi.HiWord((int)lparam));
    System.Drawing.Point mousePosWindow = mousePosScreen;
    mousePosWindow.Offset(-windowPosition.Left, -windowPosition.Top);

    //偷懶邊框固定設8
    //x,y是從左上開始往下右,如x=0,y=0,width=8,height=8 是左上方的邊角
    Rectangle topleft = new Rectangle(0, 0, 8, 8);
    if (topleft.Contains(mousePosWindow))
        return new IntPtr(HTTOPLEFT);

    Rectangle topright = new Rectangle((int)Width - 8, 0, 8, 8);
    if (topright.Contains(mousePosWindow))
        return new IntPtr(HTTOPRIGHT);

    Rectangle botleft = new Rectangle(0, (int)Height - 8, 8, 8);
    if (botleft.Contains(mousePosWindow))
        return new IntPtr(HTBOTTOMLEFT);

    Rectangle botright = new Rectangle((int)Width - 8, (int)Height - 8, 8, 8);
    if (botright.Contains(mousePosWindow))
        return new IntPtr(HTBOTTOMRIGHT);

    Rectangle top = new Rectangle(0, 0, (int)Width, 8);
    if (top.Contains(mousePosWindow))
        return new IntPtr(HTTOP);

    //偷懶x直接設40,因為那是Logo的位址
    Rectangle cap = new Rectangle(40, 8, (int)Width, this.dwmMargins.cyTopHeight - 8);
    if (cap.Contains(mousePosWindow))
        return new IntPtr(HTCAPTION);

    Rectangle left = new Rectangle(0, 0, 8, (int)Height);
    if (left.Contains(mousePosWindow))
        return new IntPtr(HTLEFT);

    Rectangle right = new Rectangle((int)Width - 8, 0, 8, (int)Height);
    if (right.Contains(mousePosWindow))
        return new IntPtr(HTRIGHT);

    Rectangle bottom = new Rectangle(0, (int)Height - 8, (int)Width, 8);
    if (bottom.Contains(mousePosWindow))
        return new IntPtr(HTBOTTOM);

    return new IntPtr(HTCLIENT);
}

 

 

如果沒有開啟Aero Theme

使用DWM必需要Vista作業系統以上,以及開啟Aero Theme不然就會像圖六一樣。

圖六

圖六 如果沒開Aero Theme將沒有效果

 

NOTE:

後來有發現Microsoft Ribbon中同樣的效果,是用WPF Shell Integration Library組件中WindowChrome來處理,看原始檔同樣都是呼叫DWM,不過它會處理不少東西,可省下不少功夫,不過還沒研究,等下一篇在來說明如何使用。

相關文章

[MSDN]Experiments with WindowChrome

[Code Project]WPF Custom Chrome Library

下載範例程式