准备
开发环境:VisualStudio2019;程序类型:WPF应用程序。
布局
首先在项目里新建一个UserControl用户控件,命名为CurveView。简单布下局,界面分两行,上面一行固定高50px,放工具栏,下面一行放曲线的绘制区域。
代码如下:
<UserControl x:Class="CustomCurveView.CurveView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:CustomCurveView"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="0" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Hidden">
<ToolBar>
<Label FontSize="14" Height="30" VerticalContentAlignment="Center">X轴坐标间距:</Label>
<TextBox x:Name="txt_SpanX" BorderBrush="Blue" Width="40" Height="30" FontSize="14" VerticalContentAlignment="Center">1</TextBox>
<Separator Background="Purple"></Separator>
<Label Margin="2,0,0,0" FontSize="14" Height="30" VerticalContentAlignment="Center">Y轴坐标间距:</Label>
<TextBox x:Name="txt_SpanY" BorderBrush="Blue" Width="40" Height="30" FontSize="14" VerticalContentAlignment="Center">1</TextBox>
<Separator Background="Purple"></Separator>
<Label Margin="2,0,0,0" FontSize="14" Height="30" VerticalContentAlignment="Center">X轴最小最大值:</Label>
<TextBox x:Name="txt_MinX" BorderBrush="Blue" Width="40" Height="30" FontSize="14" VerticalContentAlignment="Center">0</TextBox>
<TextBox Margin="2,0,0,0" BorderBrush="Blue" x:Name="txt_MaxX" Width="40" Height="30" FontSize="14" VerticalContentAlignment="Center">10</TextBox>
<Separator Background="Purple"></Separator>
<Label Margin="2,0,0,0" FontSize="14" Height="30" VerticalContentAlignment="Center">Y轴最小最大值:</Label>
<TextBox x:Name="txt_MinY" BorderBrush="Blue" Width="40" Height="30" FontSize="14" VerticalContentAlignment="Center">0</TextBox>
<TextBox Margin="2,0,0,0" BorderBrush="Blue" x:Name="txt_MaxY" Width="40" Height="30" FontSize="14" VerticalContentAlignment="Center">10</TextBox>
<Separator Background="Purple"></Separator>
<Button Margin="5,0,0,0" Background="SkyBlue" x:Name="btn_Setting" Width="35" Height="25"
FontSize="14" Click="btn_Setting_Click">确定</Button>
<Button Margin="5,0,0,0" Background="SkyBlue" x:Name="btn_Reset" Width="35" Height="25"
FontSize="14" Click="btn_Reset_Click">复位</Button>
</ToolBar>
</ScrollViewer>
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Canvas x:Name="cv_CurveRegion" Width="800" Height="550" Background="LightGray" ClipToBounds="True"
MouseDown="cv_CurveRegion_MouseDown" MouseMove="cv_CurveRegion_MouseMove"
SizeChanged="cv_CurveRegion_SizeChanged">
</Canvas>
</ScrollViewer>
</Grid>
</UserControl>
布局效果如下:
定义外部可设的变量及全局变量
这里我们思考一下具体情况,对于我们现在做的CurveView自定义用户控件而言,当外部使用此控件时,需要设置的主要参数为:x轴与y轴的坐标间距、坐标最小最大值,以及描绘曲线的点集合。
定义外部可设变量代码如下:
#region External settable variables.
/// <summary>
/// X轴坐标间距
/// </summary>
public double SpanX { get; set; }
/// <summary>
/// Y轴坐标间距
/// </summary>
public double SpanY { get; set; }
/// <summary>
/// X轴坐标最小值
/// </summary>
public double MinX { get; set; }
/// <summary>
/// X轴坐标最大值
/// </summary>
public double MaxX { get; set; }
/// <summary>
/// Y轴坐标最小值
/// </summary>
public double MinY { get; set; }
/// <summary>
/// Y轴坐标最大值
/// </summary>
public double MaxY { get; set; }
/// <summary>
/// 曲线上的点
/// </summary>
public PointCollection CurvePoints { get; set; }
#endregion
全局需要使用的变量有:曲线区域的起点及终点坐标、曲线区域宽度及高度、坐标原点在Canvas里的位置坐标、开始移动的坐标。
定义全局变量的代码如下:
private Point regionStartPosition; //曲线区域起点坐标
private Point regionEndPosition; //曲线区域终点坐标
private double regionWidth; //曲线区域宽度
private double regionHeight; //曲线区域高度
private double zeroPositionX = 0; //原点的位置X
private double zeroPositionY = 0; //原点的位置Y
private Point startMovePosition; //鼠标按下的位置,即开始移动的位置
初始化曲线区域
初始化曲线区域,主要就是初始设置曲线区域的宽高以及区域起点与终点坐标。代码如下:
/// <summary>
/// 初始化曲线区域
/// </summary>
private void InitCurveRegion()
{
double boardWidth = cv_CurveRegion.Width;
double boardHeight = cv_CurveRegion.Height;
double boardMargin = 40;
regionWidth = boardWidth - 2 * boardMargin;
regionHeight = boardHeight - 2 * boardMargin;
regionStartPosition = new Point(boardMargin, boardHeight - boardMargin);
regionEndPosition = new Point(boardWidth - boardMargin, boardMargin);
CurvePoints = new PointCollection();
}
画坐标轴
接下来我们在曲线区域画出坐标轴,也就是x轴与y轴的两条线。代码如下:
/// <summary>
/// 画坐标轴
/// </summary>
private void DrawAxis()
{
var xAxis = new Line
{
X1 = regionStartPosition.X,
Y1 = regionStartPosition.Y,
X2 = regionStartPosition.X,
Y2 = regionEndPosition.Y,
Stroke = new SolidColorBrush(Colors.Black)
};
cv_CurveRegion.Children.Add(xAxis);
var yAxis = new Line
{
X1 = regionStartPosition.X,
Y1 = regionStartPosition.Y,
X2 = regionEndPosition.X,
Y2 = regionStartPosition.Y,
Stroke = new SolidColorBrush(Colors.Black)
};
cv_CurveRegion.Children.Add(yAxis);
}
画出x轴与y轴坐标点
下面要把x轴与y轴完整画出来,主要分三部分,刻度线、刻度标记、网格线。相关代码如下:
/// <summary>
/// 画X轴坐标点
/// </summary>
private void DrawPointsForAxisX()
{
if (MinX >= MaxX) return;
if (SpanX < 1) SpanX = 1;
for (double i = MinX; i <= MaxX; i += SpanX)
{
double x = regionStartPosition.X + i * regionWidth / (MaxX - MinX) + zeroPositionX;
var markLine = new Line
{
X1 = x,
Y1 = regionStartPosition.Y,
X2 = x,
Y2 = regionStartPosition.Y + 5,
Stroke = Brushes.Red
};
cv_CurveRegion.Children.Add(markLine);
if (i > MinX)
{
var gridLine = new Line
{
X1 = x,
Y1 = regionStartPosition.Y,
X2 = x,
Y2 = regionEndPosition.Y,
StrokeThickness = 1,
Stroke = new SolidColorBrush(Colors.ForestGreen)
};
cv_CurveRegion.Children.Add(gridLine);
}
var markText = new TextBlock
{
Text = i.ToString(),
Width = 80,
Foreground = Brushes.Black,
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Stretch,
TextAlignment = TextAlignment.Left,
FontSize = 15
};
cv_CurveRegion.Children.Add(markText);
Canvas.SetTop(markText, regionStartPosition.Y + 5);
Canvas.SetLeft(markText, x);
}
}
/// <summary>
/// 画Y轴坐标点
/// </summary>
private void DrawPointsForAxisY()
{
if (MinY >= MaxY) return;
if (SpanY < 1) SpanY = 1;
for (double i = MinY; i <= MaxY; i += SpanY)
{
double y = regionStartPosition.Y - i * regionHeight / (MaxY - MinY) + zeroPositionY;
var markLine = new Line
{
X1 = regionStartPosition.X - 5,
Y1 = y,
X2 = regionStartPosition.X,
Y2 = y,
Stroke = Brushes.Red
};
cv_CurveRegion.Children.Add(markLine);
if (i > MinY)
{
var gridLine = new Line
{
X1 = regionStartPosition.X,
Y1 = y,
X2 = regionEndPosition.X,
Y2 = y,
StrokeThickness = 1,
Stroke = new SolidColorBrush(Colors.ForestGreen)
};
cv_CurveRegion.Children.Add(gridLine);
}
var markText = new TextBlock
{
Text = i.ToString(),
Width = 30,
Foreground = Brushes.Black,
HorizontalAlignment = HorizontalAlignment.Right,
TextAlignment = TextAlignment.Right,
FontSize = 15
};
cv_CurveRegion.Children.Add(markText);
Canvas.SetTop(markText, y - 10);
Canvas.SetLeft(markText, 0);
}
}
画曲线及点
现在坐标轴都有了,那么可以画曲线和具体的点了。代码如下:
/// <summary>
/// 画曲线点
/// </summary>
private void DrawCurvePoints()
{
var polyline = new Polyline();
foreach (var point in CurvePoints)
{
Point realPoint = GetPointRealPosition(point);
Ellipse ellipse = new Ellipse();
ellipse.Width = 6;
ellipse.Height = 6;
ellipse.Margin = new Thickness(realPoint.X - 3, realPoint.Y - 3, 0, 0);
ellipse.Stroke = new SolidColorBrush(Colors.Black);
ellipse.Fill = Brushes.Black;
cv_CurveRegion.Children.Add(ellipse);
polyline.Points.Add(realPoint);
}
polyline.Stroke = Brushes.Blue;
cv_CurveRegion.Children.Add(polyline);
}
/// <summary>
/// 得到点在Canvas里的真实位置
/// </summary>
/// <param name="point">传入的点</param>
/// <returns></returns>
private Point GetPointRealPosition(Point point)
{
var realX = regionStartPosition.X + (point.X - MinX) * regionWidth / (MaxX - MinX) + zeroPositionX;
var realY = regionEndPosition.Y + (MaxY - point.Y) * regionHeight / (MaxY - MinY) + zeroPositionY;
return new Point(realX, realY);
}
这里说明一下,因为我们传入点的坐标是相对于真实的坐标系,及数学上的二维坐标系而言。而我们呈现在界面上需要的是点在界面上的位置,所以这里写了GetPointRealPosition函数对点的坐标做了转换。
刷新曲线视图
由于我们的曲线一些参数是可以自设的,所以我们要根据相关参数实现曲线的重新绘制。代码如下:
/// <summary>
/// 刷新曲线区域
/// </summary>
private void RefreshCurveRegion()
{
cv_CurveRegion.Children.Clear();
//获取x最大值
if (MaxX < 1)
{
MaxX = CurvePoints.Max(p => p.X);
}
//获取y最大值
if (MaxY < 1)
{
MaxY = CurvePoints.Max(p => p.Y);
}
if (Math.Abs(MaxX) < 1 || Math.Abs(MaxY) < 1)
{
return;
}
DrawAxis();
DrawPointsForAxisX();
DrawPointsForAxisY();
DrawCurvePoints();
}
顺便提一下,这部分代码用到了Lambda表达式,有兴趣的朋友可以自行去学习下。
实现曲线的拖动
主要思路为:鼠标按下时记录开始移动的位置,拖动时记录当前拖动的位置、更新开始移动位置、计算原点的位置并刷新曲线区域界面。相关代码如下:
/// <summary>
/// Canvas的MouseDown事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void cv_CurveRegion_MouseDown(object sender, MouseButtonEventArgs e)
{
startMovePosition = e.GetPosition((Canvas)sender);
}
/// <summary>
/// Canvas的MouseMove事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void cv_CurveRegion_MouseMove(object sender, MouseEventArgs e)
{
Point currentMousePosition = e.GetPosition((Canvas)sender);
if (e.LeftButton == MouseButtonState.Pressed)
{
zeroPositionX = zeroPositionX + currentMousePosition.X - startMovePosition.X;
zeroPositionY = zeroPositionY + currentMousePosition.Y - startMovePosition.Y;
startMovePosition = currentMousePosition;
RefreshCurveRegion();
}
}
/// <summary>
/// Canvas的SizeChanged事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void cv_CurveRegion_SizeChanged(object sender, SizeChangedEventArgs e)
{
RefreshCurveRegion();
}
曲线设置与复位功能
最后,我们把界面上设定曲线参数和复位的功能实现。代码如下:
/// <summary>
/// 复位按钮点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btn_Reset_Click(object sender, RoutedEventArgs e)
{
zeroPositionX = 0;
zeroPositionY = 0;
RefreshCurveRegion();
}
/// <summary>
/// 确定按钮点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btn_Setting_Click(object sender, RoutedEventArgs e)
{
try
{
SpanX = Convert.ToDouble(txt_SpanX.Text);
SpanY = Convert.ToDouble(txt_SpanY.Text);
MinX = Convert.ToDouble(txt_MinX.Text);
MaxX = Convert.ToDouble(txt_MaxX.Text);
MinY = Convert.ToDouble(txt_MinY.Text);
MaxY = Convert.ToDouble(txt_MaxY.Text);
}
catch (Exception ex)
{
MessageBox.Show("请检查相关输入!\n\r" + ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
RefreshCurveRegion();
}
其他
我们把做完的CurveVIew控件引入到项目的主界面及MainWindow.xaml文件中,并初始化一些参数,然后就可以看到运行效果了。如图:
本程序的源码,有需要的朋友可以点此下载。