完善基于WPF开发的标尺控件(含实例代码)
前言:
在2021年的时候,有个拖拽的项目用到了标尺的控件,我写了一篇大概实现代码的博客:仿Word的支持横轴竖轴的WPF 标尺 - wuty007 - 博客园。在里边的实例代码里边,鼠标横纵坐标移动显示的代码没有完善。直到上个月有个水友发信息找我要源码。趁着这段时间正在使用AI编程,于是让AI帮我补充一下代码了

代码完善:
1、本次使用的是腾讯Codebuddy的AI编程插件,文件结构如下:

2、2021年开发的标尺核心的代码:
using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media.Media3D; using System.Windows.Media; using System.Windows.Shapes; using System.Windows; namespace RulerDemo { [TemplatePart(Name = "verticalTrackLine", Type = typeof(Line))] [TemplatePart(Name = "horizontalTrackLine", Type = typeof(Line))] internal class RulerControl : Control { public static readonly DependencyProperty DpiProperty = DependencyProperty.Register(nameof(Dpi), typeof(Dpi), typeof(RulerControl)); public static readonly DependencyProperty DisplayPercentProperty = DependencyProperty.Register("DisplayPercent", typeof(double), typeof(RulerControl)); public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register("DisplayType", typeof(RulerDisplayType), typeof(RulerControl)); public static readonly DependencyProperty DisplayUnitProperty = DependencyProperty.Register("DisplayUnit", typeof(RulerDisplayUnit), typeof(RulerControl)); public static readonly DependencyProperty ZeroPointProperty = DependencyProperty.Register("ZeroPoint", typeof(double), typeof(RulerControl)); /// <summary> /// 定义静态构造函数 /// </summary> static RulerControl() { DefaultStyleKeyProperty.OverrideMetadata(typeof(RulerControl), new FrameworkPropertyMetadata(typeof(RulerControl))); } #region 属性 /// <summary> /// 屏幕分辨率 /// </summary> public Dpi Dpi { get => ((Dpi)GetValue(DpiProperty)); set => SetValue(DpiProperty, value); } /// <summary> /// 设置0点从哪里开始 /// </summary> public double ZeroPoint { get => ((double)GetValue(ZeroPointProperty)); set { SetValue(ZeroPointProperty, value); InvalidateVisual(); } } /// <summary> /// 显示的比率(目前支持0-1的选项) /// </summary> public double DisplayPercent { get => ((double)GetValue(DisplayPercentProperty)); set { if (value > 1) { value = 1; } SetValue(DisplayPercentProperty, value); InvalidateVisual(); } } /// <summary> /// 显示的类型:枚举类(支持横向或者竖向) /// </summary> public RulerDisplayType DisplayType { get => ((RulerDisplayType)GetValue(DisplayTypeProperty)); set => SetValue(DisplayTypeProperty, value); } /// <summary> /// 显示的单位:cm和pixel /// </summary> public RulerDisplayUnit DisplayUnit { get => ((RulerDisplayUnit)GetValue(DisplayUnitProperty)); set => SetValue(DisplayUnitProperty, value); } #endregion #region 常量 public const double InchCm = 2.54; //一英寸为2.54cm private const int P100StepSpanPixel = 100; private const int P100StepSpanCm = 2; private const int P100StepCountPixel = 20; private const int P100StepCountCm = 20; #endregion #region 变量 private double _minStepLengthCm; private double _maxStepLengthCm; private double _actualLength; private int _stepSpan; private int _stepCount; private double _stepLength; private Line _mouseVerticalTrackLine; private Line _mouseHorizontalTrackLine; #endregion #region 标尺边框加指针显示 public void RaiseHorizontalRulerMoveEvent(MouseEventArgs e) { var mousePoint = e.GetPosition(this); Debug.WriteLine($"水平标尺 - 鼠标位置: X={mousePoint.X}, Y={mousePoint.Y}"); if (_mouseHorizontalTrackLine != null) { // 水平标尺:移动垂直跟踪线,X跟随鼠标 _mouseHorizontalTrackLine.X1 = _mouseHorizontalTrackLine.X2 = mousePoint.X; // Y坐标从0开始,延伸足够长 _mouseHorizontalTrackLine.Y1 = 0; _mouseHorizontalTrackLine.Y2 = 5000; _mouseHorizontalTrackLine.Visibility = Visibility.Visible; Debug.WriteLine($"水平标尺 - 垂直线更新: X1={_mouseHorizontalTrackLine.X1}, Y1={_mouseHorizontalTrackLine.Y1}, X2={_mouseHorizontalTrackLine.X2}, Y2={_mouseHorizontalTrackLine.Y2}, Visibility={_mouseHorizontalTrackLine.Visibility}"); } } public void RaiseVerticalRulerMoveEvent(MouseEventArgs e) { var mousePoint = e.GetPosition(this); Debug.WriteLine($"垂直标尺 - 鼠标位置: X={mousePoint.X}, Y={mousePoint.Y}"); if (_mouseVerticalTrackLine != null) { // 垂直标尺:移动水平跟踪线,Y跟随鼠标 _mouseVerticalTrackLine.Y1 = _mouseVerticalTrackLine.Y2 = mousePoint.Y; // X坐标从0开始,延伸足够长 _mouseVerticalTrackLine.X1 = 0; _mouseVerticalTrackLine.X2 = 5000; _mouseVerticalTrackLine.Visibility = Visibility.Visible; Debug.WriteLine($"垂直标尺 - 水平线更新: X1={_mouseVerticalTrackLine.X1}, Y1={_mouseVerticalTrackLine.Y1}, X2={_mouseVerticalTrackLine.X2}, Y2={_mouseVerticalTrackLine.Y2}, Visibility={_mouseVerticalTrackLine.Visibility}"); } } public override void OnApplyTemplate() { base.OnApplyTemplate(); _mouseVerticalTrackLine = GetTemplateChild("verticalTrackLine") as Line; _mouseHorizontalTrackLine = GetTemplateChild("horizontalTrackLine") as Line; Debug.WriteLine($"OnApplyTemplate调用: _mouseVerticalTrackLine={_mouseVerticalTrackLine != null}, _mouseHorizontalTrackLine={_mouseHorizontalTrackLine != null}"); } #endregion /// <summary> /// 重画标尺数据 /// </summary> /// <param name="drawingContext"></param> protected override void OnRender(DrawingContext drawingContext) { try { var pen = new Pen(new SolidColorBrush(Colors.Black), 0.8d); pen.Freeze(); Initialize(); GetActualLength(); GetStep(); base.OnRender(drawingContext); this.BorderBrush = new SolidColorBrush(Colors.Black); this.BorderThickness = new Thickness(0.1); this.Background = new SolidColorBrush(Colors.White); #region try // double actualPx = this._actualLength / DisplayPercent; var currentPosition = new Position { CurrentStepIndex = 0, Value = 0 }; switch (DisplayType) { case RulerDisplayType.Horizontal: { /* 绘制前半段 */ DrawLine(drawingContext, ZeroPoint, currentPosition, pen, 0); /* 绘制后半段 */ DrawLine(drawingContext, ZeroPoint, currentPosition, pen, 1); break; } case RulerDisplayType.Vertical: { /* 绘制前半段 */ DrawLine(drawingContext, ZeroPoint, currentPosition, pen, 0); /* 绘制后半段 */ DrawLine(drawingContext, ZeroPoint, currentPosition, pen, 1); break; } } #endregion } catch (Exception ex) { Console.WriteLine(ex.Message); } } private void DrawLine(DrawingContext drawingContext, double currentPoint, Position currentPosition, Pen pen, int type) { while (true) { var linePercent = 0d; if (currentPosition.CurrentStepIndex == 0) { var formattedText = GetFormattedText((currentPosition.Value / 10).ToString()); switch (DisplayType) { case RulerDisplayType.Horizontal: { var point = new Point(currentPoint + formattedText.Width / 2, formattedText.Height / 3); if (point.X < 0) { break; } drawingContext.DrawText(formattedText, point); break; } case RulerDisplayType.Vertical: { var point = new Point(this.ActualWidth, currentPoint + formattedText.Height / 2); var rotateTransform = new RotateTransform(90, point.X, point.Y); if (point.Y < 0) { break; } drawingContext.PushTransform(rotateTransform); drawingContext.DrawText(formattedText, point); drawingContext.Pop(); break; } } linePercent = (int)LinePercent.P100; } else if (IsFinalNum(currentPosition.CurrentStepIndex, 3)) { linePercent = (int)LinePercent.P30; } else if (IsFinalNum(currentPosition.CurrentStepIndex, 5)) { linePercent = (int)LinePercent.P50; } else if (IsFinalNum(currentPosition.CurrentStepIndex, 7)) { linePercent = (int)LinePercent.P30; } else if (IsFinalNum(currentPosition.CurrentStepIndex, 0)) { linePercent = (int)LinePercent.P70; } else { linePercent = (int)LinePercent.P20; } linePercent = linePercent * 0.01; switch (DisplayType) { case RulerDisplayType.Horizontal: { if (currentPoint > 0) { drawingContext.DrawLine(pen, new Point(currentPoint, 0), new Point(currentPoint, this.ActualHeight * linePercent)); } if (type == 0) { currentPoint = currentPoint - _stepLength; currentPosition.CurrentStepIndex--; if (currentPosition.CurrentStepIndex < 0) { currentPosition.CurrentStepIndex = _stepCount - 1; currentPosition.Value = GetNextStepValue(currentPosition.Value, _stepSpan, 0); } else if (currentPosition.CurrentStepIndex == 0) { if (currentPosition.Value % _stepSpan != 0) { currentPosition.Value = GetNextStepValue(currentPosition.Value, _stepSpan, 0); } } if (currentPoint <= 0) { return; } } else { currentPoint = currentPoint + _stepLength; currentPosition.CurrentStepIndex++; if (currentPosition.CurrentStepIndex >= _stepCount) { currentPosition.CurrentStepIndex = 0; currentPosition.Value = GetNextStepValue(currentPosition.Value, _stepSpan, 1); } if (currentPoint >= _actualLength) { return; } } break; } case RulerDisplayType.Vertical: { if (currentPoint > 0) { drawingContext.DrawLine(pen, new Point(0, currentPoint), new Point(this.ActualWidth * linePercent, currentPoint)); } if (type == 0) { currentPoint = currentPoint - _stepLength; currentPosition.CurrentStepIndex--; if (currentPosition.CurrentStepIndex < 0) { currentPosition.CurrentStepIndex = _stepCount - 1; currentPosition.Value = GetNextStepValue(currentPosition.Value, _stepSpan, 0); } else if (currentPosition.CurrentStepIndex == 0) { if (currentPosition.Value % _stepSpan != 0) { currentPosition.Value = GetNextStepValue(currentPosition.Value, _stepSpan, 0); } } if (currentPoint <= 0) { return; } } else { currentPoint = currentPoint + _stepLength; currentPosition.CurrentStepIndex++; if (currentPosition.CurrentStepIndex >= _stepCount) { currentPosition.CurrentStepIndex = 0; currentPosition.Value = GetNextStepValue(currentPosition.Value, _stepSpan, 1); } if (currentPoint >= _actualLength) { return; } } break; } } } } /// <summary> /// 获取下一个步长值 /// </summary> /// <param name="value">起始值</param> /// <param name="times">跨度</param> /// <param name="type">半段类型,分为前半段、后半段</param> /// <returns></returns> private int GetNextStepValue(int value, int times, int type) { if (type == 0) { do { value--; } while (value % times != 0); } else { do { value++; } while (value % times != 0); } return (value); } [Obsolete] private FormattedText GetFormattedText(string text) { return (new FormattedText(text, //CultureInfo.GetCultureInfo("zh-cn"), CultureInfo.GetCultureInfo("en-us"), FlowDirection.LeftToRight, new Typeface("宋体"), 12, Brushes.Black)); } private bool IsFinalNum(int value, int finalNum) { var valueStr = value.ToString(); if (valueStr.Substring(valueStr.Length - 1, 1) == finalNum.ToString()) { return (true); } return (false); } /// <summary> /// 初始化获取屏幕的DPI /// </summary> private void Initialize() { var dpi = new Dpi(); dpi.DpiX = Dpi.DpiX; dpi.DpiY = Dpi.DpiY; if (Dpi.DpiX == 0) { dpi.DpiX = 96; } if (Dpi.DpiY == 0) { dpi.DpiY = 96; } Dpi = dpi; _minStepLengthCm = 0.1; _maxStepLengthCm = 0.3; if (DisplayPercent == 0) DisplayPercent = 1; switch (DisplayUnit) { case RulerDisplayUnit.Pixel: { _stepSpan = P100StepSpanPixel; _stepCount = P100StepCountPixel; break; } case RulerDisplayUnit.Cm: { _stepSpan = P100StepSpanCm; _stepCount = P100StepCountCm; break; } } var width = 15; switch (DisplayType) { case RulerDisplayType.Horizontal: { if (this.ActualHeight == 0) { Height = width; } break; } case RulerDisplayType.Vertical: { if (this.ActualWidth == 0) { Width = width; } break; } } } /// <summary> /// 获取每一个数字间隔的跨度 /// </summary> private void GetStep() { switch (DisplayUnit) { case RulerDisplayUnit.Pixel: { while (true) { var stepSpanCm = _stepSpan / Convert.ToDouble(GetDpi()) * InchCm * DisplayPercent; var stepLengthCm = stepSpanCm / _stepCount; var type = 0; var isOut = false; if (stepLengthCm > _maxStepLengthCm) { type = 1; _stepCount = GetNextStepCount(_stepCount, type, ref isOut); } if (stepLengthCm < _minStepLengthCm) { type = 0; _stepCount = GetNextStepCount(_stepCount, type, ref isOut); } if (stepLengthCm <= _maxStepLengthCm && stepLengthCm >= _minStepLengthCm) { _stepLength = stepSpanCm / InchCm * Convert.ToDouble(GetDpi()) / _stepCount; break; } /* 已超出或小于最大步进长度 */ if (!isOut) continue; _stepSpan = GetNextStepSpan(_stepSpan, type); } break; } } } private int GetNextStepCount(int stepCount, int type, ref bool isOut) { var result = stepCount; isOut = false; switch (type) { case 0: { if (stepCount == 20) { result = 10; } else { isOut = true; } break; } case 1: { if (stepCount == 10) { result = 20; } else { isOut = true; } break; } } return result; } private int GetNextStepSpan(int stepSpan, int type) { var stepCountStr = stepSpan.ToString(); var resultStr = string.Empty; switch (DisplayUnit) { case RulerDisplayUnit.Pixel: { switch (type) { case 0: { if (stepCountStr.IndexOf('5') > -1) { resultStr = GetNumberAndZeroNum(1, stepCountStr.Length); } else if (stepCountStr.IndexOf('2') > -1) { resultStr = GetNumberAndZeroNum(5, stepCountStr.Length - 1); } else if (stepCountStr.IndexOf('1') > -1) { resultStr = GetNumberAndZeroNum(2, stepCountStr.Length - 1); } break; } case 1: { if (stepCountStr.IndexOf('5') > -1) { resultStr = GetNumberAndZeroNum(2, stepCountStr.Length - 1); } else if (stepCountStr.IndexOf('2') > -1) { resultStr = GetNumberAndZeroNum(1, stepCountStr.Length - 1); } else if (stepCountStr.IndexOf('1') > -1) { resultStr = GetNumberAndZeroNum(5, stepCountStr.Length - 2); } break; } } break; } } if (string.IsNullOrWhiteSpace(resultStr)) { return 0; } if (int.TryParse(resultStr, out var result)) { return result; } return result; } private string GetNumberAndZeroNum(int num, int zeroNum) { var result = string.Empty; result += num; for (var i = 0; i < zeroNum; i++) { result += "0"; } return (result); } private int GetDpi() { switch (DisplayType) { case RulerDisplayType.Horizontal: { return (Dpi.DpiX); } case RulerDisplayType.Vertical: { return (Dpi.DpiY); } default: { return (Dpi.DpiX); } } } private void GetActualLength() { switch (DisplayType) { case RulerDisplayType.Horizontal: { _actualLength = this.ActualWidth; break; } case RulerDisplayType.Vertical: { _actualLength = this.ActualHeight; break; } } } } public enum RulerDisplayType { Horizontal, Vertical } public enum RulerDisplayUnit { Pixel, Cm } public enum LinePercent { P20 = 20, P30 = 30, P50 = 50, P70 = 70, P100 = 100 } public struct Dpi { public int DpiX { get; set; } public int DpiY { get; set; } } public struct Position { public int Value { get; set; } public int CurrentStepIndex { get; set; } } }
3、在原来的代码中,增加对横纵坐标的定义和补充
[TemplatePart(Name = "verticalTrackLine", Type = typeof(Line))] [TemplatePart(Name = "horizontalTrackLine", Type = typeof(Line))]
4、增加对标尺控件的样式实现
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:RulerDemo"> <Style TargetType="{x:Type local:RulerControl}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:RulerControl}"> <Grid> <ContentPresenter/> <!-- 垂直跟踪线(垂直线,X坐标固定,Y跨越整个高度) --> <Line x:Name="verticalTrackLine" Stroke="Red" StrokeThickness="1" StrokeDashArray="2,2" X1="0" Y1="0" X2="0" Y2="2000" Visibility="Collapsed"/> <!-- 水平跟踪线(水平线,Y坐标固定,X跨越整个宽度) --> <Line x:Name="horizontalTrackLine" Stroke="Blue" StrokeThickness="1" StrokeDashArray="2,2" X1="0" Y1="0" X2="2000" Y2="0" Visibility="Collapsed"/> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
5、CanvaseCoreEditor控件的包装
<UserControl x:Class="RulerDemo.CanvaseCoreEditor" 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:RulerDemo" mc:Ignorable="d" x:Name="CanvaseEditor" d:DesignHeight="450" d:DesignWidth="800"> <!-- 主网格,使用 ClipToBounds 确保内容不溢出 --> <Grid x:Name="MainGrid" ClipToBounds="True"> <Grid.ColumnDefinitions> <ColumnDefinition Width="20"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="20"/> <RowDefinition/> </Grid.RowDefinitions> <!-- 横向标尺 --> <local:RulerControl DisplayUnit="pixel" DisplayType="Horizontal" Grid.Row="0" Grid.Column="1" x:Name="ucPanleHor"/> <!-- 纵向标尺 --> <local:RulerControl DisplayUnit="pixel" DisplayType="Vertical" Grid.Row="1" Grid.Column="0" x:Name="ucPanleVer"/> </Grid> </UserControl>
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace RulerDemo { /// <summary> /// CanvaseCoreEditor.xaml 的交互逻辑 /// </summary> public partial class CanvaseCoreEditor : UserControl { public CanvaseCoreEditor() { InitializeComponent(); } public void MainWindowMouseMove(MouseEventArgs e) { ucPanleHor.RaiseHorizontalRulerMoveEvent(e); ucPanleVer.RaiseVerticalRulerMoveEvent(e); } } }
6、主窗口显示调用
<Window x:Class="RulerDemo.MainWindow" 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:RulerDemo" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <local:CanvaseCoreEditor x:Name="CanvaseCoreEditor"></local:CanvaseCoreEditor> </Grid> </Window>
using System.Diagnostics; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace RulerDemo { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); MouseMove += MainWindow_MouseMove; } private void MainWindow_MouseMove(object sender, MouseEventArgs e) { Debug.WriteLine("MainWindow_MouseMove"); CanvaseCoreEditor.MainWindowMouseMove(e); } } }
7、显示效果如下图

总结:
1、本次完善都是通过腾讯Codebuddy的AI编程插件 帮忙完善,加上一小部分的人为的代码微调
2、标尺的代码已经上传至Github:wutyDemo/RulerDemo at main · wutangyuan/wutyDemo

浙公网安备 33010602011771号