WPF 实现步骤控件

  • 框架使用.NET40
  • Visual Studio 2019;
  • Step 继承 ItemsControl 使用 Grid 嵌套 ProgressBar 和 ItemsPresenter.
    • ProgressBar 用来当作步骤后面的线条,宽等于控件的(ActualWidth / Items.Count) * (Items.Count - 1),Maximum = Items.Count - 1
    • ItemsPresenter 用来展示步骤 Item 。
  • ItemsPanel - ItemsPanelTemplate - UniformGrid Rows="1" 横向展示,UniformGrid Columns="1" 可以控制竖向显示,只不过需要重新自定义 ItemContainerStyle 的样式。.
  • 然后创建 StepItem 继承 ContentControl 增加两个属性 Index 用来记录当前是步骤 与 State 记录状态 (等待中、进行中、已完成)。
    • 因为继承了 ContentControl 所以可以在使用时指定 Content 显示内容,在每个步骤下方显示。

WPF 实现步骤控件

1) Step.xaml 代码如下:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:po="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
                    xmlns:controls="clr-namespace:WPFDevelopers.Controls"
                    xmlns:converts="clr-namespace:WPFDevelopers.Converts">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="Basic/ControlBasic.xaml"/>
    </ResourceDictionary.MergedDictionaries>

    <converts:IndexConverter x:Key="IndexConverter"/>
    <Style x:Key="DefaultStepItem" TargetType="{x:Type controls:StepItem}"
           BasedOn="{StaticResource ControlBasicStyle}">
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type controls:StepItem}">
                    <StackPanel>
                        <controls:SmallPanel>
                            <Ellipse 
                                    Width="45"
                                    Height="30"
                                    Fill="{DynamicResource WindowForegroundColorBrush}"
                                    HorizontalAlignment="Center"/>
                            <Border 
                                        Background="{TemplateBinding Background}"
                                        HorizontalAlignment="Center"
                                        CornerRadius="15"
                                        BorderThickness="{TemplateBinding BorderThickness}"
                                        BorderBrush="{TemplateBinding BorderBrush}"
                                        Height="30" 
                                        Width="30">
                                <controls:SmallPanel>
                                    <TextBlock Foreground="{TemplateBinding Foreground}" 
                                                       VerticalAlignment="Center"
                                                       HorizontalAlignment="Center"
                                                       FontSize="{TemplateBinding FontSize}"
                                                       Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type controls:StepItem}}, Converter={StaticResource IndexConverter}}"
                                                       Name="PART_Index"/>
                                    <Path Data="{StaticResource PathComplete}"
                                                  Fill="{TemplateBinding Foreground}"
                                                  Stretch="Uniform"
                                                  Width="12"
                                                  Height="12"
                                                  Name="PART_PathComplete"
                                                  Visibility="Collapsed"/>
                                </controls:SmallPanel>
                            </Border>
                        </controls:SmallPanel>

                        <ContentPresenter HorizontalAlignment="Center" 
                                                  TextElement.FontWeight="Black"
                                                  ContentTemplate="{Binding ItemTemplate,RelativeSource={RelativeSource AncestorType=controls:Step}}"
                                                  TextElement.Foreground="{DynamicResource RegularTextSolidColorBrush}"
                                                  Margin="0,6,0,0"/>
                    </StackPanel>
                    <ControlTemplate.Triggers>
                        <Trigger Property="Status" Value="Waiting">
                            <Setter Property="Foreground" Value="{DynamicResource PrimaryTextSolidColorBrush}"/>
                            <Setter Property="Visibility" TargetName="PART_PathComplete" Value="Collapsed"/>
                            <Setter Property="Visibility" TargetName="PART_Index" Value="Visible"/>
                            <Setter Property="Background" Value="{DynamicResource BaseSolidColorBrush}"/>
                        </Trigger>
                        <Trigger Property="Status" Value="InProgress">
                            <Setter Property="Foreground" Value="{DynamicResource DefaultBackgroundSolidColorBrush}"/>
                            <Setter Property="Visibility" TargetName="PART_PathComplete" Value="Collapsed"/>
                            <Setter Property="Visibility" TargetName="PART_Index" Value="Visible"/>
                            <Setter Property="Background" Value="{DynamicResource PrimaryNormalSolidColorBrush}"/>
                        </Trigger>
                        <Trigger Property="Status" Value="Complete">
                            <Setter Property="BorderBrush" Value="{DynamicResource DefaultBackgroundSolidColorBrush}"/>
                            <Setter Property="Background" Value="{DynamicResource DefaultBackgroundSolidColorBrush}"/>
                            <Setter Property="Visibility" TargetName="PART_PathComplete" Value="Visible"/>
                            <Setter Property="Visibility" TargetName="PART_Index" Value="Collapsed"/>
                            <Setter Property="Foreground" Value="{DynamicResource PrimaryNormalSolidColorBrush}"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <Style x:Key="DefaultStep" TargetType="{x:Type controls:Step}" 
           BasedOn="{StaticResource ControlBasicStyle}">
        <Setter Property="ItemContainerStyle" Value="{StaticResource DefaultStepItem}"/>
        <Setter Property="VerticalContentAlignment" Value="Top"/>
        <Setter Property="HorizontalContentAlignment" Value="Center"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type controls:Step}">
                    <controls:SmallPanel>
                        <ProgressBar x:Name="PART_ProgressBar" 
                                             Margin="0,18"
                                             Height="1"
                                             Value="{Binding StepIndex,RelativeSource={RelativeSource AncestorType=controls:Step}}"
                                             VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                             HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
                        <ItemsPresenter/>
                    </controls:SmallPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <UniformGrid Rows="1"/>
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <Style TargetType="{x:Type controls:StepItem}" BasedOn="{StaticResource DefaultStepItem}" />
    <Style TargetType="{x:Type controls:Step}" BasedOn="{StaticResource DefaultStep}" />
</ResourceDictionary>

2) Step.cs 代码如下:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;

namespace WPFDevelopers.Controls
{
    [TemplatePart(Name = ProgressBarTemplateName, Type = typeof(ProgressBar))]
    public class Step : ItemsControl
    {
        private const string ProgressBarTemplateName = "PART_ProgressBar";
        private ProgressBar _progressBar;
        public int StepIndex
        {
            get => (int)GetValue(StepIndexProperty);
            set => SetValue(StepIndexProperty, value);
        }

        public static readonly DependencyProperty StepIndexProperty = DependencyProperty.Register(
           "StepIndex", typeof(int), typeof(Step), new PropertyMetadata(0, OnStepIndexChanged));

        private static void OnStepIndexChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var step = (Step)d;
            var stepIndex = (int)e.NewValue;
            step.UpdateStepItemState(stepIndex);
        }
        void UpdateStepItemState(int stepIndex)
        {
            var count = Items.Count;
            if (count <= 0) return;
            if (stepIndex >= count)
            {
                StepIndex--;
                return;
            }
            if (stepIndex < 0)
            {
                StepIndex++;
                return;
            }
            for (var i = 0; i < stepIndex; i++)
            {
                if (ItemContainerGenerator.ContainerFromIndex(i) is StepItem stepItem)
                    stepItem.Status = Status.Complete;
            }

            if (ItemContainerGenerator.ContainerFromIndex(stepIndex) is StepItem itemInProgress)
                itemInProgress.Status = Status.InProgress;
            for (var i = stepIndex + 1; i < Items.Count; i++)
            {
                if (ItemContainerGenerator.ContainerFromIndex(i) is StepItem stepItem)
                    stepItem.Status = Status.Waiting;
            }
        }
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _progressBar = GetTemplateChild(ProgressBarTemplateName) as ProgressBar;
        }
        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);
            var count = Items.Count;
            if (_progressBar == null || count <= 0) return;
            _progressBar.Maximum = count - 1;
            _progressBar.Value = StepIndex;
            _progressBar.Width = (ActualWidth / count) * (count - 1);


        }
        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return item is StepItem;
        }
        protected override DependencyObject GetContainerForItemOverride()
        {
            return new StepItem();
        }
        public Step()
        {
            ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
        }

        public void Next()
        {
            StepIndex++;
        }
        public void Previous()
        {
            StepIndex--;
        }
        private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
        {
            if (ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
            {
                var count = Items.Count;
                if (count <= 0) return;
                UpdateStepItemState(StepIndex);
            }
        }
    }
}

3) StepItem.cs 代码如下:

using System.Windows;
using System.Windows.Controls;

namespace WPFDevelopers.Controls
{
    public class StepItem : ContentControl
    {

        public static readonly DependencyProperty IndexProperty = DependencyProperty.Register(
            "Index", typeof(int), typeof(StepItem), new PropertyMetadata(-1));

        public int Index
        {
            get => (int)GetValue(IndexProperty);
            internal set => SetValue(IndexProperty, value);
        }


        public static readonly DependencyProperty StatusProperty = DependencyProperty.Register(
            "Status", typeof(Status), typeof(StepItem), new PropertyMetadata(Status.Waiting));


        public Status Status
        {
            get => (Status)GetValue(StatusProperty);
            internal set => SetValue(StatusProperty, value);
        }
    }
}

4) Status.cs 代码如下:

namespace WPFDevelopers.Controls
{
    /// <summary>
    ///状态值
    /// </summary>
    public enum Status
    {
        /// <summary>
        /// 等待中
        /// </summary>
        Waiting,
        /// <summary>
        /// 正在进行中
        /// </summary>
        InProgress,
        /// <summary>
        /// 完成
        /// </summary>
        Complete
    }
}

5) StepExample.xaml 代码如下:

<UserControl x:Class="WPFDevelopers.Samples.ExampleViews.StepExample"
             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:controls="clr-namespace:WPFDevelopers.Samples.Controls"
             xmlns:wd="https://github.com/WPFDevelopersOrg/WPFDevelopers"
             xmlns:local="clr-namespace:WPFDevelopers.Samples.ExampleViews"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <controls:CodeViewer>
        <StackPanel  VerticalAlignment="Center" >
            <UniformGrid Columns="2" Name="PART_UniformGrid">
                <wd:Step x:Name="PART_Step" StepIndex="{Binding Progress}">
                    <wd:StepItem Content="填写账号"/>
                    <wd:StepItem Content="身份验证"/>
                    <wd:StepItem Content="设置新密码"/>
                    <wd:StepItem Content="完成"/>
                </wd:Step>
                <wd:Step StepIndex="0" ItemsSource="{Binding Steps}">
                </wd:Step>
            </UniformGrid>
            <StackPanel Orientation="Horizontal"
                        VerticalAlignment="Center" 
                        HorizontalAlignment="Center"
                        Margin="10">
                <Button Content="上一步"  Command="{Binding PreviousCommand}" 
                    CommandParameter="{Binding ElementName=PART_UniformGrid}"
                    Style="{StaticResource PrimaryButton}"/>
                <Button Content="下一步"   Command="{Binding NextCommand}" 
                    CommandParameter="{Binding ElementName=PART_UniformGrid}"
                    Style="{StaticResource PrimaryButton}"/>
               
            </StackPanel>
            
        </StackPanel>
        <controls:CodeViewer.SourceCodes>
            <controls:SourceCodeModel 
                CodeSource="/WPFDevelopers.SamplesCode;component/ExampleViews/StepExample.xaml" 
                CodeType="Xaml"/>
            <controls:SourceCodeModel 
                CodeSource="/WPFDevelopers.SamplesCode;component/ExampleViews/StepExample.xaml.cs" 
                CodeType="CSharp"/>
        </controls:CodeViewer.SourceCodes>
    </controls:CodeViewer>
</UserControl>

6) StepExample.xaml.cs 代码如下:

using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using WPFDevelopers.Controls;
using WPFDevelopers.Samples.Helpers;

namespace WPFDevelopers.Samples.ExampleViews
{
    /// <summary>
    /// StepExample.xaml 的交互逻辑
    /// </summary>
    public partial class StepExample : UserControl
    {
        public ObservableCollection<string> Steps
        {
            get;
            set;
        }
        public StepExample()
        {
            InitializeComponent();
            Steps = new ObservableCollection<string>();
            Steps.Add("Step 1");
            Steps.Add("Step 2");
            Steps.Add("Step 3");
            Steps.Add("Step 4");
            this.DataContext = this;
        }
        public ICommand NextCommand => new RelayCommand(new Action<object>((sender) =>
        {
            var uniformGrid = sender as UniformGrid;
            if (uniformGrid == null) return;
            foreach (var step in uniformGrid.Children.OfType<Step>())
                step.Next();

        }));
        public ICommand PreviousCommand => new RelayCommand(new Action<object>((sender) =>
        {
            var uniformGrid = sender as UniformGrid;
            if (uniformGrid == null) return;
            foreach (var step in uniformGrid.Children.OfType<Step>())
                step.Previous();
        }));
    }
}

WPF 实现步骤控件