Artykuł

wrz 10 2011
0

WPF Tutorial - zasoby i style

W poprzedniej części tutoriala, poświęconej tworzeniu własnych kontrolek użytkownika, wspomniałem co nieco o zasobach i stylach, które możemy umieścić w pliku dodatkowym, bądź też w aktualnie przetwarzanym dokumencie. Dziś chciałbym rozszerzyć trochę temat i pokazać bardziej praktyczne wykorzystanie zasobów. Dowiecie się między innymi o tym jak:

  • Utworzyć zasoby lokalne dla wybranego okna/elementu
  • Utworzyć zasoby globalne dla całej aplikacji
  • Jak tworzyć powtarzalne elementy jako zasoby (np. menu kontekstowe) i wykorzystać je w praktyce

Zasoby lokalne okna/elementu

Utworzenie zasobów lokalnych, na potrzeby danego okna/elementu, to praktycznie najprostszy środek do wielokrotnego wykorzystania określonej partii kodu w WPFie. Działają tu relacje dziedziczenia - zresztą, tak jak wszędzie w WPFie. Dla przykładu, jeśli zdefiniujemy zasoby okna, to elementy zdefiniowane w tych zasobach, będą dostępne wszędzie w obrębie tego okna. Jeśli natomiast zdefiniujemy zasoby/style wewnątrz określonego elementu to będą one dostępne dla tego elementu i elementów jemu potomnych.

W ten sposób, możemy generować np. style dla poszczególnych wierszy listy i stosować je w razie potrzeby.

Lokalne wykorzystanie zasobów, może się okazać przydatne jeśli wiemy, że żaden z tych elementów nie zostanie wykorzystany np. w innym oknie tej aplikacji.

Jeśli planujemy aplikację, zbudowaną z wielu różnych okien (taka sytuacja ma raczej miejsce w większości tworzonych przez nas aplikacji), to istnieje spore prawdopodobieństwo powtórzenia określonej partii kodu. Np. z pewnością będziemy chcieli zachować spójny charakter przycisków i innych wizualnych elementów okna.

Zasoby, umieszczamy w węźle Resources dla elementu, którego mają dotyczyć, co implikuje tym, że wskazane struktury i style automatycznie będą dostępne dla wszystkich elementów potomnych, w stosunku do bieżącego.

Spójrzmy zatem w jaki sposób, możemy zdefiniować zasoby dla okna:

<Window ...>
    <Window.Resources>
        <!-- ... -->
    </Window.Resources>
</Window

Oczywiście wykropkowane elementy, należy zastąpić własną treścią. Zasoby możemy zdefiniować również np. dla wspomnianej wcześniej listy (ListView). Zrobimy to w sposób analogiczny:

<ListView>
    <ListView.Resources>
        <!-- ... -->
    </ListView.Resources>
</ListView>

Kropka użyta w obu konstrukcjach, determinuje relacje dziedziczenia. W tym przypadku, zasoby okna i listy są zagnieżdżone i odnoszą się dokładnie do tych elementów. Jeśli dobrze sobie przypomnicie wpis wprowadzający, to podobna technika była wykorzystana np. w przypadku układu typu Grid i pozwalała na odpowiednie rozlokowanie elementów umieszczonych w siatce.

Zatem, jak powyższą wiedzę możemy zastosować w praktyce? Powiedzmy, że chcemy np. wystylizować wszystkie przyciski dla danego okna, w tym celu musimy zastosować następujący kod:

<Window ...>
    <Window.Resources>
        <Style TargetType="Button">
            <Setter Property="BorderBrush" Value="Gray" />
            <Setter Property="Background" Value="#FAFAFA" />
            <Setter Property="Padding" Value="2" />
        </Style>
    </Window.Resources>
    <Grid>
        <Button Content="1" />
        <Button Content="2" />
        <Button Content="3" />
    </Grid>
    <!-- ... -->
</Window>

Oczywiście kod został ocenzurowany ze zbędnych elementów (kropki i komentarze). W tym przykładzie, tak jak wspomniałem wcześniej, stylizować będziemy wszystkie przyciski w obrębie okna. W linii drugiej, otwieramy węzeł zasobów. Następnie, definiujemy styl dla docelowego elementu typu Button (właściwość TargetType). W ten sposób, wystylizowane zostaną wszystkie elementy typu Button leżące wewnątrz okna.

W liniach 4-6 określamy przykładowe właściwości przycisków. Chcemy by nasze przyciski miały szare obramowanie, jasne tło oraz delikatny padding.

Każdą z właściwości definiujemy podając jej nazwę (Property) oraz wartość (Value). Cały ten kod wystarczy, do zapewnienia jednolitej stylizacji wszystkich przycisków. Nic nie stoi jednak na przeszkodzie, by w dowolnym przycisku wewnątrz okna, nadpisać wybraną właściwość.

Ograniczamy stylizację do wybranych elementów

Korzystający z porady dla okna, opisanej w poprzedniej sekcji, skazujemy wszystkie podległe przyciski na stylizację według wyznaczonych reguł. Tak jak wspomniałem wcześniej, styl dla dowolnego elementu można nadpisać, ale po co to robić jeśli można wszystko rozplanować odpowiednio prędzej?

Problem możemy rozwiązać zasadniczo na dwa sposoby. Po pierwsze, możemy ustawić style w zasobach przeznaczonych dla określonego elementu. Powiedzmy, ze chcemy by style otrzymały wszystkie przyciski, które zostały umieszczone wewnątrz określonego elementu Grid. W tym celu, musimy przerobić nas pierwotny listing do następującej postaci:

<Window ...>
    <Grid>
		<Grid.Resources>
			<Style TargetType="Button">
				<Setter Property="BorderBrush" Value="Gray" />
				<Setter Property="Background" Value="#FAFAFA" />
				<Setter Property="Padding" Value="2" />
			</Style>
		</Grid.Resources>	
        <Button Content="1" />
        <Button Content="2" />
        <Button Content="3" />
    </Grid>
    <!-- ... -->
</Window>

W ten sprytny sposób, przenieśliśmy stylizację przycisków do Grida. Dzięki temu, przyciski umieszczone w innych elementach strony (nie będących potomkami grida), automatycznie użyją domyślnych wartości styli.

Drugi sposób, to wykorzystanie kluczy dla utworzonych przez nas reguł styli. W tym przypadku, oprócz określenia typu docelowego elementu, musimy określić również specjalny klucz, który będziemy musieli zastosować w wybranych przez nas kontrolkach.

Jeśli ktoś z Was, zajmował się kiedyś kaskadowymi arkuszami styli, to pewnie dostrzeże tu wiele podobieństw. W istocie choć sam zapis wygląda inaczej, to w praktyce działa to dokładnie tak samo jak w CSS.

Spójrzmy zatem, na kolejny przykład:

<Window ...>
    <Window.Resources>
        <Style TargetType="Button" x:Key="MyButton">
            <Setter Property="BorderBrush" Value="Gray" />
            <Setter Property="Background" Value="#FAFAFA" />
            <Setter Property="Padding" Value="2" />
        </Style>
    </Window.Resources>
    <Grid>
        <Button Content="1" Style="{StaticResource MyButton}" />
        <Button Content="2" />
        <Button Content="3" />
    </Grid>
	<!-- ... -->
</Window>

Oczywiście w tym przypadku, również działa zasada zagnieżdżania. Styl o danym kluczu umieszczony w oknie, może być więc wywołany dla dowolnego przycisku podrzędnego względem okna.

Jeśli przeniesiemy go do Grida, to mogą to być już tylko przyciski umieszczone w gridzie itd.

Aby wywołać styl o danym kluczu musimy:

  • Wybrać element typu docelowego, stylu dla przycisku nie przypiszemy polu tekstowemu
  • Zastosować właściwość Style dla wszystkich przycisków, które mają otrzymać styl

Jeśli oba te warunki zostaną spełnione (tak jak ma to miejsce na listingu), to program się poprawnie skompiluje, a my otrzymamy wystylizowany element.

Zasoby globalne dla całej aplikacji

O zasobach globalnych, napomniałem już we wpisie poświęconym tworzeniu kontrolek użytkownika. Dziś zatem, odświeżymy te informacje.

W przypadku zasobów globalnych, konieczne jest utworzenie specjalnego, dedykowanego im pliku np. Resources.xaml. Plik tego typu, możemy utworzyć poprzez wybranie typu ResourceDictionary na ekranie tworzenia nowego pliku. Pusty plik, prezentować będzie się następująco:

<ResourceDictionary 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
	<!-- Style i zasoby -->
</ResourceDictionary>

Co zatem należy zrobić, by nadać styl wszystkim przyciskom naszej aplikacji? Wykorzystać poniższy kod:

<ResourceDictionary 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
	<Style TargetType="Button">
		<Setter Property="BorderBrush" Value="Gray" />
		<Setter Property="Background" Value="#FAFAFA" />
		<Setter Property="Padding" Value="2" />
	</Style>
</ResourceDictionary>

W ten sposób, wystylizowaliśmy wszystkie przyciski, przynajmniej z pozoru. Dlaczego z pozoru? Ponieważ plik zasobów, nie został w żaden sposób skojarzony z naszą aplikacją. Czas to zmienić. W tym celu, potrzebny nam będzie plik App.xaml, który jak już wielokrotnie pisałem, jest integralnym składnikiem każdej WPFowej aplikacji.

Uruchamiamy zatem plik App.xaml i odnajdujemy już znajomy węzeł Application.Resources. Uzupełniamy jego zawartość w sposób następujący:

<Application ...>
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

W ten sposób, podpięliśmy dla zasobów naszej aplikacji plik Resources.xaml. Węzeł ResourceDictionary.MergedDictionaries pozwala na podpięcie większej liczby plików. Dzięki temu, zasoby możemy porządkować w bardziej logiczny sposób.

Zasób menu kontekstowego

Domyślnie, dla każdego TextBoxa dodawane jest menu kontekstowego pozwalające na obsługę schowka. Nie wiem dokładnie z czego to wynika, ale w moim przypadku jest ono zawsze w język angielskim. Jeśli chcielibyśmy zatem mieć spolszczone menu, w którym pojawią się np. dodatkowe ikonki dla każdej akcji, czy też skróty klawiszowe wywołujące wybraną akcję, to musimy sami stworzyć swoje menu.

Stworzone przez nas menu, prawdopodobnie będziemy chcieli wykorzystać jednak wielokrotnie, prawdopodobnie również do innych kontrolek niż TextBox i nie tylko w jednym oknie aplikacji. Umieszczenie tego menu jako jeden z elementów zasobów, sprawdzi się w tym przypadku znakomicie. O tym jak wygląda takie menu w praktyce, możecie się przekonać pobierając moją autorską aplikację Multime.

Zdefiniujemy zatem nasze założenia. Chcemy stworzyć menu kontekstowe, które będzie spolszczone, będzie zawierać ikony oraz skróty klawiszowe.

Naszym głównym obszarem działań, będzie plik zasobów Resources.xaml, który stworzyliśmy sobie przed chwilą.

Najpierw, utworzymy styl MenuIcon, który zastosujemy poszczególnym elementom menu kontekstowego i który pozwoli na określenie wymiarów ikon w menu kontekstowym:

<Style x:Key="MenuIcon" TargetType="Image">
    <Setter Property="Width" Value="16" />
    <Setter Property="Height" Value="16" />
</Style>

Styl ten, zostanie zastosowany dla elementów typu Image, dla których użyto klucza MenuIcon. Dla każdego takiego obrazu, ustawiamy szerokość i wysokość równą 16.

Po zdefiniowaniu ikony, możemy przystąpić do określenia zasobu menu kontekstowego:

<ContextMenu x:Key="MyClipboardContextMenu">
	<MenuItem Header="Wytnij" Command="Cut" InputGestureText="Ctrl+X">
		<MenuItem.Icon>
			<Image Source="..." Style="{StaticResource MenuIcon}" />
		</MenuItem.Icon>
	</MenuItem>
	<MenuItem Header="Kopiuj" Command="Copy" InputGestureText="Ctrl+C">
		<MenuItem.Icon>
			<Image Source="..." Style="{StaticResource MenuIcon}" />
		</MenuItem.Icon>
	</MenuItem>
	<MenuItem Header="Wklej" Command="Paste" InputGestureText="Ctrl+V">
		<MenuItem.Icon>
			<Image Source="..." Style="{StaticResource MenuIcon}" />
		</MenuItem.Icon>
	</MenuItem>
</ContextMenu>

Jak widać, cały kod jest krótki, schematyczny i raczej nieskomplikowany. Zaczynamy w pierwszej linii, od określenia klucza, który później wykorzystamy do przypisania menu. Następnie w liniach 2-16, dodajemy trzy kolejne elementy menu, ponieważ każdy z nich dodawany jest na tej samej zasadzie, to opiszę tylko element związany z opcją wycinania (linie 2-6).

Zaczynamy w linii 2, od określenia podstawowych właściwości elementu MenuItem. Są to kolejno:

  • Header - nazwa wyświetlana elementu
  • Command - nazwa komendy, jaka ma być wywołana po kliknięciu na ten element. W tym przypadku, korzystamy z komend predefiniowanych (Cut, Copy, Paste), które również automatycznie mają przypisane skróty klawiszowe. Nic nie stoi jednak na przeszkodzie, by podpiąć tutaj własną komendę
  • InputGestureText - choć nasze komendy mają automatycznie przypisane skróty klawiszowe, to o tym czy taki skrót ma się wyświetlić w menu, decydujemy sami, zapisując kombinację klawiszy właśnie w tej właściwości (jeśli właściwość zostawimy pustą, to skrót klawiszowy niezostanie wyświetlony). Jeśli wpisalibyśmy zupełnie inną, niepasującą do naszej komendy kombinację, to kod również by zadziałał, aczkolwiek byłoby to nielogiczne i mylące:)

W liniach 3-5, definiujemy ikonę dla elementu. Po raz kolejny sprytnie wykorzystujemy zagnieżdżanie. Najpierw ikonę umieszczamy w elemencie menu, a następnie w ikonie umieszczamy obrazek. Ścieżkę do obrazku, możemy wskazać za pomocą okienka właściwości Visual Studio (pamiętajcie, że powinien być to obrazek o wymiarach 16x16). Dla naszego obrazka, podpinamy również wcześniej zdefiniowany styl, który określa jego wymiary.

Całą operację powtarzamy jeszcze dwukrotnie, zmieniając kolejne komendy i właściwości.

Aby skorzystać z menu kontekstowego, wystarczy teraz ustawić odpowiednią właściwość dla wybranej kontrolki:

<TextBox ContextMenu="{StaticResource MyClipboardContextMenu}" />

Pamiętajcie o zapewnieniu dostępności do zasobu i relacjach dziedziczenia!

Podsumowanie

W dzisiejszym wpisie, ugruntowaliśmy sobie wiedzę o zasobach i stylach, które możemy wykorzystać w naszych aplikacjach WPF. Style i zasoby są niezwykle ważne, ponieważ pozwalają na tworzenie elastycznego i unikalnego kodu. Jeśli w kodzie, w warstwie prezentacji istnieje ryzyko powtórzenia wybranego fragmentu kodu, to zdecydowanie warto w takim przypadku rozważyć możliwość wykorzystania zasobów.

Zmiana w jednym miejscu, jest zdecydowanie łatwiejsza i bardziej kontrolowana niż zmiana w x miejscach:) A możemy jeszcze przy okazji zaoszczędzić trochę na wielkości poszczególnych plików w naszym projekcie.

Jeśli podoba Ci się ten wpis, sprawdź inne części tutoriala WPF.

Poprzednia część | Następna część

Data ostatniej modyfikacji: 28.08.2013, 14:41.

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus