2010-03-15 52 views
14

在我正在構建的一個用戶界面中,只要面板中的某個控件具有焦點,我就想裝飾一個面板。所以我處理IsKeyboardFocusWithinChanged事件,並在獲得焦點時將元素添加到元素,並在失去焦點時移除裝飾元素。這似乎工作確定。爲什麼我的裝飾元素在應用元素髮生變化時不會重新渲染?

我遇到的問題是,如果裝飾元素的邊界發生變化,裝飾器不會重新渲染。例如,在這個簡單的例子:

<WrapPanel Orientation="Horizontal" 
      IsKeyboardFocusChanged="Panel_IsKeyboardFocusChanged"> 
    <Label>Caption</Label> 
    <TextBox>Data</TextBox> 
</WrapPanel> 

裝飾器正確地裝點WrapPanel的邊界時TextBox接收焦點,但正如我鍵入文本時,TextBox擴大裝飾器的邊緣的下方。當然,只要我做了任何迫使裝飾者渲染的東西,比如ALT-TAB不在應用程序中,或者讓另一個面板成爲焦點,它就會自行糾正。但是當裝飾元素的邊界改變時,我怎麼才能讓它重新渲染呢?

回答

54

WPF有每當相應的內置機制,使所有Adorners被重新測量,重新排列,並重新呈現AdornedElement更改大小,位置或變換。這個機制要求你在編寫你的裝飾者時遵循一定的規則,但並不是所有的文件都應該清楚地記錄下來。

我會先回答你的標題問題爲什麼你的裝潢不符合重新渲染,然後說明解決它的最好方法。

爲什麼裝飾器不重新渲染

每當AdornerLayer接收它掃描它的每一個裝飾器,看看該AdornedElement的大小,位置改變或變換LayoutChanged通知。如果是這樣,它將設置標誌來強制Adorner進行測量,排列和再次渲染 - 大致相當於InvalidateMeasure(); InvaliateArrange(); InvalidateVisual();

在這種情況下通常會發生的情況是控制首先被測量,然後進行排列,然後進行渲染。事實上,WPF試圖使這是最常見的情況,因爲它是最有效的序列。然而,在重新測量之前,很多情況下控制可能會重新排列和/或重新排列。這是WPF中事件的合法順序(允許靈活的佈局技術),但它並不常見,所以通常不會進行測試。

一個正確實施Adorner或其他UIElement會小心調用InvalidateVisual()任何時候渲染可能會受到影響,除非AffectsRender依賴屬性發生了變化。

在你的情況下,你的裝飾大小顯然會影響渲染。大小屬性不是AffectsRender依賴項屬性,因此在更改時需要手動調用InvalidateVisual()。如果你不這樣做,WPF可能永遠不知道重新渲染你的裝飾者。

什麼在你的情況正在發生的事情大概是這樣的:

  • 佈局完成和LayoutChanged事件觸發
  • AdornerLayer發現你的AdornedElement
  • AdornerLayer時間表大小改變你對重新測量裝飾器,重新佈局和重新渲染
  • 某些原因Arrange()被調用,導致在重新測量之前重新佈局和重新渲染。這導致WPF認爲裝飾者不再需要重新佈局或重新渲染。
  • 佈局引擎檢測到裝飾器需要測量,並呼籲Measure
  • 裝飾器的MeasureOverride重新計算所需的大小,但確實沒什麼好說的WPF裝飾器需要重新渲染
  • 佈局引擎決定有什麼更多的工作要做,因此裝飾器永遠不會重新呈現

你能做些什麼來解決它

解決的辦法是,當然,通過調用InvalidateVisual()以修復Adorner的bug。只要控制重新測量,像這樣:

protected override Size MeasureOverride(Size constraint) 
{ 
    var result = base.MeasureOverride(constraint); 
    // ... add custom measure code here if desired ... 
    InvalidateVisual(); 
    return result; 
} 

這樣做將使你的裝飾器始終如一地遵守WPF的所有規則,所以它會在所有情況下按預期工作。這也是最有效的解決方案,因爲InvalidateVisual()什麼都不會做,除非在真正需要的情況下。

+2

尺寸屬性​​不會影響渲染有多奇怪 - 以前沒有碰到過,但是看起來像是一個討厭的小問題,我很驚訝它不會更經常出現!你知道(或者你可以推測)爲什麼WPF設計者以這種方式構建它?無論如何,謝謝你提供了這樣一個清晰,詳細和翔實的答案。 – itowlson 2010-03-27 01:48:49

+0

@itowlson:因爲在最常見的情況下(RenderSize僅在ArrangeCore中更新並且渲染不依賴於任何其他大小),您可能從來沒有碰到過它,因爲在不需要InvalidateVisual()調用的情況下始終觸發渲染。記錄一系列ArrangeOverride和OnRender調用,以更好地感受這種工作方式。我的猜測是,設計師不想在每次設置RenderSize時自動調度OnRender,因爲他們設想RenderSize會被更改並立即改回的場景。 – 2010-03-29 21:13:28

+0

不渲染的另一個原因可能是您必須傳遞給Adorner基礎構造函數的'UIElement'不在adorner層中。例如如果使用'AdornerDecorator'來創建本地裝飾器圖層,然後錯誤地引用超出視覺/邏輯樹的'UIElement'。 – Dennis 2012-08-15 10:07:16

1

您需要調用面板上的調度程序。一個處理程序添加到TextBox SizeChanged事件:

private void myTextBox_SizeChanged(object sender, SizeChangedEventArgs e) 
    { 
     panel.Dispatcher.Invoke((Action)(() => 
     { 
      if (panel.IsKeyboardFocusWithin) 
      { 
       // remove and add adorner to reset 
       myAdornerLayer.Remove(myAdorner); 
       myAdornerLayer.Add(myAdorner); 
      } 
     }), DispatcherPriority.Render, null); 
    } 

這主要來自這篇文章:http://geekswithblogs.net/NewThingsILearned/archive/2008/08/25/refresh--update-wpf-controls.aspx

+6

這感覺有點像使用大錘拍打蒼蠅:它有效,但是很笨拙,效率低下,並沒有解決問題的根本原因,那就是你的女兒離開了屏幕門。 :-)這個原理起作用的原因是,從圖層中刪除裝飾器會導致暫時失去其啓用「需要渲染」位的PresentationSource。查看我的答案以獲取更多詳細信息和更簡單的解決方法。 – 2010-03-26 05:06:03