ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ArcGIS Engine+C#实例开发:解析MapControl与PageLayoutControl同步机制

ArcGIS Engine+C#实例开发:解析MapControl与PageLayoutControl同步机制 简介这份教程是面向 ArcGIS Engine 初学者的 C# 开发入门包以 Visual Studio 2005 为开发环境系统讲解桌面 GIS 应用程序从零搭建的完整流程。内容围绕八个专题展开第一讲构建桌面 GIS 应用程序框架第二讲菜单的添加与实现第三讲实现 MapControl 与 PageLayoutControl 的同步第四讲状态栏信息的添加第五讲鹰眼功能第六讲右键菜单第七讲图层符号选择器第八讲属性数据表的查询显示。每个专题都给出具体操作步骤和界面配置说明尤其对控件绑定、工具条命令添加等易错点做了强调方便初学者照着练习。资源包为单个 PDF 文件总大小约 2.44MB文件小、内容集中适合离线阅读。目前已有五百余人学习适合具备 C# 基础、了解 AE 基本概念并希望快速上手 GIS 桌面开发的人群。通过按章节实践可逐步构建出具备地图导航、图层管理、属性查询等功能的完整桌面 GIS 程序从而加深对 AE 体系结构和控件协作方式的理解为后续开发更复杂的 GIS 应用打下扎实基础。1. 为什么 AE 教程里最难的不是画界面而是让 MapControl 和 PageLayoutControl 同步新手拿到 ArcGIS Engine 的第一反应是拖控件但拖完 MapControl、PageLayoutControl、ToolbarControl 之后会发现地图视图和制版视图完全是两套数据。在 MapControl 里加了一个图层切到 PageLayoutControl 后什么也没有在 PageLayoutControl 里排好版回到地图视图又不见了。这时候才会意识到AE 桌面开发的核心问题不是控件摆放而是控件之间的协作。这份《ArcGIS Engine C# 实例开发教程》是 3SDN 的早期原创用 VS2005 和 C# 把八个实例从框架搭建讲到属性查询正好把这条线走通。适合刚上手 AE 9.2 的初学者也适合想弄清楚 AE 控件协作机制的从业者。2. 用 MapControl 和 ToolbarControl 搭出 AE 程序的主骨架2.1 四个核心控件的分工与布局AE 桌面应用的基础框架通常由 MapControl、PageLayoutControl、ToolbarControl、TOCControl 四个控件构成。它们在功能上各有侧重MapControl 负责数据视图的显示与交互PageLayoutControl 负责制版视图的页面排版ToolbarControl 承载工具栏按钮TOCControl 显示图层目录树。如果只做数据浏览一个 MapControl 加一个 ToolbarControl 就够了如果还要出图就得把 PageLayoutControl 和 TOCControl 一起加进来。布局时ToolbarControl 放在窗体顶部Dock 设为 Top用一个 SplitContainer 分成左右两块左侧放 TabControl分别放 TOCControl 和属性表格右侧放另一个 TabControl分别放 MapControl 和 PageLayoutControl。这样四个控件都有独立空间也方便后续做视图切换。要注意的是MapControl 和 PageLayoutControl 不要同时可见否则两个 ActiveView 并行工作是 AE 控件协作里最常见的隐患。控件主要职责Dock 建议关键接口MapControl数据视图的显示与交互Fill放在地图选项卡内IMapControl3、ActiveViewPageLayoutControl制版视图的页面布局Fill放在制版选项卡内IPageLayoutControl2、PageLayoutToolbarControl承载工具和命令TopBuddy、ICommandItemTOCControl图层目录树Fill放在图层选项卡内Buddy、ITOCControl22.2 LicenseControl 与 TabControl 的细节教程里特意提到把 LicenseControl 拖到窗体任意位置即可很多初学者会忽略它。AE 运行前需要初始化许可LicenseControl 的作用就是在窗体加载时完成许可初始化。没有它运行时会直接抛出缺少许可的异常界面都起不来。它的位置不影响布局但控件必须存在。TabControl 的 Alignment 设置为 Bottom 后选项卡标签会显示在底部这是以前 GIS 教程里常见的布局方式。添加 TabPage 时Name 用英文Text 用中文比如 tabPageMap / 地图、tabPageLayout / 制版。后续在代码里切换视图时Name 就是索引依据。右侧的 TabControl 建议把“地图”放在第一个选项卡程序启动时默认显示数据视图。2.3 Buddy 绑定把工具栏和图层树挂到地图上ToolbarControl 和 TOCControl 默认是独立的必须通过 Buddy 属性关联到 MapControl。有了 BuddyToolbarControl 才知道自己的工具作用于哪个地图TOCControl 才知道该显示哪个地图的图层。这个关系一旦缺失工具栏里的放大缩小按钮虽然能点但作用对象为空后续所有操作都没有反馈。在 VS2005 设计器里右击 ToolbarControl 选择属性把 Buddy 设置为 axMapControl1TOCControl 同理。如果项目里倾向用代码初始化可以这样写// 设置 ToolbarControl 的 Buddy 关联到 MapControl axToolbarControl1.SetBuddyControl(axMapControl1.Object); // 设置 TOCControl 的 Buddy 关联到同一个 MapControl axTOCControl1.SetBuddyControl(axMapControl1.Object);SetBuddyControl 是 AE 控件提供的标准方法参数传入的是 MapControl 的 Object 属性而不是控件实例本身。因为 Object 属性返回的是底层 COM 对象引用这样才能让 TOCControl 拿到 IBasicMap让 ToolbarControl 拿到 IMapControl3。这一步骤漏掉后面所有工具都会提示没有活动地图。2.4 往 ToolbarControl 里添加内置工具工具栏工具的添加方式很简单在 ToolbarControl 属性面板的 Items 里点 Add在 Commands 选项卡下找到需要的命令双击即可加入。常用的有 Map Navigation 下的放大、缩小、平移Map Inquiry 下的查询Feature Selection 下的选择工具。工具栏里的每一项本质上都是一个 ICommand 对象由 ToolbarControl 内部管理。这种方式比手动写工具类快得多也稳定得多。AE 9.2 内置了 ControlsMapZoomInToolClass、ControlsMapPanToolClass、ControlsMapFullExtentCommandClass 等大量现成实现在对象浏览器里都能看到。核心思路是能直接用内置 Command 或 Tool 的地方就不要自己重写交互逻辑。框架搭到这里按 F5 编译运行程序已经能用“打开”工具读取 MXD 并浏览地图但这套框架还缺菜单命令、视图同步这些模块。控件的布局和绑定只是把外壳做好真正让程序具备 GIS 功能还要靠 ICommand 的调用和 ActiveView 的管理。3. 菜单命令把 AE 内置的 Command 挂到 MenuStrip 上3.1 菜单命名、快捷键与图标资源在设计视图里MenuStrip 提供了“请在此处键入”的提示。每个菜单项都要在属性面板里设置 Name不要用中文。原因是后续双击菜单生成的事件处理函数会以 Name 为前缀比如 Name 为 Open生成的就是 Open_Click。如果 Name 是中文函数名会带着中文C# 虽然允许但维护起来非常别扭团队协作时也容易出问题。Text 属性可以写成“打开(O)”的形式这样菜单显示为“打开 O”带下划线的 O 表示在菜单弹出时按字母 O 可以定位到该项。这种快捷键只在菜单激活时生效想让快捷键在程序运行期间全局生效要在 ShortCutKeys 里设置比如 CtrlO。菜单图标通过 Image 属性导入导入后要注意在解决方案资源管理器里把图标文件的“生成操作”设置为“嵌入的资源”否则运行时找不到资源图标会丢失。3.2 在 Form1_Load 中初始化控件引用菜单功能大多需要操作地图所以要在窗体加载时把 MapControl 和 PageLayoutControl 的对象接口取出来。AE 控件是 COM 组件的封装Object 属性返回底层对象强转成接口后才能使用完整能力。private ESRI.ArcGIS.Controls.IMapControl3 m_mapControl null; private ESRI.ArcGIS.Controls.IPageLayoutControl2 m_pageLayoutControl null; private IMapDocument m_mapDocument null; private void Form1_Load(object sender, EventArgs e) { // 通过 Object 属性获取 AE 对象接口 m_mapControl (IMapControl3)axMapControl1.Object; m_pageLayoutControl (IPageLayoutControl2)axPageLayoutControl1.Object; }需要的命名空间包括 ESRI.ArcGIS.Carto、ESRI.ArcGIS.Controls、ESRI.ArcGIS.esriSystem、ESRI.ArcGIS.Display、ESRI.ArcGIS.Geometry、ESRI.ArcGIS.SystemUI。这些 using 缺失时IMapDocument、IMapControl3 这些类型都解析不了。如果在后续代码里频繁出现“类型未找到”的报错优先检查是否漏掉 esriSystem 和 SystemUI这两个最容易漏。3.3 用 ControlsAddDataCommandClass 实现添加数据添加数据菜单看起来简单但自己写“打开文件对话框 图层加载 刷新视图”至少要三四十行。AE 内置的 ControlsAddDataCommandClass 把整个过程封装好了几行代码就能完成。private void AddData_Click(object sender, EventArgs e) { // 记录添加前的图层数量便于后续校验 int currentLayerCount axMapControl1.LayerCount; // 创建 AE 内置命令对象 ICommand pCommand new ControlsAddDataCommandClass(); pCommand.OnCreate(this.axMapControl1.Object); pCommand.OnClick(); }逻辑很直接OnCreate 把命令挂到 MapControl 上OnClick 触发命令内部的文件选择和图层添加逻辑。实际运行时用户选择的图层会直接加进当前地图MapControl 自动刷新。内置命令的优势在于它知道当前活动地图是谁不需要我们去查 IMap。如果想要添加成功后定位到某个图层可以在 OnClick 之后再遍历 axMapControl1.LayerCount与添加前的数量对比从而拿到新增图层。3.4 保存与另存为的两种实现思路教程里保存用的是 PageLayoutControl 的 DocumentFilename 结合 IMapDocument 操作另存为直接调用内置命令。保存时要做几个判断当前文档路径是否为空、路径对应的 MXD 是否有效、以及拿什么内容去替换文档。这里贴上保存的完整实现private void Save_Click(object sender, EventArgs e) { // 确认当前地图文档有效 if (m_pageLayoutControl.DocumentFilename ! string.Empty axMapControl1.CheckMxFile(m_pageLayoutControl.DocumentFilename)) { IMapDocument mapDoc new MapDocumentClass(); mapDoc.Open(m_pageLayoutControl.DocumentFilename, string.Empty); // 用 PageLayout 当前内容替换文档内容 mapDoc.ReplaceContents((IMxdContents)m_pageLayoutControl.PageLayout); mapDoc.Save(mapDoc.UsesRelativePaths, false); mapDoc.Close(); } }这里有三个容易出错的地方。第一CheckMxFile 是 axMapControl1 上的方法不是地图对象的方法写错会直接编译不过。第二ReplaceContents 的参数需要把 PageLayout 转成 IMxdContents这个转换能在制版视图修改后把版面、地图、文本元素整体写回 MXD保证保存的是用户看到的最终状态。第三Save 的第二个参数设为 false表示不覆盖只读属性。另存为命令就简单很多直接调用内置命令ICommand command new ControlsSaveAsDocCommandClass(); command.OnCreate(m_controlsSynchronizer.ActiveControl); command.OnClick();注意 OnCreate 传入的是 ActiveControl也就是当前处于激活状态的控件可能是 MapControl 或 PageLayoutControl。这要求代码里已经实现了控件同步器否则这个参数无从谈起。所以菜单命令的实现顺序应该是先做控件同步再做保存类命令不然保存时不知道当前活动视图是哪一个保存出来的 MXD 可能是空的。4. 用 ControlsSynchronizer 把 MapControl 和 PageLayoutControl 绑成一个 Map4.1 两个控件为什么默认不同步MapControl 和 PageLayoutControl 各自维护一个 ActiveView。MapControl 的 ActiveView 指向数据视图PageLayoutControl 的 ActiveView 指向制版视图。如果在 MapControl 里加载了地图PageLayoutControl 并不知道这张地图的存在反过来在 PageLayoutControl 里排版也不会影响 MapControl。两者的唯一关联手段是共享同一个 IMap 对象。如果试图分别打开同一个 MXD 文件两个控件会各自有一个 Map 实例后续对其中一个做增删图层、修改符号操作另一个完全无感知。所以同步的第一步不是“复制地图文件”而是让两个控件引用同一个 IMap。4.2 共享 Map 的同步原理教程给出的方案是新建一个 ControlsSynchronizer 类核心职责有两个让 MapControl 和 PageLayoutControl 共享同一个 IMap切换视图时缓存当前控件的 CurrentTool激活另一个控件并把 Buddy 控件重新指向当前活动控件。public void BindControls(IMapControl3 mapControl, IPageLayoutControl2 pageLayoutControl) { m_mapControl mapControl; m_pageLayoutControl pageLayoutControl; // 创建一个新地图让两个控件共同引用 IMap newMap new MapClass(); newMap.Name Main Map; m_mapControl.Map newMap; // PageLayout 需要 IMaps 集合把同一张地图加进去 IMaps maps new MapsClass(); maps.Add(newMap); m_pageLayoutControl.PageLayout.ReplaceMaps(maps); }这里的关键是用同一个 MapClass 实例赋给 MapControl.Map同时放进 PageLayout 的 Maps 集合。ReplaceMaps 是 PageLayout 提供的方法用来替换整个页面里的地图集合。之所以不用 ObjectCopy 去复制是因为复制出来的地图与原图没有联动关系修改一个不会影响另一个。共享引用才是数据同步的根基。4.3 激活切换时缓存工具状态当用户在“地图”和“制版”两个选项卡之间切换时需要调用 ActivateMap 或 ActivatePageLayout。这两个方法做的事情是对称的先把当前控件的 CurrentTool 保存到临时变量停掉当前 ActiveView再激活另一个控件的 ActiveView最后恢复之前缓存的工具。public void ActivateMap() { // 如果 PageLayout 中有工具在用先缓存 if (m_pageLayoutControl.CurrentTool ! null) m_pageLayoutActiveTool m_pageLayoutControl.CurrentTool; // 停掉 PageLayout 的 ActiveView m_pageLayoutControl.ActiveView.Deactivate(); // 激活 MapControl 的 ActiveView m_mapControl.ActiveView.Activate(m_mapControl.hWnd); // 恢复 MapControl 上次使用的工具 if (m_mapActiveTool ! null) m_mapControl.CurrentTool m_mapActiveTool; m_isMapCtrlActive true; SetBuddies(m_mapControl.Object); }Activate 方法的参数 hWnd 是控件窗口的句柄必须用 hWnd 而不是 Handle这是 AE 控件暴露给 ActiveView 的原生窗口句柄。Deactivate 和 Activate 成对出现保证两个 ActiveView 不会同时进入工作状态。SetBuddies 的作用是把 ToolbarControl、TOCControl 的 Buddy 都切换到当前激活的控件上。这一步缺失的话工具栏虽然在界面上还亮着但它操作的仍然是已经失活的控件点击后没有任何反应。4.4 ReplaceMap 与工具栏状态的复位打开或新建 MXD 时需要替换两个控件共同引用的地图。教程里提供的 ReplaceMap 方法顺序很讲究先激活 PageLayout再调用 ReplaceMaps然后把新地图赋给 MapControl最后恢复到切换前的激活状态。public void ReplaceMap(IMap newMap) { IMaps maps new MapsClass(); maps.Add(newMap); bool bIsMapActive m_isMapCtrlActive; // 先激活 PageLayout因为 ReplaceMaps 属于 PageLayout 接口 ActivatePageLayout(); m_pageLayoutControl.PageLayout.ReplaceMaps(maps); // 再给 MapControl 赋同一个地图 m_mapControl.Map newMap; // 清空两个控件的工具缓存 m_pageLayoutActiveTool null; m_mapActiveTool null; // 恢复到切换前的激活状态 if (bIsMapActive) { ActivateMap(); m_mapControl.ActiveView.Refresh(); } else { ActivatePageLayout(); m_pageLayoutControl.ActiveView.Refresh(); } }这个顺序不能反。如果先给 MapControl.Map 赋值再去调 ReplaceMapsPageLayout 的 Maps 集合可能更新不到导致两个控件引用的是不同地图。清空两个 ActiveTool 缓存也很重要因为新地图没有对应的历史工具状态残留的旧工具引用可能导致切换控件时抛异常。到这里地图视图和制版视图的同步骨架就完成了。后续所有要同时影响两个视图的命令比如新建、打开、保存都可以通过这个同步器来协调。上一章里的另存为命令正是在实现了 ControlsSynchronizer 之后才能安全使用。5. 状态栏坐标、鹰眼与右键菜单的联动实现5.1 状态栏用 MouseMove 事件驱动坐标和比例尺状态栏显示鼠标坐标和比例尺是 GIS 桌面程序里最常见的反馈机制。实现思路是在 MapControl 的 OnMouseMove 事件里取到鼠标位置再通过 ToMapPoint 转成地图坐标。private void axMapControl1_OnMouseMove(object sender, IMapControlEvents2_OnMouseMoveEvent e) { // 把屏幕坐标转成地图坐标 IPoint mapPoint axMapControl1.ToMapPoint(e.x, e.y); toolStripStatusLabel1.Text string.Format(X: {0:F2} Y: {1:F2}, mapPoint.X, mapPoint.Y); // 显示当前比例尺 toolStripStatusLabel2.Text string.Format(比例尺 1:{0:F0}, axMapControl1.MapScale); }IMapControlEvents2_OnMouseMoveEvent 中的 e.x、e.y 是相对于屏幕的像素坐标ToMapPoint 会转换为地图单位。如果坐标值出现明显偏移多半是因为 MapControl 的 Dock 不是 Fill或者窗体有 ToolStrip 占用了空间导致控件实际显示区域与鼠标事件坐标不一致。比例尺直接读 MapScale 属性即可不用自己算。5.2 鹰眼靠 Extent 同步实现鹰眼的常见实现是放一个较小的 MapControl 作为总览图固定显示全图范围并用矩形标出主图当前视野。主地图范围变化时鹰眼图上的矩形同步更新用户在鹰眼图上拖动矩形主地图范围也跟着移动。// 主地图视野变化时触发 private void axMapControl1_OnExtentUpdated(object sender, IMapControlEvents2_OnExtentUpdatedEvent e) { // 获取主图当前范围 IEnvelope mainExtent axMapControl1.Extent; // 鹰眼图显示全图范围主图范围用一个矩形元素表示 axEyeMapControl.Extent mainExtent; axEyeMapControl.ActiveView.PartialRefresh( esriViewDrawPhase.esriViewGeography, null, null); }鹰眼控件加载的要素类最好和主图一致但它的 MapScale 需要设置成能容纳全图的尺度否则鹰眼图本身也只会显示局部。只设置 Extent 只能做到“跟随”如果想画出红色视野矩形需要通过 GraphicsContainer 往鹰眼图的 ActiveView 里添加矩形元素再在 OnExtentUpdated 里清除旧元素、添加新元素。绘制矩形的代码不多但要注意元素必须实现 IFillShapeElement 接口否则 PartialRefresh 不会刷新。5.3 右键菜单把 AE Tool 挂到 ContextMenuStrip右键菜单与普通菜单的区别是菜单项弹出的位置由鼠标坐标决定而且命令类型大多是工具型点击一次后自动失活。在窗体上放一个 ContextMenuStrip绑定到 MapControl 的 ContextMenuStrip 属性再在菜单项里调用内置工具。private void tsmiZoomIn_Click(object sender, EventArgs e) { // 创建内置放大工具 ICommand tool new ControlsMapZoomInToolClass(); tool.OnCreate(axMapControl1.Object); // 设置为 MapControl 的当前工具 axMapControl1.CurrentTool tool as ITool; }右键菜单里的命令很多是 ITool 类型需要通过 CurrentTool 赋值而不是调用 OnClick。这和菜单栏里保存、另存为这类 ICommand 有本质区别ITool 是一次性交互工具执行一次后自动复位ICommand 是无状态动作点一次执行一次。判断一个内置对象是实现 ICommand 还是 ITool可以在对象浏览器里查看继承接口。5.4 符号选择器让用户自己改图层样式图层符号选择器的核心是调出 AE 的 ISymbolSelector 对话框让用户从符号库中挑选样式再把选择结果赋回图层。// pSymbol 是当前图层的符号可能是 Marker/Line/Fill 三种之一 ISymbolSelector selector new SymbolSelectorClass(); selector.AddSymbol(pSymbol); // 弹出符号选择器对话框 if (selector.SelectSymbol(0)) { // 取出用户选中的符号 pSymbol selector.GetSymbol(0); // 把新符号赋回图层并刷新 featureLayer.Renderer new SimpleRendererClass() as IFeatureRenderer; // 具体赋值方式取决于符号类型这里省略 axMapControl1.ActiveView.PartialRefresh( esriViewDrawPhase.esriViewGeography, null, null); }SelectSymbol(0) 的第一个参数是父窗口句柄传 this.Handle 即可。最容易踩的坑是符号类型不匹配面图层拿到的是 IFillSymbol却把 MarkerSymbol 传进 AddSymbol选择器会直接抛异常。所以在 AddSymbol 前最好判断一下符号类型按 fill / line / marker 分别处理。另一个细节是修改 Renderer 后要同时刷新 TOCControl否则图层树里的符号预览不会更新。6. 属性表查询显示与 DataGridView 的一个务实选择6.1 教程开头的那个“Bug”提示教程第二讲专门纠正了一处错误属性选项卡里最初放的是 DataGridView其实属性查询和设置应该用 PropertyGrid。这个提醒看起来简单但在实际项目里很多人会继续误用 DataGridView 去做图层属性编辑。PropertyGrid 面向对象属性编辑DataGridView 面向要素属性表的数据展示两者用途完全不同。后续讲的属性数据表查询目标才是把查询结果塞进 DataGridView。6.2 用 QueryFilter 查询要素类属性属性表查询最常用的做法是 ITable 接口配合 QueryFilter 查询条件再用 ICursor 逐条读取记录最后填充到 DataTable 中转绑定。// 获取当前图层的属性表 IFeatureLayer featureLayer axMapControl1.Map.Layer[0] as IFeatureLayer; ITable table featureLayer.FeatureClass as ITable; // 构造查询条件 IQueryFilter queryFilter new QueryFilterClass(); queryFilter.WhereClause AREA 1000; // 执行查询并填充 DataTable DataTable dataTable new DataTable(); dataTable.Columns.Add(FID, typeof(int)); dataTable.Columns.Add(Area, typeof(double)); ICursor cursor table.Search(queryFilter, false); IRow row null; while ((row cursor.NextRow()) ! null) { dataTable.Rows.Add( row.get_Value(table.FindField(FID)), row.get_Value(table.FindField(AREA))); } dataGridView1.DataSource dataTable;QueryFilter 的 WhereClause 遵循源数据的 SQL 方言。个人地理数据库和文件地理数据库的写法差别不大但如果是 SDE 数据字段名可能需要带用户名前缀。Search 的第二个参数是 Recycling设为 false 表示每次返回新对象不会覆盖上一次的行内容如果只需要循环读一次且内存敏感可以设为 true 提升性能。6.3 为什么用 DataTable 中转而不是直接绑定 ITableAE 的对象模型里ITable 不是 DataSource 类型DataGridView 无法直接识别。用 DataTable 中转的好处是查询结果可以脱离 AE 对象生命周期独立存在前端排序、过滤、导出 CSV 都很方便。如果只需要查找一条记录可以在 QueryFilter 里加 OBJECTID 条件比如WHERE OBJECTID 123然后用 cursor.NextRow 取第一行即可避免把整表数据全部读入内存。最后一个实用技巧如果图层数据量很大不要用axMapControl1.Map.Layer[0]这种写死的索引取图层应该遍历 Map 的图层集合找到第一个可视化图层作为查询目标。遍历时要先判断图层类型再取 FeatureClass 属性避免对 GroupLayer 调用 IFeatureLayer 时报错。本文还有配套的精品资源点击获取
返回列表