您好,登錄后才能下訂單哦!
這篇文章給大家介紹如何進行WPF控件編程,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
WPF提供了一系列預定義組件以供UI開發人員使用。但軟件開發人員仍常常需要自行編寫滿足特定要求的控件。以Spinner控件為例,講解如何以派生方式完成自定義控件的編寫。
一.動手前的思考
在著手開始編寫控件之前,我們需要思考Spinner需要以怎樣一種方式實現?MSDN建議使用三種控件實現方式:從UserControl類派生,從Control類派生以及從FrameworkElement類派生。
要正確地從這三種方式中作出選擇,軟件開發人員首先需要了解這些實現方法的特點。從UserControl類派生和WPF應用程序開發模型非常類似:控件僅僅由現有控件組成,并通過XAML描述。其支持樣式和觸發器。通過這種方式定義的自定義控件并不希望軟件開發人員通過模板指定其外觀。
從Control類派生則是大多數控件開發所使用的方式。與從UserControl類派生這一方法不同,其外觀并不是由關聯的XAML文件指定的,而常常由主題文件所指定。從該類派生的特點有:1) 可以通過ControlTemplate自定義控件的外觀。2) 控件可以支持不同的主題。
而從FrameworkElement類派生則需要徹底拋棄使用控件元素組合的開發方式(無論是在Template中還是UserControl定義中)。生成基于FrameworkElement的組件有兩種標準方法:直接呈現和自定義元素組合。
直接呈現是指重寫FrameworkElement的OnRender方法,并提供顯式定義組件視覺效果的DrawingContext操作,如Image類和Border類就是通過這種方法定義的。例如精簡后的Border類的OnRender()函數如下所示:
protectedoverridevoidOnRender(DrawingContext dc) { …… dc.DrawRoundedRectangle(…); …… }
另一種則是使用Visual類實例組合對象外觀。如Track就是使用組合對象外觀的實例。Track類提供了Thumb屬性,并在其內部實現,如ArrangeOverride()函數中,都考慮了對該組成的使用。
從FrameworkElement中派生的優點有:1) 可以完成對控件外觀的精確控制,而不僅僅是簡單的元素組合。2) 通過定義自己的繪制邏輯定義控件的外觀。
很顯然,Spinner控件需要使用從Control類派生的方法,以提供對模板的支持。當然,這里并非是指Spinner控件直接從Control類派生,而是選擇Control類的一個派生類作為Spinner的基類。這實際上與WPF中的控件類型組織特點有關。WPF中,代表各個控件的類型的繼承層次按照控件特征以非常細致的方式劃分,并在每個繼承層次中僅添加對一個到兩個特征的支持。就以Button類為例。該類型與Control類之間還存在著兩層派生:ContentControl類以及ButtonBase類。這兩個類型不僅僅分別提供了Content屬性以及命令相關的屬性,更重要的是,其內部提供了支持這些屬性的默認實現。在這種情況下,軟件開發人員僅僅在默認實現不再滿足條件時才需要更改這些默認實現所提供的邏輯,從而大大減少了開發新控件所需要的時間。
正是由于這個原因,我們需要在編寫一個控件之前仔細選擇其所需要使用的基類。選擇一個合適基類的標準就是該類型提供了最多的可重用功能,卻沒有提供過多的冗余功能。而基類的尋找也按照尋找相似控件,沿相似控件的繼承層次由高到低逐個篩選兩步。
在尋找相似控件的時候,軟件開發人員需要簡單地揣摩一下該控件的使用方法,以尋找具有相似功能的控件。一般情況下,Spinner需要擁有一個***值,一個最小值,并擁有一個當前值。軟件開發人員可以通過Spinner上的按鈕調整當前值的大小,也可以通過輸入框直接輸入當前值的大小。這和滾動條控件非常相像,只不過滾動條的直接輸入是通過Thumb完成的。然后我們需要反過來想想,是否ScrollBar提供了過多的Spinner所不需要或不支持的功能。顯然ScrollBar所提供的ViewportSize、Orientation等都不是Spinner所需要的屬性,因此其并不適合作為Spinner的基類。接下來我們可以依次考慮ScrollBar的各個基類,直到選中了一個較為適合的基類為止。就Spinner而言,RangeBase類就是一個較為合適的基類。
這里我們將遇到一個岔路口,那就是是否可以通過僅僅更改現有控件的模板這一方式滿足用戶的需求。如果可以,那么使用自定義模板則是更好的選擇。
在確定需要從某個類派生之后,軟件開發人員就應該檢查該類所提供的各個依賴項屬性所具有的默認值是否是一個合理的默認值。例如RangeBase類指定了Value屬性的默認值為0,最小值為0而***值為1。而對于Spinner而言,由于其常常需要操作整數,因此這些默認值都是不適合的。軟件開發人員需要在類型的靜態構造函數中對這些默認值進行重寫:
RangeBase.ValueProperty.OverrideMetadata(typeof(Spinner), newFrameworkPropertyMetadata(10.0, OnValuePropertyChanged));
RangeBase.MaximumProperty.OverrideMetadata(typeof(Spinner), newFrameworkPropertyMetadata(20.0));
RangeBase.LargeChangeProperty.OverrideMetadata(typeof(Spinner), newFrameworkPropertyMetadata(1.0));
RangeBase.SmallChangeProperty.OverrideMetadata(typeof(Spinner), newFrameworkPropertyMetadata(1.0));
需要注意的是,OverrideMetadata()函數中所提供的屬性默認值需要與屬性的類型匹配。例如在為Maximum屬性指定默認值時使用整型數值20,那么對OverrideMetadata()函數的調用將導致程序崩潰。
在更改屬性的默認值時,軟件開發人員需要考慮控件所應實際具有的意義。就以ComboBox和ListBox為例。在什么情況下應使用ComboBox,而什么情況下應使用ListBox呢?回答該問題的決定性因素就是這兩個控件所具有的特征,進而導致的用戶體驗的區別。ComboBox可以通過下拉列表顯示所有的可選項,并通過編輯框組成顯示當前項。這種對數據的顯示方式較ListBox占用了更小的空間,并突出顯示了當前選中項。而相對于ComboBox,ListBox則在全面展示數據,尤其是關聯型數據上較有優勢。
同樣的,Spinner也有自己存在的意義。Spinner的中文名稱被稱為微調控件。從名稱上就可以看出,對Spinner的操作更多的是微小的調整。同時,Spinner所提供的輸入框常常允許用戶直接輸入需要的數值,從而達到對數值精確的控制。也就是說,相對于ScrollBar等組成,其更注重于對值的精確指定。這也便是我在Spinner中添加精度控制屬性的一個原因。當然,該部分內容我會在后面繼續介紹。
在真正開始編寫控件之前,我們還需要考慮的事情就是用戶的使用方法。一般的用戶輸入都是通過鼠標和鍵盤來完成的,因此我們就將用戶使用方法歸結為鼠標和鍵盤兩類。
先來看看鼠標。鼠標需要考慮的主要分為擊鍵和滾輪兩種操作。在鼠標左鍵點擊增加及減少按鈕時,數值需要隨鼠標的擊鍵而更改,并提供適當的外觀反饋。在鼠標左鍵點擊輸入框時,光標需要移動到相應位置。而在鼠標右鍵點擊輸入框時,對文本進行操作的菜單需要被彈出。在鼠標滾輪滾動時,Value的值需要同時進行更改。
接下來是鍵盤。一般情況下,鍵盤操作常常與非字符輸入鍵相關聯。例如用戶通過Tab等操作導航到控件之后,擁有輸入框組成的控件將自動把其內容全部選中。而用戶敲擊Enter鍵則表示他同意當前數值。輸入焦點應轉移到下一個控件以便用戶繼續操作。同時對于范圍類型控件而言,Up和Down表示小范圍數值變化,而PageUp和PageDown則表示大范圍數值變化。對于Spinner來說,微調是其主要功能,因此令小范圍數值變化和大范圍數值變化的值相等也是合情合理的。
***,在開始編寫控件之前,我們需要借鑒一下WPF中的基于同一基類的類似控件的實現。有關如何得到WPF源代碼的方式,請查看“從Dispatcher.PushFrame()說起”一文。通過觀察這些控件的實現,我們可以更好地了解基類所提供的擴展點以及這些擴展點的使用方法。
二.開始實現
在本節中,我們就將開始著手實現Spinner。右鍵點擊項目文件,并在彈出菜單中選擇“Add”->“New Item”。在彈出的對話框中選擇“Custom Control(WPF)”并在名稱輸入框中輸入“Spinner.cs”,如圖所示:
在點擊Add按鈕決定添加控件以后,Visual Studio將為我們添加兩個文件:Spinner.cs以及表示默認主題的Generic.xaml。
2.1 模板支持
通常情況下,我都會在主題文件中放置控件的一個簡單模板實現。例如一開始,我在Generic.xaml中為Spinner定義了如下外觀:
<Style TargetType="{x:Type local:Spinner}"> <Setter Property="Control.Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:Spinner}"> <Border Background="{TemplateBinding Background}"BorderThickness="0.5" BorderBrush="{TemplateBinding BorderBrush}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition Width="20"/> </Grid.ColumnDefinitions> <TextBox x:Name="PART_Input"Grid.Column="0"Grid.Row="0" Grid.RowSpan="2"Margin="0.5"BorderThickness="0" Background="{TemplateBinding Background}"/> <RepeatButton x:Name="PART_Decrease"Grid.Column="1"Grid.Row="0" Margin="0.5"/> <RepeatButton x:Name="PART_Increase"Grid.Column="1"Grid.Row="1" Margin="0.5"/> </Grid> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style>
雖然這并不是最終的控件外觀,但是通過該控件模板,我們可以隨時測試Spinner控件所包含的邏輯。
在該控件模板定義中,我們為幾個組成提供了特殊的名稱,如PART_Input。在模板定義中,以PART_開頭的名稱表示該名稱所對應的組件是在控件內部使用的模板定義中必不可少的一部分。為該控件所提供的其它模板同樣需要為這些名稱提供相應的組成。
在自定義控件的實現中,軟件開發人員需要通過FrameworkTemplate.FindName()函數從當前控件所使用模板的實例尋找具有特定名稱的組成。使用該函數的前提條件則是控件的模板已經被施行。因此,調用該函數的最適合位置就是重載函數OnApplyTemplate()函數。如下面代碼所示:
publicoverridevoidOnApplyTemplate() { mInputTextBox = null; mDecreaseButton = null; mIncreaseButton = null; base.OnApplyTemplate(); if(Template != null) { mInputTextBox = Template.FindName("PART_Input", this) asTextBox; mDecreaseButton = Template.FindName("PART_Decrease", this) asRepeatButton; mIncreaseButton = Template.FindName("PART_Increase", this) asRepeatButton; } }
為了能讓模板設計人員能夠知道這些必須在模板定義中出現的名稱以及這些名稱所對應的控件類型,WPF提供了TemplatePart特性。該特性提供了兩個屬性Name及Type。Name用來標記模板定義中需要添加的組成名稱,而Type則用來指明該名稱所需要具有的類型。如下面代碼所示:
[TemplatePart(Name="PART_Input", Type=typeof(TextBox)), TemplatePart(Name="PART_Decrease", Type=typeof(RepeatButton)), TemplatePart(Name="PART_Increase", Type=typeof(RepeatButton))]
在使用了該特性的情況下,模板設計人員可以直接通過該特性聲明得知模板中所應具有的相應元素以及其類型。
2.2 功能實現
現在我們就需要考慮如何實現控件所對應的功能。控件與模板之間進行互動的方法主要分為兩種:綁定和偵聽模板組成所發出的事件。在實現自定義控件時,我們需要盡量使用綁定。但是對于特殊的處理邏輯,我們常常不能通過綁定完成相應功能。在這種情況下,軟件開發人員就需要通過偵聽模板組成所發出的事件這一方式。
現在就來想想Spinner所需要使用的操作方式:對按鈕控件的輸入可能更改當前值,同時在輸入框中執行輸入并回車同樣可以確認當前值。對按鈕的點擊可以觸發Click事件,更可以觸發按鈕控件所關聯的命令,而在輸入框中敲擊回車鍵則只會觸發Keydown事件。因此在每次施行模板之后,我們需要為特定組成添加這些處理邏輯:
privatevoidAttach() { if(mDecreaseButton != null) mDecreaseButton.Command = mDecreaseCommand; …… if(mInputTextBox != null) { mInputTextBox.Text = Value.ToString(); mInputTextBox.InputBindings.Add(newKeyBinding(mIncreaseCommand, newKeyGesture(Key.Down))); …… mInputTextBox.PreviewKeyDown += PreviewTextBoxKeyDown; mInputTextBox.LostKeyboardFocus += TextBoxLostKeyboardFocus; } }
與之對應的是,在每次施行模板之前,我們則需要取消這些處理邏輯:
privatevoidDetach() { if(mInputTextBox != null) { mInputTextBox.PreviewKeyDown -= PreviewTextBoxKeyDown; mInputTextBox.LostKeyboardFocus -= TextBoxLostKeyboardFocus; } }
這是因為添加的消息處理函數會對消息源生成一個引用。而取消該消息的偵聽則會釋放該引用。
這里,我們來看一下Attach()函數中所展示的互動方式。
首先是命令。在這里我們使用mDecreaseCommand為按鈕指定命令。為什么使用命令,而不是路由事件?這取決于是否該用戶行為是否需要被用戶知曉。相對于路由事件,路由命令會在遇到相應的執行邏輯后不繼續執行路由,從而對用戶不可見。
為了支持這些命令,軟件開發人員需要為Spinner設置CommandBinding以及InputBinding。CommandBinding為命令指定執行邏輯,而InputBinding則為命令指定觸發命令的執行條件。這部分邏輯通常在Spinner的構造函數中完成:
publicSpinner() { CommandBindings.Add(newCommandBinding(mIncreaseCommand, OnIncreaseCommand, CanExecuteIncreaseCommand)); …… InputBindings.Add(newKeyBinding(mIncreaseCommand, newKeyGesture(Key.Down))); …… }
接下來要考慮的則是使用命令之外的另一種處理邏輯,事件。在事件PreviewKeyDown中,我們需要判斷用戶按下的是否是回車鍵。如果是,那么用戶的當前輸入將會被驗證,并根據用戶輸入的正確性決定對Value值的刷新。這里存在著幾個需要寫到的問題。首先就是為什么用Preview-事件。TextBox會在處理用戶輸入時將KeyDown事件的handled設置為true,因此軟件開發人員不能直接使用KeyDown事件,而是使用PreviewKeyDown事件。另一個則是InputBinding有效的時機。InputBinding是由KeyDown事件所驅動的。在KeyDown事件被TextBox處理之前,TextBox實例內設置的InputBinding將被處理;而在TextBox中,KeyDown事件的handled屬性會在處理過程中被設置為true,從而使TextBox的各個祖先元素失去了處理InputBinding的機會。這也便是Attach()函數為TextBox類型成員mInputTextBox添加額外的InputBinding的原因:
privatevoidAttach() { …… if(mInputTextBox != null) { …… mInputTextBox.InputBindings.Add(newKeyBinding(……)); …… } }
接下來,考慮到微調控件的每次調整可能并不是整數,因此我們還需要為Spinner提供一種控制顯示精度的方法。這便是添加Precision屬性的原因。該屬性會通過double.ToString()函數控制當前值的格式化執行方式,以顯示特定的精度:
privatestringGetValueString() { intprecision = Precision <0? 0: Precision; stringformat = string.Format("F{0}", precision); returnValue.ToString(format); }
2.3 更改主題
在實現了所有功能之后,我們就應該開始準備為控件指定主題。
首先要提及的就是如何為控件指定默認樣式。為控件指定默認樣式的方法主要是通過設置DefaultStyleKey屬性完成的。完成該工作的最常見方法就是在靜態構造函數中重寫依賴項屬性DefaultStyleKey的默認值:
FrameworkElement.DefaultStyleKeyProperty.OverrideMetadata(typeof(Spinner), newFrameworkPropertyMetadata(typeof(Spinner)));
接下來,我們就需要在默認主題文件Generic.xaml中添加Spinner的外觀定義。該外觀定義的部分代碼如下:
<ControlTemplate x:Key="RepeatButtonTemplate"TargetType="{x:Type ButtonBase}"> <Border x:Name="Chrome"BorderThickness="0, 0, 1, 1"Background="Transparent"…> <ContentPresenter …/> </Border> … </ControlTemplate> <Style TargetType="{x:Type local:Spinner}"> … <ControlTemplate TargetType="{x:Type local:Spinner}"> <Border …> … <TextBox x:Name="PART_Input"Grid.Column="0"Grid.Row="0"Grid.RowSpan="2" Background="{TemplateBinding Background}"…/> <RepeatButton x:Name="PART_Decrease"Grid.Column="1"Grid.Row="0" Template="{StaticResource RepeatButtonTemplate}"…> <Path x:Name="UpTriangle"StrokeThickness="1"Data="M 3,0 L 0,4 6,4 Z"…/> </RepeatButton> … </Border> <ControlTemplate.Triggers> <DataTrigger Binding="{Binding IsMouseOver, ElementName=PART_Decrease}"…> <Setter TargetName="UpTriangle"Property="Stroke"Value="Blue"/> <Setter TargetName="UpTriangle"Property="Fill"Value="Blue"/> </DataTrigger> … </ControlTemplate.Triggers> </ControlTemplate> … </Style>
如果軟件開發人員希望為不同的Windows主題提供不同的外觀,那么他可以通過為特定主題提供特定外觀或是偵聽WM_THEMECHANGED事件完成。在需要為特定主題提供特定外觀時,軟件開發人員需要在項目的Themes文件夾下添加對應的主題文件,如Windows經典主題對應的就是ThemesClassic.xaml。而如果對主題更改的支持是通過偵聽WM_THEMECHANGED事件完成的,那么對控件模板的更換則需要通過代碼顯式完成。
同時,軟件開發人員還可以通過ThemeInfo特性指定主題文件所存在的位置。該特性擁有兩個和主題相關的屬性:GenericDictionaryLocation以及ThemeDictionaryLocation。這兩個屬性分別指定了與主題相關的通用資源所在的位置以及特定于主題的資源所在的位置。它們都接受類型為ResourceDictionaryLocation的枚舉值。該枚舉值中,None表示不使用主題,SourceAssembly表示主題存在于當前程序集中,而ExternalAssembly則表示主題字典存在于受主題影響的外部程序集中。
對于本例的示例控件Spinner而言,對ThemeInfo主題的使用如下:
[assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)]
關于如何進行WPF控件編程就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。