2025. 3. 18. 21:48ㆍC#,WPF
MVVM 패턴에 맞게 프로젝트 디렉토리를 정리하겠습니다.
프로젝트를 진행하면서 작성했던 도형 클래스와 부모 클래스를 Model 디렉토리로 옮겨 정리합니다.
이제 모든 도형을 포함하는 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 저장소 주소입니다.
GitHub - chaewonki/DrawingShapesWPF
Contribute to chaewonki/DrawingShapesWPF development by creating an account on GitHub.
github.com
'C#,WPF' 카테고리의 다른 글
[WPF로 도형그리기] 4. 도형 모델의 이해(C) - 원, 네모, 텍스트 (0) | 2025.03.10 |
---|---|
[WPF로 도형그리기] 3. 도형 모델의 이해(B) - 화살표, 자 (0) | 2025.03.07 |
[WPF로 도형그리기] 2. 도형 모델의 이해(A) (0) | 2025.03.04 |
[WPF로 도형그리기] 1. 기본 환경설정 및 UI 구성 (0) | 2025.02.28 |
[C#, WPF] 각도기 클래스 만들고 이를 이용하여 각도기 그리기(3) (0) | 2024.10.15 |