UWP Composition API - RadialMenu

来源:http://www.cnblogs.com/FaDeKongJian/archive/2017/11/13/7797951.html
-Advertisement-
Play Games

用Windows 8.1的童鞋應該知道OneNote裡面有一個RadialMenu。如下圖,下圖是WIn10應用Drawboard PDF的RadialMenu,Win8.1的機器不好找了。哈哈,由於整個文章比較長,大家可以放《給我一首歌的時間》 邊聽邊看。<滑稽> 從設計到開發包括修複一些bug, ...


用Windows 8.1的童鞋應該知道OneNote裡面有一個RadialMenu。如下圖,下圖是WIn10應用Drawboard PDF的RadialMenu,Win8.1的機器不好找了。哈哈,由於整個文章比較長,大家可以放《給我一首歌的時間》 邊聽邊看。<滑稽>

從設計到開發包括修複一些bug,大概用了不連續的2個月,想看源代碼的童鞋可以先到 RadialMenu 查看效果和代碼。

先放上項目裡面的最終效果

下麵說下整個的過程

1.佈局

首先,可以看到這個控制項一個圓盤形狀的東東,在點擊子菜單的時候,菜單變化,並且帶有duangduangduang的特效。

先來看看RadialMenu的ControlTemplate

<ControlTemplate TargetType="local:RadialMenu">
                    <!--<Popup x:Name="Popup" IsLightDismissEnabled="False" IsOpen="{TemplateBinding IsOpen}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">-->
                        <Grid x:Name="Root" Width="{TemplateBinding Width}" Height="{TemplateBinding Height}">
                            <Grid x:Name="ContentGrid">
                                <Ellipse Fill="{TemplateBinding Background}"  StrokeThickness="{TemplateBinding ExpandAreaThickness}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"/>
                                <local:RadialMenuItemsPresenter x:Name="CurrentItemPresenter" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                             ItemsSource="{Binding CurrentItem.Items,RelativeSource={RelativeSource TemplatedParent}}" 
                             >
                                    <local:RadialMenuItemsPresenter.ItemsPanel>
                                        <ItemsPanelTemplate>
                                            <local:RadialMenuPanel/>
                                        </ItemsPanelTemplate>
                                    </local:RadialMenuItemsPresenter.ItemsPanel>
                                </local:RadialMenuItemsPresenter>
                            </Grid>
                            <local:RadialMenuNavigationButton x:Name="NavigationButton" Width="{TemplateBinding RadialMenuNavigationButtonSize}" Height="{TemplateBinding RadialMenuNavigationButtonSize}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
                        </Grid>
                    <!--</Popup>-->
                </ControlTemplate>
View Code

一共三個東西,

1)Ellipse -整個RadialMenu的外形以及背景

2)RadialMenuItemsPresenter-用來展示各個菜單

3)RadialMenuNavigationButton-中間那個導航的按鈕

1,3應該比較好理解,大家看看代碼就能明白。

我著重講下2,這個也是開發一個自定義控制項比較重要的操作,就是知道怎麼按自己的想法去佈局。

RadialMenuItemsPresenter 繼承ItemsControl,它的ItemsPanel是RadialMenuPanel。

  <local:RadialMenuItemsPresenter x:Name="CurrentItemPresenter"
   ItemsSource="{Binding CurrentItem.Items,RelativeSource={RelativeSource TemplatedParent}}" 
                             >
                                <local:RadialMenuItemsPresenter.ItemsPanel>
                                    <ItemsPanelTemplate>
                                        <local:RadialMenuPanel/>
                                    </ItemsPanelTemplate>
                                </local:RadialMenuItemsPresenter.ItemsPanel>
 </local:RadialMenuItemsPresenter>

RadialMenuPanel 就是我們最重要控制怎麼展示RadialMenuItemsPresenter 的Items。

重寫過的Panel 同學一定知道MeasureOverride ,ArrangeOverride 這2個東西。如果不清楚的一定要去看看葡萄城控制項技術團隊的2篇文章 Measure,Arrange。看過之後你將對這2個方法有新的瞭解,對控制項的佈局更加清晰。

下麵請看我優(醜)美(陋)的手畫圖

 

A菜單將它Arrange到圖中位置,B菜單也放到同樣的位置,但是給它做一定的旋轉。按做這種原理,把全部的菜單都Arrange到控制項的中上位置,並且給他們都做一定的旋轉,這樣就組成了整個的圓弧,這個遞增的角度就是 360°除以菜單的個數。

具體的代碼,請看ArrangeChildren方法。

 

2.每個菜單的設計

RadialMenuItem -最基礎的顯示文字,圖片等等

RadialColorMenuItem-用於顯示顏色

RadialNumericMenuItem-用於顯示數字,它的子集是RadialNumericMenuChildrenItem,是一個內部的類。

下來還是從我優(醜)美(陋)的手畫圖開始介紹結構吧

藍色的線是整個item的最外邊,黃色是展開子菜單的按鈕的中線。

從藍色到紅色部分就是整個展開子菜單按鈕。 就是圖中的藍色的那個部分。

那麼我們如何來做出這種效果呢。這裡我們要用到Path。

可以看到我們只需要把Path的StorkeThickness設置為展開按鈕的寬,而Path的位置為我們手繪圖中的黃色那條線。寬為藍色到紅色線之間。就可以了。代碼中具體在RadialMenuPanel.cs 中的169行。就是整個圓的半徑減去展開區域的寬度的一半。

 var expandAreaRadius = radius - Menu.ExpandAreaThickness / 2.0;

這個半徑就是ArcSegment的Size,那麼StartPoint和ArcSegment的Point怎麼計算呢。

再來上我的手繪圖,這次真是手繪的了。。

如上圖,知道了半徑,可以看到裡面有個直角三角形,一個邊是r,一個邊是x,一個邊是y,由於我們可以算出每個item占的角度,那麼這個直角三角形的角就等於item 角度的一半,使用勾股定理(話說最近項目裡面還用到很多幾何知識,還好沒有把知識還老師)可以算出x,y。

那麼StartPoint= (item的寬度-x,item的高度-y)。因為另一個點是對稱的。。不能算出Point=(item的寬度+x,item的高度-y)

那麼我們就可以畫出這個圓弧了。。

同理RadialColorMenuItem ,那個色塊跟擴展子菜單按鈕部分一樣。在代碼中具體在RadialMenuPanel.cs 中的175-177行

  var colorElementStrokeThickness = radius - Menu.ExpandAreaThickness - Menu.SelectedElementThickness - navigationButtonSize * 0.5;

  var colorElementRadius = radius - Menu.ExpandAreaThickness - Menu.SelectedElementThickness - colorElementStrokeThickness / 2.0;

這裡說明下,色塊跟擴展子菜單按鈕部分之間還有一個選中效果的色塊。

RadialNumericMenuItem 

它的子集是RadialNumericMenuChildrenItem

RadialNumericMenuChildrenItem  的選中效果和滑鼠懸停效果是2條Line,代碼中具體在RadialMenuPanel.cs 中的180-197行

3.動畫效果

這篇是Composition API,當然要用Composition API來做動畫呢。。哈哈。

先來說展開/收縮的動畫

                _contentGridVisual = ElementCompositionPreview.GetElementVisual(_contentGrid);
                _compositor = _contentGridVisual.Compositor;

                rotationAnimation = _compositor.CreateScalarKeyFrameAnimation();
                scaleAnimation = _compositor.CreateVector3KeyFrameAnimation();

                var easing = _compositor.CreateLinearEasingFunction();

                _contentGrid.SizeChanged += (s, e) =>
                {
                    _contentGridVisual.CenterPoint = new Vector3((float)_contentGrid.ActualWidth / 2.0f, (float)_contentGrid.ActualHeight / 2.0f, 0);
                };

                scaleAnimation.InsertKeyFrame(0.0f, new Vector3() { X = 0.0f, Y = 0.0f, Z = 0.0f });
                scaleAnimation.InsertKeyFrame(1.0f, new Vector3() { X = 1.0f, Y = 1.0f, Z = 0.0f }, easing);

                rotationAnimation.InsertKeyFrame(0.0f, -90.0f);
                rotationAnimation.InsertKeyFrame(1.0f, 0.0f, easing);

可以看到我準備2個動畫,一個是旋轉一個縮小放大,而他們的作用題是RadialMenu的中ContentGrid(不包括中心的那個導航按鈕)

在10586版本之上,動畫有Direction這個屬性,可以反轉動畫效果,也就是說,我這裡寫2個動畫,就可以做到展開/收縮

有沒有覺得很簡單。。為啥這裡特別提10586呢。因為後面有坑。

 

再說說切換菜單效果,就是點了某個子菜單的展開按鈕,切到它對應的菜單去的效果。其實更簡單,直接用上面的scaleAnimation就可以。不一樣的是我們使用了下麵的代碼來讓菜單先收,再開。

                var batch = _compositor.GetCommitBatch(CompositionBatchTypes.Animation);
                batch.Completed += (s, e) =>
                {
                    SetCurrentItemIn(currentItem);

                    scaleAnimation.Duration = TimeSpan.FromSeconds(0.1);
                    if (!lowerThan14393)
                    {
                        scaleAnimation.Direction = AnimationDirection.Normal;
                    }
                    _contentGridVisual.StartAnimation(nameof(_contentGridVisual.Scale), scaleAnimation);
                };

                scaleAnimation.Duration = TimeSpan.FromSeconds(0.07);
                scaleAnimation.Direction = AnimationDirection.Reverse;
                _contentGridVisual.StartAnimation(nameof(_contentGridVisual.Scale), scaleAnimation);

CompositionCommitBatch 有一個completed事件,這個就是動畫結束時候觸發的事件。

另外發現CompositionScopedBatch 也completed事件。

先看看官方介紹CompositionCommitBatch, CompositionScopedBatch

public void BatchAnimations()
{
    // Create a Scoped batch to capture animation completion events
    _batch = _compositor.CreateScopedBatch(CompositionBatchTypes.Animation);

    // Executing the Offset animation and aggregating completion event
    ApplyOffsetAnimation(_greenSquare);

    // Suspending to exclude the following Rotation animation from the batch
    _batch.Suspend();

    // Executing the Rotation animation 
    ApplyRotationAnimation(_greenSquare);

    // Resuming the batch to collect additional animations
    _batch.Resume();

    // Executing the Opacity animation and aggregating completion event
    ApplyOpacityAnimation(_greenSquare);

    // Batch is ended and no objects can be added
    _batch.End();

    // Method triggered when batch completion event fires
    _batch.Completed += OnBatchCompleted;
}

可以看到CompositionScopedBatch 多了3個方法Resume,Suspend,End,也就是說你可以暫停動畫,並且在這個時間段加入新的動畫到這個group裡面,然後啟動,最後每個動畫完成的時候都會觸發complete。

最後是整個RadialMenu的交互。就是移動動畫。代碼太簡單了。

 _radialMenuVisual = ElementCompositionPreview.GetElementVisual(this);
 _radialMenuVisual.Offset = Offset;

移動的時候處理_radialMenuVisual的Offset就可以了。。哈哈。(註意我沒有把RadialIMenu的元素放在一個Popup裡面,原因是需要滿足,點擊其他地方的時候不要關閉RadialMenu。如果Popup的IsLightDismissEnabled 的屬性設置為flase的話,其他地方點擊又不會有事件觸發。現在的方案是把這個控制項總是放在最高的level 層)

因為設置了控制項的ManipulationMode

ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY | ManipulationModes.TranslateInertia;

那麼我們可以在OnManipulationDelta 方法中去改變Offset。達到移動整個控制項的目的

        protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e)
        {
            UpdateOffset(e.Delta.Translation.X, e.Delta.Translation.Y, e.IsInertial);
        }

這裡當有慣性的時候我做了反彈效果,具體的代碼在RadilaMenu的BounceOffset 方法中。

 

不要以為這樣控制項就搞定了,坑坑坑。在等著你。。

第一個地方:

之前講了在10586上面。沒有反轉動畫這個屬性,而且我發現10586上面動畫簡直是垃圾。各種掉幀,各種失效,但是項目裡面要支持10586,那我們怎麼辦呢,嗯。別忘記了我們還有Storyboard。我們需要4個動畫

        //use animation for lower than 14393
        Storyboard expand;
        Storyboard collapse;
        Storyboard open;
        Storyboard close;

準備動畫代碼如下,說實話,確實有點麻煩難懂,這就是我為啥一直推薦使用Composition API的其中一個原因吧。

                _contentGrid.RenderTransformOrigin = new Point(0.5, 0.5);
                _contentGrid.RenderTransform = new CompositeTransform();
                #region expand
                expand = new Storyboard();
                var duk = new DoubleAnimationUsingKeyFrames();
                Storyboard.SetTarget(duk, _contentGrid);
                Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleX)");
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)), Value = 0 });
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.2)), Value = 1, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
                expand.Children.Add(duk);

                duk = new DoubleAnimationUsingKeyFrames();
                Storyboard.SetTarget(duk, _contentGrid);
                Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleY)");
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)), Value = 0 });
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.2)), Value = 1, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
                expand.Children.Add(duk);

                duk = new DoubleAnimationUsingKeyFrames();
                Storyboard.SetTarget(duk, _contentGrid);
                Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.Rotation)");
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)), Value = -90 });
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.2)), Value = 0, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
                expand.Children.Add(duk);
                #endregion

                #region collapse
                collapse = new Storyboard();
                duk = new DoubleAnimationUsingKeyFrames();
                Storyboard.SetTarget(duk, _contentGrid);
                Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleX)");
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)), Value = 1 });
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.2)), Value = 0, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
                collapse.Children.Add(duk);

                duk = new DoubleAnimationUsingKeyFrames();
                Storyboard.SetTarget(duk, _contentGrid);
                Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleY)");
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)), Value = 1 });
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.2)), Value = 0, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
                collapse.Children.Add(duk);

                duk = new DoubleAnimationUsingKeyFrames();
                Storyboard.SetTarget(duk, _contentGrid);
                Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.Rotation)");
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)), Value = 0 });
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.2)), Value = -90, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
                collapse.Children.Add(duk);

                #endregion

                #region Open
                open = new Storyboard();
                duk = new DoubleAnimationUsingKeyFrames();
                Storyboard.SetTarget(duk, _contentGrid);
                Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleX)");
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)), Value = 0 });
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.1)), Value = 1, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
                open.Children.Add(duk);

                duk = new DoubleAnimationUsingKeyFrames();
                Storyboard.SetTarget(duk, _contentGrid);
                Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleY)");
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)), Value = 0 });
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.1)), Value = 1, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
                open.Children.Add(duk);
                #endregion

                #region Close
                close = new Storyboard();
                duk = new DoubleAnimationUsingKeyFrames();
                Storyboard.SetTarget(duk, _contentGrid);
                Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleX)");
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)), Value = 1 });
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.07)), Value = 0, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
                close.Children.Add(duk);

                duk = new DoubleAnimationUsingKeyFrames();
                Storyboard.SetTarget(duk, _contentGrid);
                Storyboard.SetTargetProperty(duk, "(UIElement.RenderTransform).(CompositeTransform.ScaleY)");
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)), Value = 1 });
                duk.KeyFrames.Add(new EasingDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.07)), Value = 0, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
                close.Children.Add(duk);
                #endregion
View Code

第二個地方:

為了實現RadialNumericMenuChildrenItem 第一個和最後一個Item缺掉一個口的效果,如下圖

我為這個Path做了一個Clip。問題在於15063版本上面Path的Clip是針對它實際圖形位置,而不是Path實際的大小(從之前手繪圖看錯,實際大小是那個外面的矩形,而且它繪畫的圖像是小於它的)。所以做了下麵的特殊處理

var Build16229 = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 5);
                            //
                            var Build15063 = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 4);
                            //var Build14393 = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 3);
                            //var Build10586 = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 2);
                            bool is15063 = Build15063 && !Build16229;
                            if (!is15063)
                            {
                                if (k == 0)
                                {
                                    radialNumericMenuChildrenItem.ColorElement.Clip = new RectangleGeometry() { Rect = new Rect(sectorRect.Width / 2.0, 0, sectorRect.Width / 2.0, sectorRect.Height) };

                                }
                                else
                                {
                                    radialNumericMenuChildrenItem.ColorElement.Clip = new RectangleGeometry() { Rect = new Rect(0, 0, sectorRect.Width / 2.0, sectorRect.Height) };
                                }
                            }
                            else
                            {
                                if (k == 0)
                                {
                                    radialNumericMenuChildrenItem.ColorElement.Clip = new RectangleGeometry() { Rect = new Rect((colorElement.EndPoint.X - colorElement.StartPoint.X) / 2.0, 0, colorElement.EndPoint.X - colorElement.StartPoint.X, colorElement.Size.Height) };
                                }
                                else
                                {
                                    radialNumericMenuChildrenItem.ColorElement.Clip = new RectangleGeometry() { Rect = new Rect(0, 0, (colorElement.EndPoint.X - colorElement.StartPoint.X) / 2.0, colorElement.Size.Height) };
                                }
                            }
View Code

 

第三個地方:

RadialNumericMenuItem 的Items之前設計是使用New關鍵字直接重寫了子類的Items(RadialMenuItemCollection),為ObservableCollection<double>

在10586  release 模式上面RadialMenuPanel 的Children.Count會為一個奇怪的負數,而不是0.

修改方案之後 增加了一個ObservableCollection<double> NumericItems 屬性來設置數

並且使用 [ContentProperty(Name = "NumericItems")] 屬性標簽覆蓋子類,

但是哇在最新的SDK上面又有問題了。。debug模式下編譯能過。但是運行起來ContentProperty 沒有生效不通過。

最後也沒啥辦法了。。那就是在xaml裡面老實實 寫NumericItems等於啥吧。。( ╯□╰ )

 

第四個地方

居然是virtual 的Offset 在Page的 NavigationCacheMode 等於enable 或者request的時候。有大大大的問題啊。微軟來背鍋啊。

問題是這樣的。Radilamenu放在一個Page NavigationCacheMode =Enable的頁面上。。然後跳轉到其他頁面再跳轉回來的時候。

Virtual.Offset 的還是原來的值。而且整個RadialMenu看起來也像在原來的位置。。但是其實你用滑鼠或者觸摸的時候。發現RadialMenu觸發事件的位置居然是在左上角。就是說是Offset為0的時候那個區域。。不管用了什麼辦法。還是無解。

最後只能放棄使用Virtual.Offset ,然後改為控制控制項Popup的HorizontalOffset和VerticalOffset來移動。

 

第五個地方:

之前Popup放ControlTemplate裡面,這會導致RadilamMenu只是在你放那個父容器那一層是最上層。而一般都需要RadialMenu在整個應用的最上層。

辦法只有一個不要將Popup放進去virtual tree,而且是在代碼中new 一個出來,這樣微軟會把這個Popup的Child放進PopupRoot,這個東東是整個應用的最上層,如下圖。

PopupRoot為應用的最上層

RootScrollViewer-ScrollContentPresenter-Border-Frame-ContentPresenter 這裡就是平時大家熟悉的MainPage

不知道咋哪裡查看這個的。請打開VS,運行UWP程式,看到中間那坨黑色的地方嗎,點第一個VS的左邊就會出現Live Vistual Tree,通過這個你能查看到結構,某個控制項的當前屬性,或者設置它的屬性,反正很叼了。學習控制項的孩子一定要搞懂哈。

註意不要將RadilaMenu直接放進Virtual tree,因為RadialMenu是準備放在new 的Popup裡面的,大家都知道一個UIElement不能賦值給2個Parent的。

那麼告訴大家一個技巧。可以用AttachedProperty將RadialMenu加入到Xaml但是不放進Virtual tree。這樣我們就能在Xaml中開心的寫了。

如Sample 中RadialMenuBase 類的AttachedMenu屬性。

  <radialMenu:RadialMenuBase.AttachedMenu>
            <radialMenu:RadialMenu x:Name="radialMenu" Offset="100,100" SectorCount="8" IsExpanded="False">
                <radialMenu:RadialMenuItem Content="Color" ToolTip="Color">
                    <radialMenu:RadialColorMenuItem Color="Red"/>
                </radialMenu:RadialMenuItem>
                <radialMenu:RadialNumericMenuItem x:Name="radialNumericMenuItem" Value="6">
                    <radialMenu:RadialNumericMenuItem.Content>
                        <TextBlock>
                        <Run Text="Fontsize"/>
                        <Run Text="("/>
                        <Run Text="{Binding Value,ElementName=radialNumericMenuItem}"/>
                        <Run Text=")"/>
                        </TextBlock>
                    </radialMenu:RadialNumericMenuItem.Content>
                    <radialMenu:RadialNumericMenuItem.NumericItems>
                        <x:Double>1</x:Double>
                        <x:Double>2</x:Double>
                        <x:Double>3</x:Double>
                        <x:Double>4</x:Double>
                        <x:Double>5</x:Double>
                        <x:Double>6</x:Double>
                        <x:Double>7</x:Double>
                        <x:Double>8</x:Double>
                        <x:Double>9</x:Double>
                        <x:Double>10</x:Double>
                        <x:Double>11</x:Double>
                        <x:Double>12</x:Double>
                        <x:Double>13</x:Double>
                        <x:Double>14</x:Double>
                        <x:Double>15</x:Double>
                        <x:Double>16</x:Double>
                    </radialMenu:RadialNumericMenuItem.NumericItems>
                </radialMenu:RadialNumericMenuItem>
                <radialMenu:RadialMenuItem Content="Disabled" IsEnabled="True">
                    <radialMenu:RadialMenuItem Content="test"/>
                </radialMenu:RadialMenuItem>
            </radialMenu:RadialMenu>
        </radialMenu:RadialMenuBase.AttachedMenu>

最後最後,這個控制項裡面用到一些屬性標簽 比如

[ContentProperty(Name = "NumericItems")]

[EditorBrowsable(EditorBrowsableState.Never)]
[Browsable(false)]

這些東西跟Design Time有很大大的關係,就是你平時寫在Xaml裡面的東東。要是有不清楚的。可以留言或者私信我,歡迎投雞蛋。

最後想說。要做好一個控制項這些是遠遠不夠的,還會涉及控制項的 designtime,各種邏輯,各種版本適配,各種擴展,一個好的控制項絕對等同一個複雜的項目。我喜歡做控制項。希望給大家使用降低學習成本。

謝謝堅持看完的童鞋,整個控制項研發的過程遠遠不止這篇文章,歡迎提問。

老規矩 開源有益:RadialMenu 

想在10240上面使用這個控制項的童鞋,可以把關於Composition API的代碼都註釋掉。使用StoryBoard來做動畫就ok了。(是不是感覺我強行為Composition API 打Call,哈哈哈)

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文需要實現的功能如下:某文件夾下具有由按數字編號命名的文件夾,需要刪除除最大編碼外的文件。 具體實現 大致思路:迴圈遍歷該文件夾下所有文件,正則匹配出最大編碼文件;然後迴圈文件,刪除除最大編碼外的文件。 實現代碼如下: 實現效果:文件夾:/root/cloud/builds 執行腳本後: 用到的S ...
  • Mysql安裝 CentOS 7 版本將MySQL資料庫軟體從預設的程式列表中移除,用mariadb代替了。MariaDB資料庫管理系統是MySQL的一個分支,主要由開源社區在維護,採用GPL授權許可。開發這個分支的原因之一是:甲骨文公司收購了MySQL後,有將MySQL閉源的潛在風險,因此社區採用 ...
  • Memcached 是一個高性能的分散式記憶體對象緩存系統,用於動態Web應用以減輕資料庫負載。它通過在記憶體中緩存數據和對象來減少讀取資料庫的次數,從而提高動態、資料庫驅動網站的速度。Memcached基於一個存儲鍵/值對的hashmap。其守護進程(daemon )是用寫的,但是客戶端可以用任何語言 ...
  • ABP是“ASP.NET Boilerplate Project (ASP.NET樣板項目)”的簡稱。ASP.NET Boilerplate是一個用最佳實踐和流行技術開發現代WEB應用程式的新起點,它旨在成為一個通用的WEB應用程式框架和項目模板。框架ABP是基於最新的ASP.NET CORE,AS ...
  • 一、系統介紹 本軟體主要讓你的文件夾多彩多樣化,而不是千篇一律黃色的文件夾,相信豐富多彩的顏色更便於記憶、能讓心情愉快些。 文件夾顏色眾多,到底有多少種呢,10種? 100種? 1000種? NO,精確計算是255*255*255 , 只是很多顏色很相近, 而能夠明顯有差別的不是很多,為了便於自己記 ...
  • 在ABP框架下使用NHibernate和Dapper實現資料庫訪問。 ...
  • 如果我們要從線程池中取消某個線程的操作,應該如何實現呢?本示例使用CancellationTokenSource和CancellationToken兩個類來實現在取消線程池中的操作。 ...
  • 在進行實體轉換操作的時候如果需要在對兩個實體之間兩個屬性欄位差不多相同的類要進行一個互相的轉換,我們要把a對象的所有欄位的值都複製給b對象,我們只能用b.屬性=a.屬性來寫,如果屬性欄位太多的話,就要寫很多行複製語句,麻煩還容易漏掉一些,這個時候可以利用c#的反射來實現複製。我們可以寫這樣一個利用泛 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...