转自原文 ArcGIS engine中Display类库——Display
Display类库包括了用于显示GIS数据的对象。除了负责实际输出图像的主要显示对象(display object)外,这个类库还包含了表示符号和颜色的对象,用于控制在显示(display)中绘制时实体的属性。这个类库也包含了用户与显示(display)交互时的可视化反馈的对象。完成这些功能的对象被归并到一组类库子系统中。
这些类库子系统是:
n Display
n Dynamic Display
n Colors
n Color Ramps
n Symbols
n Display Feedbacks
n Rubber Bands
n Trackers
n Representations
显示(Display)
Display对象是对制图表面的一种抽象。这个制图表面是可以被Windows设备环境所表示的任何硬件设备、输出文件或内存流。每个显示(Display)都管理着自己的转换对象,这些对象操纵着从现实世界空间到设备空间的坐标转换,或从设备空间到现实世界空间的坐标转换。下面提供了这些标准的显示(Display):ScreenDisplay抽象了一个标准的应用程序窗口。它实现了滚动和后备存储(可能是多个图层的后备存储)。SimpleDisplay抽象了使用窗口设备环境来渲染的所有其他设备,如打印机、元文件、位图和二级窗口。
显示对象(the display object)使应用程序的开发人员很容易地在各种输出设备上绘制图形。这些对象使我们能够把按现实世界坐标存储的图形渲染到屏幕、打印机和输出文件中。应用程序的特征,如滚动,后备存储,打印输出,都能很容易的实现。如果某些需要的行为没有被这些标准的对象所支持,我们可以通过实现一个或多个这些标准的显示接口(the standard display interfaces)来生成自定义对象。
一般来说,窗口中任何绘图都需要设备环境。HDC(设备环境句柄)就定义了我们绘图下的设备环境。有许多设备,如窗口、打印机、位图和元文件。在ArcObjects中,显示(display)是对窗口设备环境的一种简单的封装。
当你想在打印机、输出文件或简单的预览窗口中绘制图形时,使用SimpleDisplay组件。如果你想使用StartDrawing,就需要指明HDC。这告诉显示(display)绘制的环境是窗口、打印机、位图还是元文件。HDC是通过调用ArcObjects以外的Windows GDI函数来生成的。
当你想绘制地图到应用程序的主窗口时,使用ScreenDisplay组件。这个类用于处理高级的应用程序特征,如显示缓存(Display Caching)和滚动条。记得指定相关窗口的HDC到StartDrawing。 正常情况下,当在应用程序的WM_PAINT处理中调用Windows GDI的BeginPaint函数时,会返回HDC。另外,我们也可以指定StartDrawing的参数HDC为0,相关窗口的HDC会自动生成。正常情况下,ScreenDisplay会使用内部的显示缓存来提高制图的性能。在绘制的过程中,输出指向活动的缓存。每隔一秒,窗口(如StartDrawing的某个HDC)会持续地从活动缓存里更新。如果你不希望持续地更新(例如,当绘制完成时,你只想更新窗口一次),可以为StartDrawing指定记录缓存HDC(IScreenDisplay::CacheMemDC(esriScreenRecording))。
使用 IDisplay接口可以在设备上绘制点、线、多边形、矩形和文本。这个接口也提供了访问Display对象的DisplayTransformation对象。
DisplayTransformation ——这个对象定义了现实世界坐标如何映射到输出设备里。三个矩形区域定义了这个转换。Bounds指定了真实世界坐标中的整个范围。VisibleBounds指定了当前的可见范围。DeviceFrame指定了输出设备中VisibleBounds显示的位置。既然DeviceFrame的屏幕高宽比不一定总是和VisibleBounds的屏幕高宽比相匹配,通过转换计算出实际的可见bounds以满足DeviceFrame。这被称之为FittedBounds,它是现实世界坐标系。通过简单地设置变换的Rotation属性,所有的坐标系都能以可见区域的中心点进行旋转。
显示缓存—— Display Caching
这里是显示缓存(display caching)的基本原理。视图(IActiveView)控制着主应用程序窗口。当前实现的视图类有两个:Map(数据视图)和PageLayout(布局视图)。ScreenDisplay使客户生成任意数量的缓存(缓存其实只是独立于设备的位图)成为可能。当一个缓存生成时,客户就获得一个cacheID。这个ID能用来指明活动缓存(StartDrawing的最后一个参数,例如输出的位置),是缓存无效,或者绘制缓存到目标HDC中。除了动态缓存外,ScreenDisplay也提供了一个记录缓存来累计发生在Display上的所有绘制。客户利用StartRecording和FinishRecording方法来管理记录。
ArcObjects是如何来实现缓存的呢?让我们来考察Map类。Map为所有的图层生成一个缓存,如果存在注记或图形的话会有另一缓存,如果存在要素选择则会有第三个缓存。它也记录了所有的输出。(除了这些缓存外,还可以通过设置Cached属性为True来为单个图层请求一个私有的缓存。如果一个图层请求了一个缓存,Map会为这个图层分配一个单独的缓存,并且根据图层的上下位置把这个缓存归并到不同的缓存中。)IActiveView::PartialRefresh利用对缓存布局的认知尽可能少地无效化缓存,便于我们尽可能多地从缓存里绘图。
利用缓存,实现下面的场景都是可能的:
l 当应用程序被移动或暴露(exposed),或者绘制图形编辑的橡皮条(rubberbanding)时,使用记录缓存来重绘。因为BitBlt只需使用一次,这是非常有效率的。
l 选择一组新的要素,只使所选的缓存无效。要素、图形和注记都从缓存里绘制。只有所选的要素需从头开始绘制。
l 从要素上挪开图形元素或注记。只是使注记缓存无效。要素和要素选择都从缓存里绘制,只有注记从头开始绘制。
l 生成一种叫动态图层的新图层。它的缓存属性总是返回True。为了显示车载GPS的运动轨迹,在图层里移动标记,只需要使动态图层无效。所有其他的图层都从缓存里绘制。只有车辆图层需要从头绘制。这就可使得图层有动态的效果。
l 通过移动几个图层到一个图层组里来生成一个基本地图,并且设置这个图层组的Cached属性为Ture。现在,我们可以编辑和交互那些绘制在基本地图顶端的图层,而不必要从头开始绘制这个基本地图。
l 显示过滤器(display filter)的概念是允许任何图层执行栅格操作,这些图层包括使用了自定义符号的要素图层。这也可能生成一个依附于图层的slider对话框。设置这个图层的Cached属性为True,使用透明的显示过滤器和slider来交互地控制图层的透明度。其他的显示过滤器也能被用来实现裁剪、对比度和亮度等。
记录缓存——Recording Cache
ScreenDisplay能够记录它要绘制的。利用StartRecording() 和 StopRecording()让显示(Display)知道什么正是需要记录的。利用DrawCache(esriScreenRecording)来显示它所记录的。对于记录位图(recording bitmap)利用get_CacheMemDC(esriScreenRecording)来获取内存设备环境句柄。这个功能有几个非常重要的用途。
- 首先,单一位图的后备存储很容易实现,其绘制过程如下:
[C#]
if ((m_pScreenDisplay.IsCacheDirty(esriScreenRecording)))
{
m_pScreenDisplay.StartRecording();
m_pDraw.StartDrawing(hDC, esriScreenCache.esriNoScreenCache);
DrawContents();
m_pDraw.FinishDrawing();
m_pScreenDisplay.StopRecording();
}
else
{
m_pScreenDisplay.DrawCache(Picture1.hDC, esriScreenCache.esriScreenRecording, 0, 0);
}
- 其次,客户可以使用已分配好的显示缓存(利用IScreenDisplay::AddCach来创建)来缓存视图绘制的不同阶段,然而为了快速地刷新,仍然可以使用单一的位图(记录)。
- 最后,当有兴趣完成一些高级的渲染技术(如半透明)的绘制,我们可以使用记录位图。
Caveats
如果地图的任何部分包含了透明,其刷新就都会受到影响。当绘制一个透明图层时,该图层下的一切都属于其渲染的部分。因此,每当透明图层的下面发生变化时,该图层都必须重新开始绘制。
对Microsoft窗口,如果开启了anti-aliasing设置,文本也会有透明度。这意味是文本使用在它下面绘制的图层来实现anti_aliasing(文本的边缘被混入到背景中)。因此,当图层改变是,注记或自动标记都必须重新绘制。
How to cache layers
通过设置图层的缓存标识来建立自己的显示缓存。然后重新激活视图。
[C#]
private void EnableLayerCaches()
{
int i;
for (i = 0; i <= m_pMap.LayerCount - 1; i++)
{
m_pMap.get_Layer(i).Cached = (chkCustomCaches.Value == 1 ? true : false);
}
...
pActiveView.Deactivate();
pActiveView.Activate(pActiveView.ScreenDisplay.hWnd);
pActiveView.Refresh();
}
旋转——Rotation
理解显示对象的旋转如何实现是很重要的,因为它关系到所有实体的显示。旋转是发生在变换层级之后的,这样DisplayTransformation客户总是处理未旋转的图形。例如,当我们从一个变换的例行程序中取回一个图形时,它是位于一个未旋转的空间。同样,当我们指定一个变换的范围时,这个范围也是位于未旋转的空间。多边形的旋转所有的都执行正常。但对于封装边界(envelope),事情就复杂了,因为矩形的旋转不可能表示出来。这能通过两个例子很好地阐明:
l 从变换中获取一个矩形
l 为变换指定一个矩形
从变换中获取一个矩形。例如,假设你想获取一个代表窗口客户区域的矩形。既然用户一直注视着旋转的空间,请求的区域就不可能表示成一个Envelope。矩形的四个角在地图空间里都有唯一的x,y值。Envelope类内部的表示假定边共享了x、y的值。因此,由DisplayTransformation来返回封装边界。FittedBounds不是我们所想要的,因为矩形多边形被要求精确地表示出未旋转的地图空间下的客户区域。当前存在一个bug,使得FittedBounds返回一个没有旋转的封装边界。当这个bug修复后,它返回的envelpe要比我们想象的稍微大一点。在旋转的情况下,大部分客户要避免使用封装边界,下面的代码实现了通过匹配用户显示下的某个矩形来发现矩形多边形:
[C#]
private void ToUnrotatedMap(tagRECT r, IGeometry pBounds, IDisplayTransformation pTransform)
{
WKSPoint[] mapPoints = new WKSPoint[5];
tagPOINT[] rectCorners = new tagPOINT[4];
rectCorners[0].x = r.left;
rectCorners[0].y = r.bottom;
rectCorners[1].x = r.left;
rectCorners[1].y = r.top;
rectCorners[2].x = r.right;
rectCorners[2].y = r.top;
rectCorners[3].x = r.right;
rectCorners[3].y = r.bottom;
//transform all 4 points.
pTransform.TransformCoords(ref mapPoints[0], ref rectCorners[0], 4, 4 | 1);
pTransform.TransformCoords(ref mapPoints[1], ref rectCorners[1], 4, 4 | 1);
pTransform.TransformCoords(ref mapPoints[2], ref rectCorners[2], 4, 4 | 1);
pTransform.TransformCoords(ref mapPoints[3], ref rectCorners[3], 4, 4 | 1);
// build polygon from mapPoints
mapPoints[4] = mapPoints[0];
IPointCollection pBoundsPointCollection;
ITopologicalOperator2 pBoundsTopologicalOperator2;
pBoundsPointCollection = (IPointCollection)pBounds;
pBoundsTopologicalOperator2 = (ITopologicalOperator2)pBounds;
pBoundsPointCollection.SetWKSPoints(1, ref mapPoints[0]);
pBoundsPointCollection.SetWKSPoints(1, ref mapPoints[1]);
pBoundsPointCollection.SetWKSPoints(1, ref mapPoints[2]);
pBoundsPointCollection.SetWKSPoints(1, ref mapPoints[3]);
pBoundsPointCollection.SetWKSPoints(1, ref mapPoints[4]);
pBoundsTopologicalOperator2.IsKnownSimple_2 = true;
}
为变换指定一个矩形。记得客户需要在未旋转的空间下工作,让变换在显示前来处理旋转。一个简单的例子如拖拽出缩放的矩形就是这种情况。首先,用户看到变换的空间,拖放出的矩形是在旋转的空间。(注意:这些工具使用像上面的代码来生成直角多边形,这种多边形代表了用户所选的区域)。在指定变换之前,需要利用工具转换矩形到未旋转的空间上。下面的代码显示了如何这样做(pRotatedExtent是一个直角多边形,它正好匹配用户所拖拽出的矩形)。
[C#]
IArea pArea = pRotatedExtent;
IPoint pCenter = pArea.Centroid;
ITransform2D pTrans = pRotatedExtent;
pTrans.Rotate(pCenter, (90 * (3.1416 / 180)));
Refreshing versus Invalidation
为了促使显示(display)的重绘,需要调用无效化(Invalidation)的操作。然而,大部分客户从来不使用IScreenDisplay::Invalidate。原因在于,我们的应用程序中存在这样的视图,如Map或PageLayout类,由它来负责屏幕的刷新,如Refresh, PartialRefresh。这种视图管理着显示的缓存(the display's caches),知道执行无效的最好方式。只是尽可能地使用大部分指定的参数来确保PartialRefresh被调用。只有在绝对必要的情况下,我们才调用Refresh,因为这经常是一个耗时的操作。
为了让视图(Map和PageLayout)能完全管理显示缓存(display caching),所有的无效都必须通过视图(来调用)。调用IActiveView::Refresh总是绘制所有的,这种做法是非常没有效率。所有应该尽可能对使用PartialRefresh方法。它让我们指定部分视图来重绘,使视图结合显示缓存来运作,这种方式就使得绘图迅速和高效。
Draw Phase |
Map |
PageLayout |
esriViewBackground |
unused |
page/snap grid |
esriViewGeography |
layers |
unused |
esriViewGeoSelection |
feature selection |
unused |
esriViewGraphics |
labels/graphics |
graphics |
esriViewGraphicSelection |
graphic selection |
element selection |
esriViewForeground |
unused |
snap guides |
PartialRefresh的参数
下面的表格显示了调用PartialRefresh方法的例子;注意参数选项的用法:
Action
|
C# Method Call |
pMap.PartialRefresh(esriViewGeography, pLayer, null); |
|
pMap.PartialRefresh(esriViewGeography, null, null); |
|
pMap.PartialRefresh(esriViewGeoSelection, null, null); |
|
pMap.PartialRefresh(esriViewGraphics, null, null); |
|
pLayout.PartialRefresh(esriViewGraphics, pElement, null); |
|
pLayout.PartialRefresh(esriViewGraphics, null, null); |
|
pLayout.PartialRefresh(esriViewGraphicSelection, null, null); |
使用PartialRefresh的例子
注意:使任何阶段(phase)无效化都会促使记录缓存的无效。为了强制从记录缓存中重绘,使用下面的调用:
[C#]
pScreenDisplay.Invalidate(null, FALSE, esriNoScreenCache);
Display Events
这节描述了地图制图的事件触发。为了更好地理解绘图事件,也会讨论绘图顺序和显示缓存。
Drawing Order
为了更好地理解绘图的事件触发,下面将描述每种视图的绘图顺序。
Map(数据视图)——下面显示了从顶部到底部的顺序,例如,上面的每一项都比下面的要先绘。
Object | Phase | Cache |
Graphic Selection
|
esriViewForeground
|
none
|
Clip Border
|
esriViewForeground
|
none
|
Feature Selection
|
esriViewGeoSelection
|
selection
|
Auto Labels
|
esriViewGraphics
|
annotation
|
Graphics
|
esriViewGraphics
|
annotation
|
Layer Annotation
|
esriViewGraphics
|
annotation
|
Layers
|
esriViewGeography
|
layer(s)
|
Background
|
esriViewBackground
|
bottom layer
|
Map类的绘制顺序
PageLayout(布局视图)——下面显示了从顶部到底部的顺序,例如,上面的每一项都比下面的要先绘。
Object | Phase | Cache |
Snap Guides
|
esriViewForeground
|
none
|
Selection
|
esriViewGraphicSelection
|
selection
|
Elements
|
esriViewGraphics
|
element
|
Snap Grid
|
esriViewBackground
|
element
|
Print Margins
|
esriViewBackground
|
element
|
Paper
|
esriViewBackground
|
element
|
PageLayout类的绘制顺序
Drawing Events
利用下面的IActiveViewEvents事件向应用程序添加自定义的和绘制。
- AfterDraw(display, esriViewBackground)
- AfterDraw(display, esriViewGeography)
- AfterDraw(display, esriViewGeoSelection)
- AfterDraw(display, esriViewGraphics)
- AfterDraw(display, esriViewGraphicSelection)
- AfterDraw(display, esriViewForeground)
- AfterItemDraw(display, idx, esriDPGeography)
- AfterItemDraw(display, idx, esriDPAnnotation)
- AfterItemDraw(display, idx, esriDPSelection)
每个绘图阶段之后都会有AfterDraw事件的触发。使用下面的方法绘制图形到缓存中:
- 生成连接活动视图(地图)的对象。例如“Events”。
- 选择绘制之后的绘图阶段(phase)。你选择的这个阶段(phase)决定了其绘制的先后顺序。
- IActiveViewEvents::AfterDraw负责绘制。
不是所有的视图都触发所有的事件。此外,如果一个视图被部分刷新,那么从缓存里绘图的绘制阶段就不会触发AfterDraw事件。例如,如果选择(要素)被刷新,那么所有的图层都从缓存里绘制。这样,AfterDraw(esriViewGeography)事件就不会被触发。然而也有例外,对esriViewForeground而言,每次只要视图绘制这个事件都会被触发。即使从记录缓存里绘制,这个背景事件也会被触发。
How to Enable item events with VerboseEvents
每个要素或图形显示时都会触发AfterItemDraw事件,如果相连的句柄没有效率的话就会严重影响制图的性能。一般情况下客户会连接AfterDraw事件。要注意检查第二个参数是否是合适的绘制阶段,因为当地图绘制时,AfterDraw过程会被调用好几次。
为了效率考虑,IActiveView 有一个叫VerboseEvents的属性。它用来限制事件触发的数目。如果VerboseEvents为false,AfterItemDraw就不会触发。这是默认的设置。
Events and Display Caching
下面的表格显示了当AfterDraw事件发生时的活动设备环境:
Event | Active HDC |
window |
|
annotation cache |
|
selection cache |
|
top layer cache |
|
bottom layer cache |
Map类AfterDraw的设备环境
Event | Active HDC |
window |
|
selection cache |
|
element cache |
|
element cache |
PageLayout类AfterDraw的设备环境
Create a private cache
在绘图(events)时,我们可能想使用esriViewGraphics。AfterDraw有两个参数,pDisplay和drawPhase。每个指定的阶段都会调用AfterDraw来确保绘制。直接绘制到显示(display)上,而不需担心缓存。方法StartDrawing和FinishDrawing由Map来调用。如果我们绘制之后的阶段被缓存了,那么我们的绘制就自动缓存了。.
- 响应IDocumentEvents::ActiveViewChanged来生成缓存。Map生成它的缓存通过响应Activate,释放它的缓存通过响应Deactivate。ActiveViewChanged事件是在Map生成完缓存之后触发的,这样如果内存不够,地图将获取这个缓存,不过私有缓存不行。
[C#]
IActiveView pActiveView = pMap As IActiveView;
IScreenDisplay pScreen = pActiveView.ScreenDisplay;
pScreen.AddCache(m_myCacheID);
- AfterDraw 应该像这样:
[C#]
if (phase != esriViewXXX)
{
return;
}
IScreenDisplay pScreen = pDisplay;
if ((!(pScreen == null)))
{
// Draw directly to output device
DrawMyStuff(pDisplay);
return;
}
// Draw to screen using cache if possible
long hWindowDC;
WindowDC = pScreen.WindowDC;
bool bDirty;
pScreen.IsCacheDirty(m_myCacheID, bDirty);
if ((bDirty))
{
// draw from scratch
pScreen.FinishDrawing();
pScreen.StartDrawing(hWindowDC, m_myCacheID);
DrawMyStuff(pDisplay);
}
else
{
// draw from cache
pScreen.DrawCache(hWindowDC, m_myCacheID, null, null);
}