[WPF로 도형그리기] 5. 프로젝트 마무리

2025. 3. 18. 21:48C#,WPF

MVVM 패턴에 맞게 프로젝트 디렉토리를 정리하겠습니다.

<그림 1. 정리 이전 디렉토리>

 

프로젝트를 진행하면서 작성했던 도형 클래스와 부모 클래스를 Model 디렉토리로 옮겨 정리합니다.

<그림 2. 정리 후 디렉토리>

 

이제 모든 도형을 포함하는 Enum을 네임스페이스에 추가하겠습니다.

// ViewModel.cs

namespace DrawingShapesWPF
{
    public enum DrawingMode
    {
        None,
        Arrow,
        Ellipse,
        Line,
        Rectangle,
        Ruler,
        Text
    }

    internal class ViewModel : ObservableObject
    (...중략...)

 

다음은 ViewModel.cs 전체 내용입니다.

// 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;
using System.Windows.Shapes;

namespace DrawingShapesWPF
{
    public enum DrawingMode
    {
        None,
        Arrow,
        Ellipse,
        Line,
        Rectangle,
        Ruler,
        Text
    }

    internal class ViewModel : ObservableObject
    {
        private DrawingMode _currentMode = DrawingMode.None;
        private Shape _currentShape;
        private Point _startPoint;
        private Canvas _drawingCanvas;

        public ViewModel(Canvas drawingCanvas)
        {
            _drawingCanvas = drawingCanvas;
        }

        #region Shapes Commands
        private RelayCommand _arrowClickCommand;
        public RelayCommand ArrowClickCommand => _arrowClickCommand ?? new RelayCommand(() => _currentMode = DrawingMode.Arrow);

        private RelayCommand _ellipseClickCommand;
        public RelayCommand EllipseClickCommand => _ellipseClickCommand ?? new RelayCommand(() => _currentMode = DrawingMode.Ellipse);

        private RelayCommand _lineClickCommand;
        public RelayCommand LineClickCommand => _lineClickCommand ?? new RelayCommand(() => _currentMode = DrawingMode.Line);

        private RelayCommand _rectangleClickCommand;
        public RelayCommand RectangleClickCommand => _rectangleClickCommand ?? new RelayCommand(() => _currentMode = DrawingMode.Rectangle);

        private RelayCommand _rulerClickCommand;
        public RelayCommand RulerClickCommand => _rulerClickCommand ?? new RelayCommand(() => _currentMode = DrawingMode.Ruler);

        private RelayCommand _textAnnotationClickCommand;
        public RelayCommand TextAnnotationClickCommand => _textAnnotationClickCommand ?? new RelayCommand(() => _currentMode = DrawingMode.Text);
        #endregion

        #region Private Methods
        private Shape CreateShape(DrawingMode mode)
        {
            switch (mode)
            {
                case DrawingMode.Arrow:
                    return new Arrow(); 
                case DrawingMode.Ellipse:
                    return new EllipseModel();
                case DrawingMode.Line:
                    return new LineModel();
                case DrawingMode.Rectangle:
                    return new RectangleModel();
                case DrawingMode.Ruler:
                    return new Ruler(); 
                case DrawingMode.Text:
                    return new TextAnnotation();
                default:
                    return null;
            }
        }

        private void SetStartPoint(Shape shape, Point startPoint)
        {
            switch (shape)
            {
                case Arrow arrow:
                    arrow.StartPoint = startPoint;
                    arrow.EndPoint = startPoint;
                    break;
                case EllipseModel ellipse:
                    ellipse.StartPoint = startPoint;
                    ellipse.EndPoint = startPoint;
                    break;
                case LineModel line:
                    line.StartPoint = startPoint;
                    line.EndPoint = startPoint;
                    break;
                case RectangleModel rectangle:
                    rectangle.StartPoint = startPoint;
                    rectangle.EndPoint = startPoint;
                    break;
                case Ruler ruler:
                    ruler.StartPoint = startPoint;
                    ruler.EndPoint = startPoint;
                    break;
                case TextAnnotation textAnnotation:
                    textAnnotation.StartPoint = startPoint;
                    textAnnotation.EndPoint = startPoint;
                    break;
                default:
                    break;
            }
        }

        private void UpdateEndPoint(Shape shape, Point endPoint)
        {
            switch (shape)
            {
                case Arrow arrow:
                    arrow.EndPoint = endPoint;
                    break;
                case EllipseModel ellipse:
                    ellipse.EndPoint = endPoint;
                    break;
                case LineModel line:
                    line.EndPoint = endPoint;
                    break;
                case RectangleModel rectangle:
                    rectangle.EndPoint = endPoint;
                    break;
                case Ruler ruler:
                    ruler.EndPoint = endPoint;
                    break;
                case TextAnnotation textAnnotation:
                    textAnnotation.EndPoint = endPoint;
                    break;
                default: 
                    break;
            }
        }
        #endregion

        #region Canvas Mouse Event Commands
        private RelayCommand<MouseButtonEventArgs> _mouseLeftButtonDownCommand;
        public RelayCommand<MouseButtonEventArgs> MouseLeftButtonDownCommand
        {
            get => _mouseLeftButtonDownCommand ??
                (_mouseLeftButtonDownCommand = new RelayCommand<MouseButtonEventArgs>((e) =>
                {

                    if (_currentMode == DrawingMode.None) return;

                    _startPoint = e.GetPosition(_drawingCanvas);
                    _currentShape = CreateShape(_currentMode);

                    if (_currentShape != null)
                    {
                        SetStartPoint(_currentShape, _startPoint);
                        _currentShape.Stroke = Brushes.Red;
                        _currentShape.StrokeThickness = 2;
                        _drawingCanvas.Children.Add(_currentShape);
                    }

                }));
        }

        private RelayCommand<MouseEventArgs> _mouseMoveCommand;
        public RelayCommand<MouseEventArgs> MouseMoveCommand
        {
            get => _mouseMoveCommand ??
                (_mouseMoveCommand = new RelayCommand<MouseEventArgs>((e) =>
                {
                    if (_currentMode == DrawingMode.None) return;
                    Point currentPoint = e.GetPosition(_drawingCanvas);
                    UpdateEndPoint(_currentShape, currentPoint);
                }));
        }

        private RelayCommand<MouseButtonEventArgs> _mouseLeftButtonUpCommand;
        public RelayCommand<MouseButtonEventArgs> MouseLeftButtonUpCommand
        {
            get => _mouseLeftButtonUpCommand ??
                (_mouseLeftButtonUpCommand = new RelayCommand<MouseButtonEventArgs>((e) =>
                {
                    _currentShape = null;
                }));
        }
        #endregion
    }
}

 

먼저 private 멤버 변와 생성자를 살펴보겠습니다.

private DrawingMode _currentMode = DrawingMode.None;
private Shape _currentShape;
private Point _startPoint;
private Canvas _drawingCanvas;

public ViewModel(Canvas drawingCanvas)
{
    _drawingCanvas = drawingCanvas;
}

_currentMode는 버튼 클릭 시마다 선택된 도형 모드를 반영하도록 정의했습니다. 또한 xaml의 Canvas 요소를 생성자 주입을 통해 주입받아 ViewModel에서 항상 접근 할 수 있도록 변경하였습니다.

 

// MainWindow.xaml.cs

using System.Windows;

namespace DrawingShapesWPF
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new ViewModel(cnvDrawing);
        }
    }
}

 

버튼 클릭으로 도형 선택을 준비할 수 있도록 커맨드를 수정합니다.

 

 private RelayCommand _arrowClickCommand;
 public RelayCommand ArrowClickCommand => _arrowClickCommand ?? new RelayCommand(() => _currentMode = DrawingMode.Arrow);

 private RelayCommand _ellipseClickCommand;
 public RelayCommand EllipseClickCommand => _ellipseClickCommand ?? new RelayCommand(() => _currentMode = DrawingMode.Ellipse);

 private RelayCommand _lineClickCommand;
 public RelayCommand LineClickCommand => _lineClickCommand ?? new RelayCommand(() => _currentMode = DrawingMode.Line);

 private RelayCommand _rectangleClickCommand;
 public RelayCommand RectangleClickCommand => _rectangleClickCommand ?? new RelayCommand(() => _currentMode = DrawingMode.Rectangle);

 private RelayCommand _rulerClickCommand;
 public RelayCommand RulerClickCommand => _rulerClickCommand ?? new RelayCommand(() => _currentMode = DrawingMode.Ruler);

 private RelayCommand _textAnnotationClickCommand;
 public RelayCommand TextAnnotationClickCommand => _textAnnotationClickCommand ?? new RelayCommand(() => _currentMode = DrawingMode.Text);

 

다음은 도형을 그리는데 있어 필요한 private 메소드를 살펴보겠습니다.

private Shape CreateShape(DrawingMode mode)
{
    switch (mode)
    {
        case DrawingMode.Arrow:
            return new Arrow(); 
        case DrawingMode.Ellipse:
            return new EllipseModel();
        case DrawingMode.Line:
            return new LineModel();
        case DrawingMode.Rectangle:
            return new RectangleModel();
        case DrawingMode.Ruler:
            return new Ruler(); 
        case DrawingMode.Text:
            return new TextAnnotation();
        default:
            return null;
    }
}

CreateShape는 현재 DrawingMode에 맞는 도형 객체를 생성해 반환합니다.

 

private void SetStartPoint(Shape shape, Point startPoint)
{
    switch (shape)
    {
        case Arrow arrow:
            arrow.StartPoint = startPoint;
            arrow.EndPoint = startPoint;
            break;
        case EllipseModel ellipse:
            ellipse.StartPoint = startPoint;
            ellipse.EndPoint = startPoint;
            break;
        case LineModel line:
            line.StartPoint = startPoint;
            line.EndPoint = startPoint;
            break;
        case RectangleModel rectangle:
            rectangle.StartPoint = startPoint;
            rectangle.EndPoint = startPoint;
            break;
        case Ruler ruler:
            ruler.StartPoint = startPoint;
            ruler.EndPoint = startPoint;
            break;
        case TextAnnotation textAnnotation:
            textAnnotation.StartPoint = startPoint;
            textAnnotation.EndPoint = startPoint;
            break;
        default:
            break;
    }
}

SetStartPoint는 도형의 시작점을 설정합니다. 여기서 EndPoint도 시작점과 동일하게 초기화하지 않으면, ShapeBase.cs에서 기본값 (0,0)으로 설정되어 의도치 않은 결과가 나올 수 있습니다.

 

private void UpdateEndPoint(Shape shape, Point endPoint)
{
    switch (shape)
    {
        case Arrow arrow:
            arrow.EndPoint = endPoint;
            break;
        case EllipseModel ellipse:
            ellipse.EndPoint = endPoint;
            break;
        case LineModel line:
            line.EndPoint = endPoint;
            break;
        case RectangleModel rectangle:
            rectangle.EndPoint = endPoint;
            break;
        case Ruler ruler:
            ruler.EndPoint = endPoint;
            break;
        case TextAnnotation textAnnotation:
            textAnnotation.EndPoint = endPoint;
            break;
        default: 
            break;
    }
}

UpdateEndPoint는 도형이 그려질 때, 마우스 왼쪽버튼을 클릭하고 움직일 때, 도형의 EndPoint를 변경합니다.

 

앞서 정의한 private 메소드를 마우스 이벤트 커맨드에 연결합니다.

private RelayCommand<MouseButtonEventArgs> _mouseLeftButtonDownCommand;
public RelayCommand<MouseButtonEventArgs> MouseLeftButtonDownCommand
{
    get => _mouseLeftButtonDownCommand ??
        (_mouseLeftButtonDownCommand = new RelayCommand<MouseButtonEventArgs>((e) =>
        {

            if (_currentMode == DrawingMode.None) return;

            _startPoint = e.GetPosition(_drawingCanvas);
            _currentShape = CreateShape(_currentMode);

            if (_currentShape != null)
            {
                SetStartPoint(_currentShape, _startPoint);
                _currentShape.Stroke = Brushes.Red;
                _currentShape.StrokeThickness = 2;
                _drawingCanvas.Children.Add(_currentShape);
            }

        }));
}

마우스 왼쪽 버튼을 누를 때, 선택된 도형을 생성하고, 시작점을 설정하며, 선의 색상과 두께를 지정한 뒤 Canvas에 추가합니다.

 

private RelayCommand<MouseEventArgs> _mouseMoveCommand;
public RelayCommand<MouseEventArgs> MouseMoveCommand
{
    get => _mouseMoveCommand ??
        (_mouseMoveCommand = new RelayCommand<MouseEventArgs>((e) =>
        {
            if (_currentMode == DrawingMode.None) return;
            Point currentPoint = e.GetPosition(_drawingCanvas);
            UpdateEndPoint(_currentShape, currentPoint);
        }));
}

마우스 왼쪽 버튼을 누른 채 이동할 때, 끝점을 실시간으로 업데이트합니다.

 

private RelayCommand<MouseButtonEventArgs> _mouseLeftButtonUpCommand;
public RelayCommand<MouseButtonEventArgs> MouseLeftButtonUpCommand
{
    get => _mouseLeftButtonUpCommand ??
        (_mouseLeftButtonUpCommand = new RelayCommand<MouseButtonEventArgs>((e) =>
        {
            _currentShape = null;
        }));
}

 

 

마우스 왼쪽 버튼을 뗄 때 도형 그리기를 종료하고 _currentShape를 초기화합니다.

 

아래는 해당 프로젝트의 GitHub 저장소 주소입니다.

chaewonki/DrawingShapesWPF

 

GitHub - chaewonki/DrawingShapesWPF

Contribute to chaewonki/DrawingShapesWPF development by creating an account on GitHub.

github.com