[UWP]佔領標題欄

  • 2019 年 10 月 3 日
  • 筆記

1. 前言

每一個有理想的UWP應用都會打標題欄的主意,尤其當微軟提供 將 Acrylic 擴展到標題欄 這個功能後,大部分Windows 10的原生應用都不乖了,紛紛佔領了標題欄的一畝三分地。這篇博客將介紹在UWP中如何自定義標題欄。

2.示例代碼

UWP的限制很多,標題欄的自定義幾乎全部內容集中在 這篇文檔 裏面。但只參考這篇文章做起來還不夠順手,我參考了微軟開源的計算器應用中的 TitleBar 寫了一個示例應用,可以在 這裡 查看它的源碼。我也把TitleBar實際應用到了我的 OnePomodoro 應用裏面了。

3. 簡單的顏色自定義

如果只想簡單地自定義標題欄的顏色可以通過ApplicationViewTitleBar,ApplicationViewTitleBar表示應用程序的標題欄,它提供了一些顏色屬性用於控制標題欄的顏色,示例代碼如下:

// using Windows.UI.ViewManagement;    var titleBar = ApplicationView.GetForCurrentView().TitleBar;    // Set active window colors  titleBar.ForegroundColor = Windows.UI.Colors.White;  titleBar.BackgroundColor = Windows.UI.Colors.Green;  titleBar.ButtonForegroundColor = Windows.UI.Colors.White;  titleBar.ButtonBackgroundColor = Windows.UI.Colors.SeaGreen;  titleBar.ButtonHoverForegroundColor = Windows.UI.Colors.White;  titleBar.ButtonHoverBackgroundColor = Windows.UI.Colors.DarkSeaGreen;  titleBar.ButtonPressedForegroundColor = Windows.UI.Colors.Gray;  titleBar.ButtonPressedBackgroundColor = Windows.UI.Colors.LightGreen;    // Set inactive window colors  titleBar.InactiveForegroundColor = Windows.UI.Colors.Gray;  titleBar.InactiveBackgroundColor = Windows.UI.Colors.SeaGreen;  titleBar.ButtonInactiveForegroundColor = Windows.UI.Colors.Gray;  titleBar.ButtonInactiveBackgroundColor = Windows.UI.Colors.SeaGreen;

有幾點需要注意:

  • 懸停和按下狀態的Background定義對關閉按鈕無效
  • Foreground不能設置透明

4. 將內容擴展到標題欄

若要隱藏默認標題欄並將你的內容擴展到標題欄區域中,請將 CoreApplicationViewTitleBar.ExtendViewIntoTitleBar 屬性設置為 true。CoreApplicationViewTitleBar允許應用定義在應用窗口中顯示的自定義標題欄。示例代碼如下:

// using Windows.ApplicationModel.Core;    // Hide default title bar.  var coreTitleBar = CoreApplication.GetCurrentView().TitleBar;  coreTitleBar.ExtendViewIntoTitleBar = true;

5. 將內容擴展到標題欄時自定義標題按鈕顏色

將內容擴展到標題欄,標題按鈕的顏色就變複雜了。因為應用內容的顏色可能和按鈕的顏色衝突。這種情況下有幾種方案,其中最簡單的一種方案是寫死為一個不會衝突的顏色,但切換主題時可能會讓這些顏色出問題。計算器應用中訂閱UISettings的ColorValuesChanged事件,動態地根據ThemeResources的值改變標題欄顏色,並且更進一步地考慮到使用高對比度主題的情況,所以訂閱了AccessibilitySettings的HighContrastChanged事件:

if (_accessibilitySettings.HighContrast)  {      // Reset to use default colors.      applicationTitleBar.ButtonBackgroundColor = null;      applicationTitleBar.ButtonForegroundColor = null;      applicationTitleBar.ButtonInactiveBackgroundColor = null;      applicationTitleBar.ButtonInactiveForegroundColor = null;      applicationTitleBar.ButtonHoverBackgroundColor = null;      applicationTitleBar.ButtonHoverForegroundColor = null;      applicationTitleBar.ButtonPressedBackgroundColor = null;      applicationTitleBar.ButtonPressedForegroundColor = null;  }  else  {      Color bgColor = Colors.Transparent;      Color fgColor = ((SolidColorBrush)Application.Current.Resources["SystemControlPageTextBaseHighBrush"]).Color;      Color inactivefgColor = ((SolidColorBrush)Application.Current.Resources["SystemControlForegroundChromeDisabledLowBrush"]).Color;      Color hoverbgColor = ((SolidColorBrush)Application.Current.Resources["SystemControlBackgroundListLowBrush"]).Color;      Color hoverfgColor = ((SolidColorBrush)Application.Current.Resources["SystemControlForegroundBaseHighBrush"]).Color;      Color pressedbgColor = ((SolidColorBrush)Application.Current.Resources["SystemControlBackgroundListMediumBrush"]).Color;      Color pressedfgColor = ((SolidColorBrush)Application.Current.Resources["SystemControlForegroundBaseHighBrush"]).Color;        applicationTitleBar.ButtonBackgroundColor = bgColor;      applicationTitleBar.ButtonForegroundColor = fgColor;      applicationTitleBar.ButtonInactiveBackgroundColor = bgColor;      applicationTitleBar.ButtonInactiveForegroundColor = inactivefgColor;      applicationTitleBar.ButtonHoverBackgroundColor = hoverbgColor;      applicationTitleBar.ButtonHoverForegroundColor = hoverfgColor;      applicationTitleBar.ButtonPressedBackgroundColor = pressedbgColor;      applicationTitleBar.ButtonPressedForegroundColor = pressedfgColor;  }

這段代碼中,當使用高對比度主題時將標題欄的按鈕顏色還原成默認值,否則設置成ThemeResource中對應的顏色,運行效果如下:

但現在的UWP應用常常在Dark和Light主題之間反覆橫跳,而Application.Current.Resources只能拿到程序加載時的ThemeResource的值,所以這段代碼在應用內的主題切換後無效。我暫時不清楚怎麼在代碼里拿到最新的ThemeResource,為解決這個問題只好讓TitleBar自己在XAML中獲取當前的ThemeResource,代碼如下:

<UserControl.Resources>      <SolidColorBrush x:Key="ButtonForegroundColor"                       Color="{ThemeResource SystemBaseHighColor}" />      <SolidColorBrush x:Key="ButtonInactiveForegroundBrush"                       Color="{ThemeResource SystemChromeDisabledLowColor}" />      <SolidColorBrush x:Key="ButtonHoverBackgroundBrush"                       Color="{ThemeResource SystemListLowColor}" />      <SolidColorBrush x:Key="ButtonHoverForegroundBrush"                       Color="{ThemeResource SystemBaseHighColor}" />      <SolidColorBrush x:Key="ButtonPressedBackgroundBrush"                       Color="{ThemeResource SystemListMediumColor}" />      <SolidColorBrush x:Key="ButtonPressedForegroundBrush"                       Color="{ThemeResource SystemBaseHighColor}" />  </UserControl.Resources>
Color fgColor = ((SolidColorBrush)Resources["ButtonForegroundColor"]).Color;  Color inactivefgColor = ((SolidColorBrush)Resources["ButtonInactiveForegroundBrush"]).Color;  Color hoverbgColor = ((SolidColorBrush)Resources["ButtonHoverBackgroundBrush"]).Color;  Color hoverfgColor = ((SolidColorBrush)Resources["ButtonHoverForegroundBrush"]).Color;  Color pressedbgColor = ((SolidColorBrush)Resources["ButtonPressedBackgroundBrush"]).Color;  Color pressedfgColor = ((SolidColorBrush)Resources["ButtonPressedForegroundBrush"]).Color;

6. 可拖動區域

都將內容擴展到標題欄了,肯定是想在標題欄上放置自己需要的UI元素,默認情況下標題欄的範圍為拖動、點擊等Windows的窗體行為保留,在這個範圍的自定義UI內容沒辦法獲取鼠標點擊。 為了讓自定義的UI內容獲取鼠標,可以用Window.SetTitleBar方法指定某一元素能用於窗體的拖動和點擊。

<Grid x:Name="LayoutRoot"        Height="32"        HorizontalAlignment="Stretch">      <Grid x:Name="BackgroundElement"            Height="32"            Background="Transparent" />      <StackPanel Orientation="Horizontal">          <StackPanel x:Name="ItemsPanel" Orientation="Horizontal">          </StackPanel>            <TextBlock x:Name="AppName"                     x:Uid="AppName"                     Text="ExtendViewIntoTitleBarDemo"          </StackPanel>  </Grid>
Window.Current.SetTitleBar(BackgroundElement);

上面的代碼指定TitlaBar中的BackgroundElement元素為可拖動區域,而下面的StackPanel則用於放置交互內容,例如標題或後退按鈕。這個StackPanel必須比BackgroundElement具有較高的Z順序才能接收到用戶的鼠標輸入。

7. 標題的系統保留區域

標題欄的右邊有188像素的系統保留區域,用於系統標題按鈕(「後退」、「最小化」、「最大化」、「關閉」)。其實這幾個按鈕也就佔用了141像素的控件,還有一小塊空間是默認的可拖動區域,這小塊空間確保了無論怎麼設置都總有一個用戶可拖動的區域。

上面說的188像素是100%縮放的情況,通過上面的截圖可以看到實際上可能不一樣,通常來說會在窗體加載時,或者訂閱CoreApplicationViewTitleBar.LayoutMetricsChanged事件,然後通過CoreApplicationViewTitleBar獲取具體的值。

_coreTitleBar.LayoutMetricsChanged += OnLayoutMetricsChanged;    private void OnLayoutMetricsChanged(CoreApplicationViewTitleBar sender, object args)  {      LayoutRoot.Height = _coreTitleBar.Height;      SetTitleBarPadding();  }    private void SetTitleBarPadding()  {      double leftAddition = 0;      double rightAddition = 0;        if (FlowDirection == FlowDirection.LeftToRight)      {          leftAddition = _coreTitleBar.SystemOverlayLeftInset;          rightAddition = _coreTitleBar.SystemOverlayRightInset;      }      else      {          leftAddition = _coreTitleBar.SystemOverlayRightInset;          rightAddition = _coreTitleBar.SystemOverlayLeftInset;      }        LayoutRoot.Padding = new Thickness(leftAddition, 0, rightAddition, 0);  }

8. 可交互區域的內容

上面的StackPanel是可交互區域,詳細的內容如下:

<StackPanel Orientation="Horizontal">      <StackPanel x:Name="ItemsPanel" Orientation="Horizontal">          <StackPanel.Resources>              <Style TargetType="Button"                     BasedOn="{StaticResource NavigationBackButtonNormalStyle}">                  <Setter Property="Foreground"                          Value="{StaticResource TitleBarForeground}" />                  <Setter Property="FontSize"                          Value="10" />                  <Setter Property="Width"                          Value="46" />                  <Setter Property="Height"                          Value="32" />                  <Setter Property="IsTabStop"                          Value="False" />              </Style>          </StackPanel.Resources>        </StackPanel>        <TextBlock x:Name="AppName"                 x:Uid="AppName"                 Text="ExtendViewIntoTitleBarDemo"                 Margin="12,0,12,0"                 HorizontalAlignment="Left"                 VerticalAlignment="Center"                 Foreground="{ThemeResource SystemControlPageTextBaseHighBrush}"                 FontSize="12"                 IsHitTestVisible="False"                 TextAlignment="Left"                 TextTrimming="CharacterEllipsis" />  </StackPanel>

其中AppName用於顯示標題欄,ItemsPanel用於放其它按鈕。TitleBar里定義了Buttons屬性,調用TitleBar可以通過Buttons屬性指定按鈕(這部分代碼我凌晨兩點寫的,寫得十分敷衍,但寫完又懶得改了)。

public ObservableCollection<Button> Buttons { get; } = new ObservableCollection<Button>();    private void OnButtonsCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)  {      ItemsPanel.Children.Clear();      foreach (var button in Buttons)      {          ItemsPanel.Children.Add(button);      }  }
<local:TitleBar>      <local:TitleBar.Buttons>          <Button x:Name="OptionsButton"                  Content="&#xE10C;"                  ToolTipService.ToolTip="Options" />          <Button Content="&#xE11C;"                  ToolTipService.ToolTip="Options" />          <Button Content="&#xE13C;"                  ToolTipService.ToolTip="Options" />          <Button Content="&#xE12C;"                  ToolTipService.ToolTip="Options" />      </local:TitleBar.Buttons>  </local:TitleBar>

按鈕的樣式來自NavigationBackButtonNormalStyle並稍作修改,大致上做到和標準的標題欄按鈕一樣。

9. 非激活狀態的標題欄顏色

當窗體處於非激活狀態應該讓按鈕和標題都變灰,可以訂閱WindowActivated事件,在非激活狀態時改變顏色:

Window.Current.Activated += OnWindowActivated;    private void OnWindowActivated(Object sender, WindowActivatedEventArgs e)  {      VisualStateManager.GoToState(          this, e.WindowActivationState == CoreWindowActivationState.Deactivated ? WindowNotFocused.Name : WindowFocused.Name, false);  }
<UserControl.Resources>      <SolidColorBrush x:Key="TitleBarForeground"                       x:Name="TitleBarForeground"                       Color="{ThemeResource SystemBaseHighColor}" />  </UserControl.Resources>  <Grid x:Name="LayoutRoot"        Height="32"        HorizontalAlignment="Stretch">      <VisualStateManager.VisualStateGroups>          <VisualStateGroup x:Name="WindowFocusStates">              <VisualState x:Name="WindowFocused" />              <VisualState x:Name="WindowNotFocused">                  <VisualState.Setters>                      <Setter Target="AppName.Foreground"                              Value="{ThemeResource SystemControlForegroundChromeDisabledLowBrush}" />                      <Setter Target="TitleBarForeground.Color"                              Value="{ThemeResource SystemChromeDisabledLowColor}" />                  </VisualState.Setters>              </VisualState>          </VisualStateGroup>       </VisualStateManager.VisualStateGroups>

10. 全屏和平板模式

當應用在全屏或平板模式下運行時,系統將隱藏標題欄和標題控制按鈕。 但是,用戶可以調用標題欄,以使其以覆蓋形式顯示在應用的 UI 頂部。 你可以處理隱藏或調用標題欄時將通知的 CoreApplicationViewTitleBar.IsVisibleChanged 事件,並根據需要顯示或隱藏你的自定義標題欄內容。

LayoutRoot.Visibility = _coreTitleBar.IsVisible ? Visibility.Visible : Visibility.Collapsed;

這部分比較難截圖就不搞了,想看效果可以試玩我的番茄鍾應用

11.結語

就這樣,令人頭痛的自定義標題欄處理完了。還好微軟開源了它的計算器里正好有我需要的代碼,抄了個爽。有一些處理得不好,如果錯誤請指正。

12.參考

標題欄自定義

calculator_TitleBar.xaml.cpp at master

ApplicationViewTitleBar Class (Windows.UI.ViewManagement) – Windows UWP applications Microsoft Docs

CoreApplicationViewTitleBar Class (Windows.ApplicationModel.Core) – Windows UWP applications Microsoft Docs

13. 源碼

DinoChan_ExtendViewIntoTitleBarDemo How to handle titlebar when ExtendViewIntoTitleBar

OnePomodoro_TitleBar.xaml at master