?WPF 實現 Gitee 泡泡菜單「完」
氣泡菜單「完」
作者:WPFDevelopersOrg
原文鏈接: ? ?https://github.com/WPFDevelopersOrg/WPFDevelopers
框架使用大于等于
.NET40
;Visual Studio 2022
;項目使用 MIT 開源許可協議;

需要實現泡泡菜單需要使用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?Eventpublic?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?Commandprivate?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?Propertiesprivate?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?Propertypublic?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?Overridepublic?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);}}
}
