C#,WPF

[WPF로 도형그리기] 3. 도형 모델의 이해(B) - 화살표, 자

rivmt 2025. 3. 7. 21:45

LineModel에 이어 Arrow, Ruler를 위한 모델을 정의하겠습니다.

 

화살표의 경우 아래와 같이 EndPoint가 움직임에 따라 화살표 꼬리의 방향이 변화합니다. 

<그림 1. 화살표 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과 같이 그리되 눈금을 표시해 주어야 합니다. 

<그림 2. 자(Ruler) 그려지는 모습>

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 인스턴스가 필요하므로 따로 정의하여 파라미터로 넘겨주었습니다.