C#中可缩放、可打印、可滚动的火车运动图表

3
我需要用C#构建一个图形化列车时刻表可视化工具。实际上,我必须在C#中重新构建这个完美的工具Marey's Trains 这些图表必须支持缩放、滚动和打印/导出为PDF格式,并带有矢量图形元素。
你能给我一些提示吗?我应该如何开始?我应该使用哪种库?
尝试使用像OxyPlot这样的图形库是否值得一试?也许它不是最好的选择,因为它需要特殊的轴和不规则网格 - 我认为如此。你的意见是什么?

example


MSChart只能导出一些EMF矢量格式(和png),而且需要使用自定义标签和所有者绘制网格来处理垂直网格。除此之外,我认为它可以完成工作。 - TaW
1个回答

1

无论使用哪种图表工具,一旦需要特殊类型的显示,您都将不得不添加一些额外的编码。

这是使用MSChart控件的示例。

请注意它在导出矢量格式方面存在限制:

它可以导出各种格式,包括3个 EMF类型;然而只有一些应用程序能够实际使用它们。关于您使用的PDF库不确定..!

如果您无法使用Emf格式,可以通过使Chart控件变得非常大,导出为Png,然后将Dpi分辨率设置得比保存后的默认屏幕分辨率更高来获得良好的结果。将其设置为6001200dpi对于大多数pdf使用应该足够。

Now lets look at an example:

enter image description here

一些注释:
  • 我在很多方面都让我的生活更加轻松。我只编写了一个方向的代码,也没有倒转公鸡,所以它只能从底部到顶部。

  • 我没有使用真实数据,而是虚构了它们。

  • 我没有创建一个或多个类来保存车站数据;相反,我使用了一个非常简单的 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列火车中有1列快速列车。
以下是我调用的例程:
设置x轴以保持一天的数据很简单。
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";
}

创建一个带有随机距离的车站列表也非常简单。我将第一个和最后一个设为终点站,每5个车站设置一个主要车站。
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轴:

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;
}

这里需要注意以下几点:
  • 由于数值相当动态,我们无法使用普通的Labels,因为它们带有固定的Interval间隔。
  • 因此,我们创建了CustomLabels
  • 对于这些标签,我们需要两个值来确定它们居中的空间。所以我们通过加/减0.1d来创建一个小跨度。
  • 我们已经计算出总距离,并将其用于设置y轴的Maximum。同样:为了模仿你展示的时间表,你需要在这里和那里做一些反转..
  • 通过添加CustomLabels,正常的标签会自动关闭。由于我们需要在不规则的间隔上显示MajorGridlines,因此我们还关闭了正常的网格线。因此,我们必须自己绘制它们。正如你所看到的那样,这并不难..:

为此,我们编写其中一个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
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);
    }
}

请注意,由于我的数据中不包含真实的到达/出发时间,我从距离和一些速度因素计算了它们。当然,你会使用你自己的数据!
还要注意,我使用了一个带有额外标记圆圈的线形图。
此外,请注意每个列车系列都可以轻松地被禁用/隐藏或重新显示。
让我们只显示快速列车:
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。您可以在这里插入此代码段(**);它将在xxxPaint事件的末尾拥有自己的Markers
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);
}

特写:

enter image description here


哇,亲爱的TaW,非常感谢您提供如此详细的解决方案和解释!这比我期望的要多得多。您为我解决了最重要的问题:不规则的Y轴和网格,现在它可以正常工作了! 与此同时,我在OxyPlot的示例库中找到了一个尝试解决此问题的案例(PlotModel TrainSchedule),但它仅包含一个常规的Y轴,没有站名。 MSChart似乎已经足够,并且文档也很好。太棒了!再次感谢,我稍后会发布结果。 - unikumi
以这种方式,即使在PrePaint或PostPaint绘制标记(此处有截图),后绘制的网格线也会出现在标记前面。是否有一种方法将它们发送到标记后面?我应该将它们移动到BackImage还是有更简单的方法? - unikumi
啊,有趣!背景图并不是一个好主意,毕竟图表需要是动态的。我已经添加了绘制标记的代码。 - TaW
谢谢更新!:) 好的,问题也出现在火车经过一个站台的情况下(此处有截图,在 D-Street 上例子):这种情况下,线被网格线覆盖了。所以我应该重新绘制靠近网格线的所有图表块(使用您为标记编写的代码扩展)。 - unikumi
嗯,不确定你指的是哪一部分。是那些略微跨越y轴的部分吗?第二张截图并不是这里的样子。你是否在正确的位置添加了代码,即在绘制网格线之后? - TaW
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接