Spiga

WPF学习笔记4:控件自定义

2022-08-29 23:03:02

一、概述

1. 个性化处理的方式

  • 控件模板
  • UserControl 用户控件
  • CustomControl 自定义控件

2. UserControl与CustomControl

开发方式与表现结构

CustomControl 模板与关键特性

区别:

1、自定义控件:注重控件对象的功能,必须遵守WPF的控件规则

  • 完全自己实现一个控件 继承现有控件进行功能扩展,并且添加新功能 WPF的控件要求
  • 后台代码(控制逻辑)和Generic.Xaml(样式 模板)进行组合
  • 支持模板重写
  • 继承Control

2、用户控件:注重复合控件组合使用,非常灵活,可以根据控件开发人员自己的意愿进行功能处理

  • 多个现有控件的集合,组成一个可复用的控件组
  • XAML和后台代码组成 绑定非常紧密
  • 不支持模板重写、样式
  • 继承UserControl

二、日期时间选择器

<UserControl x:Class="Zhaoxi.CustomLesson.Controls.DateTimePicker"
             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:local="clr-namespace:Controls"
             mc:Ignorable="d" 
             d:DesignHeight="30" d:DesignWidth="200">
    <UserControl.Resources>
        <Style TargetType="RepeatButton" x:Key="ButtonUpStyle">
            <Setter Property="Height" Value="18"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="RepeatButton">
                        <Border Background="Transparent">
                            <Path Data="M838.116 732.779 877.7 693.195 511.979 327.549 146.3 693.195 185.883 732.779 512.003 406.652Z"
                          Fill="#999" Stretch="Uniform" Margin="6"/>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="RepeatButton" x:Key="ButtonDownStyle">
            <Setter Property="Height" Value="18"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="RepeatButton">
                        <Border Background="Transparent">
                            <Path Data="M185.884 327.55 146.3 367.133 512.021 732.779 877.7 367.133 838.117 327.55 511.997 653.676Z"
                          Fill="#999" Stretch="Uniform" Margin="6"/>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="Button" x:Key="ButtonCancelStyle">
            <Setter Property="Width" Value="24"/>
            <Setter Property="Height" Value="24"/>
            <Setter Property="HorizontalAlignment" Value="Right"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Border Background="#EEE">
                            <Path Data="M557.3 512l329.3-329.4a32 32 0 1 0-45.2-45.2L512 466.7 182.6 137.4a32 32 0 1 0-45.2 45.2L466.7 512 137.4 841.4a31.9 31.9 0 0 0 0 45.2 31.9 31.9 0 0 0 45.2 0L512 557.3l329.4 329.3a31.9 31.9 0 0 0 45.2 0 31.9 31.9 0 0 0 0-45.2z"
                          Fill="#999" Stretch="Uniform" HorizontalAlignment="Center" Margin="5"/>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style TargetType="Button" x:Key="ButtonAcceptStyle">
            <Setter Property="Width" Value="24"/>
            <Setter Property="Height" Value="24"/>
            <Setter Property="HorizontalAlignment" Value="Right"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Border Background="#EEE">
                            <Path Data="M892.064 261.888a31.936 31.936 0 0 0-45.216 1.472L421.664 717.248l-220.448-185.216a32 32 0 1 0-41.152 48.992l243.648 204.704a31.872 31.872 0 0 0 20.576 7.488 31.808 31.808 0 0 0 23.36-10.112L893.536 307.136a32 32 0 0 0-1.472-45.248z"
                          Fill="#999" Stretch="Uniform" HorizontalAlignment="Center" Margin="5"/>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>


        <Style x:Key="CalendarCalendarDayButtonStyle1" TargetType="{x:Type CalendarDayButton}">
            <Setter Property="MinWidth" Value="10"/>
            <Setter Property="MinHeight" Value="10"/>
            <Setter Property="FontSize" Value="13"/>
            <Setter Property="HorizontalContentAlignment" Value="Center"/>
            <Setter Property="VerticalContentAlignment" Value="Center"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type CalendarDayButton}">
                        <Grid>
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualStateGroup.Transitions>
                                        <VisualTransition GeneratedDuration="0:0:0.1"/>
                                    </VisualStateGroup.Transitions>
                                    <VisualState x:Name="Normal"/>
                                    <VisualState x:Name="MouseOver">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0" Storyboard.TargetName="HighlightBackground" To="0.5" Storyboard.TargetProperty="Opacity"/>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="Pressed">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0" Storyboard.TargetName="HighlightBackground" To="0.5" Storyboard.TargetProperty="Opacity"/>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="Disabled">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0" Storyboard.TargetName="HighlightBackground" To="0" Storyboard.TargetProperty="Opacity"/>
                                            <DoubleAnimation Duration="0" Storyboard.TargetName="NormalText" To=".35" Storyboard.TargetProperty="Opacity"/>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                                <VisualStateGroup x:Name="SelectionStates">
                                    <VisualStateGroup.Transitions>
                                        <VisualTransition GeneratedDuration="0"/>
                                    </VisualStateGroup.Transitions>
                                    <VisualState x:Name="Unselected"/>
                                    <VisualState x:Name="Selected">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0" Storyboard.TargetName="SelectedBackground" To=".75" Storyboard.TargetProperty="Opacity"/>
                                            <ColorAnimation Duration="0" Storyboard.TargetName="NormalText" To="#FFFFFFFF" Storyboard.TargetProperty="(TextElement.Foreground).(SolidColorBrush.Color)"/>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                                <VisualStateGroup x:Name="CalendarButtonFocusStates">
                                    <VisualStateGroup.Transitions>
                                        <VisualTransition GeneratedDuration="0"/>
                                    </VisualStateGroup.Transitions>
                                    <VisualState x:Name="CalendarButtonFocused">
                                        <Storyboard>
                                            <ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="DayButtonFocusVisual" Storyboard.TargetProperty="Visibility">
                                                <DiscreteObjectKeyFrame KeyTime="0">
                                                    <DiscreteObjectKeyFrame.Value>
                                                        <Visibility>Visible</Visibility>
                                                    </DiscreteObjectKeyFrame.Value>
                                                </DiscreteObjectKeyFrame>
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="CalendarButtonUnfocused">
                                        <Storyboard>
                                            <ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="DayButtonFocusVisual" Storyboard.TargetProperty="Visibility">
                                                <DiscreteObjectKeyFrame KeyTime="0">
                                                    <DiscreteObjectKeyFrame.Value>
                                                        <Visibility>Collapsed</Visibility>
                                                    </DiscreteObjectKeyFrame.Value>
                                                </DiscreteObjectKeyFrame>
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                                <VisualStateGroup x:Name="ActiveStates">
                                    <VisualStateGroup.Transitions>
                                        <VisualTransition GeneratedDuration="0"/>
                                    </VisualStateGroup.Transitions>
                                    <VisualState x:Name="Active"/>
                                    <VisualState x:Name="Inactive">
                                        <Storyboard>
                                            <ColorAnimation Duration="0" Storyboard.TargetName="NormalText" To="#FF777777" Storyboard.TargetProperty="(TextElement.Foreground).(SolidColorBrush.Color)"/>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                                <VisualStateGroup x:Name="DayStates">
                                    <VisualStateGroup.Transitions>
                                        <VisualTransition GeneratedDuration="0"/>
                                    </VisualStateGroup.Transitions>
                                    <VisualState x:Name="RegularDay"/>
                                    <VisualState x:Name="Today">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0" Storyboard.TargetName="TodayBackground" To="1" Storyboard.TargetProperty="Opacity"/>
                                            <ColorAnimation Duration="0" Storyboard.TargetName="NormalText" To="#FFFFFFFF" Storyboard.TargetProperty="(TextElement.Foreground).(SolidColorBrush.Color)"/>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                                <VisualStateGroup x:Name="BlackoutDayStates">
                                    <VisualStateGroup.Transitions>
                                        <VisualTransition GeneratedDuration="0"/>
                                    </VisualStateGroup.Transitions>
                                    <VisualState x:Name="NormalDay"/>
                                    <VisualState x:Name="BlackoutDay">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0" Storyboard.TargetName="Blackout" To=".2" Storyboard.TargetProperty="Opacity"/>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <Rectangle x:Name="TodayBackground" Fill="Gray" Opacity="0" RadiusX="1" RadiusY="1"/>
                            <Rectangle x:Name="SelectedBackground" Fill="#409EFE" Opacity="0" RadiusX="1" RadiusY="1"/>
                            <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"/>
                            <Rectangle x:Name="HighlightBackground" Fill="#FFBADDE9" Opacity="0" RadiusX="1" RadiusY="1"/>
                            <ContentPresenter x:Name="NormalText" TextElement.Foreground="#FF333333" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="5,1,5,1" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                            <Path x:Name="Blackout" Data="M8.1772461,11.029181 L10.433105,11.029181 L11.700684,12.801641 L12.973633,11.029181 L15.191895,11.029181 L12.844727,13.999395 L15.21875,17.060919 L12.962891,17.060919 L11.673828,15.256231 L10.352539,17.060919 L8.1396484,17.060919 L10.519043,14.042364 z" Fill="#FF000000" HorizontalAlignment="Stretch" Margin="3" Opacity="0" RenderTransformOrigin="0.5,0.5" Stretch="Fill" VerticalAlignment="Stretch"/>
                            <Rectangle x:Name="DayButtonFocusVisual" IsHitTestVisible="false" RadiusX="1" RadiusY="1" Stroke="#FF45D6FA" Visibility="Collapsed"/>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style x:Key="CalendarCalendarItemStyle1" TargetType="{x:Type CalendarItem}">
            <Setter Property="Margin" Value="0,3,0,3"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type CalendarItem}">
                        <ControlTemplate.Resources>
                            <DataTemplate x:Key="{x:Static CalendarItem.DayTitleTemplateResourceKey}">
                                <TextBlock Foreground="#FF333333" FontFamily="Verdana" FontWeight="Bold" FontSize="9.5" HorizontalAlignment="Center" Margin="0,6,0,6" Text="{Binding}" VerticalAlignment="Center"/>
                            </DataTemplate>
                        </ControlTemplate.Resources>
                        <Grid x:Name="PART_Root">
                            <Grid.Resources>
                                <SolidColorBrush x:Key="DisabledColor" Color="#A5FFFFFF"/>
                            </Grid.Resources>
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualState x:Name="Normal"/>
                                    <VisualState x:Name="Disabled">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0" Storyboard.TargetName="PART_DisabledVisual" To="1" Storyboard.TargetProperty="Opacity"/>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="1">
                                <Border BorderBrush="#FFFFFFFF" BorderThickness="0" CornerRadius="1">
                                    <Grid>
                                        <Grid.Resources>
                                            <ControlTemplate x:Key="PreviousButtonTemplate" TargetType="{x:Type Button}">
                                                <Grid Cursor="Hand">
                                                    <VisualStateManager.VisualStateGroups>
                                                        <VisualStateGroup x:Name="CommonStates">
                                                            <VisualState x:Name="Normal"/>
                                                            <VisualState x:Name="MouseOver">
                                                                <Storyboard>
                                                                    <ColorAnimation Duration="0" Storyboard.TargetName="path" To="#FF73A9D8" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"/>
                                                                </Storyboard>
                                                            </VisualState>
                                                            <VisualState x:Name="Disabled">
                                                                <Storyboard>
                                                                    <DoubleAnimation Duration="0" Storyboard.TargetName="path" To=".5" Storyboard.TargetProperty="(Shape.Fill).(Brush.Opacity)"/>
                                                                </Storyboard>
                                                            </VisualState>
                                                        </VisualStateGroup>
                                                    </VisualStateManager.VisualStateGroups>
                                                    <Grid Background="Transparent">
                                                        <Path x:Name="path" Data="M671.968176 911.99957c-12.287381 0-24.576482-4.67206-33.951566-14.047144L286.048434 545.984249c-18.751888-18.719204-18.751888-49.12028 0-67.872168L638.016611 126.111222c18.751888-18.751888 49.12028-18.751888 67.872168 0 18.751888 18.719204 18.751888 49.12028 0 67.872168l-318.016611 318.047574L705.888778 830.047574c18.751888 18.751888 18.751888 49.12028 0 67.872168C696.544658 907.32751 684.255557 911.99957 671.968176 911.99957z" 
                                                              Fill="#FF333333" HorizontalAlignment="Center"  VerticalAlignment="Center"
                                                              Height="10" Width="6" Stretch="Fill" Margin="10,0"/>
                                                    </Grid>
                                                </Grid>
                                            </ControlTemplate>
                                            <ControlTemplate x:Key="NextButtonTemplate" TargetType="{x:Type Button}">
                                                <Grid Cursor="Hand">
                                                    <VisualStateManager.VisualStateGroups>
                                                        <VisualStateGroup x:Name="CommonStates">
                                                            <VisualState x:Name="Normal"/>
                                                            <VisualState x:Name="MouseOver">
                                                                <Storyboard>
                                                                    <ColorAnimation Duration="0" Storyboard.TargetName="path" To="#FF73A9D8" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"/>
                                                                </Storyboard>
                                                            </VisualState>
                                                            <VisualState x:Name="Disabled">
                                                                <Storyboard>
                                                                    <DoubleAnimation Duration="0" Storyboard.TargetName="path" To=".5" Storyboard.TargetProperty="(Shape.Fill).(Brush.Opacity)"/>
                                                                </Storyboard>
                                                            </VisualState>
                                                        </VisualStateGroup>
                                                    </VisualStateManager.VisualStateGroups>
                                                    <Grid Background="Transparent">
                                                        <Path x:Name="path" Data="M761.055557 532.128047c0.512619-0.992555 1.343475-1.823411 1.792447-2.848649 8.800538-18.304636 5.919204-40.703346-9.664077-55.424808L399.935923 139.743798c-19.264507-18.208305-49.631179-17.344765-67.872168 1.888778-18.208305 19.264507-17.375729 49.631179 1.888778 67.872168l316.960409 299.839269L335.199677 813.631716c-19.071845 18.399247-19.648112 48.767639-1.247144 67.872168 9.407768 9.791372 21.984142 14.688778 34.560516 14.688778 12.000108 0 24.000215-4.479398 33.311652-13.439914l350.048434-337.375729c0.672598-0.672598 0.927187-1.599785 1.599785-2.303346 0.512619-0.479935 1.056202-0.832576 1.567101-1.343475C757.759656 538.879828 759.199462 535.391265 761.055557 532.128047z" 
                                                              Fill="#FF333333" HorizontalAlignment="Right" VerticalAlignment="Center" 
                                                              Height="10" Stretch="Fill" Width="6" Margin="10,0"/>
                                                    </Grid>
                                                </Grid>
                                            </ControlTemplate>
                                            <ControlTemplate x:Key="HeaderButtonTemplate" TargetType="{x:Type Button}">
                                                <Grid Cursor="Hand" Background="Transparent">
                                                    <VisualStateManager.VisualStateGroups>
                                                        <VisualStateGroup x:Name="CommonStates">
                                                            <VisualState x:Name="Normal"/>
                                                            <VisualState x:Name="MouseOver">
                                                                <Storyboard>
                                                                    <ColorAnimation Duration="0" Storyboard.TargetName="buttonContent" To="#FF73A9D8" Storyboard.TargetProperty="(TextElement.Foreground).(SolidColorBrush.Color)"/>
                                                                </Storyboard>
                                                            </VisualState>
                                                            <VisualState x:Name="Disabled">
                                                                <Storyboard>
                                                                    <DoubleAnimation Duration="0" Storyboard.TargetName="buttonContent" To=".5" Storyboard.TargetProperty="Opacity"/>
                                                                </Storyboard>
                                                            </VisualState>
                                                        </VisualStateGroup>
                                                    </VisualStateManager.VisualStateGroups>
                                                    <ContentPresenter x:Name="buttonContent" ContentTemplate="{TemplateBinding ContentTemplate}" 
                                                                      Content="{TemplateBinding Content}" TextElement.Foreground="#FF333333"
                                                                      HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                                                                      Margin="1,9" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                                                </Grid>
                                            </ControlTemplate>
                                        </Grid.Resources>
                                        <Grid.ColumnDefinitions>
                                            <ColumnDefinition Width="*"/>
                                            <ColumnDefinition Width="auto"/>
                                            <ColumnDefinition Width="auto"/>
                                        </Grid.ColumnDefinitions>
                                        <Grid.RowDefinitions>
                                            <RowDefinition Height="Auto"/>
                                            <RowDefinition Height="*"/>
                                        </Grid.RowDefinitions>
                                        <Button x:Name="PART_HeaderButton" Grid.Column="0" Focusable="False" FontWeight="Bold" FontSize="14" HorizontalAlignment="Left" 
                                                VerticalContentAlignment="Center"
                                                Grid.Row="0" Template="{StaticResource HeaderButtonTemplate}" VerticalAlignment="Center" Margin="10,5"/>

                                        <Button x:Name="PART_PreviousButton" Grid.Column="1" Focusable="False" HorizontalAlignment="Center" 
                                                Template="{StaticResource PreviousButtonTemplate}" Margin="0,5"/>
                                        <Button x:Name="PART_NextButton" Grid.Column="2" Focusable="False" HorizontalAlignment="Right" 
                                                Template="{StaticResource NextButtonTemplate}" Margin="0,5"/>

                                        <Grid x:Name="PART_MonthView" Grid.ColumnSpan="3" HorizontalAlignment="Center" Margin="6,-1,6,6" Grid.Row="1" Visibility="Visible">
                                            <Grid.ColumnDefinitions>
                                                <ColumnDefinition Width="30"/>
                                                <ColumnDefinition Width="30"/>
                                                <ColumnDefinition Width="30"/>
                                                <ColumnDefinition Width="30"/>
                                                <ColumnDefinition Width="30"/>
                                                <ColumnDefinition Width="30"/>
                                                <ColumnDefinition Width="30"/>
                                            </Grid.ColumnDefinitions>
                                            <Grid.RowDefinitions>
                                                <RowDefinition Height="Auto"/>
                                                <RowDefinition Height="30"/>
                                                <RowDefinition Height="30"/>
                                                <RowDefinition Height="30"/>
                                                <RowDefinition Height="30"/>
                                                <RowDefinition Height="30"/>
                                                <RowDefinition Height="30"/>
                                            </Grid.RowDefinitions>
                                        </Grid>
                                        <Grid x:Name="PART_YearView" Grid.ColumnSpan="3" HorizontalAlignment="Center" Margin="6,-3,7,6" Grid.Row="1" Visibility="Hidden">
                                            <Grid.ColumnDefinitions>
                                                <ColumnDefinition Width="50"/>
                                                <ColumnDefinition Width="50"/>
                                                <ColumnDefinition Width="50"/>
                                                <ColumnDefinition Width="50"/>
                                            </Grid.ColumnDefinitions>
                                            <Grid.RowDefinitions>
                                                <RowDefinition Height="50"/>
                                                <RowDefinition Height="50"/>
                                                <RowDefinition Height="50"/>
                                            </Grid.RowDefinitions>
                                        </Grid>
                                    </Grid>
                                </Border>
                            </Border>
                            <Rectangle x:Name="PART_DisabledVisual" Fill="{StaticResource DisabledColor}" Opacity="0" RadiusX="2" RadiusY="2" Stroke="{StaticResource DisabledColor}" Stretch="Fill" StrokeThickness="1" Visibility="Collapsed"/>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsEnabled" Value="False">
                                <Setter Property="Visibility" TargetName="PART_DisabledVisual" Value="Visible"/>
                            </Trigger>
                            <DataTrigger Binding="{Binding DisplayMode, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Calendar}}}" Value="Year">
                                <Setter Property="Visibility" TargetName="PART_MonthView" Value="Hidden"/>
                                <Setter Property="Visibility" TargetName="PART_YearView" Value="Visible"/>
                            </DataTrigger>
                            <DataTrigger Binding="{Binding DisplayMode, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Calendar}}}" Value="Decade">
                                <Setter Property="Visibility" TargetName="PART_MonthView" Value="Hidden"/>
                                <Setter Property="Visibility" TargetName="PART_YearView" Value="Visible"/>
                            </DataTrigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="30"/>
        </Grid.ColumnDefinitions>
        <TextBox IsReadOnly="True" Name="textBox" VerticalContentAlignment="Center" BorderBrush="#DDD"
                 Text="{Binding CurrentDateTime,RelativeSource={RelativeSource AncestorType=UserControl},StringFormat={}{0:yyyy-MM-dd HH:mm:ss}}"/>
        <ToggleButton Grid.Column="1" Name="toggleButton" Background="#DDD" BorderThickness="0">
            <Path Data="M453.228 781.672H170.319V409.18h650.457c11.686 0 21.16-9.473 21.16-21.16V206.29c0-11.686-9.474-21.16-21.16-21.16H660.945v-35.972c0-11.686-9.474-21.16-21.16-21.16-11.688 0-21.16 9.474-21.16 21.16v35.972H356.247v-35.972c0-11.686-9.473-21.16-21.16-21.16s-21.16 9.474-21.16 21.16v35.972H149.16c-11.687 0-21.16 9.474-21.16 21.16V790.44c0 18.5 14.822 33.55 33.04 33.55H453.23c11.686 0 21.16-9.472 21.16-21.16 0-11.686-9.475-21.16-21.161-21.16zM170.319 227.45h143.609v35.971c0 11.687 9.473 21.16 21.16 21.16s21.159-9.472 21.159-21.16V227.45h262.377v35.971c0 11.687 9.472 21.16 21.16 21.16 11.686 0 21.16-9.472 21.16-21.16V227.45h138.672v139.412H170.319V227.45zM690.5 483.1c-113.837 0-206.45 92.613-206.45 206.453C484.05 803.388 576.663 896 690.5 896s206.45-92.613 206.45-206.447c0-113.84-92.611-206.453-206.45-206.453z m0 370.58c-90.5 0-164.13-73.628-164.13-164.131s73.628-164.132 164.13-164.132c90.503 0 164.133 73.628 164.133 164.132S781.002 853.68 690.5 853.68zM390.104 640.816H246.218c-11.687 0-21.16 9.472-21.16 21.16 0 11.685 9.473 21.158 21.16 21.158h143.885c11.686 0 21.16-9.473 21.16-21.158-0.002-11.688-9.475-21.16-21.16-21.16z m0-119.489H246.218c-11.687 0-21.16 9.473-21.16 21.158 0 11.688 9.473 21.16 21.16 21.16h143.885c11.686 0 21.16-9.472 21.16-21.16-0.002-11.683-9.475-21.158-21.16-21.158z m413.071 147.067H711.66V617.61c0-11.687-9.472-21.16-21.159-21.16s-21.16 9.473-21.16 21.16v71.943c0 11.685 9.474 21.158 21.16 21.158h112.673c11.687 0 21.16-9.473 21.16-21.158 0.001-11.688-9.473-21.16-21.16-21.16z"
                  Fill="Orange" Stretch="Fill" Margin="5"/>
        </ToggleButton>


        <Popup StaysOpen="False" IsOpen="{Binding ElementName=toggleButton,Path=IsChecked}"
               PlacementTarget="{Binding ElementName=textBox}" AllowsTransparency="True"
               HorizontalOffset="2" Name="popup">

            <Border Background="#F7F9FA" Width="auto" Height="auto" Margin="3">
                <Border.Effect>
                    <DropShadowEffect BlurRadius="5" Color="Gray" ShadowDepth="0" Opacity="1"/>
                </Border.Effect>

                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="auto"/>
                        <RowDefinition Height="auto"/>
                    </Grid.RowDefinitions>

                    <Calendar Name="calendar" CalendarItemStyle="{StaticResource CalendarCalendarItemStyle1}"
                              CalendarDayButtonStyle="{StaticResource CalendarCalendarDayButtonStyle1}"
                              BorderThickness="0"
                              Background="Transparent"/>

                    <Grid Grid.Row="1" Margin="10,5,10,10">
                        <Grid.RowDefinitions>
                            <RowDefinition/>
                            <RowDefinition Height="30"/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition/>
                            <ColumnDefinition Width="auto"/>
                            <ColumnDefinition/>
                            <ColumnDefinition Width="auto"/>
                            <ColumnDefinition/>
                            <ColumnDefinition/>
                            <ColumnDefinition/>
                        </Grid.ColumnDefinitions>

                        <RepeatButton Content="上" Style="{StaticResource ButtonUpStyle}" Click="BtnHourUp_Click"/>
                        <RepeatButton Content="上" Style="{StaticResource ButtonUpStyle}" Grid.Column="2" Click="BtnMinuteUp_Click" Tag="{Binding ElementName=tb_minute}"/>
                        <RepeatButton Content="上" Style="{StaticResource ButtonUpStyle}" Grid.Column="4" Click="BtnMinuteUp_Click" Tag="{Binding ElementName=tb_second}"/>
                        <RepeatButton Content="下" Style="{StaticResource ButtonDownStyle}" Grid.Row="2"  Click="BtnHourDown_Click"/>
                        <RepeatButton Content="下" Style="{StaticResource ButtonDownStyle}" Grid.Row="2" Grid.Column="2" Click="BtnMinuteDown_Click" Tag="{Binding ElementName=tb_minute}"/>
                        <RepeatButton Content="下" Style="{StaticResource ButtonDownStyle}" Grid.Row="2" Grid.Column="4" Click="BtnMinuteDown_Click" Tag="{Binding ElementName=tb_second}"/>


                        <TextBox Grid.Row="1" Name="tb_Hour" TextChanged="tb_Hour_TextChanged" Height="24"
                                 VerticalContentAlignment="Center" HorizontalContentAlignment="Center"/>
                        <TextBlock Text=":" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Margin="3,0"/>
                        <TextBox Grid.Row="1" Grid.Column="2" Name="tb_minute" TextChanged="tb_minute_TextChanged" Height="24"
                                 VerticalContentAlignment="Center" HorizontalContentAlignment="Center"/>
                        <TextBlock Text=":" Grid.Row="1" Grid.Column="3" VerticalAlignment="Center" Margin="3,0"/>
                        <TextBox Grid.Row="1" Grid.Column="4" Name="tb_second" TextChanged="tb_minute_TextChanged" Height="24"
                                 VerticalContentAlignment="Center" HorizontalContentAlignment="Center"/>

                        <Button Content="X" Grid.Column="5" Grid.RowSpan="3" Style="{StaticResource ButtonCancelStyle}"
         Click="BtnCancel_Click"/>
                        <Button Content="确" Grid.Column="6" Grid.RowSpan="3" Style="{StaticResource ButtonAcceptStyle}"
         Click="BtnAccept_Click"/>
                    </Grid>
                </Grid>
            </Border>
        </Popup>
    </Grid>
</UserControl>
public partial class DateTimePicker : UserControl
{
    public event EventHandler<DateTime> SelectedChanged;

    public DateTime CurrentDateTime
    {
        get { return (DateTime)GetValue(CurrentDateTimeProperty); }
        set { SetValue(CurrentDateTimeProperty, value); }
    }

    public static readonly DependencyProperty CurrentDateTimeProperty =
        DependencyProperty.Register("CurrentDateTime",
            typeof(DateTime),
            typeof(DateTimePicker),
            new PropertyMetadata(DateTime.Now, new PropertyChangedCallback(OnCurrentDateTimeChanged)));

    private static void OnCurrentDateTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        (d as DateTimePicker).Refresh();
    }

    private void Refresh()
    {
        this.calendar.SelectedDate = this.CurrentDateTime;

        this.tb_Hour.Text = this.CurrentDateTime.Hour.ToString();
        this.tb_minute.Text = this.CurrentDateTime.Minute.ToString();
        this.tb_second.Text = this.CurrentDateTime.Second.ToString();
    }

    public DateTimePicker()
    {
        InitializeComponent();
    }

    private void BtnAccept_Click(object sender, RoutedEventArgs e)
    {
        //日期时间合并
        int year = this.calendar.SelectedDate.Value.Year;
        int month = this.calendar.SelectedDate.Value.Month;
        int day = this.calendar.SelectedDate.Value.Day;

        int.TryParse(this.tb_Hour.Text.Trim(), out int hour);
        int.TryParse(this.tb_minute.Text.Trim(), out int minute);
        int.TryParse(this.tb_second.Text.Trim(), out int second);

        CurrentDateTime = new DateTime(year, month, day, hour, minute, second);

        this.popup.IsOpen = false;

        SelectedChanged?.Invoke(this, CurrentDateTime);
    }

    private void BtnHourUp_Click(object sender, RoutedEventArgs e)
    {
        int.TryParse(this.tb_Hour.Text.Trim(), out int hour);

        hour++;
        hour %= 24;

        this.tb_Hour.Text = hour.ToString();
    }

    private void BtnMinuteUp_Click(object sender, RoutedEventArgs e)
    {
        RepeatButton btn = sender as RepeatButton;
        TextBox tb = btn.Tag as TextBox;

        int.TryParse(tb.Text.Trim(), out int value);

        value++;
        value %= 60;

        tb.Text = value.ToString();
    }

    private void BtnHourDown_Click(object sender, RoutedEventArgs e)
    {
        int.TryParse(this.tb_Hour.Text.Trim(), out int hour);

        hour--;
        if (hour < 0) hour = 23;

        this.tb_Hour.Text = hour.ToString();
    }

    private void BtnMinuteDown_Click(object sender, RoutedEventArgs e)
    {
        RepeatButton btn = sender as RepeatButton;
        TextBox tb = btn.Tag as TextBox;

        int.TryParse(tb.Text.Trim(), out int value);

        value--;
        if (value < 0) value = 59;

        tb.Text = value.ToString();
    }

    private void tb_Hour_TextChanged(object sender, TextChangedEventArgs e)
    {
        var tb = sender as TextBox;

        if (!string.IsNullOrEmpty(tb.Text.Trim()))
        {
            int.TryParse(tb.Text.Trim(), out int hour);

            if (hour < 0)
            {
                hour *= -1;

                tb.TextChanged -= tb_Hour_TextChanged;
                tb.Text = tb.Text.Substring(1);
                tb.TextChanged += tb_Hour_TextChanged;
            }
            if (hour > 23)
            {
                tb.TextChanged -= tb_Hour_TextChanged;
                tb.Text = tb.Text.Substring(0, tb.Text.Length - 1);
                tb.TextChanged += tb_Hour_TextChanged;
            }
        }
    }

    private void tb_minute_TextChanged(object sender, TextChangedEventArgs e)
    {
        var tb = sender as TextBox;

        if (!string.IsNullOrEmpty(tb.Text.Trim()))
        {
            int.TryParse(tb.Text.Trim(), out int minute);

            if (minute < 0)
            {
                minute *= -1;

                tb.TextChanged -= tb_Hour_TextChanged;
                tb.Text = tb.Text.Substring(1);
                tb.TextChanged += tb_Hour_TextChanged;
            }
            if (minute > 59)
            {
                tb.TextChanged -= tb_minute_TextChanged;
                tb.Text = tb.Text.Substring(0, tb.Text.Length - 1);
                tb.TextChanged += tb_minute_TextChanged;
            }
        }
    }

    private void BtnCancel_Click(object sender, RoutedEventArgs e)
    {
        this.popup.IsOpen = false;
    }
}

三、数字框

[TemplatePart(Name = "PART_Value", Type = typeof(TextBox))]
public class NumericBox : Control
{
    public int Value
    {
        get { return (int)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value", typeof(int), typeof(NumericBox),
            new PropertyMetadata(0));


    static NumericBox()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericBox), new FrameworkPropertyMetadata(typeof(NumericBox)));
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();


        // 获取模板里的控件
        var txtValue = base.GetTemplateChild("PART_Value") as TextBox; 
        var btnIncrease = base.GetTemplateChild("PART_IncreaseButton") as RepeatButton;
        var btnDecrease = base.GetTemplateChild("PART_DecreaseButton") as RepeatButton;

        if (txtValue != null)
        {
            // 建立对象中的Value属性与模板中的TextBox控件Text属性的绑定
            Binding binding = new Binding("Value");
            binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
            binding.RelativeSource = new RelativeSource() { AncestorType = typeof(NumericBox), Mode = RelativeSourceMode.FindAncestor };
            txtValue.SetBinding(TextBox.TextProperty, binding);
        }

        if (btnIncrease != null)
            btnIncrease.Click += BtnIncrease_Click;
        if (btnDecrease != null)
            btnDecrease.Click += BtnDecrease_Click;
    }

    private void BtnDecrease_Click(object sender, RoutedEventArgs e)
    {
        this.Value--;
    }

    private void BtnIncrease_Click(object sender, RoutedEventArgs e)
    {
        this.Value++;
    }
}
<!--Themes/Aero.NormalColor.xaml-->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:c="clr-namespace:Controls">    
    <Style TargetType="{x:Type c:NumericBox}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type c:NumericBox}">
                    <Border Background="Green"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        Height="30">
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

<!--Themes/Generic.xaml-->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:c="clr-namespace:Controls">
    <Style TargetType="{x:Type c:NumericBox}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type c:NumericBox}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            CornerRadius="5">

                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition/>
                                <ColumnDefinition Width="30"/>
                            </Grid.ColumnDefinitions>



                            <TextBox Name="PART_Value" VerticalAlignment="Center" Margin="3,5" BorderThickness="0"
                                     Foreground="{TemplateBinding Foreground}"/>
                            
                            <UniformGrid Rows="2" Grid.Column="1">
                                <RepeatButton Content="+" Background="Transparent" 
                                              BorderThickness="1,0,0,0" Name="PART_IncreaseButton"
                                                  BorderBrush="#DDD"/>
                                <RepeatButton Content="-" Background="Transparent" 
                                              BorderThickness="1,1,0,0" Name="PART_DecreaseButton"
                                                  BorderBrush="#DDD"/>
                            </UniformGrid>                      
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

四、工业仪表

<UserControl x:Class="Controls.Instrument"
             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:local="clr-namespace:Controls"
             mc:Ignorable="d" 
             d:DesignHeight="400" d:DesignWidth="400">
    <Grid>
        <Border Name="border" Background="#FF030A28">
            <Border.Effect>
                <DropShadowEffect BlurRadius="20" Opacity="0.5"  ShadowDepth="0" Direction="0" Color="#FF3CAFFF"/>
            </Border.Effect>
            <Grid>
                <Path Stroke="#333CAFFF" StrokeThickness="10" Name="circle" RenderTransformOrigin="0.5,0.5">
                    <Path.RenderTransform>
                        <RotateTransform Angle="-45"/>
                    </Path.RenderTransform>
                </Path>
                <Canvas x:Name="canvasPlate" RenderTransformOrigin="0.5,0.5" Margin="0">
                    <Canvas.RenderTransform>
                        <RotateTransform Angle="-45"/>
                    </Canvas.RenderTransform>
                </Canvas>
                <Path Data="" Name="plateBorder" Stroke="#FF3CAFFF" StrokeThickness="3" RenderTransformOrigin="0.5,0.5"
                  Width="{Binding ElementName=border,Path=Width}"
                  Height="{Binding ElementName=border,Path=Height}">
                    <Path.RenderTransform>
                        <RotateTransform Angle="-45"/>
                    </Path.RenderTransform>
                </Path>
                <Path Data="M200 205,360 200,200 195,195 200 200 205" Fill="Red" RenderTransformOrigin="0.5,0.5" Name="pointer">
                    <Path.RenderTransform>
                        <RotateTransform Angle="135" x:Name="rtPointer"/>
                    </Path.RenderTransform>
                </Path>
                <Border Width="60" Height="60" CornerRadius="30" Background="#FF030A28">
                    <Border.Effect>
                        <DropShadowEffect BlurRadius="20" Opacity="0.3" ShadowDepth="0" Direction="0" Color="#FF3CAFFF"/>
                    </Border.Effect>
                    <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
                        <TextBlock Foreground="White" VerticalAlignment="Center" HorizontalAlignment="Center">
                        <Run Text="{Binding Value,RelativeSource={RelativeSource AncestorType=UserControl}}"
                             FontSize="20"/>
                        <Run Text="m³/H" FontSize="8"/>
                        </TextBlock>
                        <TextBlock Text="NATURAL GAS" Foreground="#FF8CBEF0" VerticalAlignment="Center" HorizontalAlignment="Center"
                               FontSize="6"/>
                    </StackPanel>
                </Border>
            </Grid>
        </Border>
    </Grid>
</UserControl>
public partial class Instrument : UserControl
{
    // 当前值 
    public double Value
    {
        get { return (double)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value", typeof(double), typeof(Instrument),
            new PropertyMetadata(default(double), new PropertyChangedCallback(OnPropertyChanged)));

    // 最小刻度
    public double Minimum
    {
        get { return (double)GetValue(MinimumProperty); }
        set { SetValue(MinimumProperty, value); }
    }
    public static readonly DependencyProperty MinimumProperty =
        DependencyProperty.Register("Minimum", typeof(double), typeof(Instrument),
            new PropertyMetadata(default(double), new PropertyChangedCallback(OnPropertyChanged)));

    // 最大刻度
    public double Maximum
    {
        get { return (double)GetValue(MaximumProperty); }
        set { SetValue(MaximumProperty, value); }
    }
    public static readonly DependencyProperty MaximumProperty =
        DependencyProperty.Register("Maximum", typeof(double), typeof(Instrument),
            new PropertyMetadata(default(double), new PropertyChangedCallback(OnPropertyChanged)));

    //// 间隔
    //public double Interval
    //{
    //    get { return (double)GetValue(IntervalProperty); }
    //    set { SetValue(IntervalProperty, value); }
    //}
    //public static readonly DependencyProperty IntervalProperty =
    //    DependencyProperty.Register("Interval", typeof(double), typeof(Instrument), new PropertyMetadata(default(double), new PropertyChangedCallback(OnPropertyChanged)));

    // 大刻度的个数
    public int ScaleCount
    {
        get { return (int)GetValue(ScaleCountProperty); }
        set { SetValue(ScaleCountProperty, value); }
    }
    public static readonly DependencyProperty ScaleCountProperty =
        DependencyProperty.Register("ScaleCount", typeof(int), typeof(Instrument),
            new PropertyMetadata(default(int), new PropertyChangedCallback(OnPropertyChanged)));

    // 刻度的厚度
    public double ScaleThickness
    {
        get { return (double)GetValue(ScaleThicknessProperty); }
        set { SetValue(ScaleThicknessProperty, value); }
    }
    public static readonly DependencyProperty ScaleThicknessProperty =
        DependencyProperty.Register("ScaleThickness", typeof(double), typeof(Instrument),
            new PropertyMetadata(default(double), new PropertyChangedCallback(OnPropertyChanged)));
    // 刻度的颜色
    public Brush ScaleBrush
    {
        get { return (Brush)GetValue(ScaleBrushProperty); }
        set { SetValue(ScaleBrushProperty, value); }
    }
    public static readonly DependencyProperty ScaleBrushProperty =
        DependencyProperty.Register("ScaleBrush", typeof(Brush), typeof(Instrument),
            new PropertyMetadata(default(Brush), new PropertyChangedCallback(OnPropertyChanged)));

    // 指针的颜色
    public Brush PointerBrush
    {
        get { return (Brush)GetValue(PointerBrushProperty); }
        set { SetValue(PointerBrushProperty, value); }
    }
    public static readonly DependencyProperty PointerBrushProperty =
        DependencyProperty.Register("PointerBrush", typeof(Brush), typeof(Instrument),
            new PropertyMetadata(default(Brush), new PropertyChangedCallback(OnPropertyChanged)));

    // 刻度字体大小 
    public new double FontSize
    {
        get { return (double)GetValue(FontSizeProperty); }
        set { SetValue(FontSizeProperty, value); }
    }
    public static new readonly DependencyProperty FontSizeProperty =
        DependencyProperty.Register("FontSize", typeof(double), typeof(Instrument),
            new PropertyMetadata(9.0, new PropertyChangedCallback(OnPropertyChanged)));




    public Instrument()
    {
        InitializeComponent();

        SetCurrentValue(MinimumProperty, 0d);
        SetCurrentValue(MaximumProperty, 100d);
        //SetCurrentValue(IntervalProperty, 10d);

        SizeChanged += (se, ev) => { Refresh(); };
    }

    static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        => (d as Instrument).Refresh();

    private void Refresh()
    {
        // 圆
        this.border.Width = Math.Min(RenderSize.Width, RenderSize.Height);
        this.border.Height = Math.Min(RenderSize.Width, RenderSize.Height);
        this.border.CornerRadius = new CornerRadius(this.border.Width / 2);
        // 半径
        double radius = this.border.Width / 2;

        this.canvasPlate.Children.Clear();
        if (ScaleCount <= 0 || radius <= 0) return;

        // 画边
        string borderPathData = $"M4,{radius}A{radius - 4} {radius - 4} 0 1 1 {radius} {this.border.Height - 4}";
        // 将字符串转换成Geometry
        var converter = TypeDescriptor.GetConverter(typeof(Geometry));
        //this.plateBorder.Data = (Geometry)converter.ConvertFrom(borderPathData);
        this.plateBorder.Data = PathGeometry.Parse(borderPathData);

        // 计算刻度
        double label = this.Minimum;
        double interval = 0;
        double step = 270.0 / (this.Maximum - this.Minimum);

        // 计算小刻度
        for (int i = 0; i < this.Maximum - this.Minimum; i++)
        {
            //添加刻度线
            Line lineScale = new Line();
            // 角度需要转换弧度
            lineScale.X1 = radius - (radius - 13) * Math.Cos(step * i * Math.PI / 180);
            lineScale.Y1 = radius - (radius - 13) * Math.Sin(step * i * Math.PI / 180);
            lineScale.X2 = radius - (radius - 8) * Math.Cos(step * i * Math.PI / 180);
            lineScale.Y2 = radius - (radius - 8) * Math.Sin(step * i * Math.PI / 180);

            lineScale.Stroke = this.ScaleBrush;
            lineScale.StrokeThickness = this.ScaleThickness;

            this.canvasPlate.Children.Add(lineScale);
        }
        // 计算大刻度
        do
        {
            //添加刻度线
            Line lineScale = new Line();
            lineScale.X1 = radius - (radius - 20) * Math.Cos(interval * step * Math.PI / 180);
            lineScale.Y1 = radius - (radius - 20) * Math.Sin(interval * step * Math.PI / 180);
            lineScale.X2 = radius - (radius - 8) * Math.Cos(interval * step * Math.PI / 180);
            lineScale.Y2 = radius - (radius - 8) * Math.Sin(interval * step * Math.PI / 180);

            lineScale.Stroke = this.ScaleBrush;
            lineScale.StrokeThickness = this.ScaleThickness;

            this.canvasPlate.Children.Add(lineScale);

            TextBlock txtScale = new TextBlock();
            txtScale.Text = label.ToString("0");
            txtScale.Width = 34;
            txtScale.TextAlignment = TextAlignment.Center;
            txtScale.Foreground = new SolidColorBrush(Colors.White);
            txtScale.RenderTransform = new RotateTransform() { Angle = 45, CenterX = 17, CenterY = 8 };
            txtScale.FontSize = this.FontSize;
            Canvas.SetLeft(txtScale, radius - (radius - 34) * Math.Cos(interval * step * Math.PI / 180) - 17);
            Canvas.SetTop(txtScale, radius - (radius - 34) * Math.Sin(interval * step * Math.PI / 180) - 8);
            this.canvasPlate.Children.Add(txtScale);

            interval += (this.Maximum - this.Minimum) / this.ScaleCount;
            label += (this.Maximum - this.Minimum) / this.ScaleCount;

        } while (interval <= this.Maximum - this.Minimum);


        // 修改指针
        string sData = "M{0} {1},{2} {0},{0} {3},{3} {0},{0} {1}";
        sData = string.Format(sData, radius, radius + 2, this.border.Width - radius / 10, radius - 4);
        converter = TypeDescriptor.GetConverter(typeof(Geometry));
        this.pointer.Data = (Geometry)converter.ConvertFrom(sData);
        this.pointer.Fill = this.PointerBrush;

        //DoubleAnimation da = new DoubleAnimation((Value - Minimum) * step + 135, new Duration(TimeSpan.FromMilliseconds(200)));
        //this.rtPointer.BeginAnimation(RotateTransform.AngleProperty, da);
        this.rtPointer.Angle = (Value - Minimum) * step + 135;

        // 修改圆  M100 200 A100 100 0 1 1 200 300
        // 厚度
        double thickness = radius / 2;
        this.circle.StrokeThickness = thickness;
        double startX = radius - thickness / 2;
        double startY = radius;
        double endX = radius - (radius - thickness / 2) * Math.Cos((Value - Minimum) * step * Math.PI / 180);
        double endY = radius - (radius - thickness / 2) * Math.Sin((Value - Minimum) * step * Math.PI / 180);

        int isLarge = 1;
        if ((Value - Minimum) * step < 180)
            isLarge = 0;

        sData = $"M{startX},{startY}A{radius / 2} {radius / 2} 0 1 1 {endX} {endY}";
        sData = $"M{thickness / 2},{radius}A{radius - thickness / 2} {radius - thickness / 2} 0 {isLarge} 1 {endX} {endY}";
        //sData = string.Format(sData, radius * 0.5, radius, radius * 1.5);
        this.circle.Data = (Geometry)converter.ConvertFrom(sData);
        this.circle.Visibility = Visibility.Visible;

        if (this.border.Width < 200)
            this.circle.Visibility = Visibility.Collapsed;
    }
}

五、各种常见开关

public class Switch : ToggleButton
{
    static Switch()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(Switch), new FrameworkPropertyMetadata(typeof(Switch)));
    }
}
<!--Themes/Generic.xaml-->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:c="clr-namespace:Controls">    
    <Style TargetType="{x:Type c:Switch}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type c:Switch}">
                    <Grid>
                        <Border Width="{Binding RelativeSource={RelativeSource Self},Path=ActualHeight}" 
                                CornerRadius="{Binding RelativeSource={RelativeSource Self},Path=ActualHeight}"
                                Visibility="Collapsed" Name="dropdown" Margin="-23">
                            <Border.Background>
                                <RadialGradientBrush>
                                    <GradientStop Color="Transparent" Offset="1"/>
                                    <GradientStop Color="#5500D787" Offset="0.7"/>
                                    <GradientStop Color="Transparent" Offset="0.59"/>
                                </RadialGradientBrush>
                            </Border.Background>
                        </Border>
                        <Border Width="{Binding RelativeSource={RelativeSource Self},Path=ActualHeight}"
                                BorderBrush="DarkGreen" BorderThickness="5"
                                CornerRadius="{Binding RelativeSource={RelativeSource Self},Path=ActualHeight}"
                                Background="Gray" Name="bor">
                            <Border Background="#FF00C88C" Margin="2" Name="bor1"
                                    Width="{Binding RelativeSource={RelativeSource Self},Path=ActualHeight}" 
                                    CornerRadius="{Binding RelativeSource={RelativeSource Self},Path=ActualHeight}"/>
                        </Border>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsChecked" Value="True">
                            <Trigger.EnterActions>
                                <BeginStoryboard>
                                    <Storyboard >
                                        <ColorAnimation To="White" Duration="0:0:0.5"
                                    Storyboard.TargetName="bor1"
                                    Storyboard.TargetProperty="Background.Color"/>
                                        <ColorAnimation To="#FF32FAC8" Duration="0:0:0.5"
                                    Storyboard.TargetName="bor"
                                    Storyboard.TargetProperty="BorderBrush.Color"/>
                                        <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="dropdown"
                                    Storyboard.TargetProperty="Visibility">
                                            <DiscreteObjectKeyFrame KeyTime="0:0:0.3">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </BeginStoryboard>
                            </Trigger.EnterActions>
                            <Trigger.ExitActions>
                                <BeginStoryboard>
                                    <Storyboard >
                                        <ColorAnimation Storyboard.TargetName="bor1"
                                    Storyboard.TargetProperty="Background.Color"/>
                                        <ColorAnimation Storyboard.TargetName="bor"
                                    Storyboard.TargetProperty="BorderBrush.Color"/>
                                        <ObjectAnimationUsingKeyFrames
                                    Storyboard.TargetName="dropdown"
                                    Storyboard.TargetProperty="Visibility">
                                            <DiscreteObjectKeyFrame KeyTime="0:0:0.3">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Collapsed</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </BeginStoryboard>
                            </Trigger.ExitActions>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

六、第三方控件库

1. 简单介绍

  • HandyControl:https://handyorg.github.io/
  • 图表控件库
    • LiveCharts:LiveCharts2预览版、内存管理不是很好,长时间持续更新的情况下,内存溢出,慎用!数据加载量不能太大(1000点左右 开始卡)
    • ScottPlot:考虑性能, 建议这个
    • OxyPlot
    • LightingChart

2. ScottPlot使用MVVM绑定数据

问题:ScottPlot不支持数据绑定,所以没办法直接把ViewModel中的模型数据提供给ScottPlot。

思路:遇到这种问题,我们通常可以使用附加依赖属性来实现。

  • 添加一个ScottPlotExtension,这个属性里面可以知道绑定的属性值,以及被附加的对象
public class ScottPlotExtension
{
    // 这个属性里面可以知道绑定的属性值,以及被附加的对象
    public static ObservableCollection<double> GetValues(DependencyObject obj)
    {
        return (ObservableCollection<double>)obj.GetValue(ValuesProperty);
    }

    public static void SetValues(DependencyObject obj, ObservableCollection<double> value)
    {
        obj.SetValue(ValuesProperty, value);
    }

    public static readonly DependencyProperty ValuesProperty =
        DependencyProperty.RegisterAttached(
            "Values",
            typeof(ObservableCollection<double>),
            typeof(ScottPlotExtension),
            new PropertyMetadata(null, new PropertyChangedCallback(OnValuesChanged)));

    // 当给ScottPlotExtension的Values属性赋的时候   才触发
    // new List<double>()   这种情况针对触发
    private static void OnValuesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var plt = (d as WpfPlot);
        var new_list = (ObservableCollection<double>)e.NewValue;

        var signal = plt.Plot.AddSignal(new_list.ToArray());
        // 对集合实例进行子项培养的时候,进行回调

        new_list.CollectionChanged += (s, ee) =>
        {
            if (ee.Action == NotifyCollectionChangedAction.Add)
            {
                signal.Update((s as ObservableCollection<double>).ToArray());
                plt.Refresh();
            }
        };

        plt.Refresh();
    }
}
  • 创建一个ScottPlot专用的ViewModel
public class ScottPlotViewModel : INotifyPropertyChanged
{
    public ObservableCollection<double> Datas { get; set; }

    public ScottPlotViewModel()
    {
        Datas = new ObservableCollection<double>(DataGen.RandomWalk(new Random(), 1_000_000));

        // 实时监控:持续获取数据   变更图表的数据源
        Task.Run(async () =>
        {
            while (true)
            {
                await Task.Delay(500);

                Application.Current.Dispatcher.Invoke(() =>
                {
                    Datas.RemoveAt(0);
                    Datas.Add(new Random().NextDouble());
                });
            }

        });
    }

    public event PropertyChangedEventHandler? PropertyChanged;
}
  • 界面绑定
<Window x:Class="Views.ScottPlotWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Zhaoxi.MvvmPattern.Views"
        xmlns:vm="clr-namespace:ViewModels"
        xmlns:b="clr-namespace:Base"
    mc:Ignorable="d"
        Title="ScottPlot" Height="450" Width="800">
    <Window.DataContext>
        <vm:ScottPlotViewModel/>
    </Window.DataContext>
    <Grid>
        <WpfPlot Name="wpf_plot" b:ScottPlotExtension.Values="{Binding Datas}"/>
    </Grid>
</Window>