WPF 实现 Gitee 泡泡菜单

  • 框架使用大于等于.NET40
  • Visual Studio 2022;
  • 项目使用 MIT 开源许可协议;.
WPF 实现 Gitee 泡泡菜单
  • 需要实现泡泡菜单需要使用Canvas画布进行添加内容;
  • 保证颜色随机,位置不重叠;
  • 点击泡泡获得当前泡泡的值;

1) BubblleCanvas.cs 代码如下;

using System.Windows;
using System.Windows.Controls;
using WPFDevelopers.Helpers;
using WPFDevelopers.Utilities;

namespace WPFDevelopers.Controls
{
    public class BubblleCanvas : Canvas
    {
        private double _bubbleItemX;
        private double _bubbleItemY;

        private int _number;
        private double _size;
        private const int _maxSize = 120;

        protected override Size ArrangeOverride(Size arrangeSize)
        {
            var width = arrangeSize.Width;
            var height = arrangeSize.Height;

            double left = 0d, top = 0d;
            for (var y = 0; y < (int)height / _maxSize; y++)
            {
                double yNum = y + 1;
                yNum = _maxSize * yNum;
                for (var x = 0; x < (int)width / _maxSize; x++)
                {
                    if (_number > InternalChildren.Count - 1)
                        return arrangeSize;

                    var item = InternalChildren[_number] as FrameworkElement;

                    if (DoubleUtil.IsNaN(item.ActualWidth) || DoubleUtil.IsZero(item.ActualWidth) || DoubleUtil.IsNaN(item.ActualHeight) || DoubleUtil.IsZero(item.ActualHeight))
                        ResizeItem(item);

                    _bubbleItemX = Canvas.GetLeft(item);
                    _bubbleItemY = Canvas.GetTop(item);

                    if (double.IsNaN(_bubbleItemX) || double.IsNaN(_bubbleItemY))
                    {
                        double xNum = x + 1;
                        xNum = _maxSize * xNum;
                        _bubbleItemX = ControlsHelper.NextDouble(left, xNum - _size * ControlsHelper.NextDouble(0.6, 0.9));
                        var _width = _bubbleItemX + _size;
                        _width = _width > width ? width - (width - _bubbleItemX) - _size : _bubbleItemX;
                        _bubbleItemX = _width;
                        _bubbleItemY = ControlsHelper.NextDouble(top, yNum - _size * ControlsHelper.NextDouble(0.6, 0.9));
                        var _height = _bubbleItemY + _size;
                        _height = _height > height ? height - (height - _bubbleItemY) - _size : _bubbleItemY;
                        _bubbleItemY = _height;

                    }
                    Canvas.SetLeft(item, _bubbleItemX);
                    Canvas.SetTop(item, _bubbleItemY);
                    left = left + _size;

                    _number++;

                    item.Arrange(new Rect(new Point(_bubbleItemX, _bubbleItemY), new Size(_size, _size)));
                }
                left = 0d;
                top = top + _maxSize;
            }

            return arrangeSize;
        }
        private void ResizeItem(FrameworkElement item)
        {
            if (DoubleUtil.GreaterThanOrClose(item.DesiredSize.Width, 55))
                _size = ControlsHelper.GetRandom.Next(80, _maxSize);
            else
                _size = ControlsHelper.GetRandom.Next(55, _maxSize);
            item.Width = _size;
            item.Height = _size;
        }
    }
}

2) ControlsHelper.cs 代码如下;

  • 随机Double值;
  • 随机颜色;
 private static long _tick = DateTime.Now.Ticks;
        public static Random GetRandom = new Random((int)(_tick & 0xffffffffL) | (int)(_tick >> 32));

        public static double NextDouble(double miniDouble, double maxiDouble)
        {
            if (GetRandom != null)
            {
                return GetRandom.NextDouble() * (maxiDouble - miniDouble) + miniDouble;
            }
            else
            {
                return 0.0d;
            }
        }
        public static Brush RandomBrush()
        {
            var R = GetRandom.Next(255);
            var G = GetRandom.Next(255);
            var B = GetRandom.Next(255);
            var color = Color.FromRgb((byte)R, (byte)G, (byte)B);
            var solidColorBrush = new SolidColorBrush(color);
            return solidColorBrush;
        }

3) BubbleControl.cs 代码如下;

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using WPFDevelopers.Helpers;

namespace WPFDevelopers.Controls
{
    [TemplatePart(Name = BorderTemplateName, Type = typeof(Border))]
    [TemplatePart(Name = EllipseTemplateName, Type = typeof(Ellipse))]
    [TemplatePart(Name = RotateTransformTemplateName, Type = typeof(RotateTransform))]
    public class BubblleControl : Control
    {
        private const string BorderTemplateName = "PART_Border";
        private const string EllipseTemplateName = "PART_Ellipse";
        private const string RotateTransformTemplateName = "PART_EllipseRotateTransform";
        private const string ListBoxTemplateName = "PART_ListBox";

        private static readonly Type _typeofSelf = typeof(BubblleControl);

        private ObservableCollection<BubblleItem> _items = new ObservableCollection<BubblleItem>();


        private Border _border;
        private Ellipse _ellipse;
        private RotateTransform _rotateTransform;
        private Brush[] brushs;
        private ItemsControl _listBox;
        private static RoutedCommand _clieckCommand;

        class BubblleItem
        {
            public string Text { get; set; }
            public Brush Bg { get; set; }
        }

        static BubblleControl()
        {
            InitializeCommands();
            DefaultStyleKeyProperty.OverrideMetadata(_typeofSelf, new FrameworkPropertyMetadata(_typeofSelf));
        }

        #region Event

        public static readonly RoutedEvent ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), _typeofSelf);
        public event RoutedEventHandler Click
        {
            add { AddHandler(ClickEvent, value); }
            remove { RemoveHandler(ClickEvent, value); }
        }

        #endregion

        #region Command

        private static RoutedCommand _clickCommand = null;

        private static void InitializeCommands()
        {
            _clickCommand = new RoutedCommand("Click", _typeofSelf);

            CommandManager.RegisterClassCommandBinding(_typeofSelf, new CommandBinding(_clickCommand, OnClickCommand, OnCanClickCommand));
        }

        public static RoutedCommand ClickCommand
        {
            get { return _clickCommand; }
        }

        private static void OnClickCommand(object sender, ExecutedRoutedEventArgs e)
        {
            var ctrl = sender as BubblleControl;

            ctrl.SetValue(SelectedTextPropertyKey, e.Parameter?.ToString());
            ctrl.RaiseEvent(new RoutedEventArgs(ClickEvent));
        }

        private static void OnCanClickCommand(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = true;
        }

        #endregion

        #region readonly Properties

        private static readonly DependencyPropertyKey SelectedTextPropertyKey =
           DependencyProperty.RegisterReadOnly("SelectedText", typeof(string), _typeofSelf, new PropertyMetadata(null));
        public static readonly DependencyProperty SelectedTextProperty = SelectedTextPropertyKey.DependencyProperty;
        public string SelectedText
        {
            get { return (string)GetValue(SelectedTextProperty); }
        }
        public new static readonly DependencyProperty BorderBackgroundProperty =
            DependencyProperty.Register("BorderBackground", typeof(Brush), typeof(BubblleControl),
                new PropertyMetadata(null));

        public new static readonly DependencyProperty EarthBackgroundProperty =
            DependencyProperty.Register("EarthBackground", typeof(Brush), typeof(BubblleControl),
                new PropertyMetadata(Brushes.DarkOrchid));
        public Brush BorderBackground
        {
            get => (Brush)this.GetValue(BorderBackgroundProperty);
            set => this.SetValue(BorderBackgroundProperty, (object)value);
        }
        public Brush EarthBackground
        {
            get => (Brush)this.GetValue(EarthBackgroundProperty);
            set => this.SetValue(EarthBackgroundProperty, (object)value);
        }
        #endregion

        #region Property

        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource", typeof(IEnumerable<string>), typeof(BubblleControl), new PropertyMetadata(null, OnItemsSourcePropertyChanged));
        public IEnumerable<string> ItemsSource
        {
            get { return (IEnumerable<string>)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        private static void OnItemsSourcePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var ctrl = obj as BubblleControl;
            var newValue = e.NewValue as IEnumerable<string>;

            if (newValue == null)
            {
                ctrl._items.Clear();
                return;
            }

            foreach (var item in newValue)
            {
                ctrl._items.Add(new BubblleItem { Text = item, Bg = ControlsHelper.RandomBrush() });
            }
        }

        #endregion

        #region Override

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _border = GetTemplateChild(BorderTemplateName) as Border;
            _ellipse = GetTemplateChild(EllipseTemplateName) as Ellipse;
            _rotateTransform = GetTemplateChild(RotateTransformTemplateName) as RotateTransform;
            Loaded += delegate
            {
                var point = _border.TranslatePoint(new Point(_border.ActualWidth / 2, _border.ActualHeight / 2),
                    _ellipse);
                _rotateTransform.CenterX = point.X - _ellipse.ActualWidth / 2;
                _rotateTransform.CenterY = point.Y - _ellipse.ActualHeight / 2;
            };
            _listBox = GetTemplateChild(ListBoxTemplateName) as ItemsControl;
            _listBox.ItemsSource = _items;
        }

        #endregion
    }
}

4) BubblleControl.xaml 代码如下;

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:controls="clr-namespace:WPFDevelopers.Controls">

    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="Basic/ControlBasic.xaml"/>
        <ResourceDictionary Source="Basic/Animations.xaml"/>
    </ResourceDictionary.MergedDictionaries>

    <Style TargetType="controls:BubblleControl" BasedOn="{StaticResource ControlBasicStyle}">
        <Setter Property="Width" Value="400"/>
        <Setter Property="Height" Value="400"/>
        <Setter Property="Background" Value="{StaticResource WhiteSolidColorBrush}"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="BorderBrush" Value="{StaticResource SecondaryTextSolidColorBrush}"/>
        <Setter Property="BorderBackground" Value="{StaticResource BaseSolidColorBrush}"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="controls:BubblleControl">
                    <Grid Width="{TemplateBinding Width}" Height="{TemplateBinding Height}">
                        <Border BorderBrush="{TemplateBinding BorderBrush}"
                                                BorderThickness="{TemplateBinding BorderThickness}" 
                                                Background="{TemplateBinding BorderBackground}" 
                                                Margin="45"
                                                CornerRadius="400"
                                                x:Name="PART_Border">
                            <Ellipse Fill="{TemplateBinding Background}" Margin="20"/>
                        </Border>
                        <Ellipse Fill="{TemplateBinding EarthBackground}"
                                                 Width="26" Height="26"
                                                 RenderTransformOrigin=".5,.5"
                                                 x:Name="PART_Ellipse"
                                                 VerticalAlignment="Top" Margin="0,35,0,0">
                            <Ellipse.RenderTransform>
                                <RotateTransform x:Name="PART_EllipseRotateTransform"></RotateTransform>
                            </Ellipse.RenderTransform>
                            <Ellipse.Triggers>
                                <EventTrigger RoutedEvent="Loaded">
                                    <BeginStoryboard>
                                        <Storyboard>
                                            <DoubleAnimation Storyboard.TargetProperty="(Ellipse.RenderTransform).(RotateTransform.Angle)"
                                                                             RepeatBehavior="Forever"
                                                                             From="0" To="360"
                                                                             Duration="00:00:13"></DoubleAnimation>
                                        </Storyboard>
                                    </BeginStoryboard>
                                </EventTrigger>
                            </Ellipse.Triggers>
                        </Ellipse>
                        <ItemsControl x:Name="PART_ListBox"
                                      ItemsSource="{TemplateBinding ItemsSource}">
                            <ItemsControl.ItemTemplate>
                                <DataTemplate>
                                    <Grid>
                                        <Grid Width="{TemplateBinding Width}" 
                                              Height="{TemplateBinding Height}">

                                            <Ellipse Fill="{Binding Bg}"
                                                                 Opacity=".4"/>
                                            <Ellipse Stroke="{Binding Bg}" 
                                                                 StrokeThickness=".8"/>
                                        </Grid>

                                        <TextBlock VerticalAlignment="Center" 
                                                               HorizontalAlignment="Center"
                                                               Padding="10,0">
                                                        <Hyperlink 
                                                            Foreground="{Binding Bg}"
                                                            Command="{x:Static controls:BubblleControl.ClickCommand}"
                                                            CommandParameter="{Binding Text}"
                                                            FontWeight="Normal">
                                                            <TextBlock Text="{Binding Text}"
                                                                       TextAlignment="Center"
                                                                       TextTrimming="CharacterEllipsis"
                                                                       ToolTip="{Binding Text}"/>
                                                        </Hyperlink>
                                                    </TextBlock>
                                    </Grid>
                                </DataTemplate>
                            </ItemsControl.ItemTemplate>
                            <ItemsControl.ItemsPanel>
                                <ItemsPanelTemplate>
                                    <controls:BubblleCanvas/>
                                </ItemsPanelTemplate>
                            </ItemsControl.ItemsPanel>
                        </ItemsControl>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

5) BubblleControlExample.xaml 代码如下;

  • TabItem随机 是自动设置位置和颜色;
  • TabItem自定义 可以自行定义展示的内容;
<UserControl x:Class="WPFDevelopers.Samples.ExampleViews.BubblleControlExample"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:wpfdev="https://github.com/WPFDevelopersOrg/WPFDevelopers"
             xmlns:local="clr-namespace:WPFDevelopers.Samples.ExampleViews"
             xmlns:sys="clr-namespace:System;assembly=mscorlib"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    
    <Grid>
        <TabControl>
            <TabItem Header="随机">
                <wpfdev:BubblleControl x:Name="MyBubblleControl"  Click="BubblleControl_Click">
                    <wpfdev:BubblleControl.ItemsSource>
                        <x:Array Type="sys:String">
                            <sys:String>WPF</sys:String>
                            <sys:String>ASP.NET</sys:String>
                            <sys:String>WinUI</sys:String>
                            <sys:String>WebAPI</sys:String>
                            <sys:String>Blazor</sys:String>
                            <sys:String>MAUI</sys:String>
                            <sys:String>Xamarin</sys:String>
                            <sys:String>WinForm</sys:String>
                            <sys:String>UWP</sys:String>
                        </x:Array>
                    </wpfdev:BubblleControl.ItemsSource>
                </wpfdev:BubblleControl>
            </TabItem>
            <TabItem Header="自定义">
                <wpfdev:BubblleCanvas Width="400" Height="400">
                    <Grid>
                        <Grid Width="60" 
                              Height="60">
                            <Ellipse Fill="MediumSpringGreen"
                                     Opacity=".4"/>
                            <Ellipse Stroke="MediumSpringGreen" 
                                     StrokeThickness=".8"/>
                        </Grid>
                        <TextBlock VerticalAlignment="Center" 
                                   HorizontalAlignment="Center"
                                   Padding="10,0">
                            <Hyperlink 
                                Foreground="MediumSpringGreen"
                                FontWeight="Normal"
                                Command="{Binding ClickCommand,RelativeSource={RelativeSource AncestorType=local:BubblleControlExample}}">
                                <TextBlock Text="WPF"
                                           TextAlignment="Center"
                                           TextTrimming="CharacterEllipsis"/>
                            </Hyperlink>
                        </TextBlock>
                    </Grid>
                    <Grid>
                        <Grid Width="60" 
                              Height="60">
                            <Ellipse Fill="Brown"
                                     Opacity=".4"/>
                            <Ellipse Stroke="Brown" 
                                     StrokeThickness=".8"/>
                        </Grid>
                        <TextBlock VerticalAlignment="Center" 
                                   HorizontalAlignment="Center"
                                   Padding="10,0">
                            <Hyperlink 
                                Foreground="Brown"
                                FontWeight="Normal"
                                Command="{Binding ClickCommand,RelativeSource={RelativeSource AncestorType=local:BubblleControlExample}}">
                                <TextBlock Text="MAUI"
                                           TextAlignment="Center"
                                           TextTrimming="CharacterEllipsis"/>
                            </Hyperlink>
                        </TextBlock>
                    </Grid>
                    <Grid>
                        <Grid Width="60" 
                              Height="60">
                            <Ellipse Fill="DeepSkyBlue"
                                     Opacity=".4"/>
                            <Ellipse Stroke="DeepSkyBlue" 
                                     StrokeThickness=".8"/>
                        </Grid>
                        <TextBlock VerticalAlignment="Center" 
                                   HorizontalAlignment="Center"
                                   Padding="10,0">
                            <Hyperlink 
                                Foreground="DeepSkyBlue"
                                FontWeight="Normal"
                                Command="{Binding ClickCommand,RelativeSource={RelativeSource AncestorType=local:BubblleControlExample}}">
                                <TextBlock Text="Blazor"
                                           TextAlignment="Center"
                                           TextTrimming="CharacterEllipsis"/>
                            </Hyperlink>
                        </TextBlock>
                    </Grid>
                </wpfdev:BubblleCanvas>
            </TabItem>
        </TabControl>
        
    </Grid>
</UserControl>

6) BubblleControlExample.xaml.cs 代码如下;

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using WPFDevelopers.Samples.Helpers;

namespace WPFDevelopers.Samples.ExampleViews
{
    /// <summary>
    /// BubbleControlExample.xaml 的交互逻辑
    /// </summary>
    public partial class BubblleControlExample : UserControl
    {
        public BubblleControlExample()
        {
            InitializeComponent();
        }
        public ICommand ClickCommand => new RelayCommand(delegate
        {
           WPFDevelopers.Minimal.Controls.MessageBox.Show("点击完成。");
        });

        private void BubblleControl_Click(object sender, System.Windows.RoutedEventArgs e)
        {
            MessageBox.Show($"点击了“ {MyBubblleControl.SelectedText}开发者 ”.", "提示",MessageBoxButton.OK,MessageBoxImage.Information);
        }
    }
}


WPF 实现 Gitee 泡泡菜单