c# 中的可缩放、可打印、可滚动的火车运动图
Zoomable, printable, scrollable train movement graphs in c#
我需要用 C# 构建一个图形化的火车时刻表可视化工具。实际上我必须在 C# 中重建 this perfect tool。
Marey's Trains
图表必须可缩放、可滚动,并且 printable/exportable 为带有矢量图形元素的 PDF。
你能给我一些提示吗?我应该如何开始呢?我应该使用什么样的库?
是否值得尝试使用像 OxyPlot 这样的图形库?也许它不是最好的,因为特殊的轴和不规则的网格——正如我所想的那样。你怎么看?
无论您使用哪种图表工具,一旦您需要特殊类型的显示,您总是需要添加一些额外的代码。
这里是一个使用MSChart
控件的例子。
请注意它对导出矢量格式有限制:
它可以导出为各种formats, including 3 EMF
类型;然而,只有一些应用程序可以实际使用它们。不确定您使用的 PDF 库..!
如果您不能使用 Emf
格式,您可以通过使 Chart
控件非常大,导出到 Png
然后使 Dpi
分辨率比保存后的默认屏幕分辨率大得多。将其设置为 600
或 1200dpi
应该适用于大多数 pdf 使用..
现在让我们看一个例子:
一些注意事项:
我在很多方面让我的生活变得更轻松。我只编码了一个方向,我没有把公鸡反转,所以它只能从下到上。
我没有使用真实数据,而是自己编造的。
我没有创建一个或多个类来保存站数据;相反,我使用了一个非常简单的 Tuple
.
我还没有创建 DataTable
来保存火车数据。相反,我编造它们并即时将它们添加到图表中..
我没有测试,但缩放和滚动应该也能正常工作..
这是保存我的电台数据的List<Tuple>
:
// station name, distance, type: 0=terminal, 1=normal, 2=main station
List<Tuple<string, double, int>> TrainStops = null;
我是这样设置图表的:
Setup24HoursAxis(chart1, DateTime.Today);
TrainStops = SetupTrainStops(17);
SetupTrainStopAxis(chart1);
for (int i = 0; i < 23 * 3; i++)
{
AddTrainStopSeries(chart1, DateTime.Today.Date.AddMinutes(i * 20),
17 - rnd.Next(4), i% 5 == 0 ? 1 : 0);
}
// this exports the image above:
chart1.SaveImage("D:\trains.png", ChartImageFormat.Png);
每 20 分钟创建一列火车,停靠 14-17 站,每 5 列火车创建一列快速火车。
以下是我调用的例程:
设置 x-axis 以保留一天的数据非常简单。
public static void Setup24HoursAxis(Chart chart, DateTime dt)
{
chart.Legends[0].Enabled = false;
Axis ax = chart.ChartAreas[0].AxisX;
ax.IntervalType = DateTimeIntervalType.Hours;
ax.Interval = 1;
ax.Minimum = dt.ToOADate();
ax.Maximum = (dt.AddHours(24)).ToOADate();
ax.LabelStyle.Format = "H:mm";
}
创建具有随机距离的站点列表也非常简单。我做了第一个和最后一个终端,每五个一个主站。
public List<Tuple<string, double, int>> SetupTrainStops(int count)
{
var stops = new List<Tuple<string, double, int>>();
Random rnd = new Random(count);
for (int i = 0; i < count; i++)
{
string n = (char)(i+(byte)'A') + "-Street";
double d = 1 + rnd.Next(3) + rnd.Next(4) + rnd.Next(5) / 10d;
if (d < 3) d = 3; // a minimum distance so the label won't touch
int t = (i == 0 | i == count-1) ? 0 : rnd.Next(5)==0 ? 2 : 1;
var ts = new Tuple<string, double, int>(n, d, t);
stops.Add(ts);
}
return stops;
}
现在我们有了火车停靠站,我们可以设置 y-axis:
public void SetupTrainStopAxis(Chart chart)
{
Axis ay = chart.ChartAreas[0].AxisY;
ay.LabelStyle.Font = new Font("Consolas", 8f);
double totalDist = 0;
for (int i = 0; i < TrainStops.Count; i++)
{
CustomLabel cl = new CustomLabel();
cl.Text = TrainStops[i].Item1;
cl.FromPosition = totalDist - 0.1d;
cl.ToPosition = totalDist + 0.1d;
totalDist += TrainStops[i].Item2;
cl.ForeColor = TrainStops[i].Item3 == 1 ? Color.DimGray : Color.Black;
ay.CustomLabels.Add(cl);
}
ay.Minimum = 0;
ay.Maximum = totalDist;
ay.MajorGrid.Enabled = false;
ay.MajorTickMark.Enabled = false;
}
这里需要注意几点:
- 由于值非常动态,我们不能使用固定
Interval
间距的正常 Labels
。
- 所以我们改为创建
CustomLabels
。
- 对于这些,我们需要两个值来确定 space 它们应居中的位置。所以我们通过 adding/subtracting
0.1d
. 创建一个小跨度
- 我们已经计算出总距离,并用它来设置y-axis的
Maximum
。再说一次:要模仿你展示的时间表,你必须在这里和那里做一些倒车..
- 通过添加
CustomLabels
,正常的自动关闭。由于我们在 不规则 间隔需要 MajorGridlines
,因此我们也关闭了正常间隔。因此,我们必须自己绘制它们。并不像你看到的那么难..:[=129=]
为此,我们编写了 xxxPaint
事件之一:
private void chart1_PostPaint(object sender, ChartPaintEventArgs e)
{
Axis ay = chart1.ChartAreas[0].AxisY;
Axis ax = chart1.ChartAreas[0].AxisX;
int x0 = (int) ax.ValueToPixelPosition(ax.Minimum);
int x1 = (int) ax.ValueToPixelPosition(ax.Maximum);
double totalDist = 0;
foreach (var ts in TrainStops)
{
int y = (int)ay.ValueToPixelPosition(totalDist);
totalDist += ts.Item2;
using (Pen p = new Pen(ts.Item3 == 1 ? Color.DarkGray : Color.Black,
ts.Item3 == 1 ? 0.5f : 1f))
e.ChartGraphics.Graphics.DrawLine(p, x0 + 1, y, x1, y);
}
// ** Insert marker drawing code (from update below) here !
}
注意轴的ValueToPixelPosition
转换函数的使用!
现在是最后一部分:如何添加 Series
训练数据..:[=51=]
public void AddTrainStopSeries(Chart chart, DateTime start, int count, int speed)
{
Series s = chart.Series.Add(start.ToShortTimeString());
s.ChartType = SeriesChartType.Line;
s.Color = speed == 0 ? Color.Black : Color.Brown;
s.MarkerStyle = MarkerStyle.Circle;
s.MarkerSize = 4;
double totalDist = 0;
DateTime ct = start;
for (int i = 0; i < count; i++)
{
var ts = TrainStops[i];
ct = ct.AddMinutes(ts.Item2 * (speed == 0 ? 1 : 1.1d));
DataPoint dp = new DataPoint( ct.ToOADate(), totalDist );
totalDist += TrainStops[i].Item2;
s.Points.Add(dp);
}
}
请注意,由于我的数据不包含真实的 arrival/departure 次,因此我根据距离和一些速度因素计算了它们。您当然会使用您的数据!
另请注意,我使用了带有额外 Marker
圆圈的 Line
图表。
另请注意,每个火车系列都可以轻松 disabled/hidden 或再次带回。
让我们只显示快车:
private void cbx_ShowOnlyFastTrains_CheckedChanged(object sender, EventArgs e)
{
foreach (Series s in chart1.Series)
s.Enabled = !cbx_ShowOnlyFastTrains.Checked || s.Color == Color.Brown;
}
当然,对于强大的应用程序,您不会依赖神奇的颜色;-)
相反,您可以将 Tag
对象添加到 Series
以保存各种火车信息。
更新:如您所见,绘制的 GridLines
覆盖了 Markers
。您可以在此处插入这段代码 (**);它将 owner-draw Markers
在 xxxPaint
事件结束时。
int w = chart1.Series[0].MarkerSize;
foreach(Series s in chart1.Series)
foreach(DataPoint dp in s.Points)
{
int x = (int) ax.ValueToPixelPosition(dp.XValue) - w / 2;
int y = (int) ay.ValueToPixelPosition(dp.YValues[0])- w / 2;
using (SolidBrush b = new SolidBrush(dp.Color))
e.ChartGraphics.Graphics.FillEllipse(b, x, y, w, w);
}
Close-up:
我需要用 C# 构建一个图形化的火车时刻表可视化工具。实际上我必须在 C# 中重建 this perfect tool。 Marey's Trains 图表必须可缩放、可滚动,并且 printable/exportable 为带有矢量图形元素的 PDF。
你能给我一些提示吗?我应该如何开始呢?我应该使用什么样的库?
是否值得尝试使用像 OxyPlot 这样的图形库?也许它不是最好的,因为特殊的轴和不规则的网格——正如我所想的那样。你怎么看?
无论您使用哪种图表工具,一旦您需要特殊类型的显示,您总是需要添加一些额外的代码。
这里是一个使用MSChart
控件的例子。
请注意它对导出矢量格式有限制:
它可以导出为各种formats, including 3 EMF
类型;然而,只有一些应用程序可以实际使用它们。不确定您使用的 PDF 库..!
如果您不能使用 Emf
格式,您可以通过使 Chart
控件非常大,导出到 Png
然后使 Dpi
分辨率比保存后的默认屏幕分辨率大得多。将其设置为 600
或 1200dpi
应该适用于大多数 pdf 使用..
现在让我们看一个例子:
一些注意事项:
我在很多方面让我的生活变得更轻松。我只编码了一个方向,我没有把公鸡反转,所以它只能从下到上。
我没有使用真实数据,而是自己编造的。
我没有创建一个或多个类来保存站数据;相反,我使用了一个非常简单的
Tuple
.我还没有创建
DataTable
来保存火车数据。相反,我编造它们并即时将它们添加到图表中..我没有测试,但缩放和滚动应该也能正常工作..
这是保存我的电台数据的List<Tuple>
:
// station name, distance, type: 0=terminal, 1=normal, 2=main station
List<Tuple<string, double, int>> TrainStops = null;
我是这样设置图表的:
Setup24HoursAxis(chart1, DateTime.Today);
TrainStops = SetupTrainStops(17);
SetupTrainStopAxis(chart1);
for (int i = 0; i < 23 * 3; i++)
{
AddTrainStopSeries(chart1, DateTime.Today.Date.AddMinutes(i * 20),
17 - rnd.Next(4), i% 5 == 0 ? 1 : 0);
}
// this exports the image above:
chart1.SaveImage("D:\trains.png", ChartImageFormat.Png);
每 20 分钟创建一列火车,停靠 14-17 站,每 5 列火车创建一列快速火车。
以下是我调用的例程:
设置 x-axis 以保留一天的数据非常简单。
public static void Setup24HoursAxis(Chart chart, DateTime dt)
{
chart.Legends[0].Enabled = false;
Axis ax = chart.ChartAreas[0].AxisX;
ax.IntervalType = DateTimeIntervalType.Hours;
ax.Interval = 1;
ax.Minimum = dt.ToOADate();
ax.Maximum = (dt.AddHours(24)).ToOADate();
ax.LabelStyle.Format = "H:mm";
}
创建具有随机距离的站点列表也非常简单。我做了第一个和最后一个终端,每五个一个主站。
public List<Tuple<string, double, int>> SetupTrainStops(int count)
{
var stops = new List<Tuple<string, double, int>>();
Random rnd = new Random(count);
for (int i = 0; i < count; i++)
{
string n = (char)(i+(byte)'A') + "-Street";
double d = 1 + rnd.Next(3) + rnd.Next(4) + rnd.Next(5) / 10d;
if (d < 3) d = 3; // a minimum distance so the label won't touch
int t = (i == 0 | i == count-1) ? 0 : rnd.Next(5)==0 ? 2 : 1;
var ts = new Tuple<string, double, int>(n, d, t);
stops.Add(ts);
}
return stops;
}
现在我们有了火车停靠站,我们可以设置 y-axis:
public void SetupTrainStopAxis(Chart chart)
{
Axis ay = chart.ChartAreas[0].AxisY;
ay.LabelStyle.Font = new Font("Consolas", 8f);
double totalDist = 0;
for (int i = 0; i < TrainStops.Count; i++)
{
CustomLabel cl = new CustomLabel();
cl.Text = TrainStops[i].Item1;
cl.FromPosition = totalDist - 0.1d;
cl.ToPosition = totalDist + 0.1d;
totalDist += TrainStops[i].Item2;
cl.ForeColor = TrainStops[i].Item3 == 1 ? Color.DimGray : Color.Black;
ay.CustomLabels.Add(cl);
}
ay.Minimum = 0;
ay.Maximum = totalDist;
ay.MajorGrid.Enabled = false;
ay.MajorTickMark.Enabled = false;
}
这里需要注意几点:
- 由于值非常动态,我们不能使用固定
Interval
间距的正常Labels
。 - 所以我们改为创建
CustomLabels
。 - 对于这些,我们需要两个值来确定 space 它们应居中的位置。所以我们通过 adding/subtracting
0.1d
. 创建一个小跨度
- 我们已经计算出总距离,并用它来设置y-axis的
Maximum
。再说一次:要模仿你展示的时间表,你必须在这里和那里做一些倒车.. - 通过添加
CustomLabels
,正常的自动关闭。由于我们在 不规则 间隔需要MajorGridlines
,因此我们也关闭了正常间隔。因此,我们必须自己绘制它们。并不像你看到的那么难..:[=129=]
为此,我们编写了 xxxPaint
事件之一:
private void chart1_PostPaint(object sender, ChartPaintEventArgs e)
{
Axis ay = chart1.ChartAreas[0].AxisY;
Axis ax = chart1.ChartAreas[0].AxisX;
int x0 = (int) ax.ValueToPixelPosition(ax.Minimum);
int x1 = (int) ax.ValueToPixelPosition(ax.Maximum);
double totalDist = 0;
foreach (var ts in TrainStops)
{
int y = (int)ay.ValueToPixelPosition(totalDist);
totalDist += ts.Item2;
using (Pen p = new Pen(ts.Item3 == 1 ? Color.DarkGray : Color.Black,
ts.Item3 == 1 ? 0.5f : 1f))
e.ChartGraphics.Graphics.DrawLine(p, x0 + 1, y, x1, y);
}
// ** Insert marker drawing code (from update below) here !
}
注意轴的ValueToPixelPosition
转换函数的使用!
现在是最后一部分:如何添加 Series
训练数据..:[=51=]
public void AddTrainStopSeries(Chart chart, DateTime start, int count, int speed)
{
Series s = chart.Series.Add(start.ToShortTimeString());
s.ChartType = SeriesChartType.Line;
s.Color = speed == 0 ? Color.Black : Color.Brown;
s.MarkerStyle = MarkerStyle.Circle;
s.MarkerSize = 4;
double totalDist = 0;
DateTime ct = start;
for (int i = 0; i < count; i++)
{
var ts = TrainStops[i];
ct = ct.AddMinutes(ts.Item2 * (speed == 0 ? 1 : 1.1d));
DataPoint dp = new DataPoint( ct.ToOADate(), totalDist );
totalDist += TrainStops[i].Item2;
s.Points.Add(dp);
}
}
请注意,由于我的数据不包含真实的 arrival/departure 次,因此我根据距离和一些速度因素计算了它们。您当然会使用您的数据!
另请注意,我使用了带有额外 Marker
圆圈的 Line
图表。
另请注意,每个火车系列都可以轻松 disabled/hidden 或再次带回。
让我们只显示快车:
private void cbx_ShowOnlyFastTrains_CheckedChanged(object sender, EventArgs e)
{
foreach (Series s in chart1.Series)
s.Enabled = !cbx_ShowOnlyFastTrains.Checked || s.Color == Color.Brown;
}
当然,对于强大的应用程序,您不会依赖神奇的颜色;-)
相反,您可以将 Tag
对象添加到 Series
以保存各种火车信息。
更新:如您所见,绘制的 GridLines
覆盖了 Markers
。您可以在此处插入这段代码 (**);它将 owner-draw Markers
在 xxxPaint
事件结束时。
int w = chart1.Series[0].MarkerSize;
foreach(Series s in chart1.Series)
foreach(DataPoint dp in s.Points)
{
int x = (int) ax.ValueToPixelPosition(dp.XValue) - w / 2;
int y = (int) ay.ValueToPixelPosition(dp.YValues[0])- w / 2;
using (SolidBrush b = new SolidBrush(dp.Color))
e.ChartGraphics.Graphics.FillEllipse(b, x, y, w, w);
}
Close-up: