新手第一次写博客,写的不好见谅
这里给出工程完成情况
工作进度:
1.使用C#Winfrom完成了SHP文件的读取并且绘制成地图
2.同时读取相应的DBF文件,以表格的方式展现在DataGridView控件上
3.在绘制出的地图上相应位置标出了OBJECTID
4.完成了放大和缩小的功能。
存在问题:
1.进度中的3中完成的地图标记因为阀门.shp和中压天然气管.shp文件读取的记录比较多导致标记非常多,暂时还没有完成分块显示,只有营业所范围.shp比较理想
2.每次缩放都会重新绘制一次地图,比较费时间
3.没有读取SHX文件
4.在缩放的时候标记(进度中的3)不会自动更新,必须点一次刷新按钮
5.必须逐个打开SHP文件,暂未实现多个文件同时打开的功能
首先要明白文件格式,找了很多这俩个讲的比较清楚,尤其是这个.dbf文件格式普遍讲的都不是很清楚,这位大佬讲的比较好
.shp文件格式讲解
.dbf文件格式讲解
这里先展示效果图
先添加几个控件
— —SplitContainer控件x2(左右分割,左边再加一个分割上下)
— —panel容器x2(用作画板,这里之所以添加两个,是用来实现缩放功能的,不知道怎么做只能曲线救国了,在前面的panel1上画,panel2在底下,注意一定要把panel2的AutoScroll和AutoSize属性设置为true)
— —Button控件x5
— —Label控件x1(显示缩放倍率)
— —DataGridView控件x1(显示表格)
下面就要开始写代码了
先把点线面写成类方便管理
class Point
{
public double X;
public double Y;
}
class Polyline
{
public double[] Box;
public int NumParts;
public int NumPoints;
public List<int> Parts;
public List<Point> Points;
public Polyline()
{
Box = new double[4];
Parts = new List<int>();
Points = new List<Point>();
}
}
class Polygon:Polyline
{//这里不需要添加新的变量,只需要继承线类就行了
}
class Table//这个是为了读取.dbf文件并显示为表格而定义的
{
public DataTable dt;
public List<short> columnsLength;
public List<string> columnsName;
public List<string> ID;
public int rowCount, columnsCount;
public Table()
{
dt = new DataTable();
columnsLength = new List<short>();
columnsName = new List<string>();
ID = new List<string>();
}
}
放心只有这么一个长段的代码
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;
using System.Collections;
using System.IO;
using System.Threading;
namespace ReadShapeFile_test
{
public partial class ReadSHPFileForm : Form
{//每个.shp文件都只有一个类型
List<Polygon> polygons;//如果读取文件类型是Polygon则数据储存在这里
List<Polyline> polylines;//Polyline类型
List<Point> points;//Point类型
Pen bPen, rPen, blPen;//绘图用的Pen
SolidBrush solidBrush;//填充面用的刷子
Table table;//实例table类
OpenFileDialog openFileDialog1, openFileDialog2;//打开文件窗口
int ShapeType;//记录当前.shp文件的类型
double xmin, ymin, xmax, ymax;//xy方向上的最大最小值
double n1, n2, n;//n1和n2是根据屏幕的缩放比例,n是使用放大缩小功能时的
double originWidth;//记录原始屏幕宽度
private void ReadSHPFileForm_Load(object sender, EventArgs e)
{//初始化
polygons = new List<Polygon>();
polylines = new List<Polyline>();
points = new List<Point>();
bPen = new Pen(Color.Black, 1);
rPen = new Pen(Color.Red, 1);
blPen = new Pen(Color.Blue, 1);
solidBrush = new SolidBrush(Color.Yellow);
table = new Table();
originWidth = this.DrawingBoard.Width;
xmin = ymin = xmax = ymax = 0;
n = 1;
}
private void OpenSHPFileButton_Click(object sender, EventArgs e)
{
openFileDialog1 = new OpenFileDialog();
openFileDialog1.Filter = "shapefiles(*.shp)|*.shp|All files(*.*)|*.*";//筛选出我们所需要的文件类型
openFileDialog1.FilterIndex = 1;//选择上面第一个类型为默认类型
if (openFileDialog1.ShowDialog() == DialogResult.OK)//窗口点击确认则为true
{//打开的文件是根据openFileDialog1.FileName这个属性记录的文件路径所确认的
ReadSHPFile(openFileDialog1);//调用下面自定义的读取.shp文件函数
char[] path = openFileDialog1.FileName.ToCharArray();//记录文件路径
if (path.Length != 0)//下面这一套操作是为了实现在读取.shp文件的时候同时读取同名的.dbf文件
{//因为是同名又在一个路径下,所以只需要更改文件类型就可以了
path[path.Length - 1] = 'f';
path[path.Length - 2] = 'b';
path[path.Length - 3] = 'd';
openFileDialog1.FileName = new string(path);//把新的路径重新赋值给openFileDialog1.FileName
ReadDBFFile(openFileDialog1);//用新的路径调用下面自定义的读取.dbf文件函数
}//大佬更好的同时读取的方法欢迎分享,我找了好久没找到,这个笨方法是自己想的
DrawingBoard.Refresh();//刷新panel1(这里改了个名字)
}
}
private void OpenDBFFileButton_Click(object sender, EventArgs e)
{//这里是单独打开.dbf文件的按钮,前期没有完成上面同时读取的功能所写的,现在没什么用了但是还是保留了
openFileDialog2 = new OpenFileDialog();
openFileDialog2.Filter = "dbffiles(*.dbf)|*.dbf|All files(*.*)|*.*";
openFileDialog2.FilterIndex = 1;
if (openFileDialog2.ShowDialog() == DialogResult.OK)
{
ReadDBFFile(openFileDialog2);
}
}
private void RefreshButton_Click(object sender, EventArgs e)
{
DrawingBoard.Controls.Clear();//这里是清理在地图上标记.dbf文件所记录的属性用的,可以先看后面
DrawingBoard.Refresh();//依然是刷新
}
private void MagnifyButton_Click(object sender, EventArgs e)
{//放大按钮
if (openFileDialog1 != null)
{
n += 0.1;//放大倍率加0.1
double width = xmax - xmin;//算出原始地图的宽度
double height = ymax - ymin;//高度
int w = (int)(originWidth * n);//缩放之后应显示的宽度
int h = (int)(originWidth / width * height * n);//这里是按照宽高比例算的缩放后的高度,这也就是只记录初始屏幕宽度而不记录高度的原因,缩放地图要按照地图原始的宽高比缩放
DrawingBoard.Size = new Size(w, h);//改变panel1的大小
DrawingBoard.Refresh();
}
}
private void ShrinkButton_Click(object sender, EventArgs e)
{//缩小按钮同理
if (openFileDialog1 != null)
{
n -= 0.1;
double width = xmax - xmin;
double height = ymax - ymin;
int w = (int)(originWidth * n);
int h = (int)(originWidth / width * height * n);
DrawingBoard.Size = new Size(w, h);
DrawingBoard.Refresh();
}
}
private void DarwingBoard_Paint(object sender, PaintEventArgs e)
{
double width = xmax - xmin;
double height = ymax - ymin;
n1 = (float)(originWidth * 0.9 / width);//算出地图适应屏幕而要本身要缩放的比例(不是用户操作的缩放比例),0.9是为了留出百边
n2 = (float)(n1 / width * height);
n1 *= n;//这里才是乘上用户需要的缩放比例
n2 *= n;
ZoomLabel.Text = "X" + n.ToString();//把这个比例显示在Label中
if (polygons != null) DrawPolygons(e);//画面
if (polylines != null) DrawPolylines(e);//画线
if (points != null) DrawPoints(e);//画点
}
private void ReadDBFFile(OpenFileDialog openFileDialog)
{
table.dt.Columns.Clear();//一系列的清空
table.dt.Rows.Clear();
table.columnsName.Clear();
table.columnsLength.Clear();
BinaryReader br = new BinaryReader(openFileDialog.OpenFile());
_ = br.ReadByte();//当前版本信息
_ = br.ReadBytes(3);//最近更新日期
table.rowCount = br.ReadInt32();//文件中的记录条数
table.columnsCount = (br.ReadInt16()-33)/32;//根据文件头中的字节数计算一下有多少列或者说多少个记录项(怎么算就去看上面的.dbf格式吧)
_ = br.ReadInt16();//一条记录中的字节长度
_ = br.ReadBytes(20);//系统保留
for(int i=0; i< table.columnsCount; i++)//读取记录项
{
string name = System.Text.Encoding.Default.GetString(br.ReadBytes(10));//记录项名字,有的说11个字节有的说10个字节,这里是都正确,如果有些人的不正确就改一下吧,这样下面就是弃掉5个字节了
table.dt.Columns.Add(new DataColumn(name, typeof(string)));//给表添加列名
table.columnsName.Add(name);//记录一下列名
_ = br.ReadBytes(6);//如果上面是11字节,这里就是5字节
table.columnsLength.Add(br.ReadByte());//记录你这一列数据的长度
_ = br.ReadBytes(15);//这其中包含精度,可以让数据好看一点,我比较懒就没用hhh
}
_ = br.ReadBytes(1);//终止符0x0D
for (int i = 0; i < table.rowCount; i++)//读取内容
{
_ = br.ReadByte();//占位符
DataRow dr;//一行
dr = table.dt.NewRow();
for (int j = 0; j < table.columnsCount; j++)//每一行的每一项(就是每一列嘛)
{
string temp = System.Text.Encoding.Default.GetString(br.ReadBytes(table.columnsLength[j]));//按一行中每一个项的长度读取这个项的内容,转码这里比较简单调用一下就好了,说是ASCII码但是有汉字的,所以这里使用的是Default,用ASCII汉字部分会乱码
if (j == 0) table.ID.Add(temp);//这里记录一下每一行的第一个数据以便标在地图上
dr[(string)table.columnsName[j]] = temp;//方括号里的参数是个string值也就是列头的名字,是按列头的名字去把每一项填进去的
}
table.dt.Rows.Add(dr);//把行加到表中
}
ShowBDFFileView.DataSource = table.dt;//把表赋值进去,这样就可以显示了
}
private void ReadSHPFile(OpenFileDialog openFileDialog)
{
//读取主文件头
BinaryReader br = new BinaryReader(openFileDialog.OpenFile());
_ = br.ReadBytes(24);//文件编号及未被使用的字段
_ = br.ReadInt32();//文件长度
_ = br.ReadInt32();//版本
ShapeType = br.ReadInt32();//储存类型编号
double temp = br.ReadDouble();//下面是储存地图的xy最大最小
if (xmin == 0 || temp < xmin)//因为要多个图叠加所以需要找到个最大xy和最小的xy
xmin = temp;//还有就是坐标系的不同.shp文件的坐标系原点在左下角x轴向右y轴向上
temp = -br.ReadDouble();//C#中坐标轴是原点在左上角x轴向右y轴向下
if (ymax == 0 || temp > ymax)//所以第二个数据本应该是y最小,这里加个负号就是y最大了
ymax = temp;//画个坐标轴吧,讲起来比较麻烦,只是所有的y值加了负号变换到第二坐标系了
temp = br.ReadDouble();
if (xmax == 0 || temp > xmax)
xmax = temp;
temp = -br.ReadDouble();
if (ymin == 0 || temp < ymin)
ymin = temp;
_ = br.ReadBytes(32);//Z、M的范围
//读取主文件记录内容
switch (ShapeType)
{
case 1:
points.Clear();//清理原始数据
while (br.PeekChar() != -1)//看一下这个函数的解释就知道了
{
Point point = new Point();
_ = br.ReadInt32();//记录编号
_ = br.ReadInt32();//记录内容长度
_ = br.ReadInt32();//记录内容头的图形类型编号
point.X = br.ReadDouble();
point.Y = -br.ReadDouble();//y值都加个负号
points.Add(point);//储存点
}
//下面是把坐标输出成txt文件,没有需要注释掉就好了
//StreamWriter sw = new StreamWriter("D://point.txt");
//foreach (Point p in points)
//{
// sw.WriteLine("{0},{1},{2} ", p.X, -1 * p.Y, 0);
//}
//sw.Close();
break;
case 3:
polylines.Clear();
while (br.PeekChar() != -1)
{
_ = br.ReadInt32();//记录编号
_ = br.ReadInt32();//记录内容长度
_ = br.ReadInt32();//记录内容头的图形类型编号
Polyline polyline = new Polyline();
polyline.Box[0] = br.ReadDouble();//记录每条线的xy最大最小
polyline.Box[3] = -br.ReadDouble();//这里是Box[3]哦
polyline.Box[2] = br.ReadDouble();//0,1,2,3对应下面
polyline.Box[1] = -br.ReadDouble();//xmin,ymin,xmax,ymax
polyline.NumParts = br.ReadInt32();
polyline.NumPoints = br.ReadInt32();
for (int i = 0; i < polyline.NumParts; i++)
{
polyline.Parts.Add(br.ReadInt32());//记录每部分的值,可能有点难理解部分这个概念,往下看画线函数吧
}
for (int j = 0; j < polyline.NumPoints; j++)
{
Point point = new Point();
point.X = br.ReadDouble();
point.Y = -br.ReadDouble();//同样加负号
polyline.Points.Add(point);//储存
}
polylines.Add(polyline);//储存
}
break;
case 5:
polygons.Clear();
while (br.PeekChar() != -1)
{//记录的都一样和上面的线
_ = br.ReadInt32();//记录编号
_ = br.ReadInt32();//记录内容长度
_ = br.ReadInt32();//记录内容头的图形类型编号
Polygon polygon = new Polygon();
polygon.Box[0] = br.ReadDouble();
polygon.Box[3] = -br.ReadDouble();
polygon.Box[2] = br.ReadDouble();
polygon.Box[1] = -br.ReadDouble();
polygon.NumParts = br.ReadInt32();
polygon.NumPoints = br.ReadInt32();
for (int i = 0; i < polygon.NumParts; i++)
{
polygon.Parts.Add(br.ReadInt32());
}
for (int j = 0; j < polygon.NumPoints; j++)
{
Point point = new Point();
point.X = br.ReadDouble();
point.Y = -br.ReadDouble();
polygon.Points.Add(point);
}
polygons.Add(polygon);
}
break;
}
}
private void DrawPolygons(PaintEventArgs e)
{
PointF[] point;
int count = 0;//为了标记的计数 .shp和.dbf的记录是一一对应的
foreach (Polygon p in polygons)
{
for (int i = 0; i < p.NumParts; i++)
{//part记录的是这个部分中的第一个点的索引,一般都是从1开始的连续数字
int startpoint;
int endpoint;
if (i == p.NumParts - 1)
{//先看else吧
startpoint = p.Parts[i];
endpoint = p.NumPoints;
}
else
{
startpoint = p.Parts[i];
endpoint = p.Parts[i + 1];//这里是下一个部分的第一个点的索引,因为是连续的所以这里的索引减一就是上一个部分的最后一个点的索引值
}
point = new PointF[endpoint - startpoint];//一共就这么多的点
for (int k = 0, j = startpoint; j < endpoint; j++, k++)
{//根据索引去记录每一个部分的点
Point ps = p.Points[j];
point[k].X = (float)(10 + (ps.X - xmin) * n1);//这个稍微想一想应该很好理解吧
point[k].Y = (float)(10 + (ps.Y - ymin) * n2);//加10也是为了留出白边
}
e.Graphics.DrawPolygon(bPen, point);//画面
e.Graphics.FillPolygon(solidBrush, point);//填充颜色
}
if (table.ID.Count != 0)//这里是把.dbf文件中的属性标记在.shp文件画出的地图上
{
int xx = (int)(10 + (p.Box[0] + p.Box[2] - 2 * xmin) * n1) / 2;//算出中心点
int yy = (int)(10 + (p.Box[1] + p.Box[3] - 2 * ymin) * n2) / 2;
Label label = new Label()
{//新生成一个label去标记
AutoSize = false,
Size = new Size(50, 12),
BackColor = Color.Yellow,
BorderStyle = BorderStyle.Fixed3D,
Text = "ID:" + Convert.ToInt32(table.ID[count]),//顺序一一对应的,一个一个往下就可以了,不会错的
Location = new System.Drawing.Point(xx, yy)
};
DrawingBoard.Controls.Add(label);//显示到屏幕上,上面的刷新按钮中的DrawingBoard.Controls.Clear();就是用来清空它的
++count;//下一个
}
}
}
private void DrawPolylines(PaintEventArgs e)
{//和上面是一样的
PointF[] point;
//int count = 0;
foreach (Polyline p in polylines)
{
for (int i = 0; i < p.NumParts; i++)
{
int startpoint;
int endpoint;
if (i == p.NumParts - 1)
{
startpoint = p.Parts[i];
endpoint = p.NumPoints;
}
else
{
startpoint = p.Parts[i];
endpoint = p.Parts[i + 1];
}
point = new PointF[endpoint - startpoint];
for (int k = 0, j = startpoint; j < endpoint; j++, k++)
{
Point ps = p.Points[j];
point[k].X = (float)(10 + (ps.X - xmin) * n1);
point[k].Y = (float)(10 + (ps.Y - ymin) * n2);
}
e.Graphics.DrawLines(rPen, point);//这里换成画线的函数就行了
}
//因为线比较多所以标记的时候会标记好多会很卡,不像上面只有7个面不会卡,所以就注释掉了,解决起来好像挺麻烦的,要分块显示还要根据缩放比去筛选显示,想想都麻烦
//if (table.ID.Count != 0)
//{
// int xx = (int)(10 + (p.Box[0] + p.Box[2] - 2 * xmin) * n1) / 2;
// int yy = (int)(10 + (p.Box[1] + p.Box[3] - 2 * ymin) * n2) / 2;
// Label label = new Label()
// {
// AutoSize = false,
// Size = new Size(50, 12),
// BackColor = Color.Yellow,
// BorderStyle = BorderStyle.Fixed3D,
// Text = "ID:" + Convert.ToInt32(table.ID[count]),
// Location = new System.Drawing.Point(xx, yy)
// };
// DrawingBoard.Controls.Add(label);
// ++count;
//}
}
}
private void DrawPoints(PaintEventArgs e)
{//画点就比较简单了
foreach (Point p in points)
{
PointF pp = new PointF();
pp.X = (float)(10 + (p.X - xmin) * n1);
pp.Y = (float)(10 + (p.Y - ymin) * n2);
e.Graphics.DrawEllipse(blPen, pp.X, pp.Y, 1f, 1f);
}
}
public ReadSHPFileForm()
{
InitializeComponent();
}
}
}
自己写的时候没有找的合适的教程,自己摸索着一点一点的写完,把我的代码记录下来希望对后面的人有帮助,讲的不是很明白的地方多看多想,我相信你能明白的~~~~~~~~~哈哈哈哈哈。
最后还是谢谢marine的博客给了我很多的思路。(我是在此基础上改了好多又加了好多,如果觉得我的比较乱,先看看这个吧,虽然只有.shp文件的读取但思路是一样的)
前人栽树后人乘凉,乘凉之后也要记得栽树哦。