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 |
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);
}