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&lt;System.String, List&lt;System.String&gt;&gt;.</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

 

posted @ 2024-04-17 10:41  bodong  阅读(171)  评论(0编辑  收藏  举报