2024. 10. 14. 21:01ㆍC#,WPF
각도기를 그릴 때, 마우스의 움직임에 대해 살펴보겠습니다. 시작점(StartPoint)에서 좌클릭한 이후 마우스 버튼을 누른 채로 이동하면 선분1의 길이가 변하고 사용자가 원하는 지점에서 마우스 왼쪽 버튼을 떼게 되면 끝점1(EndPoint1)이 정해집니다. 그리고 다음번 좌클릭한 지점은 끝점2(EndPoint2)의 후보가 되며 실시간으로 변화하는 선분2와 호, 각도가 사용자에게 표시되어야 합니다. 이어 마우스 왼쪽 버튼을 떼게 되면 끝점2(EndPoint2)가 확정되고 각도기가 완성됩니다.
위와 같이 사용자의 실시간 마우스 움직임으로 도형의 모양이 결정됩니다. 앞서 정의한 Protractor 클래스의 OnRender 메소드는 사용자의 실시간 움직임을 반영하기에 충분하지 않고 선분 길이의 변화와 각도의 변화를 감당해 내기에는 부하가 큽니다. 따라서 Canvas위에 임의의 선과 호, 각도를 표시하고 끝점2(EndPoint2)가 결정되는 순간 임의의 선, 호, 각도는 지우고 Protractor 객체를 화면에 표시하는 방식으로 애플리케이션을 완성하겠습니다.
Canvas 위에 임시로 그려질 각도기의 요소를 살펴보겠습니다.
이들 요소를 Protractor에 정의한 것과 같은 모습으로 보이게 그리는 것이 핵심입니다.
먼저 MainWindow.xaml 파일을 살펴보겠습니다.
<Window x:Class="ProtractorSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ProtractorSample"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Canvas x:Name="cnvDrawing"
MouseLeftButtonDown="cnvDrawing_MouseLeftButtonDown"
MouseMove="cnvDrawing_MouseMove"
MouseLeftButtonUp="cnvDrawing_MouseLeftButtonUp"
Background="White"/>
</Grid>
</Window>
Grid 안에 각도기를 표시할 Canvas를 정의하고 마우스 이벤트를 처리하기 위한 이벤트 핸들러를 등록했습니다.
다음은 코드비하인드 파일입니다.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace ProtractorSample
{
public partial class MainWindow : Window
{
private bool _isFirstLineDrawn = false;
private Line _firstLine;
private Line _secondLine;
private List<Line> _tempLines = new();
private Protractor _protractor;
private TextBlock _tbDegree;
private Point _degreePoint;
private Path _arcPath;
private PathGeometry _arcPathGeometry;
private PathFigure _arcPathFigure;
private ArcSegment _arcSegment;
private Point _arcStartPoint;
private Point _arcEndPoint;
private const double ARCRADIUS = 50;
private const double TEXTOFFSET = 60;
private const int FONTSIZE = 16;
public MainWindow()
{
InitializeComponent();
}
private void cnvDrawing_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point currentPoint = e.GetPosition(cnvDrawing);
if (!_isFirstLineDrawn)
{
DrawFirstLine(currentPoint);
}
else
{
DrawSecondLine();
}
}
private void DrawFirstLine(Point currentPoint)
{
_protractor = new Protractor()
{
Stroke = Brushes.Black,
StrokeThickness = 2,
StartPoint = currentPoint,
};
_tbDegree = new TextBlock();
_tbDegree.Text = "";
_tbDegree.FontFamily = new FontFamily("Arial");
_tbDegree.FlowDirection = FlowDirection.LeftToRight;
_tbDegree.FontSize = FONTSIZE;
_tbDegree.Foreground = Brushes.Black;
cnvDrawing.Children.Add(_tbDegree);
_firstLine = CreateLine(currentPoint.X, currentPoint.Y);
_tempLines.Add(_firstLine);
cnvDrawing.Children.Add(_firstLine);
cnvDrawing.CaptureMouse();
}
private void DrawSecondLine()
{
_secondLine = CreateLine(_firstLine.X1, _firstLine.Y1);
_tempLines.Add(_secondLine);
cnvDrawing.Children.Add(_secondLine);
_arcPath = new Path()
{
Stroke = Brushes.Black,
StrokeThickness = 2,
};
_arcPathGeometry = new PathGeometry();
_arcPathFigure = new PathFigure();
_arcSegment = new ArcSegment()
{
Size = new Size(ARCRADIUS, ARCRADIUS),
IsLargeArc = false,
SweepDirection = SweepDirection.Clockwise,
};
_arcPathFigure.Segments.Add(_arcSegment);
_arcPathGeometry.Figures.Add(_arcPathFigure);
_arcPath.Data = _arcPathGeometry;
cnvDrawing.Children.Add(_arcPath);
}
private Line CreateLine(double x, double y)
{
return new Line()
{
Stroke = Brushes.Black,
StrokeThickness = 2,
X1 = x,
Y1 = y,
X2 = x,
Y2 = y
};
}
private void cnvDrawing_MouseMove(object sender, MouseEventArgs e)
{
Point currentPoint = e.GetPosition(cnvDrawing);
if (!_isFirstLineDrawn && e.LeftButton == MouseButtonState.Pressed)
{
_firstLine.X2 = currentPoint.X;
_firstLine.Y2 = currentPoint.Y;
}
else if (_isFirstLineDrawn && e.LeftButton == MouseButtonState.Pressed)
{
_secondLine.X2 = currentPoint.X;
_secondLine.Y2 = currentPoint.Y;
UpdateDegreeAndArc(currentPoint);
}
}
private void UpdateDegreeAndArc(Point currentPoint)
{
Point startPoint = new Point(_firstLine.X1, _firstLine.Y1);
Vector vector1 = new Point(_firstLine.X2, _firstLine.Y2) - startPoint;
Vector vector2 = currentPoint - startPoint;
vector1.Normalize();
vector2.Normalize();
double angle = Vector.AngleBetween(vector1, vector2);
if (angle < 0)
angle += 360;
if (angle > 180)
{
angle = 360 - angle;
(vector1, vector2) = (vector2, vector1);
}
string angleText = $"{Math.Round(angle, 2)}°";
_tbDegree.Text = angleText;
_degreePoint = startPoint + vector1 * TEXTOFFSET;
Canvas.SetLeft(_tbDegree, _degreePoint.X);
Canvas.SetTop(_tbDegree, _degreePoint.Y);
_arcStartPoint = startPoint + vector1 * ARCRADIUS;
_arcEndPoint = startPoint + vector2 * ARCRADIUS;
_arcPathFigure.StartPoint = _arcStartPoint;
_arcSegment.Point = _arcEndPoint;
}
private void cnvDrawing_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
Point currentPoint = e.GetPosition(cnvDrawing);
if (!_isFirstLineDrawn)
FinalizeFirstLine(currentPoint);
else
FinalizeSecondLine(currentPoint);
cnvDrawing.ReleaseMouseCapture();
}
private void FinalizeFirstLine(Point currentPoint)
{
_protractor.EndPoint1 = currentPoint;
_isFirstLineDrawn = true;
}
private void FinalizeSecondLine(Point currentPoint)
{
_protractor.EndPoint2 = currentPoint;
cnvDrawing.Children.Add(_protractor);
cnvDrawing.Children.Remove(_arcPath);
cnvDrawing.Children.Remove(_tbDegree);
foreach (var line in _tempLines)
cnvDrawing.Children.Remove(line);
_tempLines.Clear();
_isFirstLineDrawn = false;
}
}
}
먼저 임시 요소와 관련된 멤버부터 살펴보겠습니다.
private bool _isFirstLineDrawn = false;
private Line _firstLine;
private Line _secondLine;
private List<Line> _tempLines = new();
private Protractor _protractor;
각도기의 선분을 그릴 때, 해당 선이 첫 번째 선인지, 두 번째 선인지 판단하는 것은 아주 중요합니다. 따라서 _isFirstLineDrawn을 정의하였고, 두 선분의 구분을 위해 _firstLine, _secondLine을 정의하였습니다. 또 이 임의의 선들을 지우기 위해 임시로 저장할 _tempLines 리스트를 정의하였고 최종적으로 화면에 표시될 각도기를 위해 _protractor를 정의하였습니다.
private TextBlock _tbDegree;
private Point _degreePoint;
다음은 각도를 표시할 텍스트를 담는 TextBlock과 이를 위치할 _degreePoint를 정의하였습니다. Protractor 클래스에서는 DrawingContext에 직접 각도를 표시했습니다.
private Path _arcPath;
private PathGeometry _arcPathGeometry;
private PathFigure _arcPathFigure;
private ArcSegment _arcSegment;
private Point _arcStartPoint;
private Point _arcEndPoint;
// Protractor 클래스와 동일한 크기와 모양의 호, 텍스트 위치를 위한 상수 정의
private const double ARCRADIUS = 50;
private const double TEXTOFFSET = 60;
private const int FONTSIZE = 16;
이어서 호와 관련된 부분입니다. Canvas에 추가할 수 있는 형식은 UIElement이어야 합니다. 이에 필요한 것이 _arcPath입니다. 이 멤버가 Canvas의 Children에 추가되면 캔버스 위에 보일 준비가 된 것입니다. Path의 Data 프로퍼티를 할당하기 위해 PathGeometry, _arcPathGeometry를 정의했습니다. 이어 PathFigure가 정의되는데, 이는 Geometry의 Figures에 추가되어 어떤 모양으로 표시될 것인가를 정합니다. 즉, 호를 정의하는 부분이라고 할 수 있습니다. Segment들의 합으로 나타낼 수 있고 실제 그려질 때 시작점과 연관이 있습니다. 다음은 PathFigure의 Segments에 추가될 ArcSegment, _arcSegment입니다. 이 멤버는 끝점과 연관이 있으며 Protractor 클래스와 동일한 크기의 호, 모양을 적용시켜야 합니다.
'C#,WPF' 카테고리의 다른 글
[WPF로 도형그리기] 1. 기본 환경설정 및 UI 구성 (0) | 2025.02.28 |
---|---|
[C#, WPF] 각도기 클래스 만들고 이를 이용하여 각도기 그리기(3) (0) | 2024.10.15 |
[C#, WPF] 각도기 클래스 만들고 이를 이용하여 각도기 그리기(1) (0) | 2024.10.14 |
[C#, WPF] Shape 클래스를 상속받아 커스텀 Ruler 만들기 (0) | 2024.08.09 |
[C#, WPF] pythonnet을 활용하여 Beautiful Soup을 WPF 애플리케이션에서 이용해보기 (0) | 2023.11.17 |