Programowanie w technologii .NET
wykład 8 – Style, listy, drzewa, toolbary, menu
Podstawy
• Chcąc wielokrotnie wykorzystać pewne ustawienia stylu, może zdefiniować je w zasobach:
<Window.Resources>
<FontWeight x:Key="ButtonFontWeight">
Bold
</FontWeight>
<SolidColorBrush x:Key="ButtonBackground">
Orange
</SolidColorBrush>
</Window.Resources>
• A następnie z nich skorzystać, ustawiając własności elementów:
<Button Background="{StaticResource ButtonBackground}"
FontWeight="{StaticResource ButtonFontWeight}">
OK
</Button>
<Button Background="{StaticResource ButtonBackground}"
FontWeight="{StaticResource ButtonFontWeight}">
Anuluj
</Button>
• Problemy:
◦ Nic (poza nazwą) nie wskazuje, że te zasoby są powiązane i że powinniśmy używać ich łącznie.
◦ Korzystanie z takich zasobów wymaga dodatkowego kodu XAMLa, przez co staje się nieczytelne.
◦ Lepsze rozwiązanie, to zdefiniowanie Stylu – pojedynczy Style będzie zawierał
<Window.Resources>
<Style x:Key="OrangeButton">
<Setter Property="Control.Background" Value="Orange"/>
<Setter Property="Control.FontWeight" Value="Bold"/>
</Style>
</Window.Resources>
• Tworzymy obiekt klasy System.Windows.Style.
• Zawiera on kolekcję Setterów – jeden na własność, której wartość chcemy ustawić.
• Setter określa nazwę własności i wartość, jaką ma jej nadać.
• Każdy element WPF może używać jednego Stylu (lub żadnego) – wybieramy go przez własność Style.
<Button Style="{StaticResource OrangeButton}">
OK
</Button>
<Button Style="{StaticResource OrangeButton}">
Anuluj
</Button>
• Możemy też ustawić to programistycznie:
• Styl ustawia bazowy wygląd elementu – możemy to nadpisać, ustawiając daną własność w tym elemencie (przeważnie jednak, zamiast na tym polegać, będziemy definiować różne warianty stylów).
• Styl pozwala stworzyć grupę powiązanych ustawień i w łatwy sposób nadać je jakiemuś elementowi. Nie musimy się martwić, jakie własności on ustawia, ani wiedzieć, gdy się zmienia:
<Window.Resources>
<Style x:Key="OrangeButton">
<Setter Property="Control.Background" Value="Orange"/>
<Setter Property="Control.FontWeight" Value="Bold"/>
<Setter Property="Control.FontSize" Value="16"/>
<Setter Property="FrameworkElement.Margin" Value="3"/>
<Setter Property="Control.Padding" Value="10,3"/>
</Style>
</Window.Resources>
Własności klasy Style:
• Setters – kolekcja Setterów i EventSetterów, ustawiających wartości własności i dołączających obsługę zdarzeń.
• Triggers – pozwalają na automatyczną modyfikację stylu, np. w reakcji na zmianę jakiejś własności lub zdarzenie.
• Resources – zasoby używane przez style dobrze jest definiować wewnątrz niego.
• BasedOn – pozwala na dziedziczenie stylów.
• TargetType – określa typ elementu, jakiego dotyczy styl; pozwala stworzyć styl, który będzie stosowany automatycznie do elementów określonego typu.
Tworzenie obiektu stylu
• Można je definiować w zasobach (na dowolnym poziomie: okna, aplikacji, kontenera, kontrolki).
• Można też ustawiać od razu w miejscu użycia:
<Button>
<Button.Style>
<Style>
<Setter .../>
<Setter .../>
</Style>
</Button.Style>
OK
</Button>
• Wydaje się to mało użyteczne w wypadku setterów, ale bywa przydatne z triggerami.
Setters
• Styl zawiera kolekcję obiektów klasy Setter.
• Każdy Setter ustawia pojedynczą własność (tylko zależnościową!) w elemencie.
• Aby ustawić złożoną wartość, możemy (jak zwykle) zastąpić atrybut zagnieżdżeniem:
<Setter Property="Control.Background">
<Setter.Value>
<LinearGradientBrush>
<GradientStop Color="AliceBlue" Offset="0"/>
<GradientStop Color="SteelBlue" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
• Aby określić własność, którą chcemy ustawić, musimy podać zarówno jej nazwę jak i nazwę klasy, w której jest zawarta. Nie musi to być jednak nazwa klasy, w której jest ona zdefiniowana (może to być klasa potomna, która dziedziczy tę własność):
<Style x:Key="OrangeButton">
<Setter Property="Button.FontWeight" Value="Bold"/>
<Setter Property="Button.FontSize" Value="16"/>
</Style>
• Działanie się nie zmieni, bo nadal odwołujemy się do tej samej własności.
• Pozwala to również ustawiać własności, które nie będą istnieć w elemencie używającym tego stylu.
• Poniższy przykład nie ustawi innego stylu dla przycisku, a innego dla pola tekstowego. Własności będą ustawione dwukrotnie, zatem w efekcie otrzymamy wynik ostatniego ustawienia. Dzieje się tak, mimo iż FontSize jest deklarowane osobno w Button i TextBlock – wskazują bowiem na tę samą własność
zależnościową.
<Style x:Key="DifferentStyles">
<Setter Property="Button.Foreground" Value="Blue"/>
<Setter Property="Button.FontSize" Value="18"/>
<Setter Property="TextBox.Foreground" Value="Green"/>
<Setter Property="TextBox.FontSize " Value="16"/>
</Style>
• Jeśli wszystkie własności, które ustawiamy, dotyczą jednego typu elementów, możemy ustawić TargetType stylu:
<Style x:Key="OrangeButton" TargetType="Button">
<Setter Property="Background" Value="Orange"/>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
EventSetters
• Dowiązują do elementu obsługę zdarzeń.
<Style x:Key="OrangeButton">
...
<EventSetter Event="UIElement.MouseEnter"
Handler="element_MouseEnter"/>
<EventSetter Event="UIElement.MouseLeave"
Handler="element_MouseLeave"/>
</Style>
• Metody obsługi zdarzeń:
private void element_MouseEnter(...) {
((UIElement)sender).FontStyle = FontStyles.Italic;
}
private void element_MouseLeave(...) {
((Control)sender).ClearValue(Control.FontStyleProperty);
}
• Zdarzenia MouseEnter i MouseLeave nie wykorzystują tunneling i bubbling, dlatego nie moglibyśmy ustawić ich np. w kontenerze dla grupy elementów.
EventSetter jest niezłym rozwiązaniem.
• EventSettery są rzadko wykorzystywane w WPF – podobną funkcjonalność mogą zapewnić nam bowiem triggery.
Dziedziczenie stylów
• Możemy zdefiniować dowolną liczbę stylów na dowolnym poziomie.
• Ale jeden element może używać tylko jednego stylu.
• Z pomocą przychodzi dziedziczenie własności i dziedziczenie stylów.
• Niektóre własności są dziedziczone z kontenera (IsEnabled, IsVisible, Foreground, własności czcionek).
• Możemy też stworzyć nowy styl na podstawie innego (atrybut BasedOn):
<Window.Resources>
<Style x:Key="OrangeButton">
<Setter Property="Control.Background" Value="Orange"/>
<Setter Property="Control.Margin " Value="3"/>
<Setter Property="Control.Padding" Value="10,3"/>
</Style>
<Style x:Key="EmphasizedOrangeButton"
BasedOn="{StaticResource OrangeButton}">
<Setter Property="Control.FontWeight" Value="Bold"/>
</Style>
</Window.Resources>
• Jeśli ponownie ustawimy tę samą własność, setter potomny nadpisze wartość z settera bazowego.
• Dziedziczenie stylów zwiększa złożoność, dlatego dobrze jest ograniczyć jego użycie do sytuacji gdy jeden styl jest pewnym wariantem drugiego. W pozostałych przypadkach lepiej jest tworzyć style z różnymi kombinacjami formatowania.
Automatyczne stosowanie stylów
• Aby automatycznie używać stylu do wszystkich elementów określonego typu, należy:
◦ ustawić własność TargetType,
◦ nie ustawiać klucza (WPF zrobi to za nas: x:Key="{x:Type Button}").
• Styl będzie automatycznie używany przez wszystkie elementy określonego typu w
dół drzewa elementów .
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="Orange"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Margin " Value="3"/>
<Setter Property="Padding" Value="10,3"/>
</Style>
</Window.Resources>
<WrapPanel>
<Button>OK</Button>
<Button Style="{x:Null}">Anuluj</Button>
</WrapPanel>
• Przycisk Anuluj zastępuje automatyczny styl swoim własnym ustawieniem (w tym wypadku nullem, co po prostu powoduje usunięcie stylu).
• W wypadku złożonych okien, kłopotliwe bywa śledzenie w jaki sposób ustawiana jest dana własność. Dlatego z automatycznych stylów lepiej korzystać oszczędnie, np. tylko aby ustawić jednakowy margines wszystkich Buttonów w kontenerze.
Triggers
• Pozwalają na automatyzację prostych zmian stylu (bez potrzeby pisania kodu) – np.
w reakcji na zdarzenie lub zmianę wartości.
• Każdy styl może mieć dowolną liczbę triggerów: obiektów klas dziedziczących z System.Windows.TriggerBase, zawartych w kolekcji Style.Triggers.
• Triggery mogą być użyte bezpośrednio w elementach, z pominięciem Stylu, dzięki kolekcji FrameworkElement.Triggers.
Klasy dziedziczące z TriggerBase
• Trigger – najprostszy z triggerów, reaguje na zmianę wartości własności zależnościowej.
• MultiTrigger – jak wyżej, ale pozwala łączyć kilka warunków.
• DataTrigger – podobny do Triggera, ale reaguje na zmianę w dowiązanych danych – nei wymaga własności zależnościowej.
• MultiDataTrigger – jak wyżej, ale dla kilku warunków.
• EventTrigger – uruchamiają określone akcji w reakcji na zdarzenie, służą np. do wyświetlania animacji.
Trigger
• Może być dołączony do dowolnej własności zależnościwej (np. IsFocused, IsMouseOver, IsPressed).
• Określamy własność, która ma być obserwowana i wartość, jakiej oczekujemy.
• Gdy własność przyjmie tę wartość, uruchomione zostaną settery z kolekcji Trigger.Setters.
• Nie jest możliwe stworzenie bardziej złożonych reguł: tylko proste porównanie wartości.
<Style TargetType="Button">
<Style.Setters>
<Setter Property="Background" Value="Orange"/>
<Setter Property="Margin " Value="3"/>
<Setter Property="Padding" Value="10,3"/>
</Style.Setters>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="White"/>
</Trigger>
</Style.Triggers>
</Style>
• Kolor znaków zmieni się, gdy wskaźnik myszy znajduje się nad przyciskiem.
• Nie musimy deklarować „powrotu do poprzedniej wartości”: oryginalna wartość zostaje zapamiętana i własność do niej wraca, gdy trigger przestanie być aktywny.
• Możemy stworzyć kilka triggerów do jednego elementu. Jeśli ustawiają one tę samą własność, wygrywa zawsze ostatni na naszej liście (niezależnie od czasu zdarzeń).
<Style TargetType="Button">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="White"/>
</Trigger>
<Trigger Property="IsFocused" Value="True">
<Setter Property="Foreground" Value="Yellow"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Foreground" Value="Magenta"/>
</Trigger>
</Style.Triggers>
</Style>
• Gdy naciskamy przycisk, wygrywa IsPressed.
Triggery można wykorzystać do informowania o błędzie:
• Pola z wiązaniem danych (i walidacją!):
<TextBox Text="{Binding Path=Imię,
ValidatesOnDataErrors=True}"/>
<TextBox Text="{Binding Path=Nazwisko, ValidatesOnDataErrors=True}"/>
• Trigger reaguje na Validation.HasError:
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Red"/>
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
MultiTrigger
Kolekcja Conditions pozwala zdefiniować szereg oczekiwanych wartości własności.
Trigger uruchamia się tylko wówczas, gdy wszystkie one są spełnione.
<Style TargetType="Button">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsFocused" Value="True"/>
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="Foreground" Value="White"/>
</MultiTrigger.Setters>
</MultiTrigger>
</Style.Triggers>
</Style>
Kolor tekstu zostanie ustawiony tylko gdy przycisk ma focusa, a wskaźnik myszy znajduje się nad nim.
EventTrigger
• Oczekuje na odpalenie konkretnego zdarzenia i uruchamia przewidzianą akcję.
Przeważnie służy to do włączania animacji.
• Przykład:
<Style TargetType="Button">
...
<Style.Triggers>
<EventTrigger RoutedEvent="Mouse.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:0.2"
Storyboard.TargetProperty="FontSize"
To="22" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
• Animacja zdefiniowana jest w Storyboardzie. Animacja sprowadza się do modyfikacji własności zależniościowych w określonym czasie.
• W przeciwieństwie do Triggerów, nie ma automatycznego „powrotu” i musimy zadeklarować to sami:
<Style TargetType="Button">
...
<Style.Triggers>
...
<EventTrigger RoutedEvent="Mouse.MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:1"
Storyboard.TargetProperty="FontSize" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
• Tym razem jednak nie podajemy docelowego rozmiaru czcionki, WPF uznaje
DataTrigger
• Jak triggery, ale nie wymagają własności zależnościowych.
<Window.Resources>
<Style x:Key="DangerStyle">
<Style.Triggers>
<DataTrigger Binding="{Binding
RelativeSource={RelativeSource Self}, Path=Text}" Value="EX">
<Setter Property="Control.Background"
Value="Orange"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel>
<TextBox Margin="3"
Style="{StaticResource DangerStyle}" />
</StackPanel>
• Style w listach: przy pomocy ItemContainerStyle możemy wskazać styl, który będzie użyty do każdego elementu listy.
<ListBox ...>
<ListBox.ItemContainerStyle>
<Style>
<Setter Property="ListBoxItem.Background"
Value="LightSteelBlue" />
<Setter Property="ListBoxItem.Margin"
Value="5" />
<Setter Property="ListBoxItem.Padding"
Value="5" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
• Przy pomocy triggerów możemy formatować wygląd elementu wybranego:
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
...
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter ... />
<Setter ... />
<Setter ... />
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
• Naprzemienne podświetlanie wierszy: AlternationCount i AlternationIndex.
<ListBox AlternationCount="2" ...>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Background" Value="LightSteelBlue" />
<Setter Property="Margin" Value="5" />
<Setter Property="Padding" Value="5" />
<Style.Triggers>
<Trigger Property="ItemsControl.AlternationIndex"
Value="1">
<Setter Property="Background" Value="LightBlue" />
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
• StyleSelector – gdy chcemy nadać różny styl bazując na bardziej złożonych warunkach. Działa bardzo podobnie, jak DataTemplateSelector.
• DataTriggery można wykorzystać również do warunkowego formatowania w szablonach danych:
<DataTemplate x:Key="DefaultBookTemplate">
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=CategoryName}"
Value="Horror">
<Setter Property="Control.Foreground"
Value="Red" />
</DataTrigger>
</DataTemplate.Triggers>
<Border ...>
<Grid ...>
...
</Grid>
</Border>
</DataTemplate>
• Wady – zmieniamy pojedyncze własności, testujemy tylko pojedyncze warunki.
• DataTrigger pozwala utworzyć listę z elementami, które wyświetlają dodatkowe dane po wybraniu (a raczej ukrywają, gdy element nie jest wybrany).
• Należy sprawdzić wartość IsSelected elementu listy i ustawić wartość Visibility innego elementu.
<DataTemplate x:Key="DefaultBookTemplate">
<Border ...">
<Grid ...>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock ... Text="{Binding Path=Title}" />
<TextBlock ... Text="{Binding Path=Author}">
<TextBlock.Style>
<Style>
...
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</Border>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}" Value="False">
<Setter Property="TextBlock.Visibility"
Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
Kontrolki list
Własności klasy ItemsControl:
• ItemsSource – źródło danych (kolekcja lub DataView)
• DisplayMemberPath – wskazuje, którą własność elementu wyświetlić
• ItemTemplate – szablon elementu
• ItemTemplateSelector – klasa dokonująca wyboru szablonu elementu
• ItemContainerStyle – styl kontenera otaczającego pojedynczy element
• ItemContainerStyleSelector
• ItemsPanel – panel, który przechowuje wszystkie elementy listy (zazwyczaj:
pionowy StackPanel)
• GroupStyle – styl formatowania grupy
• GroupStyleSelector
• Klasa Selector
◦ dodaje możliwość wybierania elementu (ToolBar czy Menu tego nie mają)
◦ własności: SelectedItem, SelectedIndex, SelectedValue (własność Value wybranego obiektu danych, zgodnie z SelectedValuePath)
◦ selector nie daje obsługi wielokrotnego wyboru – to jest dodane w ListBox, przez SelectionMode i SelectedItems
◦ zdarzenie SelectionChanged
• TreeView też pozwala wybierać elementy, ale (z powodu hierarchicznej struktury) działa to inaczej, choć własności nazywają się tak samo: SelectedItem,
SelectedValue, SelectedValue path properties. Do tego zdarzenie SelectedItemChanged.
ComboBox
• Składa się z dwóch części: okna wyboru (ukazuje aktualny wybór) i listy rozwijalnej (wszystkie możliwe wybory).
• Ustawienie IsEditable na true zmienia działanie:
◦ w okienku wyboru wyświetlany jest jedynie tekst zwrócony przez ToString
◦ użytkownik może wpisać tu jakąś wartość
◦ autouzupełnianie
<ComboBox IsEditable="True" ... />
ListView
• Umożliwia wyświetlanie różnych widoków do tych samych danych.
• Zwłaszcza przydaje się do tworzenia widoku wielokolumnowego.
• Dziedziczy z ListBox, dodając jedną własność: View.
• Widok to obiekt dziedziczący z ViewBase – klasy przechowującej wskazanie na style: styl kontrolki listy (DefaultStyleKey) i styl elementu listy
(ItemContainerDefaultStyleKey).
• Wydzielenie informacji o widoku daje lepszą organizację, pozwala np.
wykorzystywać ten sam widok w różnych listach lub używać zamienni kilku widoków w jednej liście.
• WPF dostarcza jeden gotowy widok: GridView. Mamy też możliwość tworzenia własnych.
• Dodawanie kolumn: kolekcja GridView.Columns. Header to nagłówek kolumny, a DisplayMemberBinding – wiązanie obiektem danych.
<ListView Name="lstBooks" Margin="3">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Tytuł"
DisplayMemberBinding="{Binding Path=Title}"/>
<GridViewColumn Header="Autor"
DisplayMemberBinding="{Binding Path=Author}"/>
<GridViewColumn Header="Cena"
DisplayMemberBinding="{Binding Path=Price}"/>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
• Oczywiście wszystkie rzeczy, które działały dla list (konwertery, szablony) mogą być tu stosowane.
• Użytkownik ma możliwość zmiany kolejności kolumn i zmiany ich rozmiaru (domyślnie rozmiar jest ustalany na podstawie zawartości, o ile nie określimy ręcznie własności Width).
Szablony komórek
• Podobnie jak szablony danych dla ListBoxa, ale działa tylko dla jednej kolumny.
<GridViewColumn Header="Tytuł" Width="150">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Title}"
TextWrapping="Wrap"></TextBlock>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
• W ten sam sposób możemy umieścić w kolumnach dowolne elementy, np. obrazek z poprzedniego wykładu (ImagePathConverter).
• Ponieważ szablony kolumn nie mogą być używane w innym miejscu, przeważnie definiuje się je inline.
• GridViewColumn.CellTemplateSelector pozwala na ustawienie selectora szablonu.
• GridViewColumn.Header pozwala na zdefiniowanie wyglądu nagłówka;
GridViewColumn.HeaderTemplate pozwala na wybór szablonu dla nagłówka lub GridView.ColumnHeaderTemplate na wybór jednego szablonu dla wszystkich.
DataGrid
• Oferuje wielokolumnowy widok z edycją.
• Przeznaczony do wyświetlania całych tabel danych.
• Podstawowe wykorzystanie jest bardzo proste:
<DataGrid Name="data"/>
• I w kodzie:
data.ItemsSource = books;
• RowDetailsTemplate pozwala na prezentację dodatkowych informacji w wierszu:
<DataGrid Name="data" SelectionMode="Single">
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<TextBlock Margin="10" FontWeight="Bold"
Text="{Binding Price}"/>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
• Wypełnienie kolekcji DataGrid.Columns pozwala na własne definicje kolumn:
<DataGrid Name="data" SelectionMode="Single"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Title"
Binding="{Binding Title}"/>
<DataGridTextColumn Header="Author"
Binding="{Binding Author}"/>
</DataGrid.Columns>
</DataGrid>
TreeView
• Wspiera wiązanie danych.
• TreeView to specjalizowana kontrolka listy, która zawiera elementy TreeViewItem.
• TreeViewItem to nie kontrolka zawartości, ale kolejna kontrolka listy, która może zawierać kolejne elementy TreeViewItem.
• TreeViewItem dziedziczy z HeaderedItemsControl, która dodaje własność Header.
• Prosty przykład ręcznej definicji drzewa:
<TreeView>
<TreeViewItem Header="Filmy">
<TreeViewItem Header="Komedie"/>
<TreeViewItem Header="Dramaty"/>
<TreeViewItem Header="Seriale"/>
</TreeViewItem>
<TreeViewItem Header="Książki">
<TreeViewItem Header="Fantasy"/>
<TreeViewItem Header="Science fiction"/>
<TreeViewItem Header="Horror"/>
</TreeViewItem>
</TreeView>
• Podobnie, jak w wypadku ListBox, nie musimy budować drzewa z elementów TreeViewItem. Jednak przeważnie najlepiej jest opakowywać swoje obiekty w TreeViewItem, a zawartość do wyświetlenia udostępniać przez
TreeViewItem.Header.
TreeView i wiązanie danych
• Podobnie, jak wypadku pozostałych kontrolek listy, wystarczy ustawić własność ItemsSource.
• Wypełnia to jednak tylko pierwszy poziom drzewa:
public class Category {
public string Name { get; set; } public Category(string name) {
Name = name;
} }
<Grid>
<TreeView Name="drzewo" DisplayMemberPath="Name" />
</Grid>
private void Window_Loaded(object sender, RoutedEventArgs e) {
List<Category> lst = new List<Category>();
lst.Add(new Category("Fantasy"));
lst.Add(new Category("Science fiction"));
lst.Add(new Category("Horror"));
drzewo.ItemsSource = lst;
}
• Dodajmy książki do kategorii:
public class Category {
public string Name { get; set; } public Category(string name) {
Name = name;
books = new List<Book>();
}
private List<Book> books;
public List<Book> Books {
get { return books; } }
}
private void Window_Loaded(object sender, RoutedEventArgs e) {
...
lst[0].Books.Add(new Book("Gra o tron",
"George R.R. Martin", 45M));
lst[0].Books.Add(new Book("Straż nocna",
"Terry Pratchett", 27.5M));
lst[0].Books.Add(new Book("Czarnoksiężnik z Archipelagu", "Ursula K. Le Guin", 19.9M));
...
}
• Aby wyświetlić zawartość kategorii, potrzebny jest szablon, który może obsłużyć dane hierarchiczne:
<TreeView Name="drzewo">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate>
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
• To wyświetli tylko nazwę kategorii.
• HierarchicalDataTemplate może zawierać kolejny szablon – a kolekcja elementów z pierwszego poziomu może być przekazana do kolejnego poziomu.
• Własność ItemsSource określa własność, która zawiera elementy dzieci.
<TreeView Name="drzewo">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate
ItemsSource="{Binding Path=Books}">
<TextBlock Text="{Binding Path=Name}" />
<HierarchicalDataTemplate.ItemTemplate >
<DataTemplate>
<TextBlock Text="{Binding Path=Title}" />
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
• Drzewo posługuje się teraz dwoma osobnymi szablonami dla pierwszego i drugiego poziomu.
• Drugi szablon wykorzystuje wybrany element pierwszego, jako źródło danych.
• Szablony możemy też przenieść do zasobów i wybierać na podstawie typów, a nie zagnieżdżenia:
<Window.Resources>
<HierarchicalDataTemplate
DataType="{x:Type local:Category}"
ItemsSource="{Binding Path=Books}">
<TextBlock Text="{Binding Path=Name}"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate
DataType="{x:Type local:Book}">
<TextBlock Text="{Binding Path=Title}" />
</HierarchicalDataTemplate>
</Window.Resources>
<Grid>
<TreeView Name="drzewo"> </TreeView>
</Grid>
• Pozwala to również na kolejne zagnieżdżenia elementów w drzewie. Np. tak:
public class Category {
public string Name { get; set; } public Category(string name) {
Name = name;
books = new List<object>();
}
private List<object> books;
public List<object> Books {
get { return books; } }
}
...
lst[1].Books.Add(new Category("podkategoria"));
((Category)lst[1].Books[3]).Books.Add(new Book(...));
((Category)lst[1].Books[3]).Books.Add(new Book(...));
• Tworzenie gałęzi na żądanie:
◦ Jeśli drzewo zawiera dużą ilość danych, możemy nie wypełniać od razu wszystkich podkategorii, a odwlec to do czasu rozwinięcia gałezi przez użytkownika.
◦ TreeViewItem odpala zdarzenia Expanded i Collapsed, które na o tym informują. Można to wykorzystać, by wypełnić listę danymi.
◦ Początkowo, każda kategoria powinna zawierać jakiś wypełniacz, aby wyświetlony został plus, który umożliwi jej rozwinięcie.
◦ W obsłudze zdarzenia Expanded możemy wczytać potrzebne dane i wypełnić
Menu
• Mamy dostępne dwie kontrolki: Menu (menu aplikacji) i ContextMenu (menu kontekstowe).
• Menu:
◦ Podlega zwyczajnym regułom rozmieszczania elementów (przeważnie jest na szczycie DockPanela lub w pierwszym wierszu Grida).
◦ Można dodać dowolną liczbę menu.
◦ Własność IsMainMenu ustawiona na true powoduje, że menu dostanie focusa po naciśnięciu klawisza Alt lub F10.
◦ Menu zachowało wszelkie cechy kontrolek listy (wiązanie danych, szablony danych, grupowanie, style itemów, etc.)
MenuItem
• Menu składa się z obiektów klas MenuItem oraz Separator.
• Klasa MenuItem dziedziczy z HeaderedItemsControl.
• Separator to po prostu pozioma linia, nie reagująca na wejście użytkownika.
• Menu możemy (teoretycznie) wypełniać dowolnymi elementami (ale lepiej nie przesadzać).
<Menu DockPanel.Dock="Top">
<MenuItem Header="File">
<MenuItem Header="New"></MenuItem>
<MenuItem Header="Open"></MenuItem>
<MenuItem Header="Save"></MenuItem>
<Separator></Separator>
<MenuItem Header="Exit"></MenuItem>
</MenuItem>
<MenuItem Header="Edit">
<MenuItem Header="Undo"></MenuItem>
<MenuItem Header="Redo"></MenuItem>
<Separator></Separator>
<MenuItem Header="Cut"></MenuItem>
<MenuItem Header="Copy"></MenuItem>
<MenuItem Header="Paste"></MenuItem>
</MenuItem>
</Menu>
• Można używać znaku podkreślenia, aby oznaczyć skrót z Altem.
• Reakcja na kliknięcie w element Menu:
◦ zdarzenie MenuItem.Click (osobno dla każdego elementu lub jeden handler w korzeniu),
• MenuItem wyświetla:
◦ tekst z własności Header,
◦ ikonę: MenuItem.Icon (przyjmuje dowolny obiekt, co pozwala na dołączenie również grafiki wektorowej),
◦ checkmark: własność MenuItem.IsChecked; jeśli ustawimy dodatkowo IsCheckable, kliknięcie na menu zmieni ten stan (niestety, nie ma automatycznej obsługi grup),
◦ skrót klawiaturowy: można go wyświetlić podając
MenuItem.InputGestureText (to jest tylko tekst, nie daje żadnej obsługi – tu lepiej polegać na Commands, jeśli nie chcemy robić tego ręcznie).
<MenuItem Command="Open"></MenuItem>
• MenuItem pozwala na sprawdzenie stanu: IsChecked, IsHighlighted, IsPressed, IsSubmenuOpen. Możemy wykorzystać to np. w triggerach.
ContextMenu
• Podobnie jak Menu, przechowuje kolekcję obiektów MenuItem.
• Jedyna różnica – nie może być umieszczone w oknie.
• Służy do ustawienia własności ContextMenu innych elementów (pokazuje się na prawy przycisk myszy lub Shift+F10):
<TreeView ...>
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Command="Undo"/>
<MenuItem Command="Redo"/>
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
• Menu kontekstowe domyślnie nie pokazuje się, jeśli element jest wyszarzony (isEnabled równe False), ale można to zmienić ustawiając
ContextMenuService.ShowOnDisabled na True.
Separator
• Służy do dzielenia elementów na grupy.
• Zawartość separatora można zmieniać przy pomocy szablonów – pozwala to definiować własne, nieklikalne elementy menu (nawet jeśli dodamy do menu wrzucimy element nie będący MenuItem, to będzie się zachowywał jak one;
Separator nie – np. nie jest podświetlany).
<Separator>
<Separator.Template>
<ControlTemplate>
<Border CornerRadius="4" Padding="5"
Background="Black">
<TextBlock FontWeight="Bold">
Editing Commands </TextBlock>
</Border>
</ControlTemplate>
</Separator.Template>
</Separator>
• Niestety, Separator to nie kontrolka zawartości, zatem nie można oddzielić zawartości od formatowania i zawartość musi siedzieć w szablonie.
Toolbar i StatusBar
• Standardowo pasek narzędzi przechowuje przyciski, a pasek statusu – tekst i kontrolki, z ktorymi użytkownik nie wchodzi w interakcję (np. pasek postępu).
• Nie ma elementów przeznaczonych specjalnie dla paska narzędzi czy statusu – można w nim umieszczać wszelkie podstawowe elementy WPF.
ToolBar
• Przeważnie zawiera obiekty typu Button, ComboBox, CheckBox, RadioButton i Separator.
• Button w toolbarze wygląda inaczej niż w oknie, gdyż toolbar nadpisuje domyślny styl przycisku.
• Własność Orientation pozwala na stworzenie pionowego toolbara.
<ToolBar DockPanel.Dock="Top">
<CheckBox FontWeight="Bold">Bold</CheckBox>
<CheckBox FontStyle="Italic">Italic</CheckBox>
<CheckBox>
<TextBlock TextDecorations="Underline">
Underline </TextBlock>
</CheckBox>
<Separator></Separator>
<ComboBox SelectedIndex="1">
<ComboBoxItem>120%</ComboBoxItem>
<ComboBoxItem>100%</ComboBoxItem>
<ComboBoxItem>80%</ComboBoxItem>
</ComboBox>
<Separator></Separator>
• Gdy wszystkie elementy paska narzędzi nie mieszczą się w oknie, zostają przeniesione do rozwijalnego menu.
• Elementy trafiają automatycznie, ale możemy wskazać (przy pomocy własności dołączonej ToolBar.OverflowMode), jakie elementy nie powinny tam trafić
(OverflowMode.Never), a jakie powinny być tam zawsze (OverflowMode.Always)
ToolBarTray
• Przechowuje kolekcję toolbarów (własność ToolBars) i daje możliwość ich przemieszczania przez użytkownika (o ile nie ustawiliśmy ToolBarTray.IsLocked na true).
• ToolBar przy pomocy własności Band może określić swoje położenie w ToolBarTray.
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<Button>One</Button> <Button>Two</Button>
<Button>Three</Button>
</ToolBar>
<ToolBar>
<Button>A</Button> <Button>B</Button>
<Button>C</Button>
</ToolBar>
<ToolBar Band="1">
<Button>Red</Button>
<Button>Blue</Button>
<Button>Green</Button>
<Button>Black</Button>
</ToolBar>
StatusBar
• Może przechowywać dowolną zawartość (która zostaje automatycznie opakowana przez StatusBarItem).
• Nadpisuje domyślne style niektórych elementów.
• Używany głównie do wyświetlania statycznej informacji.
• Domyślnie rozmieszcza elementy od lewej do prawej. Można podmienić mu panel, aby rozmieszczać w inny sposób.
<StatusBar DockPanel.Dock="Bottom">
<StatusBar.ItemsPanel>
<ItemsPanelTemplate>
<Grid><Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions></Grid>
</ItemsPanelTemplate>
</StatusBar.ItemsPanel>
<TextBlock>2/10</TextBlock>
<StatusBarItem Grid.Column="1">
<TextBlock>120%</TextBlock>
</StatusBarItem>