C#,WPF

[WPF로 도형그리기] 2. 도형 모델의 이해(A)

rivmt 2025. 3. 4. 21:01

도형의 전체 모습을 살펴보겠습니다. 순서는 Line(선), Arrow(화살표), Ruler(자), Ellipse(타원), Rectangle(사각형), Text(텍스트)입니다. 

<그림 1. 모든 도형>

 

모든 도형위에 두 점을 찍어보겠습니다.

<그림 2. 도형 위의 점>

 

모든 도형은 두개의 점, 즉 시작점(StartPoint)과 끝점(EndPoint)으로 표현할 수 있습니다. 이를 반영하여 모든 도형의 부모 클래스인 ShapeBase를 WPF의 Shape 클래스를 상속받아 추상클래스로 정의하겠습니다. 

// ShapeBase.cs

using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

namespace DrawingShapesWPF
{
    public abstract class ShapeBase : Shape
    {
        public static readonly DependencyProperty StartPointProperty =
        DependencyProperty.Register(nameof(StartPoint), typeof(Point), typeof(ShapeBase),
            new FrameworkPropertyMetadata(new Point(0,0), FrameworkPropertyMetadataOptions.AffectsRender));

        public static readonly DependencyProperty EndPointProperty =
            DependencyProperty.Register(nameof(EndPoint), typeof(Point), typeof(ShapeBase),
                new FrameworkPropertyMetadata(new Point(0, 0), FrameworkPropertyMetadataOptions.AffectsRender));

        public Point StartPoint
        {
            get => (Point)GetValue(StartPointProperty);
            set => SetValue(StartPointProperty, value);
        }

        public Point EndPoint
        {
            get => (Point)GetValue(EndPointProperty);
            set => SetValue(EndPointProperty, value);
        }

        protected override Geometry DefiningGeometry => CreateGeometry();
        protected abstract Geometry CreateGeometry();
    }
}

Shape 클래스를 상속 받을 경우 DefiningGeometry 멤버를 반드시 구현해줘야 합니다. 이를 하위 클래스에서 처리할 수 있도록 CreateGeometry라는 추상 메서드를 정의하여 위임했습니다.

 

이제 ShapeBase 클래스를 상속받아 LineModel 클래스를 구현하겠습니다. CreateGeometry 메서드를 오버라이드하여 선을 그리는 기능을 수행합니다.

// LineModel.cs

using System.Windows.Media;

namespace DrawingShapesWPF
{
    public class LineModel : 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;
        }
    }
}

StreamGeometry를 활용하여 선을 그려줍니다.

 

이제 LineModel이 잘 작동하는 지 확인하기 위해 ViewModel과 MainWindow를 수정하겠습니다.

// ViewModel.cs

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Windows.Input;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace DrawingShapesWPF
{
    internal class ViewModel : ObservableObject
    {
        private LineModel lineModel;
        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);
                    lineModel = new LineModel();
                    lineModel.StartPoint = currentPoint;
                    lineModel.EndPoint = currentPoint;
                    lineModel.Stroke = Brushes.Red;
                    lineModel.StrokeThickness = 2;

                    drawingCanvas = (Canvas)VisualTreeHelper.HitTest(Application.Current.MainWindow, new Point(0, 0))?.VisualHit;
                    drawingCanvas.Children.Add(lineModel);
                }));
        }

        private RelayCommand<MouseEventArgs> _mouseMoveCommand;
        public RelayCommand<MouseEventArgs> MouseMoveCommand
        {
            get => _mouseMoveCommand ??
                (_mouseMoveCommand = new RelayCommand<MouseEventArgs>((e) =>
                {
                    //Debug.WriteLine("MOVE");
                    if (e.LeftButton == MouseButtonState.Pressed)
                        lineModel.EndPoint = e.GetPosition(null);
                }));
        }

        private RelayCommand<MouseButtonEventArgs> _mouseLeftButtonUpCommand;
        public RelayCommand<MouseButtonEventArgs> MouseLeftButtonUpCommand
        {
            get => _mouseLeftButtonUpCommand ??
                (_mouseLeftButtonUpCommand = new RelayCommand<MouseButtonEventArgs>((e) =>
                {
                    //Debug.WriteLine("UP");
                    lineModel = null;
                }));
        }
        #endregion

        #region Shapes Commands
        private RelayCommand _arrowClickCommand;
        public RelayCommand ArrowClickCommand => _arrowClickCommand ?? new RelayCommand(() => MessageBox.Show("ARROW"));

        private RelayCommand _ellipseClickCommand;
        public RelayCommand EllipseClickCommand => _ellipseClickCommand ?? new RelayCommand(() => MessageBox.Show("ELLIPSE"));

        private RelayCommand _lineClickCommand;
        public RelayCommand LineClickCommand => _lineClickCommand ?? new RelayCommand(() => MessageBox.Show("LINE"));

        private RelayCommand _rectangleClickCommand;
        public RelayCommand RectangleClickCommand => _rectangleClickCommand ?? new RelayCommand(() => MessageBox.Show("RECTANGLE"));

        private RelayCommand _rulerClickCommand;
        public RelayCommand RulerClickCommand => _rulerClickCommand ?? new RelayCommand(() => MessageBox.Show("RULER"));

        private RelayCommand _textAnnotationClickCommand;
        public RelayCommand TextAnnotationClickCommand => _textAnnotationClickCommand ?? new RelayCommand(() => MessageBox.Show("TEXT"));
        #endregion
    }
}

LineModel을 위한 멤버, 이를 그리게 될 Canvas를 위해 멤버를 추가했습니다. 마우스 왼쪽 버튼을 누를 때 해당 Point에서 LineModel을 생성하여 캔버스 위에 추가합니다. 

 

drawingCanvas = (Canvas)VisualTreeHelper.HitTest(Application.Current.MainWindow, new Point(0, 0))?.VisualHit;

캔버스위 좌표 (0,0)은 통상 그림을 그리는 영역이 아니기 때문에 마우스 클릭 시, 항상 Canvas를 얻기 위한 상수 좌표로 사용하였습니다. 

 

이어 왼쪽 버튼이 눌린 상태에서 마우스가 움직일 때 앞서 생성한 LineModel의 EndPoint를 해당 마우스포인트로 설정합니다. 그리고 마우스 버튼을 떼게되면 LineModel 그리기가 완료 됩니다.

private RelayCommand<MouseEventArgs> _mouseMoveCommand;
public RelayCommand<MouseEventArgs> MouseMoveCommand
{
    get => _mouseMoveCommand ??
        (_mouseMoveCommand = new RelayCommand<MouseEventArgs>((e) =>
        {
            //Debug.WriteLine("MOVE");
            if (e.LeftButton == MouseButtonState.Pressed)
                lineModel.EndPoint = e.GetPosition(null);
        }));
}

private RelayCommand<MouseButtonEventArgs> _mouseLeftButtonUpCommand;
public RelayCommand<MouseButtonEventArgs> MouseLeftButtonUpCommand
{
    get => _mouseLeftButtonUpCommand ??
        (_mouseLeftButtonUpCommand = new RelayCommand<MouseButtonEventArgs>((e) =>
        {
            //Debug.WriteLine("UP");
            lineModel = null;
        }));
}

 

 

<그림 3. 마우스 조작에 따라 LineModel 그려지는 모습>