[WPF로 도형그리기] 2. 도형 모델의 이해(A)
도형의 전체 모습을 살펴보겠습니다. 순서는 Line(선), Arrow(화살표), Ruler(자), Ellipse(타원), Rectangle(사각형), Text(텍스트)입니다.
모든 도형위에 두 점을 찍어보겠습니다.
모든 도형은 두개의 점, 즉 시작점(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;
}));
}