用DotSpatial下载谷歌瓦片图并展示到地图控件上

上一篇文章讲解如何加载各地图的WMS地图服务。虽然不涉及到瓦片,但是每次地图刷新都要请求网络,造成不小的网络负载。虽然判断视野是否改变确定是否请求网络来减小网络负载,但是这个方法仍然不理想。

谷歌的地图底图自带高程视觉,公路分级样式、行政区域分级样式、地图数据即时的更新速度等等优点,让人觉得有必要开发一个地图下载器。虽然谷歌本身被墙,但是谷歌地图还是可以访问的。地址如下:

http://www.google.cn/maps(可以手动输入:http://maps.google.cn

一、新建WinForm项目

谷歌已经关闭了开发者API,现在只能自己动手做一个了。下面仍然新建一个WinForm程序,增加对DotSpatial的引用,加入DotSpatial控件,代码如下:

using DotSpatial.Controls;
using DotSpatial.Data;
using DotSpatial.Projections;
using DotSpatial.Topology;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace GoogleWmts
{
    public partial class MainForm : Form
    {
        private Map mapCtrl;
        private ProjectionInfo currentProjection;
        public const double WUHAN_WGS84_COORDINATE_Y = 30.883124;
        public const double WUHAN_WGS84_COORDINATE_X = 114.419915;
        private Coordinate wuhanCoordinate;
        public Size lastSize;
        public MainForm()
        {
            mapCtrl = new Map()
            {
                Left = 0,
                Top = 0,
                Size = new Size(0, 0),
                Dock = DockStyle.Fill,
                FunctionMode = FunctionMode.Pan
            };
            InitProjection();
            InitializeComponent();
            Controls.Add(mapCtrl);
        }

        private void InitProjection()
        {
            currentProjection = ProjectionInfo.FromEpsgCode(2432);
            var xy = new double[2] { WUHAN_WGS84_COORDINATE_X, WUHAN_WGS84_COORDINATE_Y };
            var z = new double[1];
            Reproject.ReprojectPoints(xy, z, KnownCoordinateSystems.Geographic.World.WGS1984, currentProjection, 0, 1);
            wuhanCoordinate = new Coordinate(xy);
        }
    }
}

很遗憾的是DotSpatial内建的坐标系统并不支持Google的900913坐标系,这里我使用Beijing1954坐标系,中央经线是105度,EPSG的CRSID是2432,预定义武汉的经纬度,用于设定地图初始化视野。currentProjection定义当前地图控件使用的坐标系。天地图、OSM地图、腾讯地图、谷歌的地图都是基于分辨率设定视野,因此新建一个ResolutionLayer类型的图层,以便通用,代码如下:

using DotSpatial.Controls;
using DotSpatial.Data;
using DotSpatial.Projections;
using DotSpatial.Symbology;
using DotSpatial.Topology;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace GoogleWmts
{
    class ResolutionLayer : Layer, IMapLayer
    {
        public const double GOOGLE_ORIGIN_X = -20037508.3427892;
        public const double GOOGLE_ORIGIN_Y = 20037508.3427892;
        public const int TILE_WIDTH = 256;
        public const int TILE_HEIGHT = 256;
        public const int SCREEN_MILLIMETER_WIDTH = 564;
        public const int SCREEN_PIXEL_WIDTH = 1600;
        public const int MAX_ZOOM_LEVEL = 18;
        private Dictionary<int, double> resolutions;
        string urlFormat = "http://mt2.google.cn/vt/[email protected]&hl=zh-CN&gl=cn&x={0}&y={1}&z={2}&s=Galil";//X瓦图号,Y瓦片号,比例尺缩放层级
        private Extent defaultExtent;
        public int ZoomLevel { get; set; }
        public Size WindowSize { get; set; }
        public bool WindowCreated { get; set; }
        public bool IsBusy { get; set; }

        public ResolutionLayer()
        {
            defaultExtent = new Extent(-2000000000, -2000000000, 2000000000, 2000000000);
            InitDirectory();
            SetResolutionsByMath();
        }

        private void InitDirectory()
        {
            var rpt = @"Tiles\Google\";
            if (!Directory.Exists(rpt))
                Directory.CreateDirectory(rpt);
            for (var i = 0; i <= MAX_ZOOM_LEVEL; i++)
            {
                var rp = rpt + @"\" + i;
                if (!Directory.Exists(rp))
                    Directory.CreateDirectory(rp);
            }
        }

        public override Extent Extent
        {
            get
            {
                return defaultExtent;
            }
        }
    }
}

GOOGLE_ORIGIN_X与GOOGLE_ORIGIN_Y两个常量记录谷歌地图的坐标原点,TILE_WIDTH与TILE_HEIGHT两个常量记录单个瓦片图文件的像素大小。SCREEN_MILLIMETER_WIDTH常量记录当前显示屏幕的物理大小。我当前的显示屏是19吋,物理大小是564毫米,请朋友在使用之前务必改成您自己的显示屏幕的物理大小。SCREEN_PIXEL_WIDTH常量记录的是当前显示屏的素大小。如果您不知道您当前显示设备的像素大小,请查看显示屏属性,找到当前设置的分辨率。我当前的显示屏幕是1600像素的宽度,请朋友在使用之前务必改成你自己的屏幕的分辨率宽度。这里提供一个简单的方法获取屏幕物理大小与像素大小,代码如下:

        [DllImport("gdi32.dll", EntryPoint = "GetDeviceCaps", CallingConvention = CallingConvention.Winapi)]
        public static extern int GetDeviceCaps(IntPtr hdc, int code);
        public const int HORZSIZE = 4;
        var g = CreateGraphics();
        var millimeterLength = NativeAPI.GetDeviceCaps(g.GetHdc(), NativeAPI.HORZSIZE);
        var pixelLength = Screen.PrimaryScreen.Bounds.Width;
        g.Dispose();

MAX_ZOOM_LEVEL是最大缩放级别,也就是街道级别。resolutions是各层级比例尺下的分辨率。defaultExtent是给DotSpatial计算图层最大视野用的。此变量必须给,否则看不到地图。这与DotSpatial计算视野,确定窗口更新区域的算法有关系。放在这里吧。ZoomLevel 是当前缩放级别,WindowSize记录窗体的实际大小,WindowCreated指示窗口是否已经创建成功。IsBusy指示图层当前是否正在下载瓦片图。如果正在下载中,那么不响应用户放大、缩小、移动等地图操作。InitDirectory方法设定瓦片的存储路径,组织方式是在当前软件的文件夹下新建一个Tiles文件夹,再新建一个Google文件夹,然然针对每一个比例尺新建文件夹,瓦片图文件名称以瓦片索引命名。

谷歌地图的分辨率可能通过计算的方法获取,代码如下:

        private void SetResolutionsByMath()
        {
            resolutions = new Dictionary<int, double>();
            for (var i = 1; i <= MAX_ZOOM_LEVEL; i++)
                resolutions.Add(i, 20037508.3427892 * 2 / 256 / Math.Pow(2, i));
        }

二、坐标转换

上面说过,DotSpatial不支持Google的900913坐标系,那么必须进行坐标转换。在这里我使用Proj.4 C++库,并封装一个Win32动态库给C#调用,C++的调用Proj.4的代码如下:

typedef __declspec(dllexport) struct _COORDINATE
{
	double x;
	double y;
	double z;
	double m;
	int srid;
}COORDINATE, *PCOORDINATE;
BRIDGE_API BOOL proj4_transform(PCSTR proj4_from, PCSTR proj4_to, COORDINATE* coordinate)
{
	if (proj4_from == nullptr || strlen(proj4_from) < 5)
		return FALSE;
	if (proj4_to == nullptr || strlen(proj4_to) < 5)
		return FALSE;
	projPJ from = pj_init_plus(proj4_from);
	projPJ to = pj_init_plus(proj4_to);
	if (from == nullptr || to == nullptr)
		return FALSE;
	int code = pj_transform(from, to, 1, 1, &coordinate->x, &coordinate->y, &coordinate->z);
	return !code;
}

C#调用代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace GK.Collector.Server.Entity.DllImports
{
    [StructLayout(LayoutKind.Explicit)]
    public struct COORDINATE
    {
        [FieldOffset(0)]
        public double x;
        [FieldOffset(8)]
        public double y;
        [FieldOffset(16)]
        public double z;
        [FieldOffset(24)]
        public double m;
        [FieldOffset(32)]
        int srid;
    }
}
        [DllImport("bridge.dll", EntryPoint = "proj4_transform", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
        public static extern bool Proj4Transform(string proj4From, string projTo, IntPtr coordinate);
        public static bool Transform(double[] xyz, string proj4From, string proj4To = "")
        {
            if (xyz.Length < 3)
                return false;
            if (string.IsNullOrWhiteSpace(proj4From))
                return false;
            if (string.IsNullOrWhiteSpace(proj4To))
                proj4To = " +proj=longlat +datum=WGS84 +no_defs";
            var c = new COORDINATE() { x = xyz[0], y = xyz[1], z = xyz[2] };
            var ptr = Marshal.AllocHGlobal(Marshal.SizeOf(c));
            Marshal.StructureToPtr(c, ptr, true);
            Proj4Transform(proj4From, proj4To, ptr);
            c = (COORDINATE)Marshal.PtrToStructure(ptr, typeof(COORDINATE));
            Marshal.FreeHGlobal(ptr);
            xyz[0] = c.x;
            xyz[1] = c.y;
            xyz[2] = c.z;
            return true;
        }

把常用的坐标系设定为字符串常量,以方便使用,代码如下:

        public const string BJ2432_PROJ = "+proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3,0,0,0,0 +units=m +no_defs ";
        public const string WORLD3857_PROJ = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m [email protected] +wktext  +no_defs";
        public const string GOOGLE_PROJ = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m [email protected] +no_defs ";
        public const string WGS84_PROJ = "+proj=longlat +datum=WGS84 +no_defs ";

三、计算瓦片索引并下载

瓦片索引的计算是重中之重。其中包括瓦片对齐到用户窗口,避免地图移位、拖动不顺畅的问题,代码如下:

        /// <summary>
        /// 获取瓦片图索引以及偏移
        /// </summary>
        /// <param name="cxy">原始坐标</param>
        /// <param name="txy">瓦片图索引</param>
        /// <param name="rxy">偏移</param>
        private void GetTileIndexByCoordinate(double[] cxy, int[] txy, double[] rxy)
        {
            var z = new double[1];
            var coef = resolutions[ZoomLevel] * TILE_WIDTH;
            var wgs84Proj = KnownCoordinateSystems.Geographic.World.WGS1984;
            //DotSpatial.Projections.Reproject.ReprojectPoints(cxy, z, Projection, wgs84Proj, 0, 1);
            //cxy[0] = cxy[0] * 20037508.3427892 / 180;
            //cxy[1] = Math.Log(Math.Tan((90 + cxy[1]) * Math.PI / 360)) / (Math.PI / 180);
            //cxy[1] = cxy[1] * 20037508.3427892 / 180;
            Transform(cxy, BJ2432_PROJ, GOOGLE_PROJ);
            txy[0] = (int)((cxy[0] - GOOGLE_ORIGIN_X) / coef);
            txy[1] = (int)((GOOGLE_ORIGIN_Y - cxy[1]) / coef);
            rxy[0] = (cxy[0] - GOOGLE_ORIGIN_X) / coef - txy[0];
            rxy[1] = (GOOGLE_ORIGIN_Y - cxy[1]) / coef - txy[1];
        }

得到瓦片索引就可以下载了。用WebClient直接下载发现被谷歌屏蔽,通过Fiddler抓包工具发现可以顺利通过谷歌验证的HTTP包,代码如下:

       private Image GetImageByWebClient(double tilex, double tiley)
        {
            var rp = @"Tiles\Google\" + ZoomLevel + @"\" + tilex + "_" + tiley + ".png";
            if (File.Exists(rp))
            {
                var tb = Image.FromFile(rp);
                //Console.WriteLine(rp);
                return tb;
            }
            else
            {
                string url = string.Format(urlFormat, tilex, tiley, ZoomLevel);
                //Console.WriteLine(url);
                var downloader = new WebClient();
                downloader.Headers.Add("Upgrade-Insecure-Requests: 1");
                downloader.Headers.Add("User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36");
                downloader.Headers.Add("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
                downloader.Headers.Add("Accept-Encoding: gzip, deflate, sdch");
                downloader.Headers.Add("Accept-Language: zh-CN,zh;q=0.8");
                try
                {
                    var bts = downloader.DownloadData(url);
                    var str = new MemoryStream(bts);
                    var img = Image.FromStream(str);
                    img.Save(rp);
                    str.Close();
                    str.Close();
                    return img;
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.Message);
                }
                downloader.Dispose();
            }
            return null;
        }

四、拼接瓦片图

拼接瓦片图的核心思想是找到左下角坐标对应的瓦片图索引与右上角坐标的瓦片图索引,通过一个循环就可以找到所有需要的瓦片图,并与文件系统已经存储的瓦片图缓存比对,缺少的就下载,已经有的就读取本地文件,代码如下:

        public void DrawRegions(MapArgs args, List<DotSpatial.Data.Extent> regions)
        {
            if (!WindowCreated || WindowSize.Width <= 160 || WindowSize.Height <= 30)
                return;
            IsBusy = true;
            var img = new Bitmap(args.ImageRectangle.Width, args.ImageRectangle.Height);
            var g = Graphics.FromImage(img);
            var resolution = resolutions[ZoomLevel];
            var toffset = new double[2];
            foreach (var region in regions)
            {
                var leftxy = new double[3] { region.MinX, region.MinY, 0 };
                var txy = new int[2];
                var rxy = new double[2];
                toffset[0] = -1;
                toffset[1] = -1;
                GetTileIndexByCoordinate(leftxy, txy, rxy);
                for (var i = region.MinX; i < region.MaxX; i += resolution * TILE_WIDTH)
                {
                    for (var j = region.MinY; j < region.MaxY ; j += resolution * TILE_HEIGHT)
                    {
                        var tb = GetImageByWebClient(txy[0] + toffset[0], txy[1] + toffset[1]);
                        var tx = Convert.ToInt32((toffset[0] - rxy[0]) * TILE_WIDTH);
                        var ty = Convert.ToInt32((toffset[1] - rxy[1]) * TILE_HEIGHT);
                        g.DrawImage(tb, tx, ty);
                        toffset[1]++;
                    }
                    toffset[0]++;
                    toffset[1] = 0;
                }
            }
            args.Device.DrawImage(img, 0, 0);
            g.Dispose();
            img.Dispose();
            IsBusy = false;
        }

下面两个方法用来计算有效的视野,给地图初始化之用,代码如下:

        public Extent GetAvailableExtent(Coordinate center, Size rc)
        {
            var ext = new Extent();
            var horizontal = resolutions[ZoomLevel] * rc.Width;
            var vertical = resolutions[ZoomLevel] * rc.Height;
            ext.MinX = center.X - horizontal / 2;
            ext.MinY = center.Y - vertical / 2;
            ext.MaxX = center.X + horizontal / 2;
            ext.MaxY = center.Y + vertical / 2;
            return ext;
        }

        public void GetDistance(double[] xy)
        {
            var resolution = resolutions[ZoomLevel];
            xy[0] = xy[0] * resolution;
            xy[1] = xy[1] * resolution;
        }

至此瓦片图图层完成。

五、瓦片图地图函数

DotSpatial地图控件默认没有比例尺,也就是自由比例尺,可以无限制的缩放。而在线地图只有18个缩放级别,如果不用地图函数限制DotSpatial地图控件的行为,就会导致地图移位。代码如下:

using DotSpatial.Controls;
using DotSpatial.Topology;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GoogleWmts
{
    class TileMapFunction : MapFunction
    {
        private int zoomLevel = 11;
        private System.Drawing.Point firstPoint;
        private System.Drawing.Point lastPoint;
        private ResolutionLayer layer;
        public TileMapFunction(IMap mapCtrl, ResolutionLayer layer) :
            base(mapCtrl)
        {
            this.layer = layer;
        }

        protected override void OnMouseDown(GeoMouseArgs e)
        {
            firstPoint = e.Location;
            base.OnMouseDown(e);
        }

        protected override void OnMouseUp(GeoMouseArgs e)
        {
            lastPoint = e.Location;
            //var offset = new double[2] { firstPoint.X - lastPoint.X, firstPoint.Y - lastPoint.Y };
            //layer.GetDistance(offset);
            //Map.ViewExtents.SetCenter(new Coordinate(Map.ViewExtents.Center.X + offset[0], Map.ViewExtents.Center.Y + offset[1]));
            base.OnMouseUp(e);
        }

        protected override void OnMouseWheel(GeoMouseArgs e)
        {
            e.Handled = true;
            if (layer.IsBusy)
                return;
            if (e.Delta > 0)
                zoomLevel++;
            else
                zoomLevel--;
            if (zoomLevel < 0)
                zoomLevel = 0;
            if (zoomLevel > ResolutionLayer.MAX_ZOOM_LEVEL)
                zoomLevel = ResolutionLayer.MAX_ZOOM_LEVEL;
            layer.ZoomLevel = zoomLevel;
            Console.WriteLine("中心点:" + Map.ViewExtents.Center.X + "," + Map.ViewExtents.Center.Y);
            //Map.ViewExtents = layer.GetAvailableExtent(Map.ViewExtents.Center, Map.ClientRectangle.Size);
            base.OnMouseWheel(e);
        }

        protected override void OnMouseMove(GeoMouseArgs e)
        {
            e.Handled = true;
            base.OnMouseMove(e);
        }
    }
}

六、完善WinForm窗口事件

首先声明瓦片图图层与地图函数对象,加入到地图控件,代码如下:

            private ResolutionLayer layer;
            private TileMapFunction func;
            layer = new ResolutionLayer()
            {
                Projection = currentProjection,
                WindowSize = mapCtrl.Size,
                ZoomLevel = 10
            };
            func = new TileMapFunction(mapCtrl, layer);
            mapCtrl.Layers.Add(layer);
            mapCtrl.Projection = currentProjection;
            mapCtrl.MapFunctions.Add(func);
            mapCtrl.ActivateMapFunction(func);

在窗口完成加载时初始化地图视野,设置图层记录的窗口大小以通知图层准备绘图。

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            layer.WindowCreated = true;
            mapCtrl.ViewExtents = layer.GetAvailableExtent(wuhanCoordinate, layer.WindowSize);
        }

处理窗品大小改变事件,使地图始终铺满窗口,代码如下:

        protected override void OnSizeChanged(EventArgs e)
        {
            base.OnSizeChanged(e);
            if(lastSize!=Size)
            {
                lastSize = Size;
                if (WindowState != FormWindowState.Minimized && mapCtrl.Width <= 0)
                {
                    layer.WindowSize = this.Size;
                    mapCtrl.Size = this.Size;
                    Console.WriteLine("中心点:" + mapCtrl.ViewExtents.Center.X + "," + mapCtrl.ViewExtents.Center.Y);

                }
            }
        }

七、最终效果

时间: 2024-07-31 08:17:00

用DotSpatial下载谷歌瓦片图并展示到地图控件上的相关文章

my97DatePicker日期控件——日期输入框联动,使用focus使第二个输入框没展示出日期控件

描述问题场景: 1.jquery使用的版本是jquery-1.7.2.min.js 2.代码不是写在页面上的,是通过事件后追加的 1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <title></title> 6 <script src="js/jquery-1.7.2.min.js" type="te

基于MySQL + Node.js + Leaflet的离线地图展示,支持百度、谷歌、高德、腾讯地图

1. 基本说明 本项目实现了离线展示百度.谷歌.高德.腾讯地图.主要功能如下: 实现了地图瓦片图下载.存储.目前支持存储至MySQL Node.js服务调用MySQL中的瓦片图 Leaflet展示地图 展示效果如下: 2. 地图瓦片下载工具及配置 工具下载链接: http://pan.baidu.com/s/1qYoHj4K 密码: ehgh 工具使用方法: 数据库配置 打开工具目录中的 MapDownloader.exe.config 文件.根据实际情况填写如图中的配置信息. 地图瓦片图下载

High-speed Charting Control--MFC绘制图表(折线图、饼图、柱形图)控件

原文地址:https://www.codeproject.com/articles/14075/high-speed-charting-control 本文翻译在CodeProject上的介绍(主要还是谷歌翻译,看不太明白的地方,请对比原文,敬请原谅),方便自己和后面人的学习(花费了两天时间,希望是值得的).推荐一个前辈写的东西:TeeChart替代品,MFC下好用的高速绘图控件-(Hight-Speed Charting),自己也转载了这篇文章,在转载的文章中根据自己的实验修改了一些东西,修改

谷歌约束控件(ConstraintLayout)扁平化布局入门

序 在Google IO大会中不仅仅带来了Android Studio 2.2预览版,同时带给我们一个依赖约束的库. 简单来说,她是相对布局的升级版本,但是区别与相对布局更加强调约束.何为约束,即控件之间的关系. 她能让你的布局更加扁平化,一般来说一个界面一层就够了:同时借助于AS我们能极其简单的完成界面布局. 准备 1.准备好Android Studio 2.2预览版,在这里给大家准备好了下载链接: https://dl.google.com/dl/android/studio/ide-zip

将展示内容(div、iframe)放在Expand控件中

Expand是ArcGIS JavaScript API 4.3推出的一个widget(控件),用于承载一个HTML DOM元素,可以把一个自己编写的div或者是一个其他的Esri widget控件放到Expand里面.Expand在地图上显示为一个小方块按钮,点击可以展开或关闭它所承载的内容.关于Expand控件的详细内容,请查看:https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Expand.

Gantt Chart Library项目管理控件下载及介绍

Gantt Chart Library是一款专业的项目管理控件,包含了两个与甘特图有关的Windows客户端控件:GanttChartView, ResourceLoadChartView,可以用于Windows应用程序,支持数据绑定,标准的外观和操作自定义设置,拖拉操作本地化.主题风格以及打印支持. 具体功能: 在界面.操作.开发上都和标准的DataGridView相似 支持在表格视图界面里显示自定义列 支持在表格视图里使某一列作为树型结构显示 支持自定义甘特图标尺比例(如预先确定的时间比例:

Qt实现九宫图类控件

<1>. 头文件(类声明)class CPreviewWidge : public QWidget{    Q_OBJECTpublic:     CPreviewWidge( const QString &strPath, QWidget *parent = 0);     ~CPreviewWidge(); QSize sizeHint()const; protected:     void paintEvent(QPaintEvent *e); //九宫图重绘函数 private

界面控件Essential Studio for ASP.NET正式发布2015 v3[附下载]

Essential Studio for ASP.NET界面控件包含了商业Web应用程序开发中所需的所有控件,如grids.charts.gauges.menus.calendars.editors等等.同时,Essential Studio for ASP.NET中高性能的界面控件库还允许您的应用程序浏览和创建Excel.Word和PDF格式的文件. 免费下载:Essential Studio for ASP.NET 2015 v3 此次Essential Studio v3更新为Syncfu

KS Gantt甘特图控件通过递归加载无限层级的数据

从服务器拉下来的数据,是反序列后的对象数据,通过id和parentid可以组织成对象树,然后将对象树绑定到甘特图控件上. public class KSGanttHelper { #region 往界面添加项 public static void FillDataToGanttControl(List<ConstructionTaskItemTree> taskItemTreeList, Gantt gantt) { gantt.SuspendItemLayout(); taskItemTre