前陣子在使用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 – ★★★
[Code Project]Vista Aero ToolStrip on Non-Client Area - ★★★★★
[Code Project]How to Support the Ribbon and a Menu in the Same Executable - C++
圖二 範例執行畫面
用戶區(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,不過它會處理不少東西,可省下不少功夫,不過還沒研究,等下一篇在來說明如何使用。
相關文章