看到這個標題,您可能會在腦中產生一個疑問:Adorner是什么?Adorner是WPF窗口中獨立的一層,支持在界面元素之上執行獨立的繪制及用戶交互。可以說,Adorner在您的WPF程序中無處不在。在WPF中,從編輯框控件中光標的顯示和選中效果的支持,到具有數據焦點的控件所具有的虛線外框,都是通過Adorner實現的。
什么是Adorner
鑒於您可能不熟悉Adorner這種組成,因此我在這里單獨列出一節文字對其進行介紹。首先請您想象一下WPF如何對編輯框中光標和選中效果的支持:
按照較為常見的WPF開發方式,您可能需要為這兩種情況分別提供一個非常繁瑣的解決方案。
對於對光標的支持而言,我們可以根據當前的光標位置將字符串分為兩個子串,並在依次顯示這兩個子串之間插入對光標的顯示。但是這種方法會隨着眾多細節的加入變得十分繁瑣:光標的顯示需要占用一定的空間,如兩個像素的寬度。那么在光標位置發生變化的時候,光標前后的字符會因為光標的位置變化而產生一定的位移。例如在上圖中,如果將光標位置移動到“S”以后,那么字符“S”就應向前移動兩個像素。在某些組成中,該實現是不能忍受的。例如在FlowDocument中,某些文字可能是以斜體的方式顯示的,此時斜體的光標將可能導致光標兩邊的字符間距達到10個像素,甚至更多。
同樣對於選中這一效果而言,我們的確可以通過在選中區域放入一個帶有填充色的矩形來解決問題,但這同樣需要動態地拆分字符串並動態計算矩形的大小:矩形的大小需要根據當前選中的子串經過布局計算得到,而經過布局計算后再次插入矩形將導致布局計算的重新開啟。可以說,這個解決方案更會導致非常復雜的布局執行邏輯。
好了,現在發布答案。在WPF中,對光標以及選中效果的支持是通過Adorner類的派生類CaretElement來完成的。Adorner可以使一些界面元素的顯示處於單獨的層次中,因此不再參與其它界面元素的布局計算,從而使這些界面元素的顯示不再影響軟件界面的布局。WPF為光標及選中效果提供的解決方案正利用了Adorner所具有的最大特點:具有獨立的布局系統。另外,Adorner所需要顯示的內容常常顯示在普通的界面元素之上也是其所具有的一大特點。
其實不僅僅是對光標的支持,WPF中還有眾多對Adorner的使用。例如WPF通過FocusVisualStyle定義某個控件擁有輸入焦點時所需顯示的樣式,如具有焦點的按鈕上的虛線矩形:
總的來說,WPF中裝飾器的常見應用包括:
-
- 向界面元素添加控制點,從而允許用戶通過這些元素按照特定方式執行對元素的操作。如通過Grip調整大小、旋轉、重新定位等。
- 在界面元素上提供視覺效果,以提示用戶當前元素處於特定狀態。如在特定文本上繪制下划線等。
- 遮擋界面元素的部分或全部,如IE上方的搜索欄在沒有輸入時顯示當前搜索引擎的名稱。
現在我們從編程的角度來看看Adorner。
首先就是WPF對Adorner的支持。WPF對Adorner的繪制是在單獨的一個層,AdornerLayer中完成的。該層總位於普通界面元素之上,並在執行布局計算時單獨地執行measure-arrange流程。
那么誰具有Adorner呢?就現有的WPF實現代碼來看,答案是AdornerDecorator以及ScrollContentPresenter。這兩個組成常常由WPF內部實現邏輯隱式添加至程序界面中。
使用Adorner時的另一個組成就是其所裝飾的元素。Adorner的繪制位置和其所裝飾的元素相關,而該元素所在的位置則是由窗口的布局計算所決定的。同時Adorner的布局計算和窗口的布局計算是相互獨立的,因此Adorner所裝飾的元素是兩種布局計算相互聯系的一個關鍵點。
簡單的Adorner編程
通常情況下,您可能需要按照如下形式將Adorner綁定到特定的界面元素:
-
- 調用靜態方法AdornerLayer.GetAdornerLayer(),並將需要被Adorner裝飾的界面元素當作參數傳入。該函數會從該界面元素開始沿視覺樹向上查找,並返回它所發現的第一個AdornerLayer。
- 調用AdornerLayer.Add()函數將需要添加的裝飾器加入AdornerLayer中。
就以示例SampleAdorner為例:
1 AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(_label);
2 adornerLayer.Add(new ScaleAdorner(_label));
其中_label就是要修飾的界面元素,而SimpleCircleAdorner則是該界面元素所需要使用的Adorner類型。一般情況下,Adorner的派生類型需要考慮通過重寫OnRender()或AddVisualChild()函數來指定Adorner如何繪制其外觀:
1 protected override void OnRender(DrawingContext drawingContext)
2 {
3 // 繪制虛線矩形。注意右下角的矩形並不是由OnRender繪制的,而是由
4 // AddVisualChild()添加的
5 SolidColorBrush renderBrush = new SolidColorBrush(Colors.Green);
6 Pen renderPen = new Pen(new SolidColorBrush(Colors.Navy), 1);
7 renderPen.DashStyle = new DashStyle(new double[] { 2.5, 2.5 }, 0);
8
9 Rect rect = new Rect(0, 0, _adornedElement.ActualWidth, _adornedElement.ActualHeight);
10 drawingContext.DrawRectangle(Brushes.Transparent, renderPen, rect);
11
12 base.OnRender(drawingContext);
13 }
14
15 private void CreateGrip()
16 {
17 // Scaling grip
18 Rectangle rect = new Rectangle();
19 rect.Stroke = Brushes.Blue;
20 rect.Fill = Brushes.White;
21 rect.Cursor = Cursors.SizeNWSE;
22
23 rect.MouseDown += OnGripMouseDown;
24 rect.MouseUp += OnGripMouseUp;
25 rect.MouseMove += OnGripMouseMove;
26 // 添加子元素,從而允許基類的OnRender()函數繪制該界面元素。
27 // 同時需要重寫VisualChildrenCount屬性及GetVisualChild()函數
28 AddVisualChild(rect);
29 _scalingGrip = rect;
30 }
同時,軟件開發人員可能還需要考慮重寫GetDesiredTransform()來指定Adorner的顯示位置。在默認情況下,Adorner會使用其所裝飾元素的左上角作為2-D坐標原點進行定位。但是在有些情況下,如目標元素使用了RenderTransform將元素繪制到了其它位置的時候,我們需要根據被修飾元素的實際信息執行適當地更改。
很多人在使用函數AdornerLayer.GetAdornerLayer()時都會對該函數如何工作的持有懷疑。首先,AdornerLayer存在於哪里?其次,對該函數進行調用時傳入不同的界面元素是否能得到不同的AdornerLayer?搞清楚這些問題明顯對我們編寫更高效安全的代碼有幫助。我是通過WPF源碼得到這些問題的答案的:
1 public static AdornerLayer GetAdornerLayer(Visual visual)
2 {
3 ……
4 // 沿視覺樹自下向上依次查找
5 for (Visual visual2 = VisualTreeHelper.GetParent(visual) as Visual;
6 visual2 != null; visual2 = VisualTreeHelper.GetParent(visual2) as Visual)
7 {
8 if (visual2 is AdornerDecorator)
9 return ((AdornerDecorator) visual2).AdornerLayer;
10 if (visual2 is ScrollContentPresenter)
11 return ((ScrollContentPresenter) visual2).AdornerLayer;
12 }
13 return null;
14 }
也就是說,AdornerLayer.GetAdornerLayer()將返回遇到的第一個AdornerDecorator以及ScrollContentPresenter所關聯的AdornerLayer。因此在調用該函數的時候,我們最好使用需要被裝飾的元素作為參數,否則該函數所返回的可能並不是最接近該元素的Adorner。
該函數所提供的另外一個信息則是Adorner的顯示范圍。在上面的函數中,我們可以看出對Adorner的尋找是沿着視覺樹向上進行的。由於一個視覺樹的最根部就是當前窗口,因此AdorerLayer並不是全局的,也即不能超出當前窗口的顯示范圍。
Adorner的定位
前面已經提到,Adorner編程最需要考慮的一個問題就是Adorner的顯示位置。雖然在前面的敘述中已經提到Adorner會默認使用其所裝飾元素的左上角作為原點進行定位,但您可能對具體的布局計算何時發生,怎樣根據被裝飾元素進行定位存有興趣。因此在本節中,我們將從WPF源碼的層次分析Adorner的布局計算。
由於Adorner是存在於AdornerLayer中的顯示元素,因此研究AdornerLayer的布局計算將作為了解Adorner定位方式的切入點。
首先是AdornerLayer的Measure-Arrange。在WPF中,如果AdornerDecorator以及ScrollContentPresenter的MeasureOverride()和ArrangeOverride()被調用,那么與之關聯的AdornerLayer的相應布局計算函數將被調用。為AdornerDecorator組成所提供的大小將被傳入到AdornerLayer的相應函數中,從而啟動了對AdornerLayer的布局計算:
1 // AdornerDecorator.MeasureOverride()
2 protected override Size MeasureOverride(Size constraint)
3 {
4 Size size = base.MeasureOverride(constraint);
5 if (VisualTreeHelper.GetParent(this._adornerLayer) != null)
6 {
7 this._adornerLayer.Measure(constraint);
8 }
9 return size;
10 }
被傳遞給AdornerLayer的MeasureOverride()以及ArrangeOverride()的調用將會使用相同大小調用各個Adorner的相應布局函數:
1 protected override Size ArrangeOverride(Size finalSize)
2 {
3 DictionaryEntry[] array = new DictionaryEntry[this._zOrderMap.Count];
4 this._zOrderMap.CopyTo(array, 0);
5 for (int i = 0; i < array.Length; i++)
6 {
7 ArrayList list = (ArrayList) array[i].Value;
8 int num2 = 0;
9 while (num2 < list.Count)
10 {
11 AdornerInfo info = (AdornerInfo) list[num2++];
12 if (!info.Adorner.IsArrangeValid)
13 {
14 Point location = new Point();
15 // 對Adorner.Arrange()進行調用,以控制布局
16 info.Adorner.Arrange(new Rect(location, info.Adorner.DesiredSize));
17 GeneralTransform desiredTransform =
18 info.Adorner.GetDesiredTransform(info.Transform);
19 GeneralTransform proposedTransform =
20 this.GetProposedTransform(info.Adorner, desiredTransform);
21 ……
22 }
23 ……
24 }
25 }
26 return finalSize;
27 }
在上面所展示的代碼中,AdornerLayer.ArrangeOverride()函數還通過一系列對變換的操作控制各個Adorner所需要繪制的位置。首先,該函數獲取了與Adorner相關聯元素實際繪制位置的變換,並作為參數傳入Adorner.GetDesiredTransform()函數中。查看代碼后可以知道,Adorner.GetDesiredTransform()函數的默認實現僅僅是將傳入的參數返回。鑒於該函數被聲明為虛函數,因此非常容易地得知其是Adorner實現中用以控制繪制位置的擴展點。
綜上所述,WPF將使用被裝飾元素的左上角作為Adorner的默認坐標原點。同時,軟件開發人員可以通過重寫Adorner.GetDesiredTransform()函數來控制Adorner的顯示位置。
Adorner的繪制
首先,對Adorner進行繪制的最直觀方法就是重寫OnRender()函數。重寫OnRender()函數以控制外觀實際上是UIElement類的派生類更改默認繪制行為的常用方法,而並非是Adorner所獨有的擴展方式。在該函數中,軟件開發人員也可以通過AdornedElement元素訪問被裝飾的界面元素。示例SampleAdorner也展示了這種繪制方式:
1 protected override void OnRender(DrawingContext drawingContext)
2 {
3 SolidColorBrush renderBrush = new SolidColorBrush(Colors.Green);
4 Pen renderPen = new Pen(new SolidColorBrush(Colors.Navy), 1);
5 renderPen.DashStyle = new DashStyle(new double[] { 2.5, 2.5 }, 0);
6
7 Rect rect = new Rect(0, 0, _adornedElement.ActualWidth, _adornedElement.ActualHeight);
8 // drawingContext所提供的眾多成員函數可以用來繪制各種圖形。之所以在基類的OnRender()
9 // 函數調用之前執行對DrawRectangle()函數的調用則是為了能讓基類OnRender()函數所顯示
10 // 的內容存在於當前所繪制內容之前
11 drawingContext.DrawRectangle(Brushes.Transparent, renderPen, rect);
12
13 // 由於Adorner可能包含其它可視組成,因此很多時候需要調用基類的OnRender()函數,
14 // 執行對其它可視組成的繪制
15 base.OnRender(drawingContext);
16 }
您可能會問,如果需要在Adorner中顯示WPF控件,我們應該怎么做呢?答案是調用AddVisualChild()函數。該函數用來設置兩個Visual之間的關系。在調用了該函數以后,您還需要重寫VisualChildrenCount屬性,GetVisualChild()函數,以正確地反映該關系。而對於該界面元素的繪制則是通過基類的OnRender()函數所提供的默認執行邏輯完成的。
這就導致了一個問題:我們該如何控制OnRender()函數所繪制的界面元素的位置?答案是重寫Adorner的ArrangeOverride()函數,並在該函數中調用界面元素的Arange()函數:
1 protected override Size ArrangeOverride(Size finalSize)
2 {
3 Size size = base.ArrangeOverride(finalSize);
4 if (_scalingGrip != null)
5 _scalingGrip.Arrange(new Rect(finalSize.Width - 5, finalSize.Height - 5, 10, 10));
6 return size;
7 }
在繪制Adorner時,軟件開發人員需要注意被裝飾的元素上所使用的變換。就以控件的FocusStyle為例。具有Focus的控件所具有的虛線矩形即為FocusVisualStyle。其在AdornerLayer中繪制,並獨立於控件的Style。如果一個控件使用了RenderTransform,那么由於FocusVisual繪制於AdornerLayer中,且最常見的AdornerLayer屬於窗口,因此該控件的RenderTransform對FocusVisual不起作用。解決該問題的方法則是以該元素為子結點聲明一個AdornerDecorator並將RenderTransform施行於其上。此時通過AdornerLayer.GetAdorner()所獲得的AdornerLayer將是與AdornerDecorator所關聯的AdornerLayer,從而同時應用了該RenderTransform。
另一個與Adorner相關的話題則是Adorner在ValidationTemplate中的使用。在創建ValidationTemplate的時候,軟件開發人員可以用AdornedElementPlaceholder來表示被修飾的元素。整個模板除了AdornedElementPlaceholder之外均處於Adorner層中。例如在TextBox上使用Validation.ErrorTemplate標示下面的模板之后會在輸入非法時顯示一個嘆號:
1 <ControlTemplate x:Key="validationTemplate">
2 <StackPanel Orientation="Horizontal">
3 <TextBlock Foreground="Red" FontSize="22" FontWeight="Bold" Margin="0,0,5,0">!</TextBlock>
4 <AdornedElementPlaceholder />
5 </StackPanel>
6 </ControlTemplate>
Adorner和用戶的交互
雖然說Adorner是處於應用程序主渲染界面之外的額外一層,但是其仍會像其它界面元素一樣接收輸入事件。又由於AdornerLayer的z順序總高於其所裝飾的元素,因此Adorner可決定該用戶操作是否需要被Adorner處理,並可以選擇阻止輸入事件向Adorner所修飾的界面元素傳遞。
該特性同樣表現在點擊測試這一功能上。在執行點擊測試的函數調用時,如果函數探測到的是Adorner中的界面元素,那么該界面元素將被返回。當然,如果需要進行命中測試的並不是Adorner中的界面元素,那么您需要將Adorner的IsHitTestVisible屬性設置為false。
反過來說,如果您希望處理Adorner中的界面元素所發出的消息,如鼠標按下的消息,那么您不能再僅僅在OnRender()函數中繪制出界面元素的外觀,而需要將該界面元素真正添加到元素樹中,就像AdornerSample中的ScaleAdorner一樣:
1 private void CreateGrip()
2 {
3 // Scaling grip
4 Rectangle rect = new Rectangle();
5 rect.Stroke = Brushes.Blue;
6 rect.Fill = Brushes.White;
7 rect.Cursor = Cursors.SizeNWSE;
8
9 rect.MouseDown += OnGripMouseDown;
10 rect.MouseUp += OnGripMouseUp;
11 rect.MouseMove += OnGripMouseMove;
12 AddVisualChild(rect);
13 _scalingGrip = rect;
14 }
在該函數中,我們創建了一個Rectangle的實例並通過AddVisualChild()函數將其添加至元素樹中。同時,我們在該函數中還為Rectangle的實例添加了三個事件響應函數。通過這種方法,我們就可以在這些事件響應函數中為這些事件執行特殊的執行邏輯。如在MouseMove事件中,我們支持了在鼠標左鍵按下時對界面元素的大小進行更改:
1 private void OnGripMouseMove(object sender, MouseEventArgs args)
2 {
3 if (args.LeftButton != MouseButtonState.Pressed)
4 return;
5
6 Rectangle rect = sender as Rectangle;
7 if (rect == null || _adornedElement == null)
8 return;
9
10 Point point = args.GetPosition(_adornedElement);
11 _adornedElement.Width = point.X > 0 ? point.X : 0;
12 _adornedElement.Height = point.Y > 0 ? point.Y : 0;
13 }
源碼地址:http://download.csdn.net/detail/silverfox715/4191103
轉載請注明原文地址:http://www.cnblogs.com/loveis715/archive/2012/03/31/2427734.html
商業轉載請事先與我聯系:silverfox715@sina.com