C#,WPF
[WPF로 도형그리기] 3. 도형 모델의 이해(B) - 화살표, 자
rivmt
2025. 3. 7. 21:45
LineModel에 이어 Arrow, Ruler를 위한 모델을 정의하겠습니다.
화살표의 경우 아래와 같이 EndPoint가 움직임에 따라 화살표 꼬리의 방향이 변화합니다.
// Arrow.cs
private void CalculateTailPoints()
{
Vector direction = EndPoint - StartPoint;
direction.Normalize();
double angle45 = Math.PI / 4;
Vector rotatedDirection1 = new Vector(
direction.X * Math.Cos(angle45) - direction.Y * Math.Sin(angle45),
direction.X * Math.Sin(angle45) + direction.Y * Math.Cos(angle45)
);
Vector rotatedDirection2 = new Vector(
direction.X * Math.Cos(-angle45) - direction.Y * Math.Sin(-angle45),
direction.X * Math.Sin(-angle45) + direction.Y * Math.Cos(-angle45)
);
TailPoint1 = StartPoint + rotatedDirection1 * TailLength;
TailPoint2 = StartPoint + rotatedDirection2 * TailLength;
}
화살표의 시작점에서 45도 방향으로 꼬리 끝점을 정해줍니다. 이어서 CreateGeometry를 구현하여 Arrow 클래스를 완성합니다.
// Arrow.cs
using System;
using System.Windows;
using System.Windows.Media;
namespace DrawingShapesWPF
{
public class Arrow : ShapeBase
{
private Point TailPoint1 { get; set; }
private Point TailPoint2 { get; set; }
private const int TailLength = 20;
protected override Geometry CreateGeometry()
{
CalculateTailPoints();
var geometry = new StreamGeometry();
using (var ctx = geometry.Open())
{
ctx.BeginFigure(StartPoint, false, false);
ctx.LineTo(EndPoint, true, false);
ctx.BeginFigure(StartPoint, false, false);
ctx.LineTo(TailPoint1, true, false);
ctx.BeginFigure(StartPoint, false, false);
ctx.LineTo(TailPoint2, true, false);
}
return geometry;
}
private void CalculateTailPoints()
{
Vector direction = EndPoint - StartPoint;
direction.Normalize();
double angle45 = Math.PI / 4;
Vector rotatedDirection1 = new Vector(
direction.X * Math.Cos(angle45) - direction.Y * Math.Sin(angle45),
direction.X * Math.Sin(angle45) + direction.Y * Math.Cos(angle45)
);
Vector rotatedDirection2 = new Vector(
direction.X * Math.Cos(-angle45) - direction.Y * Math.Sin(-angle45),
direction.X * Math.Sin(-angle45) + direction.Y * Math.Cos(-angle45)
);
TailPoint1 = StartPoint + rotatedDirection1 * TailLength;
TailPoint2 = StartPoint + rotatedDirection2 * TailLength;
}
}
}
이어 ViewModel을 Arrow에 맞게 수정합니다. 이후 그려질 도형에 대해선 따로 ViewModel 파일을 제시하지 않겠습니다.
private Arrow arrow;
private Canvas drawingCanvas;
#region Canvas Mouse Event Commands
private RelayCommand<MouseButtonEventArgs> _mouseLeftButtonDownCommand;
public RelayCommand<MouseButtonEventArgs> MouseLeftButtonDownCommand
{
get => _mouseLeftButtonDownCommand ??
(_mouseLeftButtonDownCommand = new RelayCommand<MouseButtonEventArgs>((e) =>
{
//Debug.WriteLine("LEFT DOWN");
Point currentPoint = e.GetPosition(null);
arrow = new Arrow();
arrow.StartPoint = currentPoint;
arrow.EndPoint = currentPoint;
arrow.Stroke = Brushes.Red;
arrow.StrokeThickness = 2;
drawingCanvas = (Canvas)VisualTreeHelper.HitTest(Application.Current.MainWindow, new Point(0, 0))?.VisualHit;
drawingCanvas.Children.Add(arrow);
}));
}
private RelayCommand<MouseEventArgs> _mouseMoveCommand;
public RelayCommand<MouseEventArgs> MouseMoveCommand
{
get => _mouseMoveCommand ??
(_mouseMoveCommand = new RelayCommand<MouseEventArgs>((e) =>
{
//Debug.WriteLine("MOVE");
if (e.LeftButton == MouseButtonState.Pressed)
arrow.EndPoint = e.GetPosition(null);
}));
}
private RelayCommand<MouseButtonEventArgs> _mouseLeftButtonUpCommand;
public RelayCommand<MouseButtonEventArgs> MouseLeftButtonUpCommand
{
get => _mouseLeftButtonUpCommand ??
(_mouseLeftButtonUpCommand = new RelayCommand<MouseButtonEventArgs>((e) =>
{
//Debug.WriteLine("UP");
arrow = null;
}));
}
#endregion
다음은 자 입니다. Ruler 클래스의 경우 화살표와 마찬가지로 시작점과 끝점을 LineModel과 같이 그리되 눈금을 표시해 주어야 합니다.
public class Ruler : ShapeBase
{
protected override Geometry CreateGeometry()
{
var geometry = new StreamGeometry();
using (var ctx = geometry.Open())
{
ctx.BeginFigure(StartPoint, false, false);
ctx.LineTo(EndPoint, true, false);
}
return geometry;
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
double interval = 15.0;
double tickLength = 10.0;
Pen pen = new Pen(Stroke, StrokeThickness);
Vector lineVector = new Vector(EndPoint.X - StartPoint.X, EndPoint.Y - StartPoint.Y);
double lineLength = lineVector.Length;
lineVector.Normalize();
Vector perpendicularVector = new Vector(-lineVector.Y, lineVector.X);
for (double i = 0; i < lineLength; i += interval)
{
Point tickPoint = StartPoint + lineVector * i;
Point tickStart = tickPoint - perpendicularVector * tickLength / 2;
Point tickEnd = tickPoint + perpendicularVector * tickLength / 2;
drawingContext.DrawLine(pen, tickStart, tickEnd);
}
}
}
OnRender 메소드가 재정의 되었습니다. UI 요소인 자(Ruler)위에 눈금을 표시하기 위해 사용되었습니다. Ruler의 시작점과 끝점을 이용해 벡터를 계산하고 수직벡터를 구하여 임의의 간격과 길이로 눈금(tick)을 그려줍니다. 이 때, DrawLine 메소드를 사용하기 위해서 Pen 인스턴스가 필요하므로 따로 정의하여 파라미터로 넘겨주었습니다.