仿Word的支持横轴竖轴的WPF 标尺
最近在 https://mp.weixin.qq.com/s/3dEO0NZQv5YLqK72atG4Wg 官方公众号看到了 用WPF 制作 标尺
在去年项目上也接到了一个需求,用于排版自定义拖拽控件画布对齐的标尺,当时接到的要求是 需要横纵对齐的表次,并且鼠标滑动,刻度的上方需要跟着有影子划过的效果。
具体实现如下:
创建 标尺控件 RulerControl.cs
1 [TemplatePart(Name = "trackLine", Type = typeof(Line))] 2 internal class RulerControl : Control 3 { 4 public static readonly DependencyProperty DpiProperty = DependencyProperty.Register("Dpi", typeof(Dpi), typeof(RulerControl)); 5 public static readonly DependencyProperty DisplayPercentProperty = DependencyProperty.Register("DisplayPercent", typeof(double), typeof(RulerControl)); 6 public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register("DisplayType", typeof(RulerDisplayType), typeof(RulerControl)); 7 public static readonly DependencyProperty DisplayUnitProperty = DependencyProperty.Register("DisplayUnit", typeof(RulerDisplayUnit), typeof(RulerControl)); 8 public static readonly DependencyProperty ZeroPointProperty = DependencyProperty.Register("ZeroPoint", typeof(double), typeof(RulerControl)); 9 10 /// <summary> 11 /// 定义静态构造函数 12 /// </summary> 13 static RulerControl() 14 { 15 DefaultStyleKeyProperty.OverrideMetadata(typeof(RulerControl), new FrameworkPropertyMetadata(typeof(RulerControl))); 16 } 17 18 #region 属性 19 /// <summary> 20 /// 屏幕分辨率 21 /// </summary> 22 public Dpi Dpi 23 { 24 get 25 { 26 return ((Dpi)GetValue(DpiProperty)); 27 } 28 set 29 { 30 SetValue(DpiProperty, value); 31 } 32 } 33 34 /// <summary> 35 /// 设置0点从哪里开始 36 /// </summary> 37 public double ZeroPoint 38 { 39 get 40 { 41 return ((double)GetValue(ZeroPointProperty)); 42 } 43 set 44 { 45 SetValue(ZeroPointProperty, value); 46 InvalidateVisual(); 47 } 48 } 49 50 /// <summary> 51 /// 显示的比率(目前支持0-1的选项) 52 /// </summary> 53 public double DisplayPercent 54 { 55 get 56 { 57 return ((double)GetValue(DisplayPercentProperty)); 58 } 59 set 60 { 61 if (value > 1) 62 { 63 value = 1; 64 } 65 SetValue(DisplayPercentProperty, value); 66 InvalidateVisual(); 67 } 68 } 69 70 /// <summary> 71 /// 显示的类型:枚举类(支持横向或者竖向) 72 /// </summary> 73 public RulerDisplayType DisplayType 74 { 75 get 76 { 77 return ((RulerDisplayType)GetValue(DisplayTypeProperty)); 78 } 79 set 80 { 81 SetValue(DisplayTypeProperty, value); 82 } 83 } 84 85 /// <summary> 86 /// 显示的单位:cm和pixel 87 /// </summary> 88 public RulerDisplayUnit DisplayUnit 89 { 90 get 91 { 92 return ((RulerDisplayUnit)GetValue(DisplayUnitProperty)); 93 } 94 set 95 { 96 SetValue(DisplayUnitProperty, value); 97 } 98 } 99 #endregion 100 101 #region 常量 102 public const double _inchCm = 2.54; //一英寸为2.54cm 103 private const int _p100StepSpanPixel = 100; 104 private const int _p100StepSpanCm = 2; 105 private const int _p100StepCountPixel = 20; 106 private const int _p100StepCountCm = 20; 107 108 #endregion 109 110 #region 变量 111 private double _minStepLengthCm; 112 private double _maxStepLengthCm; 113 private double _actualLength; 114 private int _stepSpan; 115 private int _stepCount; 116 private double _stepLength; 117 Line mouseVerticalTrackLine; 118 Line mouseHorizontalTrackLine; 119 #endregion 120 121 #region 标尺边框加指针显示 122 public void RaiseHorizontalRulerMoveEvent(MouseEventArgs e) 123 { 124 Point mousePoint = e.GetPosition(this); 125 mouseHorizontalTrackLine.X1 = mouseHorizontalTrackLine.X2 = mousePoint.X; 126 } 127 public void RaiseVerticalRulerMoveEvent(MouseEventArgs e) 128 { 129 Point mousePoint = e.GetPosition(this); 130 mouseVerticalTrackLine.Y1 = mouseVerticalTrackLine.Y2 = mousePoint.Y; 131 } 132 133 public override void OnApplyTemplate() 134 { 135 base.OnApplyTemplate(); 136 mouseVerticalTrackLine = GetTemplateChild("verticalTrackLine") as Line; 137 mouseHorizontalTrackLine = GetTemplateChild("horizontalTrackLine") as Line; 138 mouseVerticalTrackLine.Visibility = Visibility.Visible; 139 mouseHorizontalTrackLine.Visibility = Visibility.Visible; 140 } 141 #endregion 142 143 /// <summary> 144 /// 重画标尺数据 145 /// </summary> 146 /// <param name="drawingContext"></param> 147 protected override void OnRender(DrawingContext drawingContext) 148 { 149 try 150 { 151 Pen pen = new Pen(new SolidColorBrush(Colors.Black),0.8d); 152 pen.Freeze(); 153 Initialize(); 154 GetActualLength(); 155 GetStep(); 156 base.OnRender(drawingContext); 157 158 this.BorderBrush = new SolidColorBrush(Colors.Black); 159 this.BorderThickness = new Thickness(0.1); 160 this.Background = new SolidColorBrush(Colors.White); 161 162 #region try 163 // double actualPx = this._actualLength / DisplayPercent; 164 Position currentPosition = new Position 165 { 166 CurrentStepIndex = 0, 167 Value = 0 168 }; 169 170 switch (DisplayType) 171 { 172 case RulerDisplayType.Horizontal: 173 { 174 /* 绘制前半段 */ 175 DrawLine(drawingContext, ZeroPoint, currentPosition, pen, 0); 176 /* 绘制后半段 */ 177 DrawLine(drawingContext, ZeroPoint, currentPosition, pen, 1); 178 break; 179 } 180 case RulerDisplayType.Vertical: 181 { 182 /* 绘制前半段 */ 183 DrawLine(drawingContext, ZeroPoint, currentPosition, pen, 0); 184 /* 绘制后半段 */ 185 DrawLine(drawingContext, ZeroPoint, currentPosition, pen, 1); 186 break; 187 } 188 } 189 #endregion 190 } 191 catch (Exception ex) 192 { 193 Console.WriteLine(ex.Message); 194 } 195 } 196 197 private void DrawLine(DrawingContext drawingContext, double currentPoint, Position currentPosition, Pen pen, int type) 198 { 199 double linePercent = 0d; 200 while (true) 201 { 202 if (currentPosition.CurrentStepIndex == 0) 203 { 204 205 FormattedText formattedText = GetFormattedText((currentPosition.Value / 10).ToString()); 206 207 208 switch (DisplayType) 209 { 210 case RulerDisplayType.Horizontal: 211 { 212 var point = new Point(currentPoint + formattedText.Width / 2, formattedText.Height / 3); 213 if (point.X<0) 214 { 215 break; 216 } 217 drawingContext.DrawText(formattedText, point); 218 break; 219 } 220 case RulerDisplayType.Vertical: 221 { 222 Point point = new Point(this.ActualWidth, currentPoint + formattedText.Height / 2); 223 RotateTransform rotateTransform = new RotateTransform(90, point.X, point.Y); 224 if (point.Y<0) 225 { 226 break; 227 } 228 drawingContext.PushTransform(rotateTransform); 229 drawingContext.DrawText(formattedText, point); 230 drawingContext.Pop(); 231 break; 232 } 233 } 234 235 linePercent = (int)LinePercent.P100; 236 } 237 else if (IsFinalNum(currentPosition.CurrentStepIndex, 3)) 238 { 239 linePercent = (int)LinePercent.P30; 240 } 241 else if (IsFinalNum(currentPosition.CurrentStepIndex, 5)) 242 { 243 linePercent = (int)LinePercent.P50; 244 } 245 else if (IsFinalNum(currentPosition.CurrentStepIndex, 7)) 246 { 247 linePercent = (int)LinePercent.P30; 248 } 249 else if (IsFinalNum(currentPosition.CurrentStepIndex, 0)) 250 { 251 linePercent = (int)LinePercent.P70; 252 } 253 else 254 { 255 linePercent = (int)LinePercent.P20; 256 } 257 258 linePercent = linePercent * 0.01; 259 260 switch (DisplayType) 261 { 262 case RulerDisplayType.Horizontal: 263 { 264 if (currentPoint > 0) 265 { 266 drawingContext.DrawLine(pen, new Point(currentPoint, 0), new Point(currentPoint, this.ActualHeight * linePercent)); 267 } 268 269 if (type == 0) 270 { 271 currentPoint = currentPoint - _stepLength; 272 currentPosition.CurrentStepIndex--; 273 274 if (currentPosition.CurrentStepIndex < 0) 275 { 276 currentPosition.CurrentStepIndex = _stepCount - 1; 277 currentPosition.Value = GetNextStepValue(currentPosition.Value, _stepSpan, 0); 278 } 279 else if (currentPosition.CurrentStepIndex == 0) 280 { 281 if (currentPosition.Value % _stepSpan != 0) 282 { 283 currentPosition.Value = GetNextStepValue(currentPosition.Value, _stepSpan, 0); 284 } 285 } 286 287 if (currentPoint <= 0) 288 { 289 return; 290 } 291 } 292 else 293 { 294 currentPoint = currentPoint + _stepLength; 295 currentPosition.CurrentStepIndex++; 296 297 if (currentPosition.CurrentStepIndex >= _stepCount) 298 { 299 currentPosition.CurrentStepIndex = 0; 300 currentPosition.Value = GetNextStepValue(currentPosition.Value, _stepSpan, 1); 301 } 302 303 if (currentPoint >= _actualLength) 304 { 305 return; 306 } 307 } 308 break; 309 } 310 case RulerDisplayType.Vertical: 311 { 312 if (currentPoint > 0) 313 { 314 drawingContext.DrawLine(pen, new Point(0, currentPoint), new Point(this.ActualWidth * linePercent, currentPoint)); 315 } 316 if (type == 0) 317 { 318 currentPoint = currentPoint - _stepLength; 319 currentPosition.CurrentStepIndex--; 320 321 if (currentPosition.CurrentStepIndex < 0) 322 { 323 currentPosition.CurrentStepIndex = _stepCount - 1; 324 currentPosition.Value = GetNextStepValue(currentPosition.Value, _stepSpan, 0); 325 } 326 else if (currentPosition.CurrentStepIndex == 0) 327 { 328 if (currentPosition.Value % _stepSpan != 0) 329 { 330 currentPosition.Value = GetNextStepValue(currentPosition.Value, _stepSpan, 0); 331 } 332 } 333 334 if (currentPoint <= 0) 335 { 336 return; 337 } 338 } 339 else 340 { 341 currentPoint = currentPoint + _stepLength; 342 currentPosition.CurrentStepIndex++; 343 344 if (currentPosition.CurrentStepIndex >= _stepCount) 345 { 346 currentPosition.CurrentStepIndex = 0; 347 currentPosition.Value = GetNextStepValue(currentPosition.Value, _stepSpan, 1); 348 } 349 350 if (currentPoint >= _actualLength) 351 { 352 return; 353 } 354 } 355 break; 356 } 357 } 358 } 359 } 360 361 /// <summary> 362 /// 获取下一个步长值 363 /// </summary> 364 /// <param name="value">起始值</param> 365 /// <param name="times">跨度</param> 366 /// <param name="type">半段类型,分为前半段、后半段</param> 367 /// <returns></returns> 368 private int GetNextStepValue(int value, int times, int type) 369 { 370 if (type == 0) 371 { 372 do 373 { 374 value--; 375 } 376 while (value % times != 0); 377 } 378 else 379 { 380 do 381 { 382 value++; 383 } 384 while (value % times != 0); 385 } 386 return (value); 387 } 388 389 [Obsolete] 390 private FormattedText GetFormattedText(string text) 391 { 392 return (new FormattedText(text, 393 //CultureInfo.GetCultureInfo("zh-cn"), 394 CultureInfo.GetCultureInfo("en-us"), 395 FlowDirection.LeftToRight, 396 new Typeface("宋体"), 397 12, 398 Brushes.Black)); 399 } 400 401 private bool IsFinalNum(int value, int finalNum) 402 { 403 string valueStr = value.ToString(); 404 if (valueStr.Substring(valueStr.Length - 1, 1) == finalNum.ToString()) 405 { 406 return (true); 407 } 408 return (false); 409 } 410 411 /// <summary> 412 /// 初始化获取屏幕的DPI 413 /// </summary> 414 private void Initialize() 415 { 416 Dpi dpi = new Dpi(); 417 dpi.DpiX = Dpi.DpiX; 418 dpi.DpiY = Dpi.DpiY; 419 if (Dpi.DpiX == 0) 420 { 421 dpi.DpiX = 96; 422 } 423 424 if (Dpi.DpiY == 0) 425 { 426 dpi.DpiY = 96; 427 } 428 429 Dpi = dpi; 430 _minStepLengthCm = 0.1; 431 _maxStepLengthCm = 0.3; 432 433 if (DisplayPercent == 0) 434 DisplayPercent = 1; 435 436 switch (DisplayUnit) 437 { 438 case RulerDisplayUnit.pixel: 439 { 440 _stepSpan = _p100StepSpanPixel; 441 _stepCount = _p100StepCountPixel; 442 break; 443 } 444 case RulerDisplayUnit.cm: 445 { 446 _stepSpan = _p100StepSpanCm; 447 _stepCount = _p100StepCountCm; 448 break; 449 } 450 } 451 int width = 15; 452 switch (DisplayType) 453 { 454 case RulerDisplayType.Horizontal: 455 { 456 if (this.ActualHeight == 0) 457 { 458 Height = width; 459 } 460 break; 461 } 462 case RulerDisplayType.Vertical: 463 { 464 if (this.ActualWidth == 0) 465 { 466 Width = width; 467 } 468 break; 469 } 470 } 471 } 472 473 /// <summary> 474 /// 获取每一个数字间隔的跨度 475 /// </summary> 476 private void GetStep() 477 { 478 switch (DisplayUnit) 479 { 480 case RulerDisplayUnit.pixel: 481 { 482 double stepSpanCm; 483 while (true) 484 { 485 stepSpanCm = _stepSpan / Convert.ToDouble(GetDpi()) * _inchCm * DisplayPercent; 486 double stepLengthCm = stepSpanCm / _stepCount; 487 int type = 0; 488 bool isOut = false; 489 if (stepLengthCm > _maxStepLengthCm) 490 { 491 type = 1; 492 _stepCount = GetNextStepCount(_stepCount, type, ref isOut); 493 } 494 495 if (stepLengthCm < _minStepLengthCm) 496 { 497 type = 0; 498 _stepCount = GetNextStepCount(_stepCount, type, ref isOut); 499 } 500 501 if (stepLengthCm <= _maxStepLengthCm && stepLengthCm >= _minStepLengthCm) 502 { 503 _stepLength = stepSpanCm / _inchCm * Convert.ToDouble(GetDpi()) / _stepCount; 504 break; 505 } 506 /* 已超出或小于最大步进长度 */ 507 if (isOut) 508 { 509 _stepSpan = GetNextStepSpan(_stepSpan, type); 510 continue; 511 } 512 } 513 break; 514 } 515 } 516 } 517 518 519 private int GetNextStepCount(int stepCount, int type, ref bool isOut) 520 { 521 int result = stepCount; 522 isOut = false; 523 switch (type) 524 { 525 case 0: 526 { 527 if (stepCount == 20) 528 { 529 result = 10; 530 } 531 else 532 { 533 isOut = true; 534 } 535 break; 536 } 537 case 1: 538 { 539 if (stepCount == 10) 540 { 541 result = 20; 542 } 543 else 544 { 545 isOut = true; 546 } 547 548 break; 549 } 550 } 551 return result; 552 } 553 554 555 private int GetNextStepSpan(int stepSpan, int type) 556 { 557 string stepCountStr = stepSpan.ToString(); 558 string resultStr = string.Empty; 559 560 switch (DisplayUnit) 561 { 562 case RulerDisplayUnit.pixel: 563 { 564 switch (type) 565 { 566 case 0: 567 { 568 if (stepCountStr.IndexOf('5') > -1) 569 { 570 resultStr = GetNumberAndZeroNum(1, stepCountStr.Length); 571 } 572 else if (stepCountStr.IndexOf('2') > -1) 573 { 574 resultStr = GetNumberAndZeroNum(5, stepCountStr.Length - 1); 575 } 576 else if (stepCountStr.IndexOf('1') > -1) 577 { 578 resultStr = GetNumberAndZeroNum(2, stepCountStr.Length - 1); 579 } 580 break; 581 } 582 case 1: 583 { 584 if (stepCountStr.IndexOf('5') > -1) 585 { 586 resultStr = GetNumberAndZeroNum(2, stepCountStr.Length - 1); 587 } 588 else if (stepCountStr.IndexOf('2') > -1) 589 { 590 resultStr = GetNumberAndZeroNum(1, stepCountStr.Length - 1); 591 } 592 else if (stepCountStr.IndexOf('1') > -1) 593 { 594 resultStr = GetNumberAndZeroNum(5, stepCountStr.Length - 2); 595 } 596 break; 597 } 598 } 599 break; 600 } 601 } 602 603 int result = 0; 604 if (string.IsNullOrWhiteSpace(resultStr)) 605 { 606 return 0; 607 } 608 609 if (int.TryParse(resultStr, out result)) 610 { 611 return result; 612 } 613 return result; 614 } 615 616 617 private string GetNumberAndZeroNum(int num, int zeroNum) 618 { 619 string result = string.Empty; 620 result += num; 621 for (int i = 0; i < zeroNum; i++) 622 { 623 result += "0"; 624 } 625 return (result); 626 } 627 628 629 private int GetDpi() 630 { 631 switch (DisplayType) 632 { 633 case RulerDisplayType.Horizontal: 634 { 635 return (Dpi.DpiX); 636 } 637 case RulerDisplayType.Vertical: 638 { 639 return (Dpi.DpiY); 640 } 641 default: 642 { 643 return (Dpi.DpiX); 644 } 645 } 646 } 647 648 private void GetActualLength() 649 { 650 switch (DisplayType) 651 { 652 case RulerDisplayType.Horizontal: 653 { 654 _actualLength = this.ActualWidth; 655 break; 656 } 657 case RulerDisplayType.Vertical: 658 { 659 _actualLength = this.ActualHeight; 660 break; 661 } 662 } 663 } 664 } 665 666 public enum RulerDisplayType 667 { 668 Horizontal, Vertical 669 } 670 671 public enum RulerDisplayUnit 672 { 673 pixel, cm 674 } 675 676 public enum LinePercent 677 { 678 P20 = 20, P30 = 30, P50 = 50, P70 = 70, P100 = 100 679 } 680 681 public struct Dpi 682 { 683 public int DpiX 684 { 685 get; set; 686 } 687 public int DpiY 688 { 689 get; set; 690 } 691 } 692 693 public struct Position 694 { 695 public int Value 696 { 697 get; set; 698 } 699 public int CurrentStepIndex 700 { 701 get; set; 702 } 703 }
控件的引用:
1 <UserControl x:Class="Hjmos.DataPainter.Controls.CanvaseCoreEditor" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 5 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 6 xmlns:local="clr-namespace:Hjmos.DataPainter.Controls" 7 mc:Ignorable="d" 8 x:Name="CanvaseEditor" 9 d:DesignHeight="450" d:DesignWidth="800"> 10 <Grid> 11 12 <Grid.ColumnDefinitions> 13 <ColumnDefinition Width="20"/> 14 <ColumnDefinition/> 15 </Grid.ColumnDefinitions> 16 <Grid.RowDefinitions> 17 <RowDefinition Height="20"/> 18 <RowDefinition/> 19 </Grid.RowDefinitions> 20 <!--横向标尺--> 21 <local:RulerControl DisplayUnit="pixel" DisplayType="Horizontal" Grid.Row="0" Grid.Column="1" x:Name="ucPanleHor"/> 22 <!--纵向标尺--> 23 <local:RulerControl DisplayUnit="pixel" DisplayType="Vertical" Grid.Row="1" Grid.Column="0" x:Name="ucPanleVer"/> 24 25 26 </Grid> 27 </UserControl>
鼠标在画布移动的时候触发标尺上的刻度阴影移动
/// <summary> /// 鼠标在画布移动的时候触发标尺上的刻度阴影移动 /// </summary> /// <param name="obj"></param> private void _diagramView_MouseMove(MouseEventArgs e) { ucPanleHor.RaiseHorizontalRulerMoveEvent(e); ucPanleVer.RaiseVerticalRulerMoveEvent(e); }
总结:技术点主要是用到了WPF 内部的渲染机制,调用 OnRender的方法,实现对线条绘制;通过计算总长度从而得出步长,绘制不同的长度线的标尺刻度。