WPF to Go: sposoby na zachowanie proporcji kontrolek

Dodane przez yosz

Męczyłem się wczoraj z różnymi sposobami żeby kontrolka zachowywała ustalone proporcje. Sposobów w sumie będzie kilka, ale chyba tylko jeden wydaje mi utrzymywalny i daje efekt taki jakiego można się spodziewać. A więc dzisiaj będzie o "aspect ratio".

Post widoczny również na [zine.net]

Jeszcze słowem wstępu. Zmieniłem nazwę cyklu na WPF to Go (głównie żeby pasował do nazwy bloga na zine.net) oraz pozbyłem się numeracji odcinków, żeby nie wprowadzać zamieszania. W międzyczasie dwa "odcinki" cyklu pojawiły się na spotkaniach warszawskiej grupy .NET (WG.NET): wykład o RoutedEvents, oraz godzinna klepanina kodu pt: zróbmy własny custom Panel (układanie kontrolek a'la wachlarz kart na ręku). RoutedEvents posiadam w wersji screencastowej ale bez dźwięku, który leży gdzieś tam i czeka na obrobienie przez życzliwą duszę (jeżeli masz doświadczenie w edycji audio i video zgłoś się do WG.NET - potrzebujemy Cię !!). Custom Panel najprawdopodobniej wrzucę tutaj jak tylko znajdę czas i natchnienie ;). Ok to zaczynamy.

Potrzebowałem aby Grid tudzież inna kontrolka zachowayała proporcje np aby boki miały długości w propocjach 3/2. Po kilku minutach znalazłem kontrolkę Viewbox. Marzenie: w Viewboxa wrzucamy co nam się podoba (sztuk jeden) i ustawiamy właściwość Stretch, która mówi czy zagnieżdżony element będzie zachowywał proporcje (Uniform) czy np. wypełniał całą dostępną przestrzeń (Fill) itp. Wygląda to mniej więcej tak

<Viewbox Stretch="Uniform">

    <!-- Jaka˜ tam kontrolka -->

</Viewbox>


Niestety jest jeden minus: to co jest w Viewboxie musi mieć wprost podane wymiary: Width i Height. Kłóci się to dla mnie z ideą WPF'a gdzie powinno się unikać takich manewrów. Ale spróbować warto. Wrzuciłem w Viewboxa Grida z ustawionymi rozmiarami 300 / 200, włączyłem gridline'y (ShowGridLines="True") i nacisnąłem F5. Zobaczyłem to:

Viewbox powiększa / pomniejsza nie tylko kontrolkę, ale wszystko inne co ma rozmiar np: border'y, gridline'y i zapewne też paddingi i marginy. Słabe...

Podejście drugie: odziedziczę jakiś tam mój panel po Gridzie, w metodzie MeasureOverride  ustawię jeden z rozmiarów "na sztywno" na podstawie drugiego rozmiaru i jakiego DependencyProperty (właściwości) takiego jak "AspectRatio". Do moich celów by wystarczyło (przyjąłem że Grid ma być wyższy niż szerszy, więc Width byłoby wyliczane). Coś mi jednak mówiło, że musi być prostsza metoda... mam za każdym dziedziczyć kontrolkę kiedy chcę zrobić jakąś mini rzecz? To na pewno się da zrobić w XAML'u...

Oczywiście,  że się da ;) Do pomocy przyszedł DataBinding i Convertery. Nie będę się teraz rozpisywał o DataBindingu bo przyjdzie na to czas - jest banalnie prosty. Może wyglądać mniej więcej tak:

<Grid x:Name="PlayersLayout"

           Width="{Binding

                           RelativeSource={RelativeSource self},

                           Path=ActualHeight,

                           Mode=OneWay}">


RelativeSource w Bindingu może wskazywać na dowolny element potomny czy poprzedzający w hierarchi kontrolek (np można użyć FindAncestor zamiast self, który będzie szukać przodka). Użyłem self czyli właśiwość Width będzie powiązana z inną właściwością Grid'a - podaje się ją jako Path (w tym przypadku ActualHeight). Bindowanie jest tylko w jedna stronę (tak na wszelki wypadek i żeby WPF się za bardzo nie męczył. Tym samym udało się spowodować że kontrolka zachowuje proporcje 1:1. W Path niestety nie można wpisać np ActualHeight * 2 ;) Do pomocy przychodzi interfejs IValueConverter.

Czasem zdarzy się że oprócz tego, że chcemy się zbindować do jakiejś wartości, będziemy ją chcieli przekonwertować (najprostszy przykład przekonwertować np typy). W tym celu należy zaimplementować IValueConverter z dwiema metodami Convert i ConvertBack:

    1 [ValueConversion(typeof(Double), typeof(Double))]

    2 public class AspectRatioConverter : IValueConverter

    3 {

    4     #region IValueConverter Members

    5 

    6     public object Convert(object value, Type targetType,

    7         object parameter, CultureInfo culture)

    8     {

    9         Double num = (Double)value;

   10 

   11         if (parameter != null)

   12             num /= System.Convert.ToDouble(parameter, NumberFormatInfo.InvariantInfo);

   13 

   14         return num;

   15     }

   16 

   17     public object ConvertBack(object value, Type targetType,

   18         object parameter, CultureInfo culture)

   19     {

   20         Double num = (Double)value;

   21 

   22         if (parameter != null)

   23             num *= System.Convert.ToDouble(parameter);

   24 

   25         return num;

   26     }

   27 

   28     #endregion

   29 }


Tak naprawdę wszystko co robią te metody to biorą jeden obiekt, mnożą go dzielą przez parametr i zwracają. Atrybut ValueConversion nie jest niezbędny, ale zaleca się stosowanie go, aby inni wiedzieli jak go stosować i/lub podpowiadało nam VS (w końcu metody przyjmują gołe obiekty). Najważniejsze w tym kodzie jest chyba ta linijka i NumberFormatInfo.

num /= System.Convert.ToDouble(parameter, NumberFormatInfo.InvariantInfo);

Oczywiście kiedy parametr w XAMLu przekazałem jako cyfrę 1.5 dostałem wyjątkiem FormatException (w końcu w Polsce używa się przecinka a nie kropki). Zmiana na przecinek oczywiście spowodowała, że rozwiązanie się nie kompilowało. Trzeba przekazać NumberFormatInfo.InvariantInfo (chociaż podejrzewałem że będzie przekazywany InvariantCulture domyślnie... widać nie).

Żeby użyć naszego konwertera w XAMLu trzeba się trochę nagimnastykować. Trzeba stworzyć jego instancję i jakoś wykorzystać w XAML'u - służą do tego Resource'y (znowu  wybiegam trochę z cyklem ale co tam ;)). Najprościej będzie zrobić to np: w oknie, w którym Converter będzie użyty:

<Window

        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        x:Class="Zombiaki.App.MainWin"

        x:Name="Window"

        xmlns:zombiaki="clr-namespace:Zombiaki.UI;assembly=Zombiaki.UI">

        <Window.Resources>

                <zombiaki:AspectRatioConverter x:Key="aspectRatio" />

        </Window.Resources>


Po pierwsze należy zdefinować przestrzeń nazw xml'ową, wskazującą na przestrzeń nazw CLR'ową, a następnie przypisać jakiś klucz naszemu konwerterowi (Jeżeli ktoś się zastanawia co to za namespace: Zombiaki... to jest już inna historia, o której opowiadałem w czwartek na spotkaniu WG.NET ;)). Teraz w naszym Gridzie można zrobić tak (i w każdej innej kontrolce):

<Grid x:Name="PlayersLayout"

            Width="{Binding

            RelativeSource={RelativeSource self},

            Path=ActualHeight,

            Mode=OneWay,

            Converter={StaticResource aspectRatio},

            ConverterParameter=1.2}"

>


Do Bindigu dodajemy elementy Converter (wskazujący na StaticResource, który przed chwilą zdefiniowaliśmy) oraz ConverterParameter, czyli nasz aspect ratio.

Trochę wyprzedziłem cykl opowiadając o DataBindigu i Resource'ach - wydaje mi się jednak że jest to na tyle proste, że można się w tym połapać. Convertery są bardzo proste i bardzo potężne. W połączeniu z DataBindigiem dają nam naprawdę dużo możliwości. Zachęcam do eksperymentowania.

Komentarze

Dodane 11 maja 2008 08:46 przez zine.net.pl

Pingback from zine.net.pl

.NET to go : WPF to Go: sposoby na zachowanie proporcji kontrolek

Dodaj komentarz


 

[b][/b] - [i][/i] - [u][/u]- [quote][/quote]



Podgląd

Dodane 28 sierpnia 2008 13:36 przez