WPF中為Popup和ToolTip使用WindowMaterial特效 win10/win11

TwilightLemon發表於2024-10-15

先看效果圖:

FluentToolTipFluentPopup

大致思路是:透過反射獲取Popup內部的原生視窗控制代碼,然後透過前文已經實現的WindowMaterial類來應用視窗特效;對於ToolTip,為了保持其易用性,我使用了附加屬性+全域性樣式的方式來實現,ToolTip也是一個特殊的Popup.
前文連結:WPF 模擬UWP原生視窗樣式——亞克力|雲母材質、自定義標題欄樣式、原生DWM動畫 (附我封裝好的類)
本文的Demo:

TwilightLemon/WindowEffectTest: 測試win10/11的模糊效果 (github.com)

一、獲取原生視窗控制代碼

透過查閱.NET原始碼得知,Popup內部透過一個型別為PopupSecurityHelper的私有欄位_secHelper來管理視窗hWnd,並且在建立完成之時會觸發Popup.Opened事件。
透過反射來獲取視窗控制代碼:

const BindingFlags privateInstanceFlag = BindingFlags.NonPublic | BindingFlags.Instance;
public static IntPtr GetNativeWindowHwnd(this Popup popup)
{
    //獲取Popup內部的_secHelper欄位Info
    var field = typeof(Popup).GetField("_secHelper", privateInstanceFlag);
    if (field != null)
    {
        //獲取popup的_secHelper欄位值
        if (field.GetValue(popup) is { } _secHelper)
        {
            //獲取_secHelper的Handle屬性Info
            if (_secHelper.GetType().GetProperty("Handle", privateInstanceFlag) is { } prop)
            {
                if (prop.GetValue(_secHelper) is IntPtr handle)
                {
                    //返回控制代碼
                    return handle;
                }
            }
        }
    }
    //未找到
    return IntPtr.Zero;
}

同樣地,能在ToolTip內部找到私有欄位_parentPopup

public static IntPtr GetNativeWindowHwnd(this ToolTip tip)
{
    var field=tip.GetType().GetField("_parentPopup", privateInstanceFlag);
    if (field != null)
    {
        if(field.GetValue(tip) is Popup{ } popup)
        {
            return popup.GetNativeWindowHwnd();
        }
    }
    return IntPtr.Zero;
}

二、應用WindowMaterial特效

有了視窗控制代碼那麼一切都好辦了,直接呼叫我封裝好的WindowMaterial類,如果你想了解更多請檢視前文。

public static void SetPopupWindowMaterial(IntPtr hwnd,Color compositionColor,
     MaterialApis.WindowCorner corner= MaterialApis.WindowCorner.Round)
{
    if (hwnd != IntPtr.Zero)
    {
        int hexColor = compositionColor.ToHexColor();
        var hwndSource = HwndSource.FromHwnd(hwnd);
        //----
        MaterialApis.SetWindowProperties(hwndSource, 0);
        MaterialApis.SetWindowComposition(hwnd, true, hexColor);
        //----
        MaterialApis.SetWindowCorner(hwnd, corner);
    }
}

根據微軟的設計規範,這裡預設對普通Popup使用圓角,對ToolTip使用小圓角,使用亞克力材質並附加compositionColor。
在github中獲取完整的WindowMaterial.cs,我可能會不定期地更新它:WindowEffectTest/WindowMaterial.cs at master · TwilightLemon/WindowEffectTest (github.com)

如果你想使用Mica或MicaAlt等材質則將上面框起來的程式碼替換為:

MaterialApis.SetWindowProperties(hwndSource, -1);
MaterialApis.SetBackDropType(hwnd, MaterialType.Mica);
MaterialApis.SetDarkMode(hwnd, isDarkMode: true);

三、沒錯我又封裝了一個即開即用的類

在Demo中檢視封裝好的類:WindowEffectTest/FluentPopup.cs at master · TwilightLemon/WindowEffectTest (github.com)

使用FluentPopup

<local:FluentPopup x:Name="testPopup"
                   StaysOpen="False"
                   Placement="Mouse"
                   Background="{DynamicResource PopupWindowBackground}">
    <Grid Height="120" Width="180">
        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center">
            nihao 
        </TextBlock>
    </Grid>
</local:FluentPopup>

其中,Background屬性是我自定義的一個依賴屬性,只允許使用SolidColorBrush,用於設定亞克力特效的CompositionColor。
如果你希望像Demo中那樣讓Popup失焦自動關閉,可以設定StaysOpenFalse,在後臺開啟Popup時使用:

private async void ShowPopupBtn_Click(object sender, RoutedEventArgs e)
{
    await Task.Yield();
    testPopup.IsOpen = true;
}

關於為什麼要加上await Task.Tield(),可以看看呂毅大佬的文章:一點點從坑裡爬出來:如何正確開啟 WPF 裡的 Popup? - Walterlv

在全域性內使用FluentToolTip

我自定義了一個附加屬性FluentTooltip.UseFluentStyle,你只需要在App.xaml中設定即可全域性生效: 這裡的PopupWindowBackgroundForeColor是我自定義的顏色資源,你可以根據自己的需要來設定。
同樣地Background僅支援SolidColorBrush

<Style TargetType="{x:Type ToolTip}">
    <Setter Property="local:FluentTooltip.UseFluentStyle"
            Value="True" />
    <Setter Property="Background"
            Value="{DynamicResource PopupWindowBackground}" />
    <Setter Property="Foreground"
            Value="{DynamicResource ForeColor}" />
</Style>

這樣你就可以方便地建立一個Fluent風格的ToolTip了:

<Button 
    ToolTip="xxxxx"
    />

參考連結

Popup.cs at Dotnet Source

ToolTip.cs at Dotnet Source

一點點從坑裡爬出來:如何正確開啟 WPF 裡的 Popup? - Walterlv

WPF 模擬UWP原生視窗樣式——亞克力|雲母材質、自定義標題欄樣式、原生DWM動畫 (附我封裝好的類) - TwilightLemon

本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名TwilightLemon(https://blog.twlmgatito.cn),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。

文章及其程式碼倉庫可能不時更新,檢視原文:WPF中為Popup和ToolTip使用WindowMaterial特效 win10/win11 - Twlm's Blog (twlmgatito.cn)

相關文章