Avalonia实现Visual Studio风格标题栏的方法
Visual Studio风格的标题栏可以更节省屏幕空间,个人认为其实比Ribbonbar和传统菜单都要更先进一些,更紧凑,利用效率更高。
我在AvaloniaSamples项目中添加了一个这种Demo,展示了如何在Avalonia 11中分别实现经典风格、Macos风格和Visual Studio风格的标题栏:
关键点就在于MainWindow.axaml和MainWindowViewModel.cs中。
using Avalonia.Platform; using System.ComponentModel; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; namespace AvaloniaVisualStudioTitleBar.ViewModels { public class MainWindowViewModel : ViewModelBase { public string AppName => "Avalonia.Visual Studio Title Bar Demo"; /// <summary> /// Gets the modern style. /// </summary> /// <value>The modern style.</value> public ModernStyleDataModel ModernStyle { get; private set; } = new ModernStyleDataModel(); public object[] Styles => Enum.GetValues(typeof(ModernStyleType)).Cast<object>().ToArray(); public object Style { get => ModernStyle.Style; set => ModernStyle.Style = value != null ? (ModernStyleType)value :ModernStyleType.Default; } } #region Demo Support /// <summary> /// Enum ModernStyleType /// </summary> public enum ModernStyleType { /// <summary> /// The default /// </summary> Default, /// <summary> /// The windows metro /// </summary> WindowsMetro, /// <summary> /// The mac os /// </summary> MacOS, /// <summary> /// The classic /// </summary> Classic } #region Attributes /// <summary> /// Class DependsOnPropertyAttribute. /// Implements the <see cref="Attribute" /> /// </summary> /// <seealso cref="Attribute" /> [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] public class DependsOnPropertyAttribute : Attribute { /// <summary> /// The dependency properties /// </summary> public readonly string[] DependencyProperties; /// <summary> /// Initializes a new instance of the <see cref="DependsOnPropertyAttribute"/> class. /// </summary> /// <param name="propertyNames">The property names.</param> public DependsOnPropertyAttribute(params string[] propertyNames) { DependencyProperties = propertyNames; } } #endregion #region ReactiveObject /// <summary> /// Interface IReactiveObject /// Implements the <see cref="BluePrint.Common.ComponentModel.INotifyPropertyChanged" /> /// Implements the <see cref="BluePrint.Common.ComponentModel.INotifyPropertyChanging" /> /// </summary> /// <seealso cref="BluePrint.Common.ComponentModel.INotifyPropertyChanged" /> /// <seealso cref="BluePrint.Common.ComponentModel.INotifyPropertyChanging" /> public interface IReactiveObject : INotifyPropertyChanged { /// <summary> /// Raises the property changed. /// </summary> /// <param name="propertyName">Name of the property.</param> void RaisePropertyChanged(string propertyName); } public static class TypeExtensions { /// <summary> /// Gets the custom attributes. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="memberInfo">The member information.</param> /// <param name="inherit">if set to <c>true</c> [inherit].</param> /// <returns>T[].</returns> public static T[] GetCustomAttributes<T>(this MemberInfo memberInfo, bool inherit = true) where T : Attribute { var Attrs = memberInfo.GetCustomAttributes(typeof(T), inherit); if (Attrs != null && Attrs.Length > 0) { return Attrs.Cast<T>().ToArray(); } return new T[0]; } } /// <summary> /// Class ReactiveObject. /// Implements the <see cref="BluePrint.Common.ComponentModel.INotifyPropertyChanged" /> /// </summary> /// <seealso cref="BluePrint.Common.ComponentModel.INotifyPropertyChanged" /> public class ReactiveObject : IReactiveObject { #region Properties private Stack<string> ProcessStack = new Stack<string>(); #endregion #region Constructor /// <summary> /// Initializes a new instance of the <see cref="ReactiveObject"/> class. /// </summary> public ReactiveObject() { PropertyChanged += ProcessPropertyChanged; AutoCollectDependsInfo(); } private static Dictionary<System.Type, Dictionary<string, List<string>>> MetaCaches = new Dictionary<Type, Dictionary<string, List<string>>>(); /// <summary> /// Automatics the collect depends information. /// </summary> private void AutoCollectDependsInfo() { var type = GetType(); if (MetaCaches.TryGetValue(type, out var cache)) { return; } cache = new Dictionary<string, List<string>>(); foreach (var property in type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)) { foreach (var attr in property.GetCustomAttributes<DependsOnPropertyAttribute>()) { foreach (var name in attr.DependencyProperties) { if (!cache.TryGetValue(name, out var relevance)) { relevance = new List<string>(); cache.Add(name, relevance); } relevance.Add(property.Name); } } } if (cache.Count > 0) { MetaCaches[type] = cache; } else { MetaCaches[type] = null; } } /// <summary> /// Gets the cache. /// </summary> /// <param name="type">The type.</param> /// <returns>Dictionary<System.String, List<System.String>>.</returns> private static Dictionary<string, List<string>> GetCache(Type type) { MetaCaches.TryGetValue(type, out var cache); return cache; } #endregion #region Interfaces /// <summary> /// Occurs when a property value changes. /// </summary> public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; /// <summary> /// Raises the property changed. /// </summary> /// <param name="propertyName">Name of the property.</param> public void RaisePropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); } #endregion #region Methods /// <summary> /// Processes the property changed. /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param> protected virtual void ProcessPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { if (ProcessStack.Contains(e.PropertyName)) { return; } var RelativeProperties = GetCache(GetType()); if (RelativeProperties != null && RelativeProperties.TryGetValue(e.PropertyName, out var relevance)) { try { ProcessStack.Push(e.PropertyName); foreach (var r in relevance) { RaisePropertyChanged(r); } } finally { ProcessStack.Pop(); } } } #endregion } #endregion #region Extensions /// <summary> /// Class ReactiveObjectExtensions. /// </summary> public static class ReactiveObjectExtensions { /// <summary> /// Raises the and set if changed. /// </summary> /// <typeparam name="TObj">The type of the t object.</typeparam> /// <typeparam name="TRet">The type of the t ret.</typeparam> /// <param name="reactiveObject">The reactive object.</param> /// <param name="backingField">The backing field.</param> /// <param name="newValue">The new value.</param> /// <param name="propertyName">Name of the property.</param> /// <returns>TRet.</returns> public static TRet RaiseAndSetIfChanged<TObj, TRet>(this TObj reactiveObject, ref TRet backingField, TRet newValue, [CallerMemberName] string propertyName = null ) where TObj : IReactiveObject { if (EqualityComparer<TRet>.Default.Equals(backingField, newValue)) { return newValue; } backingField = newValue; reactiveObject.RaisePropertyChanged(propertyName); return newValue; } /// <summary> /// Raises the property changed. /// </summary> /// <typeparam name="TSender">The type of the t sender.</typeparam> /// <param name="reactiveObject">The reactive object.</param> /// <param name="propertyName">Name of the property.</param> public static void RaisePropertyChanged<TSender>(this TSender reactiveObject, [CallerMemberName] string propertyName = null) where TSender : IReactiveObject { if (propertyName is not null) { reactiveObject.RaisePropertyChanged(propertyName); } } } #endregion /// <summary> /// Class ModernStyleDataModel. /// Implements the <see cref="ReactiveObject" /> /// </summary> /// <seealso cref="ReactiveObject" /> public class ModernStyleDataModel : ReactiveObject { bool ExtendClientAreaToDecorationsHintCore = false; /// <summary> /// Gets or sets a value indicating whether [extend client area to decorations hint]. /// </summary> /// <value><c>true</c> if [extend client area to decorations hint]; otherwise, <c>false</c>.</value> public bool ExtendClientAreaToDecorationsHint { get => ExtendClientAreaToDecorationsHintCore; set => this.RaiseAndSetIfChanged(ref ExtendClientAreaToDecorationsHintCore, value); } int ExtendClientAreaTitleBarHeightHintCore = 0; /// <summary> /// Gets or sets the extend client area title bar height hint. /// </summary> /// <value>The extend client area title bar height hint.</value> public int ExtendClientAreaTitleBarHeightHint { get => ExtendClientAreaTitleBarHeightHintCore; set => this.RaiseAndSetIfChanged(ref ExtendClientAreaTitleBarHeightHintCore, value); } /// <summary> /// The extend client area chrome hints core /// </summary> ExtendClientAreaChromeHints ExtendClientAreaChromeHintsCore = ExtendClientAreaChromeHints.Default; /// <summary> /// Gets or sets the extend client area chrome hints. /// </summary> /// <value>The extend client area chrome hints.</value> public ExtendClientAreaChromeHints ExtendClientAreaChromeHints { get => ExtendClientAreaChromeHintsCore; set => this.RaiseAndSetIfChanged(ref ExtendClientAreaChromeHintsCore, value); } /// <summary> /// Gets a value indicating whether this instance is windows style. /// </summary> /// <value><c>true</c> if this instance is windows style; otherwise, <c>false</c>.</value> [DependsOnProperty(nameof(Style))] public bool IsWindowsStyle => Style == ModernStyleType.WindowsMetro || (Style == ModernStyleType.Default && OperatingSystem.IsWindows()); /// <summary> /// Gets a value indicating whether this instance is mac os style. /// </summary> /// <value><c>true</c> if this instance is mac os style; otherwise, <c>false</c>.</value> [DependsOnProperty(nameof(Style))] public bool IsMacOSStyle => Style == ModernStyleType.MacOS || (Style == ModernStyleType.Default && OperatingSystem.IsMacOS()); /// <summary> /// Gets a value indicating whether this instance is classic style. /// </summary> /// <value><c>true</c> if this instance is classic style; otherwise, <c>false</c>.</value> [DependsOnProperty(nameof(Style))] public bool IsClassicStyle => Style == ModernStyleType.Classic || (Style == ModernStyleType.Default && OperatingSystem.IsLinux()); /// <summary> /// Gets a value indicating whether this instance is modern style. /// </summary> /// <value><c>true</c> if this instance is modern style; otherwise, <c>false</c>.</value> [DependsOnProperty(nameof(Style))] public bool IsModernStyle => IsWindowsStyle || IsMacOSStyle; ModernStyleType StyleCore = ModernStyleType.Default; /// <summary> /// Gets or sets the style. /// </summary> /// <value>The style.</value> public ModernStyleType Style { get => StyleCore; set => this.RaiseAndSetIfChanged(ref StyleCore, value); } /// <summary> /// Initializes a new instance of the <see cref="ModernStyleDataModel"/> class. /// </summary> public ModernStyleDataModel() { StyleCore = ApplicationSettings.Default.ModernStyle; this.PropertyChanged += OnPropertyChanged; // force reset values RaisePropertyChanged(nameof(Style)); ApplicationSettings.Default.PropertyChanged += OnApplicationSettingChanged; } private void OnApplicationSettingChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(ApplicationSettings.Default.ModernStyle)) { this.Style = ApplicationSettings.Default.ModernStyle; } } private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(Style)) { if (IsMacOSStyle || IsWindowsStyle) { ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome; ExtendClientAreaTitleBarHeightHint = -1; ExtendClientAreaToDecorationsHint = true; } else { ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.Default; ExtendClientAreaTitleBarHeightHint = 0; ExtendClientAreaToDecorationsHint = false; } } } } public class ApplicationSettings : ReactiveObject { public static readonly ApplicationSettings Default = new ApplicationSettings(); ModernStyleType _ModernStyle = ModernStyleType.Default; public ModernStyleType ModernStyle { get => _ModernStyle; set => this.RaiseAndSetIfChanged(ref _ModernStyle, value); } public void Save() { // todo... } } #endregion }
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:AvaloniaVisualStudioTitleBar.ViewModels" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="AvaloniaVisualStudioTitleBar.Views.MainWindow" Icon="/Assets/avalonia-logo.ico" Title="{Binding AppName}" ExtendClientAreaToDecorationsHint="{Binding ModernStyle.ExtendClientAreaToDecorationsHint}" ExtendClientAreaTitleBarHeightHint="{Binding ModernStyle.ExtendClientAreaTitleBarHeightHint}" ExtendClientAreaChromeHints="{Binding ModernStyle.ExtendClientAreaChromeHints}" > <Grid RowDefinitions="Auto,*" ColumnDefinitions="1*,Auto,1*"> <Grid ColumnDefinitions="Auto, Auto, *,Auto"> <Image Grid.Column="0" IsVisible="{Binding ModernStyle.IsWindowsStyle}" Source="/Assets/avalonia-logo.ico" Width="24" Height="24" Margin="4" VerticalAlignment="Center" HorizontalAlignment="Center" DoubleTapped="CloseWindow"></Image> <StackPanel Name="macButtonsStackPanel" Orientation="Horizontal" DockPanel.Dock="Left" Grid.Column="0" Spacing="6" Margin="6,0,0,0" IsVisible="{Binding ModernStyle.IsMacOSStyle}" Background="Transparent"> <StackPanel.Styles> <Style Selector="StackPanel:pointerover Path"> <Setter Property="IsVisible" Value="true"></Setter> </Style> <Style Selector="StackPanel:not(:pointerover) Path"> <Setter Property="IsVisible" Value="false"></Setter> </Style> </StackPanel.Styles> <Button Name="macCloseButton" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" VerticalAlignment="Center" Click="CloseWindow2" Width="16" Height="16"> <Button.Resources> <CornerRadius x:Key="ControlCornerRadius">12</CornerRadius> </Button.Resources> <Button.Styles> <Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter"> <Setter Property="Background" Value="#99FF5D55"/> </Style> <Style Selector="Button:not(:pointerover) /template/ ContentPresenter#PART_ContentPresenter"> <Setter Property="Background" Value="#FF5D55"/> </Style> </Button.Styles> <Path Data="M 0,0 l 10,10 M 0,10 l 10,-10" Stroke="#4C0102" StrokeThickness="1" Width="10" Height="10"></Path> </Button> <Button Name="macMinimizeButton" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" VerticalAlignment="Center" Click="MinimizeWindow" Width="16" Height="16"> <Button.Resources> <CornerRadius x:Key="ControlCornerRadius">12</CornerRadius> </Button.Resources> <Button.Styles> <Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter"> <Setter Property="Background" Value="#99FFBC2E"/> </Style> <Style Selector="Button:not(:pointerover) /template/ ContentPresenter#PART_ContentPresenter"> <Setter Property="Background" Value="#FFBC2E"/> </Style> </Button.Styles> <Path Data="M 0,0 l 12,0" Stroke="#985712" StrokeThickness="1" Width="12" Height="1"></Path> </Button> <Button Name="macZoomButton" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" VerticalAlignment="Center" Click="MaximizeWindow" Width="16" Height="16"> <Button.Resources> <CornerRadius x:Key="ControlCornerRadius">12</CornerRadius> </Button.Resources> <Button.Styles> <Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter"> <Setter Property="Background" Value="#9928C83E"/> </Style> <Style Selector="Button:not(:pointerover) /template/ ContentPresenter#PART_ContentPresenter"> <Setter Property="Background" Value="#28C83E"/> </Style> </Button.Styles> <Path Data="M 0,10 l 8,0 l -8,-8 l 0,8 M 10,0 l 0,8 l -8,-8 l 8,0" Fill="#0A630C" StrokeThickness="0" Width="10" Height="10"></Path> </Button> </StackPanel> <Menu Name="sharedMainMenu" Grid.Column="1"> <MenuItem Header="File"> <MenuItem Header="New"></MenuItem> <Separator/> <MenuItem Header="Open"></MenuItem> </MenuItem> </Menu> <Panel Grid.Column="2" IsVisible="{Binding ModernStyle.IsMacOSStyle}" IsHitTestVisible="False"></Panel> <Image Grid.Column="3" IsVisible="{Binding ModernStyle.IsMacOSStyle}" IsHitTestVisible="False" Source="/Assets/avalonia-logo.ico" Width="24" Height="24" Margin="4" VerticalAlignment="Center" HorizontalAlignment="Right"></Image> </Grid> <StackPanel Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center" Orientation="Horizontal"> <TextBlock Text="{Binding AppName}" IsVisible="{Binding !ModernStyle.IsClassicStyle}" IsHitTestVisible="False" VerticalAlignment="Center"></TextBlock> </StackPanel> <StackPanel IsVisible="{Binding ModernStyle.IsWindowsStyle}" HorizontalAlignment="Right" Orientation="Horizontal" Spacing="0" Grid.Column="2" > <Button Width="46" Height="30" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" BorderThickness="0" Name="winMinimizeButton" Click="MinimizeWindow" ToolTip.Tip="Minimize"> <Button.Resources> <CornerRadius x:Key="ControlCornerRadius">0</CornerRadius> </Button.Resources> <Button.Styles> <Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter"> <Setter Property="Background" Value="#44AAAAAA"/> </Style> <Style Selector="Button:not(:pointerover) /template/ ContentPresenter#PART_ContentPresenter"> <Setter Property="Background" Value="Transparent"/> </Style> </Button.Styles> <Path Margin="10,0,10,0" Stretch="Uniform" Fill="{DynamicResource SystemControlForegroundBaseHighBrush}" Data="M2048 1229v-205h-2048v205h2048z"></Path> </Button> <Button Width="46" VerticalAlignment="Stretch" VerticalContentAlignment="Center" BorderThickness="0" Click="MaximizeWindow" Name="winMaximizeButton"> <ToolTip.Tip> <ToolTip Content="Maximize" Name="MaximizeToolTip"></ToolTip> </ToolTip.Tip> <Button.Resources> <CornerRadius x:Key="ControlCornerRadius">0</CornerRadius> </Button.Resources> <Button.Styles> <Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter"> <Setter Property="Background" Value="#44AAAAAA"/> </Style> <Style Selector="Button:not(:pointerover) /template/ ContentPresenter#PART_ContentPresenter"> <Setter Property="Background" Value="Transparent"/> </Style> </Button.Styles> <Path Margin="10,0,10,0" Stretch="Uniform" Fill="{DynamicResource SystemControlForegroundBaseHighBrush}" Name="winMaximizeIcon" Data="M2048 2048v-2048h-2048v2048h2048zM1843 1843h-1638v-1638h1638v1638z"></Path> </Button> <Button Width="46" VerticalAlignment="Stretch" VerticalContentAlignment="Center" BorderThickness="0" Name="winCloseButton" Click="CloseWindow2" Grid.Column="0" ToolTip.Tip="Close"> <Button.Resources> <CornerRadius x:Key="ControlCornerRadius">0</CornerRadius> </Button.Resources> <Button.Styles> <Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter"> <Setter Property="Background" Value="Red"/> </Style> <Style Selector="Button:not(:pointerover) /template/ ContentPresenter#PART_ContentPresenter"> <Setter Property="Background" Value="Transparent"/> </Style> <Style Selector="Button:pointerover > Path"> <Setter Property="Fill" Value="White"/> </Style> <Style Selector="Button:not(:pointerover) > Path"> <Setter Property="Fill" Value="{DynamicResource SystemControlForegroundBaseHighBrush}"/> </Style> </Button.Styles> <Path Margin="10,0,10,0" Stretch="Uniform" Data="M1169 1024l879 -879l-145 -145l-879 879l-879 -879l-145 145l879 879l-879 879l145 145l879 -879l879 879l145 -145z"></Path> </Button> </StackPanel> <ComboBox Grid.Row="1" Grid.Column="1" Width="250" ItemsSource="{Binding Styles}" SelectedItem="{Binding Style}"> </ComboBox> </Grid> </Window>
完整代码请参考github仓库:https://github.com/bodong1987/AvaloniaSamples/tree/main/AvaloniaVisualStudioTitleBar