我需要在C#中构建一个图形化的火车时刻表可视化工具。实际上,我必须在C#中重建this perfect tool
Marey's Trains
图形必须是可缩放的,可滚动的,可打印的/可导出到带有矢量图形元素的PDF。

你能给我一些提示吗?我应该如何开始?我应该使用哪种类型的库?

是否值得尝试使用OxyPlot之类的图形库?也许不是最好的,因为特殊的轴和不规则的网格-我认为。你怎么看?

c# - C#中的可缩放,可打印,可滚动的火车运动图-LMLPHP

最佳答案

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

这是使用MSChart控件的示例。

请注意,它在导出矢量格式方面有局限性:

它可以导出到各种formats,包括3种EMF类型。但是,只有某些应用程序可以实际使用它们。不确定所使用的PDF库。

如果不能使用Emf格式,可以通过将Chart控件变大,导出到Png,然后将Dpi分辨率提高为保存后的默认屏幕分辨率来获得不错的结果。 ..将其设置为6001200dpi应该适用于大多数pdf使用。

现在来看一个例子:

c# - C#中的可缩放,可打印,可滚动的火车运动图-LMLPHP

一些注意事项:


我通过多种方式使我的生活更轻松。我只编码了一个方向,也没有反转雄鸡,所以它只从下到上。
我没有使用真实的数据,但是将它们整理了。
我尚未创建一个或多个用于保存站点数据的类;相反,我使用了一个非常简单的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轴以保存一天的数据非常简单。

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


请注意,由于我的数据不包含实际的到达/离开时间,因此我根据距离和某些速度因数进行了计算。您当然会使用您的数据!

另请注意,我在Line图表中使用了额外的Marker圆圈。

还要注意,每个火车系列都可以轻松地被禁用/隐藏或重新带回。

让我们只显示快速火车:

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。您可以在此处插入这段代码(**);它会在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);
}


特写:

c# - C#中的可缩放,可打印,可滚动的火车运动图-LMLPHP

关于c# - C#中的可缩放,可打印,可滚动的火车运动图,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/43441412/

10-12 15:50