WPF 製作支援點選穿透的高效能的透明背景異形視窗
技術標籤:WPFc#dotnetcoredotnetC#WPF
預設的 WPF 的支援點選穿透的透明背景視窗,是通過 AllowsTransparency 實現的,但是此方法的效能比較低。本文來告訴大家一個高效能的方法,通過此方法制作出來的 WPF 視窗可以獲取很高的效能,設定透明和設定視窗不透明之間幾乎沒有效能差別
本文的方法由 少珺 小夥伴提供,我只是代為整理部落格。本文的方法是基於 WPF 製作高效能的透明背景異形視窗(使用 WindowChrome 而不要使用 AllowsTransparency=True) - walterlv 但是 walterlv 大大的方法沒有提供可穿透的功能,而本文是提供了全穿透的功能
預設的 WPF 提供的 AllowsTransparency 的方法,這個方法可以適用在讓視窗透明的部分能點選穿透,視窗不透明部分點選不穿透。但根據 WPF 從最底層原始碼瞭解 AllowsTransparency 效能差的原因 可以瞭解到此方法的效能比較低
本文提供的方法是使用 WPF 製作高效能的透明背景異形視窗(使用 WindowChrome 而不要使用 AllowsTransparency=True) - walterlv 來實現高效能的,同時通過 WS_EX_TRANSPARENT 設定整個視窗全穿透
因此本文的方法是要麼整個視窗透明不穿透,要麼就是整個視窗透明穿透。而做不到和 WPF 提供的 AllowsTransparency 的方法讓透明的部分支援穿透。但本文的方法的效能特別強
在開始之前,請完全抄襲 WPF 製作高效能的透明背景異形視窗(使用 WindowChrome 而不要使用 AllowsTransparency=True) - walterlv 這篇部落格的內容
接下來給上面的這個方法新增支援全視窗點選穿透功能,因為本文使用到 WS_EX_TRANSPARENT 的方法設定視窗全穿透,此時需要給視窗加上 WS_EX_LAYERED 樣式。而在 WPF 中,如果視窗在未設定 AllowsTransparency = true 時,會自動去掉 WS_EX_LAYERED 樣式。根據完全開源的 WPF 倉庫,可以找到這段邏輯,放在 HwndTarget 類,如下面程式碼
public class HwndTarget : CompositionTarget
{
/// <summary>
/// The HwndTarget needs to see all windows messages so that
/// it can appropriately react to them.
/// </summary>
internal IntPtr HandleMessage(WindowMessage msg, IntPtr wparam, IntPtr lparam)
{
switch (msg)
{
// 忽略其他程式碼
case WindowMessage.WM_STYLECHANGING:
unsafe
{
NativeMethods.STYLESTRUCT * styleStruct = (NativeMethods.STYLESTRUCT *) lparam;
if ((int)wparam == NativeMethods.GWL_EXSTYLE)
{
// 這裡的 UsesPerPixelOpacity 屬性就是由 AllowsTransparency 決定的
if(UsesPerPixelOpacity)
{
// We need layered composition to accomplish per-pixel opacity.
//
styleStruct->styleNew |= NativeMethods.WS_EX_LAYERED;
}
else
{
// No properties that require layered composition exist.
// Make sure the layered bit is off.
//
// Note: this prevents an external program from making
// us system-layered (if we are a top-level window).
//
// If we are a child window, we still can't stop our
// parent from being made system-layered, and we will
// end up leaving visual artifacts on the screen under
// WindowsXP.
//
styleStruct->styleNew &= (~NativeMethods.WS_EX_LAYERED);
}
}
}
break;
}
}
}
為了能夠讓 WPF 支援在沒有設定 AllowsTransparency = true 時也能設定為 WS_EX_LAYERED 樣式,就需要使用一點 Hack 的程式碼,感謝 少珺 小夥伴找到這個有趣的方法。在 WPF 機制裡面,新增 AddHook 執行邏輯是有順序的,而上面程式碼的 HandleMessage 其實也是一個訊息迴圈的 Hook 的邏輯。為了讓 WPF 支援設定 WS_EX_LAYERED 樣式,可以在上面 HwndTarget 的邏輯執行完成之後,執行咱自己的邏輯,再設定一遍。此時因為咱的邏輯在 HwndTarget 之後執行,因此咱的邏輯就覆蓋了 HwndTarget 的設定
在視窗的 Loaded 事件裡面新增下面程式碼
private void PerformanceDesktopTransparentWindow_Loaded(object sender, RoutedEventArgs e)
{
((HwndSource)PresentationSource.FromVisual(this)).AddHook((IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) =>
{
//想要讓視窗透明穿透滑鼠和觸控等,需要同時設定 WS_EX_LAYERED 和 WS_EX_TRANSPARENT 樣式,
//確保視窗始終有 WS_EX_LAYERED 這個樣式,並在開啟穿透時設定 WS_EX_TRANSPARENT 樣式
//但是WPF視窗在未設定 AllowsTransparency = true 時,會自動去掉 WS_EX_LAYERED 樣式(在 HwndTarget 類中),
//如果設定了 AllowsTransparency = true 將使用WPF內建的低效能的透明實現,
//所以這裡通過 Hook 的方式,在不使用WPF內建的透明實現的情況下,強行保證這個樣式存在。
if (msg == (int)Win32.WM.STYLECHANGING && (long)wParam == (long)Win32.GetWindowLongFields.GWL_EXSTYLE)
{
var styleStruct = (STYLESTRUCT)Marshal.PtrToStructure(lParam, typeof(STYLESTRUCT));
styleStruct.styleNew |= (int)Win32.ExtendedWindowStyles.WS_EX_LAYERED;
Marshal.StructureToPtr(styleStruct, lParam, false);
handled = true;
}
return IntPtr.Zero;
});
}
此時就完成了讓視窗設定 WS_EX_LAYERED 這個樣式的功能了,以上程式碼完成之後,在設定視窗是否點選穿透,就可以用上 WS_EX_TRANSPARENT 樣式了,如下面程式碼
/// <summary>
/// 設定點選穿透到後面透明的視窗
/// </summary>
public void SetTransparentHitThrough()
{
if (_dwmEnabled)
{
Win32.User32.SetWindowLongPtr(_hwnd, Win32.GetWindowLongFields.GWL_EXSTYLE,
(IntPtr)(int)((long)Win32.User32.GetWindowLongPtr(_hwnd, Win32.GetWindowLongFields.GWL_EXSTYLE) | (long)Win32.ExtendedWindowStyles.WS_EX_TRANSPARENT));
}
else
{
Background = Brushes.Transparent;
}
}
/// <summary>
/// 設定點選命中,不會穿透到後面的視窗
/// </summary>
public void SetTransparentNotHitThrough()
{
if (_dwmEnabled)
{
Win32.User32.SetWindowLongPtr(_hwnd, Win32.GetWindowLongFields.GWL_EXSTYLE,
(IntPtr)(int)((long)Win32.User32.GetWindowLongPtr(_hwnd, Win32.GetWindowLongFields.GWL_EXSTYLE) & ~(long)Win32.ExtendedWindowStyles.WS_EX_TRANSPARENT));
}
else
{
Background = BrushCreator.GetOrCreate("#0100000");
}
}
通過 WS_EX_TRANSPARENT 樣式,就能設定視窗是否全穿透。上面程式碼用到了我定義的 Win32 的相關方法,這部分程式碼很多用到了 Enum 列舉的二進位制計算方法,因此看起來相對複雜一點
細心的小夥伴會看到,其實我是區分了 _dwmEnabled
才決定是否使用 WS_EX_TRANSPARENT 的方式設定透明,原因是 WPF 製作高效能的透明背景異形視窗(使用 WindowChrome 而不要使用 AllowsTransparency=True) - walterlv 的方法只支援在有開啟 DWM 的模式下才能用上,否則透明部分會顯示黑色
判斷是否開啟 DWM 可以使用 Dwmapi.dll 提供的 DwmIsCompositionEnabled 方法,如下面程式碼
public static class Dwmapi
{
public const string LibraryName = "Dwmapi.dll";
[DllImport(LibraryName, ExactSpelling = true, PreserveSig = false)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DwmIsCompositionEnabled();
}
在 win7 系統,可以動態更改這個值。但是在 Win10 系統預設都是開啟的
如果沒有開啟 DwmIsCompositionEnabled 那麼依然只能使用 AllowsTransparency 的方式設定透明
本文的沒有在部落格寫的程式碼包括了,如何設定視窗樣式以及 win32 方法的定義,這些程式碼我都放在 github 歡迎小夥伴訪問,這裡麵包含了所有邏輯,包括部落格裡面沒有放的程式碼
儘管上面程式碼有點 Hack 但我已經在嘗試在產品級使用了,暫時還沒有發現什麼鍋
我搭建了自己的部落格 https://blog.lindexi.com/ 歡迎大家訪問,裡面有很多新的部落格。只有在我看到部落格寫成熟之後才會放在csdn或部落格園,但是一旦釋出了就不再更新
如果在部落格看到有任何不懂的,歡迎交流,我搭建了 dotnet 職業技術學院 歡迎大家加入
如有不方便在部落格評論的問題,可以加我 QQ 2844808902 交流
本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名林德熙(包含連結:http://blog.csdn.net/lindexi_gd ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。如有任何疑問,請與我聯絡。