使用 WPF 和 C# 绘制图形
绘图困难
此示例展示了如何在 WPF 和 C# 中绘制图形。绘制图形总是很棘手,因为您通常需要在至少两个不同的坐标系中工作。首先,您要为图形使用世界坐标。例如,您可能希望 X 值的范围为 2000 年至 2020 年,Y 值的范围为 10,000 美元至 100,000 美元之间的销售额值。
第二个坐标系是以屏幕上的像素为单位测量的 设备坐标系。
显然,在绘制图形本身之类的东西时,您需要使用世界坐标系。最棘手的部分发生在您需要在世界坐标中定位某些东西但在设备坐标中绘制时。例如,假设您要绘制带有 5 个像素长的刻度标记的 X 轴和 Y 轴。您使用世界坐标来确定刻度标记应放置在何处,但随后您需要计算设备坐标中刻度标记的长度(以像素为单位)。
类似地,假设您想在图表上绘制一些文本来标记某些内容。您将文本定位在世界坐标中,但您可能希望在设备坐标中绘制文本。否则很难将文本居中并对齐。
我要提到的最后一个怪异问题是让图形的线条具有一致的粗细。假设您在某个规范化的空间中绘制图形,然后缩放它以适合设备区域。例如,您在世界坐标空间 2000 <= x <= 2020、$10,000 <= y <= $100,000 中绘制图形,然后使用 LayoutTransform使图形适合Canvas控件。当变换拉伸图形时。它还会拉伸您为图形绘制的线条。除非垂直和水平比例因子相同,否则线条在垂直和水平方向上的拉伸量会不同。文本也会被拉伸,从而产生一些非常烦人的结果。
无论如何,要真正将所有东西都准确地放置在您想要的位置,您需要能够在世界坐标和设备坐标中自由工作。这篇文章是一系列简短文章的开篇,这些文章探讨了您可以用来在 WPF 和 C# 中绘制图形的技术。
一个简单的图表
此示例仅使用设备坐标绘制了一个简单的图形。换句话说,所有位置都以像素为单位测量,左上角为 (0, 0)。以下帖子将展示如何在更方便的世界坐标中工作。
以下代码显示了构建该程序的 XAML。
<Window x:Class="howto_wpf_graph.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="howto_wpf_graph"
Height="250" Width="335" Loaded="Window_Loaded">
<Grid Background="LightGreen">
<Canvas Name="canGraph" Background="White"
Width="300" Height="200"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Grid>
</Window>
该窗口的主要子窗口是Grid,其中包含一个名为canGraph的Canvas。
在 WPF 中,您通常不会直接在绘图表面上绘图。如果确实需要,您可以这样做,但通常使用Line、Ellipse、Rectangle和其他形状控件进行绘制。如果愿意,您可以将这些对象包含在 XAML 代码中,但如果您要绘制非平凡图形,则需要使用代码来完成。
当此示例启动时,以下事件处理程序将构建图形。(请注意窗口的 XAML 声明中的Loaded="Window_Loaded"部分。这告诉程序Window_Loaded方法是窗口的Loaded事件的事件处理程序。)
// Draw a simple graph.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
const double margin = 10;
double xmin = margin;
double xmax = canGraph.Width - margin;
double ymin = margin;
double ymax = canGraph.Height - margin;
const double step = 10;
// Make the X axis.
GeometryGroup xaxis_geom = new GeometryGroup();
xaxis_geom.Children.Add(new LineGeometry(
new Point(0, ymax), new Point(canGraph.Width, ymax)));
for (double x = xmin + step;
x <= canGraph.Width - step; x += step)
{
xaxis_geom.Children.Add(new LineGeometry(
new Point(x, ymax - margin / 2),
new Point(x, ymax + margin / 2)));
}
Path xaxis_path = new Path();
xaxis_path.StrokeThickness = 1;
xaxis_path.Stroke = Brushes.Black;
xaxis_path.Data = xaxis_geom;
canGraph.Children.Add(xaxis_path);
// Make the Y ayis.
GeometryGroup yaxis_geom = new GeometryGroup();
yaxis_geom.Children.Add(new LineGeometry(
new Point(xmin, 0), new Point(xmin, canGraph.Height)));
for (double y = step; y <= canGraph.Height - step; y += step)
{
yaxis_geom.Children.Add(new LineGeometry(
new Point(xmin - margin / 2, y),
new Point(xmin + margin / 2, y)));
}
Path yaxis_path = new Path();
yaxis_path.StrokeThickness = 1;
yaxis_path.Stroke = Brushes.Black;
yaxis_path.Data = yaxis_geom;
canGraph.Children.Add(yaxis_path);
// Make some data sets.
Brush[] brushes = { Brushes.Red, Brushes.Green, Brushes.Blue };
Random rand = new Random();
for (int data_set = 0; data_set < 3; data_set++)
{
int last_y = rand.Next((int)ymin, (int)ymax);
PointCollection points = new PointCollection();
for (double x = xmin; x <= xmax; x += step)
{
last_y = rand.Next(last_y - 10, last_y + 10);
if (last_y < ymin) last_y = (int)ymin;
if (last_y > ymax) last_y = (int)ymax;
points.Add(new Point(x, last_y));
}
Polyline polyline = new Polyline();
polyline.StrokeThickness = 1;
polyline.Stroke = brushes[data_set];
polyline.Points = points;
canGraph.Children.Add(polyline);
}
}
代码首先为图定义一些边界。
接下来,程序创建一个GeometryGroup对象来表示 X 轴。GeometryGroup可以容纳其他几何对象,例如线条。代码创建一个Line来表示轴的基线并将其添加到组中。然后,它使用循环创建一组Line对象来表示刻度标记并将它们添加到组中。
在创建完所有轴的Line对象并将其添加到GeometryGroup后,程序将创建一个Path对象并设置其StrokeThickness和Stroke属性。然后它将路径的Data属性设置为等于GeometryGroup。
最后,代码将路径添加到canGraph对象的Children集合中。
然后代码重复这些步骤来创建 Y 轴。
接下来,代码生成一些图形数据。对于每个数据集,代码都会创建一个PointCollection对象。它会生成一堆随机点并将它们添加到集合中。完成数据生成后,程序会创建一个Polyline,设置其绘制属性,并将其Points属性设置为点集合。最后,代码将Polyline添加到canGraph对象的Children集合中。
这就是程序需要做的全部工作。当窗口出现时,Line和Polyline对象会根据需要自行绘制。