ColorPicker
故事背景
項目裡面需要一個像Winfrom裡面那樣的顏色選擇器,如下圖所示:
在網上看了一下。沒有現成的東東可以拿來使用。大概查看了一下關於顏色的一些知識,想著沒人種樹,那就由我自己來種樹,大家來乘涼好了。
設計過程
由於要考慮到手機上的效果,所以說這種向右展開的方式,不是太合適手機,所以最外層我考慮使用Pivot來存放基本顏色和自定義顏色這2頁。
第一頁是基本顏色,第二頁是自定義的顏色,如下圖。
ColorPicker這個控件,主要是由一個Button以及FlyoutBase.AttachedFlyout中的Flyout來組成的。
由Button的點擊來控制Flyout的打開或者是關閉。
<Button x:Name="ToggleButton" Padding="{TemplateBinding Padding}" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}"> <Grid Padding="{TemplateBinding Padding}" Background="#01010101"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="auto"/> </Grid.ColumnDefinitions> <Rectangle HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <Rectangle.Fill> <!--failed to use TemplateBinding--> <SolidColorBrush Color="{Binding SelectedColor,RelativeSource={RelativeSource TemplatedParent}}"/> </Rectangle.Fill> </Rectangle> <TextBlock x:Name="ArrowPolygon" Foreground="{TemplateBinding Foreground}" Visibility="{TemplateBinding ArrowVisibility}" Grid.Column="1" Text="" FontSize="{TemplateBinding FontSize}" FontFamily="Segoe UI Symbol" FontWeight="Normal" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="5,0,5,0"/> </Grid> <FlyoutBase.AttachedFlyout> <Flyout x:Name="Flyout"> <Flyout.FlyoutPresenterStyle> <Style TargetType="FlyoutPresenter"> <Setter Property="ScrollViewer.VerticalScrollMode" Value="Disabled"/> <Setter Property="ScrollViewer.HorizontalScrollMode" Value="Disabled"/> <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/> <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/> <!--<Setter Property="MaxHeight" Value="NaN"/> <Setter Property="MaxWidth" Value="NaN"/>--> <Setter Property="MinHeight" Value="0"/> <Setter Property="MinWidth" Value="0"/> <Setter Property="Padding" Value="0,0,0,0"/> <Setter Property="Margin" Value="0,0,0,0"/> <Setter Property="BorderThickness" Value="0"/> <Setter Property="Background" Value="White"/> <!--<Setter Property="BorderBrush" Value="#A4AFBA"/>--> <Setter Property="MaxWidth" Value="NaN"/> <Setter Property="MaxHeight" Value="NaN"/> <Setter Property="Background" Value="Transparent"/> <Setter Property="VerticalContentAlignment" Value="Center"/> <Setter Property="HorizontalContentAlignment" Value="Center"/> </Style> </Flyout.FlyoutPresenterStyle> <Grid Background="#FFD1DCE8" RequestedTheme="Light" BorderBrush="#A4AFBA" BorderThickness="1" Width="{TemplateBinding FlyoutWidth}" Height="{TemplateBinding FlyoutHeight}"> <Pivot x:Name="Pivot" > <Pivot.Resources> <!--<Style TargetType="TextBlock"> <Setter Property="Foreground" Value="Black"/> </Style>--> <Style TargetType="PivotHeaderItem" BasedOn="{StaticResource ColorPickerPivotHeaderItem}"/> <Style TargetType="PivotItem"> <Setter Property="HorizontalContentAlignment" Value="Stretch"/> <Setter Property="Margin" Value="0"/> <Setter Property="Padding" Value="0"/> <Setter Property="MinWidth" Value="0"/> </Style> </Pivot.Resources> <PivotItem> <PivotItem.Header> <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Padding="5"> <Grid.ColumnDefinitions> <ColumnDefinition Width="17"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Border Width="13" Height="13" Background="#FF97AEBF"> <Grid> <Rectangle Height="10" HorizontalAlignment="Left" VerticalAlignment="Top" Width="3" Fill="#FFFF0000" Margin="1 1 0 0"/> <Rectangle Height="5" HorizontalAlignment="Left" VerticalAlignment="Top" Width="3" Fill="#FFFFC000" Margin="5 1 0 0"/> <Rectangle Height="5" HorizontalAlignment="Left" VerticalAlignment="Top" Width="3" Fill="#FFFFFF00" Margin="9 1 0 0"/> <Rectangle Height="5" HorizontalAlignment="Left" VerticalAlignment="Top" Width="3" Fill="#FF92D050" Margin="1 5 0 0"/> <Rectangle Height="5" HorizontalAlignment="Left" VerticalAlignment="Top" Width="3" Fill="#FF00B050" Margin="5 5 0 0"/> <Rectangle Height="5" HorizontalAlignment="Left" VerticalAlignment="Top" Width="3" Fill="#FF0C8242" Margin="9 5 0 0"/> <Rectangle Height="5" HorizontalAlignment="Left" VerticalAlignment="Top" Width="3" Fill="#FF0070C0" Margin="1 9 0 0"/> <Rectangle Height="5" HorizontalAlignment="Left" VerticalAlignment="Top" Width="3" Fill="#FF002060" Margin="5 9 0 0"/> <Rectangle Height="5" HorizontalAlignment="Left" VerticalAlignment="Top" Width="3" Fill="#FF7030A0" Margin="9 9 0 0"/> </Grid> </Border> <TextBlock HorizontalAlignment="Left" VerticalAlignment="Center" Text="基本顏色" TextWrapping="Wrap" Grid.Column="1"> </TextBlock> </Grid> </PivotItem.Header> <StackPanel Orientation="Vertical"> <Border Margin="0,5,0,0" HorizontalAlignment="Stretch" BorderBrush="#A4AFBA" BorderThickness="0,0,0,1" Height="30"> <TextBlock Margin="5,0" VerticalAlignment="Center"> <Run Text="{Binding Title,RelativeSource={RelativeSource TemplatedParent}}"/> <Run Text=" - "/> <Run Text="基本顏色"/> </TextBlock> </Border> <local:ColorPickerItemsControl x:Name="BasicColorItems" MinHeight="43"/> <Border Margin="0,5,0,0" BorderBrush="#A4AFBA" BorderThickness="0,0,0,1" HorizontalAlignment="Stretch" Height="30"> <TextBlock Margin="5,0" Text="最近使用顏色" VerticalAlignment="Center"/> </Border> <local:ColorPickerItemsControl x:Name="RecentColorItems" MinHeight="43"/> </StackPanel> </PivotItem> <PivotItem> <PivotItem.Header> <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Padding="5"> <Grid.ColumnDefinitions> <ColumnDefinition Width="17"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Ellipse Height="14" Margin="0.5,-1,3,-1" Fill="#FFFFFFFF" Width="14"/> <Ellipse Width="14" Height="14" Margin="0.5,-1,3,-1"> <Ellipse.Fill> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="#FFFF0000" Offset="0.1"/> <GradientStop Color="#00FF0000" Offset="0.5"/> </LinearGradientBrush> </Ellipse.Fill> </Ellipse> <Ellipse Height="14" HorizontalAlignment="Stretch" Margin="0.5,-1,3,-1" VerticalAlignment="Stretch" Width="14"> <Ellipse.Fill> <LinearGradientBrush EndPoint="0.982999980449677,0.179000005125999" StartPoint="0.0879999995231628,0.753000020980835"> <GradientStop Color="#FF079BF0" Offset="0.1"/> <GradientStop Color="#00079BF0" Offset="0.5"/> </LinearGradientBrush> </Ellipse.Fill> </Ellipse> <Ellipse Height="14" HorizontalAlignment="Stretch" Margin="0.5,-1,3,-1" VerticalAlignment="Stretch" Width="14"> <Ellipse.Fill> <LinearGradientBrush EndPoint="0.136000007390976,0.174999997019768" StartPoint="0.843999981880188,0.822000026702881"> <GradientStop Color="#FFF2F413" Offset="0.1"/> <GradientStop Color="#00F2F413" Offset="0.5"/> </LinearGradientBrush> </Ellipse.Fill> </Ellipse> <Ellipse Height="14" HorizontalAlignment="Stretch" Margin="0.5,-1,3,-1" VerticalAlignment="Stretch" Width="14" Visibility="Visible"> <Ellipse.Fill> <LinearGradientBrush> <GradientStop Color="#00000000" Offset="0.772"/> <GradientStop Color="#4C000000" Offset="1"/> </LinearGradientBrush> </Ellipse.Fill> </Ellipse> <Ellipse Height="15" HorizontalAlignment="Stretch" Margin="-0.5,-1.5,2.5,-1.5" VerticalAlignment="Stretch" Width="15" Stroke="#FF8AA3B5"/> <TextBlock HorizontalAlignment="Left" VerticalAlignment="Center" Text="自定義顏色" TextWrapping="Wrap" Grid.Column="1"> </TextBlock> </Grid> </PivotItem.Header> <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <Grid.RowDefinitions> <RowDefinition Height="auto"/> <RowDefinition Height="*"/> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <Grid.Resources> <Style TargetType="local:NumericTextBox"> <Setter Property="InputScope" Value="Number"/> <Setter Property="ValueFormat" Value="F0"/> <Setter Property="Minimum" Value="0"/> <Setter Property="Maximum" Value="255"/> <Setter Property="MinWidth" Value="0"/> <Setter Property="Margin" Value="5,0,0,0"/> <Setter Property="HorizontalContentAlignment" Value="Center"/> </Style> </Grid.Resources> <Border Margin="0,5,0,0" HorizontalAlignment="Stretch" BorderBrush="#A4AFBA" BorderThickness="0,0,0,1" Height="30"> <TextBlock Margin="5,0" VerticalAlignment="Center"> <Run Text="{Binding Title,RelativeSource={RelativeSource TemplatedParent}}"/> <Run Text=" - "/> <Run Text="自定義顏色"/> </TextBlock> </Border> <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="1" BorderBrush="#A4AFBA" BorderThickness="0,0,0,1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="auto"/> <ColumnDefinition Width="auto"/> </Grid.ColumnDefinitions> <ContentControl x:Name="ChoiceGridParent" Grid.Column="0" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"> <Grid x:Name="ChoiceGrid" HorizontalAlignment="Stretch" Margin="5,5,0,5" VerticalAlignment="Stretch" > <!--<Grid.Background> <LinearGradientBrush StartPoint="0,0" EndPoint="1,0"> <LinearGradientBrush.GradientStops> <GradientStop Offset="0.0" Color="White"/> <GradientStop Offset="1" Color="#00FFFFFF"/> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Grid.Background>--> <Rectangle HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <Rectangle.Fill> <LinearGradientBrush StartPoint="0,0" EndPoint="1,0"> <LinearGradientBrush.GradientStops> <GradientStop Offset="0.0" Color="White"/> <GradientStop Offset="1" Color="#00FFFFFF"/> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> <Rectangle HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <Rectangle.Fill> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <LinearGradientBrush.GradientStops> <GradientStop Offset="0.0" Color="#00000000"/> <GradientStop Offset="1" Color="Black"/> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> <Canvas x:Name="PadCanvas"> <Canvas x:Name="Indicator"> <Ellipse Height="6" Width="6" Fill="Transparent" Stroke="#FFFFFFFF" StrokeThickness="1" Margin="-3 -3 0 0" /> <Ellipse Height="12" Width="12" Fill="Transparent" Stroke="#FF737373" Margin="-6 -6 0 0" /> </Canvas> </Canvas> </Grid> </ContentControl> <Slider x:Name="Hue" Margin="5,5,0,5" Grid.Column="1"> <Slider.Background> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <GradientStop Offset="0.0" Color="#FFFF0000"/> <GradientStop Offset="0.2" Color="#FFFFFF00"/> <GradientStop Offset="0.4" Color="#FF00FF00"/> <GradientStop Offset="0.6" Color="#FF0000FF"/> <GradientStop Offset="0.8" Color="#FFFF00FF"/> <GradientStop Offset="1.0" Color="#FFFF0000"/> </LinearGradientBrush> </Slider.Background> </Slider> <Slider x:Name="Alpha" Margin="5" Grid.Column="2"> <Slider.Background> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <GradientStop Color="Black" Offset="0"/> <GradientStop Color="Transparent" Offset="1"/> </LinearGradientBrush> </Slider.Background> </Slider> </Grid> <Grid Margin="0,0,5,0" Padding="0,0,0,5" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="2" BorderBrush="#A4AFBA" BorderThickness="0,0,0,1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <local:NumericTextBox x:Name="AColor" Grid.Column="0"> <local:NumericTextBox.Header> <TextBlock Text="透明度(A)" HorizontalAlignment="Center"/> </local:NumericTextBox.Header> </local:NumericTextBox> <local:NumericTextBox x:Name="RColor" Grid.Column="1" > <local:NumericTextBox.Header> <TextBlock Text="紅(R)" HorizontalAlignment="Center"/> </local:NumericTextBox.Header> </local:NumericTextBox> <local:NumericTextBox x:Name="GColor" Grid.Column="2" > <local:NumericTextBox.Header> <TextBlock Text="綠(G)" HorizontalAlignment="Center"/> </local:NumericTextBox.Header> </local:NumericTextBox> <local:NumericTextBox x:Name="BColor" Grid.Column="3" > <local:NumericTextBox.Header> <TextBlock Text="藍(B)" HorizontalAlignment="Center"/> </local:NumericTextBox.Header> </local:NumericTextBox> </Grid> <Grid Grid.Row="3" Margin="5"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="auto"/> </Grid.ColumnDefinitions> <Grid HorizontalAlignment="Stretch" Margin="0,0,10,0"> <local:TransparentBackground/> <Rectangle x:Name="CustomColorRectangle" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <Rectangle.Fill> <SolidColorBrush Color="{Binding CurrentCustomColor,RelativeSource={RelativeSource TemplatedParent}}"/> </Rectangle.Fill> <ToolTipService.ToolTip> <ToolTip> <Binding Converter="{StaticResource ColorToStringConverter}" Path="CurrentCustomColor" RelativeSource="{RelativeSource TemplatedParent}"/> </ToolTip> </ToolTipService.ToolTip> </Rectangle> </Grid> <Button x:Name="CustomColorOkButton" Grid.Column="1" Content="確定" VerticalAlignment="Center" HorizontalAlignment="Right"/> </Grid> </Grid> </PivotItem> </Pivot> <Button x:Name="CloseButton" Content="關閉" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="5"/> </Grid> </Flyout> </FlyoutBase.AttachedFlyout> <ToolTipService.ToolTip> <ToolTip> <Binding Path="SelectedColor" RelativeSource="{RelativeSource TemplatedParent}" Converter="{StaticResource ColorToStringConverter}"/> </ToolTip> </ToolTipService.ToolTip> </Button>View Code
通過重寫Pivot的模板我們可以輕松得到PiovtHeaderItem 在下面的效果(修改Header和PivotItemPresenter的行號)
Pivot部分模板代碼如下,注意藍色部分:
<Grid x:Name="PivotLayoutElement"> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Grid.RenderTransform> <CompositeTransform x:Name="PivotLayoutElementTranslateTransform" /> </Grid.RenderTransform> <ContentPresenter Grid.Row="1" x:Name="LeftHeaderPresenter" Content="{TemplateBinding LeftHeader}" ContentTemplate="{TemplateBinding LeftHeaderTemplate}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" /> <ContentControl Grid.Row="1" x:Name="HeaderClipper" Grid.Column="1" UseSystemFocusVisuals="False" HorizontalContentAlignment="Stretch"> <ContentControl.Clip> <RectangleGeometry x:Name="HeaderClipperGeometry" /> </ContentControl.Clip> <Grid Background="Transparent" BorderBrush="#A4AFBA" BorderThickness="0,1,0,0"> <PivotHeaderPanel x:Name="StaticHeader" Visibility="Collapsed" /> <PivotHeaderPanel x:Name="Header"> <PivotHeaderPanel.RenderTransform> <TransformGroup> <CompositeTransform x:Name="HeaderTranslateTransform" /> <CompositeTransform x:Name="HeaderOffsetTranslateTransform" /> </TransformGroup> </PivotHeaderPanel.RenderTransform> </PivotHeaderPanel> </Grid> </ContentControl> <Button Grid.Row="1" x:Name="PreviousButton" Grid.Column="1" Template="{StaticResource PreviousTemplate}" Width="20" Height="36" UseSystemFocusVisuals="False" Margin="{ThemeResource PivotNavButtonMargin}" IsTabStop="False" IsEnabled="False" HorizontalAlignment="Left" VerticalAlignment="Top" Opacity="0" Background="Transparent" /> <Button Grid.Row="1" x:Name="NextButton" Grid.Column="1" Template="{StaticResource NextTemplate}" Width="20" Height="36" UseSystemFocusVisuals="False" Margin="{ThemeResource PivotNavButtonMargin}" IsTabStop="False" IsEnabled="False" HorizontalAlignment="Right" VerticalAlignment="Top" Opacity="0" Background="Transparent" /> <ContentPresenter Grid.Row="1" x:Name="RightHeaderPresenter" Grid.Column="2" Content="{TemplateBinding RightHeader}" ContentTemplate="{TemplateBinding RightHeaderTemplate}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" /> <ItemsPresenter x:Name="PivotItemPresenter" Grid.Row="0" Grid.ColumnSpan="3"> <ItemsPresenter.RenderTransform> <TransformGroup> <TranslateTransform x:Name="ItemsPresenterTranslateTransform" /> <CompositeTransform x:Name="ItemsPresenterCompositeTransform" /> </TransformGroup> </ItemsPresenter.RenderTransform> </ItemsPresenter> </Grid>
這個色塊就比較簡單了,通過Just Color Picker 把Winform 裡面的顏色都給搞出來,通過ItemsControl把他們都布局在一塊。
最近使用顏色,這個就是記錄最近你點擊修改的顏色,我這裡用了一個幫助類來進行管理。
internal static class ColorPickerColorHelper { const string ColorPickerRecentColorsKey = "ColorPickerRecentColors.json"; private static ObservableCollection<Color> RecentColors; //private static List<Color> systemColors; //private static List<Color> basicColors; private static bool hasLoadedRecentColors; //public static List<Color> BasicColors //{ // get // { // return basicColors; // } //} static ColorPickerColorHelper() { //basicColors = new List<Color>(); RecentColors = new ObservableCollection<Color>(); //systemColors = new List<Color>(); //foreach (var color in typeof(Colors).GetRuntimeProperties()) //{ // basicColors.Add((Color)color.GetValue(null)); //} } public static async Task<ObservableCollection<Color>> GetRecentColorsAsync() { if (!hasLoadedRecentColors) { hasLoadedRecentColors = true; RecentColors = await GetRecentColorsAsyncInternal(); var temp = await GetRecentColorsAsyncInternal(); if (temp != null) { RecentColors = temp; } } return RecentColors; } public async static Task SetRecentColorsAsync(Color color) { if (RecentColors != null) { if (RecentColors.LastOrDefault() == color) { return; } RecentColors.Add(color); if (RecentColors.Count > 8) { RecentColors.RemoveAt(0); } await SaveRecentColorsAsync(); } } private static async Task<ObservableCollection<Color>> GetRecentColorsAsyncInternal() { var jsonText = await StorageHelper.ReadFileAsync(ColorPickerRecentColorsKey); return JsonConvert.DeserializeObject<ObservableCollection<Color>>(jsonText); } private static async Task SaveRecentColorsAsync() { string jsonText = ""; if (RecentColors.Count > 0) { jsonText = JsonConvert.SerializeObject(RecentColors); } await StorageHelper.WriteFileAsync(ColorPickerRecentColorsKey, jsonText); } } }
第二頁是自定義的色盤
這裡用到HSL 色彩模式,之前不了解的小伙伴可以先去看一下,RGB→HSL 和 HSL→RGB轉換的算法也有。
HSL通道 透明度通道 這個2個我用到了Slider控件,當然模板我重新寫了一下
你可以通過拖拽、點擊、鍵盤上下左右來微調顏色數值,這個屬於比較簡單的拖拽實現,Ellipse通過計算得出它的位置。
當然你可以通過直接設置ARGB來設置顏色。這個輸入框,我設計成了NumericTextBox繼承於TextBox控件,支持Format
public class NumericTextBox : TextBox { private bool _isChangingTextWithCode; private bool _isChangingValueWithCode; private const double Epsilon = .00001; public event EventHandler ValueChanged; public double Value { get { return (double)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } } // Using a DependencyProperty as the backing store for Value. This enables animation, styling, binding, etc... public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(NumericTextBox), new PropertyMetadata(0.0, new PropertyChangedCallback(OnValueChanged))); private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { (d as NumericTextBox).UpdateValueText(); (d as NumericTextBox).OnValueChanged(); } public string ValueFormat { get { return (string)GetValue(ValueFormatProperty); } set { SetValue(ValueFormatProperty, value); } } // Using a DependencyProperty as the backing store for ValueFormat. This enables animation, styling, binding, etc... public static readonly DependencyProperty ValueFormatProperty = DependencyProperty.Register("ValueFormat", typeof(string), typeof(NumericTextBox), new PropertyMetadata("F0")); public double Minimum { get { return (double)GetValue(MinimumProperty); } set { SetValue(MinimumProperty, value); } } // Using a DependencyProperty as the backing store for Minimum. This enables animation, styling, binding, etc... public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register("Minimum", typeof(double), typeof(NumericTextBox), new PropertyMetadata(double.MinValue)); public double Maximum { get { return (double)GetValue(MaximumProperty); } set { SetValue(MaximumProperty, value); } } // Using a DependencyProperty as the backing store for Maximum. This enables animation, styling, binding, etc... public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum", typeof(double), typeof(NumericTextBox), new PropertyMetadata(double.MaxValue)); public NumericTextBox() { Text = this.Value.ToString(CultureInfo.CurrentCulture); TextChanged += this.OnValueTextBoxTextChanged; KeyDown += this.OnValueTextBoxKeyDown; PointerExited += this.OnValueTextBoxPointerExited; } private void OnValueTextBoxPointerExited(object sender, PointerRoutedEventArgs e) { } private void OnValueTextBoxKeyDown(object sender, KeyRoutedEventArgs e) { } private void OnValueTextBoxTextChanged(object sender, TextChangedEventArgs e) { this.UpdateValueFromText(); } protected override void OnGotFocus(RoutedEventArgs e) { base.OnGotFocus(e); } protected override void OnLostFocus(RoutedEventArgs e) { this.UpdateValueFromText(); base.OnLostFocus(e); } private void UpdateValueText() { _isChangingTextWithCode = true; this.Text = this.Value.ToString(this.ValueFormat); this.SelectionStart = this.Text.Length; _isChangingTextWithCode = false; } private void OnValueChanged() { if (ValueChanged != null) { ValueChanged(null, null); } } private bool UpdateValueFromText() { if (_isChangingTextWithCode) { return false; } double val; if (double.TryParse(this.Text, NumberStyles.Any, CultureInfo.CurrentUICulture, out val) || Calculator.TryCalculate(this.Text, out val)) { _isChangingValueWithCode = true; if (val < Minimum) { val = Minimum; } if (val > Maximum) { val = Maximum; } this.Value = val; UpdateValueText(); _isChangingValueWithCode = false; return true; } else { if (this.Text == "") { this.Value = Minimum; } UpdateValueText(); } return false; } private bool SetValueAndUpdateValidDirections(double value) { // Range coercion is handled by base class. var oldValue = this.Value; if (value < Minimum) { value = Minimum; } if (value > Maximum) { value = Maximum; } this.Value = value; if (value < Minimum || value > Maximum) { UpdateValueText(); } //this.SetValidIncrementDirection(); return Math.Abs(this.Value - oldValue) > Epsilon; } }View Code
最後這個色塊就是顯示的最終的顏色,點擊確認會生產自定義的顏色。這裡說一下透明色的效果是怎麼做成的。
在我們VS裡面當把顏色設置為Transparent的時候,效果是如下圖
其實就是添加了些灰色的Rect,知道效果,怎麼做就簡單了,代碼如下
public class TransparentBackground : Grid { public double SquareWidth { get { return (double)GetValue(SquareWidthProperty); } set { SetValue(SquareWidthProperty, value); } } // Using a DependencyProperty as the backing store for SquareWidth. This enables animation, styling, binding, etc... public static readonly DependencyProperty SquareWidthProperty = DependencyProperty.Register("SquareWidth", typeof(double), typeof(TransparentBackground), new PropertyMetadata(4.0, new PropertyChangedCallback(OnUpdateSquares))); private static void OnUpdateSquares(DependencyObject d, DependencyPropertyChangedEventArgs e) { (d as TransparentBackground).UpdateSquares(); } public Brush SquareBrush { get { return (Brush)GetValue(SquareBrushProperty); } set { SetValue(SquareBrushProperty, value); } } // Using a DependencyProperty as the backing store for SquareBrush. This enables animation, styling, binding, etc... public static readonly DependencyProperty SquareBrushProperty = DependencyProperty.Register("SquareBrush", typeof(Brush), typeof(TransparentBackground), new PropertyMetadata(new SolidColorBrush(Color.FromArgb(0xFF, 0xd7, 0xd7, 0xd7)), new PropertyChangedCallback(OnUpdateSquares))); public Brush AlternatingSquareBrush { get { return (Brush)GetValue(AlternatingSquareBrushProperty); } set { SetValue(AlternatingSquareBrushProperty, value); } } // Using a DependencyProperty as the backing store for AlternatingSquareBrush. This enables animation, styling, binding, etc... public static readonly DependencyProperty AlternatingSquareBrushProperty = DependencyProperty.Register("AlternatingSquareBrush", typeof(Brush), typeof(TransparentBackground), new PropertyMetadata(new SolidColorBrush(Colors.White), new PropertyChangedCallback(OnUpdateSquares))); public TransparentBackground() { HorizontalAlignment = HorizontalAlignment.Stretch; VerticalAlignment = VerticalAlignment.Stretch; //this.SizeChanged += (s, e) => //{ // if (e.NewSize != e.PreviousSize) // { // UpdateSquares(); // } //}; } Size pre = Size.Empty; protected override Size ArrangeOverride(Size finalSize) { if (pre != finalSize) { UpdateSquares(finalSize); pre = finalSize; } return base.ArrangeOverride(finalSize); } private void UpdateSquares(Size? finalSize = null) { Size size = finalSize == null ? new Size(this.ActualWidth, this.ActualHeight) : finalSize.Value; //size = new Size(this.ActualWidth, this.ActualHeight); this.Children.Clear(); for (int x = 0; x < size.Width / SquareWidth; x++) { for (int y = 0; y < size.Height / SquareWidth; y++) { var rectangle = new Rectangle(); rectangle.Fill = ((x % 2 == 0 && y % 2 == 0) || (x % 2 == 1 && y % 2 == 1)) ? SquareBrush : AlternatingSquareBrush; rectangle.Width = Math.Max(0, Math.Min(SquareWidth, size.Width - x * SquareWidth)); rectangle.Height = Math.Max(0, Math.Min(SquareWidth, size.Height - y * SquareWidth)); rectangle.Margin = new Thickness(x * SquareWidth, y * SquareWidth, 0, 0); rectangle.HorizontalAlignment = HorizontalAlignment.Left; rectangle.VerticalAlignment = VerticalAlignment.Top; this.Children.Add(rectangle); } } } }
這樣子我們整個控件就差不多了。
擴展
由於項目裡面,一個頁面上需要有很多個這樣的控件,感覺如果有10個需要選擇顏色的地方,就要有10個實例的話,比較傻,固做以下的擴展。
添加了
Owner 屬性-作為ColorPicker 顏色改變的接受源
PlacementTarget 屬性- 作為ColorPicker 彈出的Target
Show 方法- 能夠使用代碼顯示ColorPicker
用法如下:
前台Xaml
<control:ColorPicker x:Name="colorPicker" Width="300" Height="40" Opacity="0" Closed="colorPicker_Closed" SelectedColorChanged="colorPicker_SelectedColorChanged" Placement="BottomCenter" HorizontalAlignment="Center" VerticalAlignment="Top" SelectedColor="Transparent" ArrowVisibility="Visible"/> <Rectangle x:Name="rectangle1" Width="100" Height="30" Margin="100" Fill="Green" Tapped="Rectangle_Tapped"/> <Rectangle x:Name="rectangle2" Width="100" Height="30" Margin="100" Fill="Yellow" Tapped="Rectangle_Tapped"/>
後台cs
private void Rectangle_Tapped(object sender, TappedRoutedEventArgs e) { colorPicker.Placement = AdvancedFlyoutPlacementMode.RightCenter; colorPicker.PlacementTarget = (sender as FrameworkElement); colorPicker.Owner = sender; colorPicker.Show(); } private void colorPicker_SelectedColorChanged(object sender, EventArgs e) { if (colorPicker.Owner!=null) { (colorPicker.Owner as Rectangle).Fill = new SolidColorBrush(colorPicker.SelectedColor); colorPicker.Owner = null; } } private void colorPicker_Closed(object sender, object e) { colorPicker.PlacementTarget = null; }
總結
其實ColorPicker這個控件總體來說還是比較簡單的,搞清楚UI 和HSL算法就ok。對了Colorpicker是固定了主題Light和大小的,黑色主題太丑了,而且會使色塊看著及其不爽,所以背景和主題以及大小我都是寫死了的。
AdvancedFlyout
背景
做這個東西,是被微軟逼的。
10586 和 14393上面Flyout這個控件 行為上有很大區別。
主要問題是在10586上面,不能支持同時2個Flyout打開,就是說打開一個。再打開下一個的時候會關閉上一個。
沒辦法,只有自己搞一個。
AdvancedFlyoutBase/AdvancedFlyout
把微軟的FlyoutBase/Floyout 屬性方法都搞過來,我們自己用Popup來實現。
/// <summary> /// to solve issue that can't open two flyouts in 10586. /// </summary> [ContentProperty(Name = nameof(Content))] public class AdvancedFlyout : AdvancedFlyoutBase { public UIElement Content { get; set; } /// <summary> /// FlyoutPresenter Style /// </summary> public Style FlyoutPresenterStyle { get; set; } protected override Control CreatePresenter() { var fp = base.CreatePresenter() as FlyoutPresenter; if (FlyoutPresenterStyle != null) { fp.Style = FlyoutPresenterStyle; } fp.Content = Content; return fp; } }
主要的實現在於控制Popup的位置。
AdvancedFlyoutBase 裡面我添加了
FlyoutBase 沒有的三個屬性:
IsLightDismissEnabled
VerticalOffset
HorizontalOffset
這3個屬性都是Popup的。主要是在Placement的基准上再給於用戶微調的權利。PlacementMode是一個枚舉,比微軟的分的更細。
public enum AdvancedFlyoutPlacementMode { TopLeft = 0, TopCenter, TopRight, BottomLeft, BottomCenter, BottomRight, LeftTop, LeftCenter, LeftBottom, RightTop, RightCenter, RightBottom, FullScreen, CenterScreen, }
我們在ShowAt方法中來實現計算Popup的具體位置
public void ShowAt(FrameworkElement placementTarget) { if (Opening != null) { Opening(this, null); } if (_popup == null) { _popup = new Popup(); _popup.ChildTransitions = new TransitionCollection() { new PopupThemeTransition() }; _popup.Opened += _popup_Opened; _popup.Closed += _popup_Closed; _popup.Child = CreatePresenter(); } reCalculatePopupPosition = !CalculatePopupPosition(placementTarget); _popup.IsLightDismissEnabled = IsLightDismissEnabled; this.placementTarget = placementTarget; if (reCalculatePopupPosition || FlyoutPresenter.Style == null) { _popup.Opacity = 0; } _popup.HorizontalOffset += HorizontalOffset; _popup.VerticalOffset += VerticalOffset; _popup.IsOpen = true; }
其中CalculatePopupPosition 是我們的重中之重。
我們計算Popup的位置需要參考下面幾樣:
1.PlacementTarget在頁面上的位置
其實就是控件相對於Window的位置,由以下代碼獲得
var placementTargetRect = placementTarget.TransformToVisual(Window.Current.Content as FrameworkElement).TransformBounds(new Rect(0, 0, placementTarget.ActualWidth, placementTarget.ActualHeight));
2.彈出頁面的大小
FlyoutPresenter的實際大小,由以下代碼獲得
var fp = FlyoutPresenter; fp.Width = double.NaN; fp.Height = double.NaN; if (fp.DesiredSize == fpSize) { fp.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); } fpSize = fp.DesiredSize;
3.Window 的大小
var windowSize = new Size(Window.Current.Bounds.Width, Window.Current.Bounds.Height);
有了之上3個參考數據,那麼我們就很容易來計算出Popup顯示的位置,
下面以Top為例:
private bool TryHandlePlacementTop(Rect placementTargetRect, Size fpSize, Size windowSize) { if (placementTargetRect.Y - fpSize.Height < 0) { return false; } double x = 0; _popup.VerticalOffset = placementTargetRect.Y - fpSize.Height; if (fpSize.Width > windowSize.Width) { _popup.HorizontalOffset = 0; return true; } switch (Placement) { case AdvancedFlyoutPlacementMode.TopLeft: x = placementTargetRect.X; break; case AdvancedFlyoutPlacementMode.TopCenter: x = placementTargetRect.X + placementTargetRect.Width / 2 - fpSize.Width / 2; if (x < 0) { x = 0; } break; case AdvancedFlyoutPlacementMode.TopRight: x = placementTargetRect.X + placementTargetRect.Width - fpSize.Width; if (x < 0) { x = 0; } break; default: goto case AdvancedFlyoutPlacementMode.TopCenter; } if (x + fpSize.Width > windowSize.Width) { x = windowSize.Width - fpSize.Width; } _popup.HorizontalOffset = x; return true; }
如果target控件上面的空間不夠,那麼肯定我們不能把Popup放上面,故return false,再嘗試把Popup放在其他方位上。
如果可以放的話,我們再按照是Left,Center,Right的參考位置來計算,注意我們要考慮到Window的大小,不能超出Window。
最終Top的代碼如下圖
case AdvancedFlyoutPlacementMode.TopLeft: case AdvancedFlyoutPlacementMode.TopCenter: case AdvancedFlyoutPlacementMode.TopRight: if (!TryHandlePlacementTop(placementTargetRect, fpSize, windowSize)) { if (!TryHandlePlacementBottom(placementTargetRect, fpSize, windowSize)) { if (!TryHandlePlacementLeft(placementTargetRect, fpSize, windowSize)) { if (!TryHandlePlacementRight(placementTargetRect, fpSize, windowSize)) { TryHandlePlacementCenterScreen(fpSize, windowSize); } } } } break;
在開發過程中發現
如果在Popup Open之前計算FlyoutPresenter的大小,
可能導致Size不正確,如果沒有給FlyoutPresenter 賦Style,這個時候還不會使用默認FlyoutPresenter 的樣式,Pading,Margin這些參數還沒得到賦值。
或者拋異常,比如FlyoutPresenter內部是Pivot的時候會拋異常。
所以我增加了容錯。
在計算出錯或者FlyoutPresenter的Style 為Null的時候,講Popup的Opacity設置為0,
並且在Popup Open之後 重寫計算位置,然後把Popup Opacity設置1.
if (reCalculatePopupPosition || FlyoutPresenter.Style == null) { _popup.Opacity = 0; } private void _popup_Opened(object sender, object e) { //DesiredSize was not right when style was null before opened //we should re-calcuatePopupPosition after FlyoutPresenter get default values from default style or app resource style if (FlyoutPresenter.Style == null || reCalculatePopupPosition) { CalculatePopupPosition(placementTarget); _popup.HorizontalOffset += HorizontalOffset; _popup.VerticalOffset += VerticalOffset; _popup.Opacity = 1; } if (Opened != null) { Opened(this, e); } }
這樣就解決位置不對的問題。。其實我在使用Flyout的時候也遇到過顯示的位置從左上角 跳到正確位置的情況,估計跟我這個原因一樣。。估計微軟也做了容錯。不過沒把Opacity設置一下。
總結
其實在開發中,有時間去抱怨微軟版本控件有問題,不如靜下心來想想其他辦法,也需會比微軟更好的版本,也更容易方便我們自定義。
開源有益,源碼GitHub地址。
最後放上2個控件在項目裡面的合體照。