转自原文 ArcGIS engine中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
Refresh Layer
pMap.PartialRefresh(esriViewGeography, pLayer,
null);
Refresh All Layers
pMap.PartialRefresh(esriViewGeography, null,
null);
Refresh Selection
pMap.PartialRefresh(esriViewGeoSelection, null,
null);
Refresh Labels
pMap.PartialRefresh(esriViewGraphics, null,
null);
Refresh Element
pLayout.PartialRefresh(esriViewGraphics,
pElement, null);
Refresh All Elements
pLayout.PartialRefresh(esriViewGraphics, null,
null);
Refresh Selection
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
esriViewForeground
window
esriViewGraphics
annotation cache
esriViewGeoSelection
selection cache
esriViewGeography
top layer cache
esriViewBackground
bottom layer cache
Map类AfterDraw的设备环境
Event
Active HDC
esriViewForeground
window
esriViewGraphicSelection
selection cache
esriViewGraphics
element cache
esriViewBackground
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);
}
透明——Transparency
符号和图像都能使用透明的颜色。透明(α混合)的算法是基于栅格的。为了支持透明,矢量的图形必须首先转化为栅格。透明的对象被绘制到源位图中。那些绘制在背景上的对象必须以背景位图的形式存储。对所有的比特位使用单一的透明,或者对每单个像素使用包含了透明值的mask位图,通过这种方式把源位图混合掺入到背景位图中来实现透明。为了支持透明,IDisplay提供BackgroundDC属性来获取位图,该位图包含了发生在当前绘制过程中所有的图形。
显示透明——Display Transparency
透明算法封装在显示过滤对象TransparencyDisplayFilter中。相同的过滤器类能被要素图层,栅格图层,元素,第三方程序等使用。
通过下面的方式来绘制透明:
[C#]
ITransparencyDisplayFilter pFilter = new TransparencyDisplayFilterClass();
pFilter.Transparency = 100;
pDisplay.StartDrawing(hdc, cacheID);
pDisplay.Filter = pFilter;
DrawToDisplay();
pDisplay.Filter = 0;
pDisplay.FinishDrawing();
对于栅格图层,DrawToDisplay()意味著从显示(display)中获取目标DC,然后BitBlt栅格图像给它。
当指定了过滤器时,显示(display)会生成一个内部的过滤器缓存,这个缓存随着记录缓存为过滤器提供必要的栅格信息。Output通向过滤缓存,这样当栅格图层请求目标DC时,它能得到过滤缓存DC。当pDisplay.Filter设置为0时,显示(display)应用过滤器。应用接受当前的背景位图(记录缓存),不透明的栅格图层影像(过滤缓存)和目标(窗口)。过滤器知道透明度的值是100,这样它混合(do the blending)并且发送结果到窗口。
符号透明——Symbol Transparency
我们如何使符号和图像支持透明呢?视图对象(数据和布局)处理绘图地图(drawing
maps)到输出设备(窗口和打印机)和输出文件(位图和元文件)。所有的图形元素和图层都需要通报是否它们使用了透明颜色。这样,当视图开始生成输出是,它能检查是否存在透明的颜色。如果存在,它使用下面的算法来产生输出:
划分显示(display)表面成不同的波段,这样使要求生成透明颜色的内存中的位图不会消耗更多的内存。
生成一个波段大小的BackingDisplay。BackingDisplay有一个独立位图的内部设备,它具有和输出设备相同的颜色深度和分辨率。直接绘制到位图上,在绘制完成之后,位图被复制到实际的输出设备(或文件)上。图层和符号使用BackingDisplay就像任何其它显示(display)一样,不需要特殊的编码。
对于每个波段:根据当前的波段来调节显示(display)的转换,绘制视图。因为转换的可见范围(bounds)等于波段的矩形,所以渲染将自动地剪切波段。
这将导致一系列的位图(波段)被复制到输出上或导出文件中。如果在地图中不存在透明的颜色,元文件将会自动生成,例如,它将包含一系列的矢量图形。
快速显示——Rapid Display
经常,有必要快速更新显示(Display)来显示活动对象的运动情况。例如,具有GPS跟踪的商业活动。存在两个方面的问题:
如何存储活动数据
如何快速地绘图到显示(Display)
设计模式——Design Patterns
在我们的应用程序中有多种方式来存储和绘制时间数据。大部分通用的方法如下:
l 必要地生成要素类,添加和删除要素。使用标准的渲染或自定义符号来绘制要素。在其它图层的上面或下面绘图。为了更好的性能,捆绑具有自己显示缓存的要素图层(例如,设置ILayer::Cached为true)。
l 生成自定义图层。使用属性数据或要素类来存储要素。对图层完成自定义渲染(例如,直接使用GDI+)。在其它图层的上面或下面绘图。为了更好的性能,对每个要素图层指定自己的显示缓存。
l 生成图形图层。以图形元素的方式来存储数据。使用标准的或自定义符号来渲染。在所有其它图层上面绘制他。
l 响应IActiveViewEvents::AfterDraw(esriViewForeground)来绘图。基于私有数据来存储数据。直接使用GDI or IDisplay来绘图。在其他图层的顶板来绘图。快!GPS扩展来使用这种方法。
有三种通用的方式来存储自定义数据:
1. GeoDatabase里的要素
2. 图形图层里的元素
3. 私有的数据结构
哪一种方式是最好的选择取决于:在绘制顺序中绘制的位置,是否使用标准的渲染对象,是否需要支持私有的数据格式。
在所有的情况下,都应该使用标准的无效绘制模型。这就是生成绘图对象(如,图层,图形元素,时间句柄),把它插入到地图中,当想要绘制的时候调用IActiveView::PartialRefresh。
动画支持——Animation Support
在ArcObjects
9的版本中,一个新的接口IviewRefresh,大大地简化了快速地刷新显示(display)来显示活动对象。客户应该使用AnimationRefresh例行程序取代PartialRefresh来使他们的自定义绘图对象无效。例如,我们可以使用一个带有自己显示缓存的图层来存储“活动”要素。动画(Animation)是通过移动要素,无效化图层(利用AnimationRefresh)和自然地重绘来实现的。当用AnimationRefresh代替PartialRefresh时,下面的optimizations
/ tradeoffs启用了:
文本反锯齿(Text Anti-aliasing)临时无效了。这是为了防止当动画图层无效时标注必须每次都重绘。记住,反锯齿文本利用背景作为渲染的部分,当其下面有任何改变时,文本也需要从头开始绘制。
动画图层上面的透明图层不会随着动画图层而自动无效。 这加快了重绘的速度但有其局限性:位于动画图层里的要素将不再通过其上的透明图层显示出来。
所有的图形直接绘到记录缓存HDC而不是窗口。这就使得所有的绘制在幕后发生。当绘制完成时,窗口只需要从也完成的记录缓存中更新一次。这就很少有闪烁。
为了避免在快速绘制过程中过多地占用CPU的资源,我们可以在旧的无效位置和新的无效位置调用一次UpdateWindow。
显示设计模式——Display Design Patterns
为了帮助理解各种显示对象如何一起工作,解决一般的开发需求,下面将给出了几个应用场景的详细实现。显示对象开始使用这些模式工作。
应用程序窗口——The application window
在应用程序窗口的客户区域绘制地图是大部分普通任务之一,这些窗口支持滚动和后备存储。显示对象通过如下方式来实现这些任务。
初始化——Initialization
当窗口创建时先生成ScreenDisplay。我们也需要生成一个或多个符号来绘制图形。转发应用程序的hWnd给pScreenDisplay.hWnd。从ScreenDisplay中获取其IDisplayTransformation接口,使用pTransformation.Bounds和pDisplayTransform.VisibleBounds设置地图的全部范围和可见范围。可见范围决定了当前缩放的等级。ScreenDisplay负责更新显示转换的DeviceFrame。ScreenDisplay监测窗口消息,自动地处理通用的事件,如窗口大小的改变或滚动。
[C#]
private IScreenDisplay m_pScreenDisplay;
private ISimpleFillSymbol m_pFillSymbol;
private void Form_Load(object sender, System.EventArgs e)
{
m_pScreenDisplay = new ScreenDisplayClass();
m_pScreenDisplay.hWnd = Picture1.hWnd;
m_pFillSymbol = new SimpleFillSymbolClass();
IEnvelope pEnv = new EnvelopeClass();
pEnv.PutCoords(0, 0, 50, 50);
m_pScreenDisplay.DisplayTransformation.bounds = pEnv;
m_pScreenDisplay.DisplayTransformation.VisibleBounds = pEnv;
}
绘制——Drawing
显示对象(the display
object)定义了一个通用的接口IDraw,这个接口使得我们绘制图形到任何显示(display)上变得容易。只要我们使用IDraw
或IDisplay来实现绘制代码,就不必担心所绘制到的设备类型。绘制的过程以StartDrawing开始到FinishDrawing结束。例如,编写这样一个程序,在屏幕中央建立一个多边形,然后绘制出来。这个图形使用默认的符号来绘制。例子如下:
[C#]
private IPolygon GetPolygon()
{
IPolygon GetPolygon;
GetPolygon = new PolygonClass();
IPointCollection pPointCollection;
pPointCollection = GetPolygon as IPointCollection;
IPoint pPoint = new PointClass();
pPoint.PutCoords(20, 20);
pPointCollection.AddPoints(1, ref pPoint);
pPoint.PutCoords(30, 20);
pPointCollection.AddPoints(1, ref pPoint);
pPoint.PutCoords(30, 30);
pPointCollection.AddPoints(1, ref pPoint);
pPoint.PutCoords(20, 30);
pPointCollection.AddPoints(1, ref pPoint);
GetPolygon.Close();
return GetPolygon;
}
private void MyDraw(IDisplay pDisplay, esriSystem.OLE_HANDLE hDC)
{
// Draw from Scratch
IDraw pDraw;
pDraw = pDisplay;
pDraw.StartDrawing(hDC, esriNoScreenCache);
IPolygon pPoly;
pPoly = GetPolygon();
pDraw.SetSymbol(m_pFillSymbol);
pDraw.Draw(pPoly);
pDraw.FinishDrawing();
}
这个程序能用来绘制多边形到任何设备环境中。然而,我们需要绘制的第一个地方是窗口。为了处理这种情况,需要在Picture Box的Paint方法里写些代码,这个方法传递应用程序的ScreenDisplay指针和Picture Box HDC到MyDraw程序(routine)中。注意这个程序接受显示指针(a display pointer)和窗口设备环境。
[C#]
private void Picture1_Paint()
{
MyDraw(m_pScreenDisplay, Picture1.hDC);
}
转发DC让显示(display)来授权这个剪切范围,Windows把它设置到paint的HDC中。(Forwarding the DC allows the display to honor the clipping regions that Windows sets into the paint HDC.)
添加显示缓存——Adding Display Caching
许多绘制过程需要花费一定时间来完成。优化我们应用程序的一个简单的办法就是开启显示缓存。这就涉及到ScreenDisplay's的能力,它可以把绘图过程记录到一个位图中,之后当Paint方法调用的时候,使用这个位图来刷新这个图片窗口。直到我们的数据变化了和我们调用IScreenDisplay::Invalidate来使缓存无效时,缓存才被使用。有两种类型的缓存:记录缓存和用户分配的缓存。在这个例子中,在应用程序的Paint方法使用记录缓存来实现显示缓存。
[C#]
private void Picture1_Paint()
{
if ((m_pScreenDisplay.IsCacheDirty(esriScreenRecording)))
{
m_pScreenDisplay.StartRecording();
MyDraw(m_pScreenDisplay, Picture1.hDC);
m_pScreenDisplay.StopRecording();
}
else
{
tagRECT rect;
m_pScreenDisplay.DrawCache(Picture1.hDC, esriScreenRecording, rect, rect);
}
}
当你执行这段代码是,将发现屏幕上没什么绘制出来。这是因为ScreenRecording缓存没有设置dirty标识。为了确保当第一个绘图消息传来时MyDraw函数被调用,我们必须是缓存无效。在方法的结尾处添加下面一行代码:
[C#]
m_pScreenDisplay.Invalidate(null, true, esriScreenRecording);
许多应用程序,例如ArcMap,可能需要多个显示缓存。为了利用多缓存,照下面的步骤来做:
1. 使用IScreenDisplay::AddCache来添加新的缓存。保存它返回的缓存ID。
2. 为了绘制缓存,给StartDrawing指定缓存ID。
3. 为了使缓存无效,给Invalidate指定缓存ID。
4. 为了从缓存里绘图,给DrawCache指定缓存ID。
为了使这个例子程序支持它自己的缓存,作如下改变:
添加一个成员变量来保存新的缓存。
[C#]
private long m_lCacheID;
在Form_Load方法里生成这个缓存。
[C#]
m_lCacheID = m_pScreenDisplay.AddCache;
使用变量来改变恰当的调用,从Paint方法里清除开始和停止的记录。
漫游,缩放和旋转——Pan, zoom, and rotate
显示对象(The display
object)一个强大的功能(feature)是对我们的图形进行放大和缩小。很容易实现这个工具让用户缩放或漫游。滚动能自动地操作。为了对我们的图形进行缩放,只需简单地设置显示(display)的可见范围。例如,添加一个命令按钮到form上,在这个按钮的事件里通过合适的数量来缩放屏幕,代码如下:
[C#]
private void Command1_Click()
{
IEnvelope pEnv = m_pScreenDisplay.DisplayTransformation.VisibleBounds;
pEnv.Expand(0.75, 0.75, true);
m_pScreenDisplay.DisplayTransformation.VisibleBounds = pEnv;
m_pScreenDisplay.Invalidate(null, true, esriAllScreenCaches);
}
ScreenDisplay实现了TrackPan,它在响应mouse down的事件里被调用,让用户在显示(display)上漫游。通过设置DisplayTransformation旋转属性为非零值,我们可以以屏幕为中心来旋转整个图形。旋转以度数的形式被指定。ScreenDisplay实现了TrackRotate,它在响应mouse down的事件里被调用,让用户在显示(display)上交互旋转。
打印——Printing
打印与绘图到屏幕上非常相似。因为绘图到打印机上时不必担心缓存或滚动,所以可以使用SimpleDisplay。生成一个SimpleDisplay对象,通过拷贝ScreenDisplay的转换来初始化它的转换。设置打印机转换的DeviceFrame到打印机页面的像素范围。最后,利用SimpleDisplay和打印机的HDC从头开始绘制。
输出到元文件——Output to a metafile
GDIDisplay对象用来代表一个元文件。生成一个元文件和生成一个打印机没有什么不同。如果我们指定CreateEnhMetaFile的lpBounds参数为0,MyDraw程序可以被使用。只是hPrinterDC把替换为hMetafileDC。如果你想为CreateEnhMetafFile(以HIMETRIC为单位)指定一个范围,设置DisplayTransformation的DeviceFrame为相同矩形的像素版本。
Print to a frame
许多工程可能要求直接打印输出到输出设备的某个子矩形中。这容易处理,通过设置DisplayTransformation的设备框架给一个像素范围,这个范围要比整个设备的范围要小。
过滤器——Filters
使用显示过滤器(display
filters),非常先进的绘图效果,如颜色透明,都能够被实现。过滤器(Filter)同显示缓存一起工作,可以操纵栅格版本的图形。当为显示(使用IDisplay::putref_DisplayFilte)指定了一个过滤器时,显示生成了一个内部的过滤缓存,这个过滤缓存同记录缓存一起为过滤器提供了栅格信息。输出都会经过过滤缓存直到这个过滤器被清除(这就是putref_DisplayFilter(0))。在这时,display调用IDisplayFilter::Apply。Apply接受当前的背景位图(记录缓存),绘制缓存(包括在绘制中发生的所有的,因为过滤器被指定了),和目标HDC。透明过滤器对这些位图进行透明混合处理,然后把它们绘制到目标HDC来达到颜色的透明。能生产新的过滤器能来实现其他的效果。