Artykuł

lip 10 2011
0

WPF Tutorial - tworzenie kontrolek użytkownika

Jeden z czynników, który warunkuje dobrego programistę, to umiejętność tworzenia elastycznego i niepowtarzalnego kodu. Jeśli jakiś element naszego kodu jest powtarzalny, bądź schematyczny, to z pewnością warto się zastanowić nad napisaniem metody, która zawrze ten kod i będzie mogła być wykorzystana wielokrotnie. Napisana przez nas aplikacja stanie się bardziej przejrzysta i z pewnością zmaleje również ryzyko popełnienia błędu, a wszelkie zmiany będzie można wykonać w jednym miejscu.

Podobnie sprawa wygląda z GUI. Wielokrotnie tworząc złożone interfejsy użytkownika, tworzymy określoną, powtarzalna kombinację kontrolek, bądź też kontrolkę o specjalnych właściwościach, którą również wykorzystujemy wielokrotnie. Zbrodnią byłoby by w tym przypadku, powtarzać ten sam kod wielokrotnie. Nie chodzi już tu w tym przypadku tylko o zmysł gustu i dobrego smaku w programowaniu, ale o przyszłą elastyczność, bo GUI bywa szczególnie zmienne, a zmiana w jednym miejscu jest stokroć tańsza, aniżeli zmiana w niezliczonej liczbie miejsc w całej aplikacji. Jaki morał, z tego wyjątkowo długiego wstępu? A no taki, że warto zainteresować się tworzeniem własnych kontrolek, czyli w WPFie tzw. kontrolek użytkownika (z ang. UserControl).

Nasz cel - kontrolka przycisku z obrazkiem oraz tekstem

Naszym celem, będzie stworzenie kontrolki, do której będziemy mogli dodać tekst oraz obrazek. Założenie będzie takie, że obrazek znajdować będzie się z lewej strony, a tekst po prawej. W Windows Forms, do tego celu wystarczył zwykły button, w WPF, tak jak wspominałem w poprzednich częściach, w kontrolce Button, możemy bezpośrednio umieścić jeden element zawartości.

Istnieje jednak obejście, które łatwo naprawia ten problem, czyli umieszczenie dowolnego panelu w środku przycisku i w nim zawarcie obrazka, bloku tekstu, czy właściwie dowolnego innego elementu.

Kod takiego przycisku, jest stosunkowo prosty, jednak przyciski tego typu pojawiają się na tyle często w naszych aplikacjach, że warto zastanowić się nad przeniesieniem tego kodu osobnej kontrolki. Co też dzisiaj uczynimy:)

Plan prac

Utworzenie porządnie zrealizowanej kontrolki użytkownika, będzie wymagało trochę pracy. Cały proces podzielimy zatem na etapy:

  1. Utworzenie szkieletu kontrolki
  2. Stylizacja
  3. Utworzenie DependencyProperties
  4. Utworzenie RoutedEvents

Czas przystąpić do pracy:)

Tworzenie szkieletu kontrolki

Pracę z naszą kontrolką, rozpoczynamy oczywiście od uruchomienia Visual Studio i utworzenia nowej solucji. W moim przypadku jest to CustomButtonControl, ale oczywiście możecie sobie ją nazwać jak chcecie. Jeśli macie jakąś inną solucję, czy nawet bezpośrednio projekt, w którym chcecie utworzyć tą kontrolkę, możecie ten punkt pominąć.

Następnie dodajemy nową kontrolkę użytkownika (UserControl (WPF)) do projektu, a jeśli nie mamy samego projektu, to również go tworzymy.

Naszą nową kontrolkę, możemy nazwać np. CustomButton. Oczywiście pamiętajmy o rozszerzeniu xaml.

Po utworzeniu, możemy dodać jej szkielet, poprzez napisanie kodu XAML:

<UserControl x:Class="CustomButtonTutorial.CustomButton"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    mc:Ignorable="d" 
    d:DesignHeight="300" d:DesignWidth="300" Name="btnCustom">
    <Button>
        <WrapPanel>
            <Image/>
            <TextBlock />
        </WrapPanel>
    </Button>
</UserControl>

Większość kodu została wygenerowana automatycznie. Do głównego znacznika, czyli UserControl dodałem nazwę kontrolki, a standardowy panel Grid, zastąpiłem kontrolką przycisku z WrapPanelem w środku. Tak spreparowany kod, kompiluje się chodź na podglądzie pokazywanym w Visual Studio, może wyglądać dziwnie.

Mamy brzydki przycisk, więc pora by zadbać o jego warstwę wizualną:)

Stylizowanie kontrolki

Ze stylizowaniem kontrolki, trochę się już zapoznaliśmy, bo przecież ustawialiśmy marginesy, padding i inne właściwości, które w sposób empiryczny wpływały na wygląd naszych kontrolek.

Dziś chciałbym Wam zaprezentować alternatywą metodę, czyli stylizowanie przy pomocy pliku zasobów.

WPF, umożliwia tworzenie pliku zasobów, w którym mogą znajdować się struktury kontrolek, ich właściwości pogrupowane według klucza, typu itp. Jak za chwilę zobaczycie, znowu przypomina to tworzenie stron WWW, w tym konkretnym przypadku, powinno się to Wam skojarzyć z tworzeniem arkuszy styli CSS.

Aby rozpocząć zabawę ze stylizacją, musimy do naszego projektu dodać element typu ResourceDictionary. No dobra, nie musimy bo możemy zawrzeć określone reguły styli np. w zasobach określonego okna, czy też kontrolki, ale sterowanie regułami wyglądu z jednego miejsca w całej aplikacji jest po prostu bardziej eleganckie i zmniejsza ryzyko popełnienia błędu. Dodajmy zatem do naszego projektu nowy element ResourceDictionary i nazwijmy go np. Theme.xaml.

Następnie wewnątrz węzła ResourceDictionary, dodajemy nowy styl:

<Style x:Key="CustomButtonStyle" TargetType="Button">
    <Setter Property="BorderBrush" Value="DarkGray" />
    <Setter Property="Background" Value="#EEE" />
    <Setter Property="Padding" Value="4" />
    <Style.Resources>
        <Style TargetType="Image">
            <Setter Property="Width" Value="24" />
            <Setter Property="Height" Value="24" />
            <Setter Property="Margin" Value="0,0,3,0" />
        </Style>
        <Style TargetType="TextBlock">
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
    </Style.Resources>
</Style>

Tutaj pojawiło się już bardzo dużo nowych rzeczy, tak więc zacznijmy od początku. W linii 1, ustawiamy klucz (to na jego podstawie będziemy korzystać z określonego stylu, oraz typu elementu docelowego dla jakiego może być aplikowany ten styl). Jeśli nie określilibyśmy klucza, to zostałby on automatycznie zastosowany do wszystkich elementów typu Button - możemy takie coś zastosować gdy chcemy wystylizować określony element w wybrany przez nas sposób w całej aplikacji.

W liniach 2-4, określamy kolejne właściwości, które zastosowane zostaną bezpośrednio do samego przycisku. Właściwości te określamy poprzez podanie ich nazwy oraz wartości.

W liniach 5-13, stylizować będziemy kontrolki użyte wewnątrz przycisku. Ważne w tym przypadku jest użycie Style.Resources, żeby wskazać że chodzi nam o wewnętrzne zasoby tego konkretnego przycisku. W tym przypadku, również określamy typ elementu docelowego (już bez podawania klucza), a następnie jego właściwości.

To na co warto zwrócić tutaj uwagę, to np. to że wymiary obrazka zostały zdefiniowane na kwadrat o rozmiarach 24x24 oraz że obrazek znajduje się po lewej stronie przycisku, a tekst po prawej (wspominałem o tym wcześniej).

Po zdefiniowaniu styli, powinniśmy skonfigurować naszą aplikację, tak aby korzystała z niego. Najłatwiej to zrobić w głównym pliku aplikacji, czyli App.xaml. Modyfikujemy jego zawartość, tak aby wyglądał mniej więcej tak:

<Application x:Class="CustomButtonTutorial.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Theme.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Główna zmiana, to wprowadzenie węzła Application.Resources i zdefiniowanie w nim ścieżki do pliku Theme.xaml, które przechowuje zasób typu ResourceDictionary.

Aby wystylizować teraz nasz przycisk, wystarczy w deklaracji przycisku, dodać kod:

<Button Click="Button_Click" Style="{StaticResource CustomButtonStyle}">
//...
</Button>

Gdzie CustomButtonStyle, to nazwa klucza stylu, wcześniej przez nas zdefiniowanego.

Nasz kod dalej się kompiluje i dodana kontrolka, również powinna wyglądać jako tako. Niestety nie możemy w tej chwili jeszcze ustawić jej żadnego tekstu oraz obrazka, ale zaraz się tym zajmiemy:)

DependencyProperties

Pojawił się problem. Zbudowaliśmy kontrolkę, wystylizowaliśmy ją, ale zarazem zatraciliśmy właściwości charakterystyczne dla obrazka i bloku tekstu. Na szczęście z odsiedzą przychodzą DependencyProperties

Tworząc aplikację, na pewno niejednokrotnie korzystaliście z właściwości. Pojawiały się one choćby w każdej implementacji testowej klasy Person. W WPFie, pojawiają się właściwości nowego rodzaju, które nie są bezpośrednio składowane w klasie jako jej pola, ale zostały ulokowane w specjalnym słowniku. Ponadto każda z tych właściwości, posiada interfejs wyzwalany w momencie zmiany tej właściwości oraz wykorzystuje mechanizm dziedziczenia.

Mechanizm dziedziczenia działa na takiej zasadzie, że jeżeli określona właściwość nie zostanie odnaleziona dla określonego elementu, to idziemy w górę drzewa logicznego, w poszukiwaniu wartości dla tej właściwości (o drzewie logicznym pisałem już wcześniej tutaj).

Jak to działa w praktyce? Przykładowo, jeśli zdefiniujemy wielkość obramowania równą 5, dla określonego panelu, to taka wartość zostanie przypisana wszystkim elementom wewnątrz tego panelu, niezależnie od poziomu zagłębienia, aż do momentu, w którym nie zostanie nadpisana przez inną wartość w jakimś elemencie.

Co to nam daje? DepedencyProperties, pozwoli na zdefiniowanie właściwości, które zostaną następnie odziedziczone przez TextBlock oraz Image wewnątrz przycisku i zmapowane na odpowiednie właściwości tych kontrolek.

Definiowanie właściwości jest bardzo proste, a ich kod musimy umieścić w pliku Code-Behind naszej kontrolki. Spójrzmy na pierwszą właściwość, czyli właściwość Text:

public static readonly DependencyProperty TextProperty =
    DependencyProperty.Register("Text", typeof(String),
    typeof(CustomButton), new FrameworkPropertyMetadata(string.Empty));

public String Text
{
    get { return GetValue(TextProperty).ToString(); }
    set { SetValue(TextProperty, value); }
}

W pierwszych trzech liniach, rejestrujemy naszą właściwość - TextProperty - nazwa musi koniecznie kończyć się słowem Property. Do rejestracji, wykorzystywana jest metoda Register, która w tej postaci przyjmuje cztery argumenty:

  • Nazwę właściwości z jakiej będziemy korzystać w kontrolce
  • Typ danych przechowywanych
  • Typ danych rodzica
  • Obiekt klasy FrameworkPropertyMetadata, w którym podajemy wartość domyślną właściwości

W liniach 5-9, definiujemy normalną właściwość, która będzie opakowywać powyższy kod - zwróćcie uwagę na sposób ustawiania i pobierania właściwości!

Podobny kod, tworzymy dla właściwości ImageSource:

public static readonly DependencyProperty ImageSourceProperty =
    DependencyProperty.Register("ImageSource", typeof(ImageSource),
    typeof(CustomButton), new FrameworkPropertyMetadata(null));

public ImageSource ImageSource
{
    get { return GetValue(ImageSourceProperty) as ImageSource; }
    set { SetValue(ImageSourceProperty, value); }
}

Po zdefiniowaniu właściwości, musimy je zbindować w naszej kontrolce (tutaj kłania się wpis o bindowaniu). Modyfikujemy zatem kod XAML kontrolki:

<UserControl x:Class="CustomButtonTutorial.CustomButton"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    mc:Ignorable="d" 
    d:DesignHeight="300" d:DesignWidth="300" Name="btnCustom">
    <Button Style="{StaticResource CustomButtonStyle}">
        <WrapPanel>
            <Image Source="{Binding ElementName=btnCustom, Path=ImageSource}"/>
            <TextBlock Text="{Binding ElementName=btnCustom, Path=Text}" />
        </WrapPanel>
    </Button>
</UserControl>

W bindowaniu wskazujemy, że element który nas interesuje to btnCustom (nazwa naszej kontrolki nadana wcześniej), a ścieżka do bindowania to właściwości odpowiednio ImageSource oraz Text, które zdefiniowaliśmy przed chwila.

Kolejny postęp. Kod się kompiluje, w naszym przycisku możemy ustawić tekst oraz obrazek. Niestety nie możemy za to wykonać jeszcze żadnej akcji..

RoutedEvents

RoutedEvents, to technika obsługi zdarzeń używana w WPF, która oprócz standardowej obsługi zdarzenia dla aktualnego elementu pozwala na poruszanie się w górę, lub w dól po drzewie wizualnym WPF (wspominałem o nim we wcześniejszych wpisach). Mamy zatem trzy możliwe opcje:

  • Tunelling - zdarzenie przechwytywane jest w głównym elemencie drzewa i wędruje w dół, aż do momentu napotkania kontrolki, która je wyzwoliła, lub do oznaczenia zdarzenia jako wykonane
  • Bubbling - zdarzenie wyzwolone jest na elemencie źródłowym i porusza się w górę drzewa aż do korzenia, lub do oznaczenia zdarzenia jako wykonane
  • Direct - zdarzenie wyzwolone jest na elemencie źródłowym i musi zostać przez niego obsłużone

W naszym przypadku, w którym przycisk jest wewnątrz znacznika UserControl, musimy skorzystać z opcji Bubbling, ponieważ nasze zdarzenie będzie szło w górę drzewa, czyli naszym celem jest skojarzenie zdarzenia Click z naszą kontrolką użytkownika.

Zasadniczo, jeśli zamiast UserControl, umieścilibyśmy na głównym poziomie drzewa Button, to ominąłby nas ten problem, ale ten przykład świetnie nadaje się w celach instruktażowych.

Napiszmy zatem kod, który wybubla nasze zdarzenie:

public static readonly RoutedEvent ClickEvent =
    EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble,
    typeof(RoutedEventHandler), typeof(CustomButton));

public event RoutedEventHandler Click
{
    add { AddHandler(ClickEvent, value); }
    remove { RemoveHandler(ClickEvent, value); }
}

Kod wydaje się być bardzo podobny do DependencyProperties. Najpierw rejestrujemy nowe zdarzenie za pomocą metody RegisterRoutedEvent, która przyjmuje następujące parametry:

  • Nazwę nowego zdarzenia
  • Wartość enumeracji określający sposób wyzwalania zdarzenia
  • Typ obsługiwanego zdarzenia
  • Typ danych rodzica

Aby teraz, obsłużyć zdarzenia kliknięcia, dodajemy już standardową metodę obsługi zdarzenia, w której wywołujemy stworzone przez nas zdarzenie. Dzięki temu użytkownicy będą mogli podpinać własne zdarzenia obsługi kliknięcia, dokładnie tak samo jak robili w przypadku normalnego przycisku:

private void Button_Click(object sender, RoutedEventArgs e)
{
    RaiseEvent(new RoutedEventArgs(ClickEvent));
}

I aktualizujemy kod XAML kontrolki:

<UserControl x:Class="CustomButtonTutorial.CustomButton"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    mc:Ignorable="d" 
    d:DesignHeight="300" d:DesignWidth="300" Name="btnCustom">
    <Button Click="Button_Click" Style="{StaticResource CustomButtonStyle}">
        <WrapPanel>
            <Image Source="{Binding ElementName=btnCustom, Path=ImageSources}"/>
            <TextBlock Text="{Binding ElementName=btnCustom, Path=Text}" />
        </WrapPanel>
    </Button>
</UserControl>

Nasza kontrolka jest gotowa, kompiluje się i wygląda jako tako. Czas sprawdzić efekty naszej pracy:)

Test

Aby przeprowadzić test naszej kontrolki, otwórzmy plik MainWindow.xaml, który utworzył się z naszym projektem. Do znacznika Window dodajmy nowy namespace:

xmlns:my="clr-namespace:CustomButtonTutorial

Który wskazuje na namespace użyty w naszym projekcie, a konkretniej ten, w którym znajduje się kontrolka - akurat w tym przypadku jest to ten sam, w którym znajduje się nasz aktualny plik - MainWindow.xaml.

Przedrostek my zdefiniowany w deklaracji namespace'a, będzie później używany do umieszczenia kontrolek, które w tym namespacie się znajdują. Żeby umieścić kontrolkę w kodzie XAML, musimy wpisać mniej więcej taki kod:

<my:CustomButton Text="Test" Width="100" Height="35" 
    ImageSource="/CustomButtonTutorial;component/Images/global.png" 
    Click="CustomButton_Click"/>

Tak jak wspomniałem, wykorzystujemy przedrostek my. Jak widać, działają właściwości Text oraz ImageSource, które możemy ustawić nawet za pomocą okna Properties w VisualStudio. Podobnie zresztą jak zdarzenie Click. Kod je obsługujący znajdziemy oczywiście w pliku Code-Behind:

private void CustomButton_Click(object sender, RoutedEventArgs e)
{
    MessageBox.Show("Alt Control Delete");
}

A efekt, całej naszej dzisiejszej pracy, widoczny jest na screenie 4:

Działający projekt, utworzony na potrzeby dzisiejszego wpisu, do pobrania z działu Download.

Podsumowanie

Stworzyliśmy dziś całkiem pożyteczną kontrolkę użytkownika, a przy okazji poznaliśmy kilka kluczowych elementów ważnych w WPF, czyli m.i. DependencyProperties oraz RoutedEvents.

Warto wiedzieć, że oprócz kontrolek tworzonych na bazie innych kontrolek, możemy również tworzyć własne kontrolki. W tym celu, potrzebne jest jednak dodatkowe oprogramowanie, takie jak np. Microsoft Expression.

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:40.

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

Send to Kindle

Komentarze

blog comments powered by Disqus