完善基于WPF开发的标尺控件(含实例代码)


引言


在WPF应用程序开发中,自定义控件是提升用户体验和界面美观度的重要手段。标尺控件作为测量和定位工具,在图像编辑、文档处理等场景中具有广泛应用。本文将详细介绍如何完善一个基于WPF的自定义标尺控件,涵盖设计思路、实现方法和完整代码示例。


一、设计思路

1.1 核心需求分析


完善的标尺控件应具备以下特性:


支持水平和垂直方向显示

可自定义刻度间隔和单位

支持缩放功能

可显示当前测量值

支持DPI适配

良好的性能表现(支持大量刻度)

1.2 架构设计


采用MVVM模式分离视图和逻辑:


视图层‌:XAML实现标尺的视觉表现

视图模型层‌:处理刻度计算、缩放逻辑等

模型层‌:定义刻度数据结构和单位转换

二、实现方法

2.1 创建自定义控件


首先创建一个继承自Control的自定义控件:


csharp

Copy Code

public class RulerControl : Control

{

    // 定义依赖属性

    public static readonly DependencyProperty OrientationProperty =

        DependencyProperty.Register("Orientation", typeof(Orientation), typeof(RulerControl),

            new PropertyMetadata(Orientation.Horizontal));


    public Orientation Orientation

    {

        get { return (Orientation)GetValue(OrientationProperty); }

        set { SetValue(OrientationProperty, value); }

    }


    // 其他依赖属性定义...

}


2.2 刻度数据模型


定义刻度数据模型,包含数值和可见性属性:


csharp

Copy Code

public class RulerChip

{

    public double Number { get; set; }

    public Visibility NumberVisibility { get; set; }

}


2.3 刻度生成逻辑


在视图模型中实现刻度生成算法:


csharp

Copy Code

public class RulerViewModel

{

    private double _scale = 1.0;

    private double _unit = 1.0; // 1表示像素,0.1表示厘米等

    

    public List<RulerChip> Chips { get; } = new List<RulerChip>();

    

    public double Scale

    {

        get => _scale;

        set

        {

            if (_scale != value)

            {

                _scale = value;

                GenerateChips();

                OnPropertyChanged(nameof(Scale));

            }

        }

    }


    public double Unit

    {

        get => _unit;

        set

        {

            if (_unit != value)

            {

                _unit = value;

                GenerateChips();

                OnPropertyChanged(nameof(Unit));

            }

        }

    }


    private void GenerateChips()

    {

        Chips.Clear();

        

        // 根据缩放比例和单位计算刻度

        double interval = 10 * _unit; // 主刻度间隔

        double subInterval = 1 * _unit; // 次刻度间隔

        

        for (double i = 0; i < 1000; i += subInterval)

        {

            bool isMain = (i % interval == 0);

            Chips.Add(new RulerChip

            {

                Number = i / _unit,

                NumberVisibility = isMain ? Visibility.Visible : Visibility.Hidden

            });

        }

    }

}


2.4 缩放功能实现


添加缩放滑块控件和事件处理:


csharp

Copy Code

public class RulerControl : Control

{

    // ... 其他依赖属性

    

    public event EventHandler ScaleChanged;

    

    public double ScaleFactor

    {

        get { return (double)GetValue(ScaleFactorProperty); }

        set { SetValue(ScaleFactorProperty, value); }

    }

    

    public static readonly DependencyProperty ScaleFactorProperty =

        DependencyProperty.Register("ScaleFactor", typeof(double), typeof(RulerControl),

            new PropertyMetadata(1.0, ScaleFactorChanged));


    private static void ScaleFactorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

    {

        ((RulerControl)d).ScaleChanged?.Invoke(d, EventArgs.Empty);

    }

}


2.5 DPI适配


实现DPI感知功能,确保在不同DPI设置下显示一致:


csharp

Copy Code

public class RulerControl : Control

{

    public static readonly DependencyProperty DpiProperty =

        DependencyProperty.Register("Dpi", typeof(double), typeof(RulerControl),

            new PropertyMetadata(96.0)); // 默认96dpi

    

    public double Dpi

    {

        get { return (double)GetValue(DpiProperty); }

        set { SetValue(DpiProperty, value); }

    }


    protected override void OnRender(DrawingContext drawingContext)

    {

        // 根据DPI调整绘制参数

        double dpiScale = Dpi / 96.0;

        double scaledWidth = ActualWidth * dpiScale;

        double scaledHeight = ActualHeight * dpiScale;

        

        // 绘制刻度...

    }

}


三、完整代码示例

3.1 XAML实现

xml

Copy Code

<UserControl x:Class="WpfRulerDemo.MainWindow"

             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

             xmlns:local="clr-namespace:WpfRulerDemo">

    <Grid>

        <Grid.RowDefinitions>

            <RowDefinition Height="Auto"/>

            <RowDefinition Height="*"/>

        </Grid.RowDefinitions>

        

        <!-- 缩放滑块 -->

        <Slider x:Name="ScaleSlider" Grid.Row="0" Minimum="0.1" Maximum="10" Value="1" 

                ValueChanged="ScaleSlider_ValueChanged"/>

        

        <!-- 水平标尺 -->

        <local:RulerControl x:Name="HorizontalRuler" Grid.Row="1" Orientation="Horizontal"

                            ScaleFactor="{Binding ScaleFactor, ElementName=ScaleSlider}"

                            Unit="0.1" Dpi="96"/>

        

        <!-- 垂直标尺 -->

        <local:RulerControl x:Name="VerticalRuler" Grid.Row="1" Orientation="Vertical"

                            ScaleFactor="{Binding ScaleFactor, ElementName=ScaleSlider}"

                            Unit="0.1" Dpi="96"/>

    </Grid>

</UserControl>


3.2 视图模型

csharp

Copy Code

public class MainWindowViewModel : INotifyPropertyChanged

{

    private double _scaleFactor = 1.0;

    

    public double ScaleFactor

    {

        get => _scaleFactor;

        set

        {

            if (_scaleFactor != value)

            {

                _scaleFactor = value;

                OnPropertyChanged(nameof(ScaleFactor));

            }

        }

    }


    public event PropertyChangedEventHandler PropertyChanged;

    

    protected virtual void OnPropertyChanged(string propertyName)

    {

        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    }

}


3.3 刻度绘制逻辑

csharp

Copy Code

public class RulerControl : Control

{

    protected override void OnRender(DrawingContext drawingContext)

    {

        base.OnRender(drawingContext);

        

        if (Orientation == Orientation.Horizontal)

        {

            DrawHorizontalRuler(drawingContext);

        }

        else

        {

            DrawVerticalRuler(drawingContext);

        }

    }


    private void DrawHorizontalRuler(DrawingContext drawingContext)

    {

        double dpiScale = Dpi / 96.0;

        double scaledWidth = ActualWidth * dpiScale;

        

        // 绘制刻度线...

        for (int i = 0; i < scaledWidth; i += 10 * ScaleFactor * dpiScale)

        {

            double x = i;

            double y = 0;

            

            // 主刻度

            if (i % (50 * ScaleFactor * dpiScale) == 0)

            {

                drawingContext.DrawLine(new Pen(Brushes.Black, 2 * ScaleFactor * dpiScale), 

                                      new Point(x, y), new Point(x, 10 * ScaleFactor * dpiScale));

                

                // 绘制刻度值

                FormattedText text = new FormattedText(

                    (i / dpiScale / ScaleFactor).ToString("0.0"),

                    CultureInfo.CurrentCulture,

                    FlowDirection.LeftToRight,

                    new Typeface("Arial"),

                    8 * ScaleFactor * dpiScale,

                    Brushes.Black);

                

                drawingContext.DrawText(text, new Point(x - text.Width / 2, y + 15 * ScaleFactor * dpiScale));

            }

            // 次刻度

            else if (i % (10 * ScaleFactor * dpiScale) == 0)

            {

                drawingContext.DrawLine(new Pen(Brushes.Gray, ScaleFactor * dpiScale), 

                                      new Point(x, y), new Point(x, 5 * ScaleFactor * dpiScale));

            }

        }

    }

    

    // 垂直标尺绘制类似...

}


四、性能优化

4.1 虚拟化技术


对于大量刻度,使用VirtualizingStackPanel提高性能:


xml

Copy Code

<ListBox x:Name="RulerListBox" VirtualizingStackPanel.IsVirtualizing="True">

    <ListBox.ItemTemplate>

        <DataTemplate>

            <Rectangle Width="2" Height="10" Fill="{Binding NumberVisibility, Converter={StaticResource VisibilityToBrushConverter}}"/>

        </DataTemplate>

    </ListBox.ItemTemplate>

</ListBox>


4.2 数据绑定优化


使用INotifyPropertyChanged实现高效数据绑定:


csharp

Copy Code

public class RulerChip : INotifyPropertyChanged

{

    private double _number;

    private Visibility _numberVisibility;

    

    public double Number

    {

        get => _number;

        set

        {

            if (_number != value)

            {

                _number = value;

                OnPropertyChanged(nameof(Number));

            }

        }

    }

    

    // 其他属性...

}


五、总结


本文详细介绍了如何完善一个基于WPF的自定义标尺控件,从设计思路到具体实现,提供了完整的代码示例和性能优化方案。通过使用WPF的数据绑定、依赖属性和自定义绘制等技术,可以创建出功能丰富、性能优良的自定义控件。实际应用中,可以根据需求进一步扩展功能,如添加测量工具、支持更多单位等。