2013. május 10., péntek

WPF #11 Dependency Property

A Wpf egyik (ha nem a legnagyobb) újítása a .Net rendszerben egy újfajta tulajdonság - a DependencyProperty - bevezetése volt.

Mindenféle ismeret nélkül - akaratlanul - számtalan alkalom adódhatott, ahol ezen tulajdonság felhasználásra került. Lehetett az stílus definíció vagy animáció.

Az alábbi gomb definíció két darab DependencyProperty használatával állítja be a gomb szélességét és magasságát.
<Button Width="100" Height="20"/>

Ezt a kódot bárhova beillesztve egy meghatározott méretű gomb kerül kirajzolásra. A gomb megjelenítéséhez azonban szükség van egy sor egyéb tulajdonságra (pl háttérszín), amelyek nem kerültek meghatározásra, a rendszer mégis adott neki egy meghatározott alapértéket...

Az eddig adatkötés kivétel nélkül szimpla .Net tulajdonságok kerültek felhasználásra, így jogosan merül fel a kérdés, mivel tud többet, illetve miben különbözik tőle a DependencyProperty.

Minden .Net tulajdonság egy adott osztály privát tagját éri el, ezzel szemben a DependencyProperty dinamikusan kerül meghatározásra, amikor a konkrét értéke lekérdezésre kerül. (Ezt talán a legegyszerűbb egy Key/Value gyűjteményhez hasonlítani, ahol a kulcs alapján lehet az értéket lekérdezni).

Minden DependencyProperty csak egyszer kerül eltárolásra, és a keretrendszer mindössze a változásokat tárolja külön. Ezzel óriási memória foglalási problémát old fel. Egyetlen Button is számos tulajdonsággal rendelkezik amelyeket ha mind tárolni kellene akkor hamar betelne a memória.

Minden XAML-ben elhelyezett elem tulajdonsága stílusok segítségével is meghatározható. ( ajánlott)
Stíluson belül csak DependencyProperty-re lehet hivatkozni.

Szintén fontos megjegyezni, hogy csak DependencyProperty-t lehet animációban célként megadni.

Az egymásba ágyazott elemek - általában - öröklik a szülő objektum beállított DependencyProperty értékét. Pl egy Grid megadott fontSize értékét felveszi a benne elhelyezett TextBlock.

Minden DependencyProperty automatikusan rendelkezik számos callBack függvénnyel, amelyek az értékének megváltozásakor azonnal értesítést küldenek. Ezen tuljadonsága tökéletesen kihasználható adatkötésre.

Zárójelben jegyzem meg, hogy egy egyszerű adatkötéshez inkább érdemes .Net tulajdonságot használni, mert egyetlen DependecyProperty létrehozása, felparaméterezése jóval több munkát igényel, mint egy szimpla tulajdonság és az INotifyPropertyChanged interface használata.
 
Mivel egy DependencyProperty értékének beállítására számos lehetőség van, ezt a .Net keretrendszer az alábbi prioritási listát használja a végső érték meghatározására.

1. Animációban meghatározott érték
2. Adatkötés
3. Locálisan megadott érték
4. Áltatunk írt stílusban meghatározott trigger hatására
5. Általunk írt Templateben meghatározott trigger hatására
6. Általunk írt stílusban meghatározott érték
7. A Wpf által alapból beállított stílusban meghatározott trigger hatására
8. A Wpf által alapból beállított stílusban meghatározott érték
9. Örökölt érték
10. A DependencyProperty alapértéke.

A 10-es pontot látva egyértleművé válik, hogy az elöbb kirajzolásra került gomb a Width és a Height tulajdonságán kívűl honnan veszi az összes többi tulajdonságának az alapértékét.
 
Az eddigiektől eltérően az alábbiakban egy kész program minden forráskódja lesz a kiindulópont.
A program egy újfajta Grid elmet hoz létre (GridWithHueProperty).
Ezen Grid hátterét a Hue érték megadásával lehet megváltoztatni.

A hue a HSB (hue-saturation-brightness) színmód színkód értéke. Az alábbi képen a jobb oldalon látható színátmenet a színpaletta összes maximális telítettségű és fényerejű színét tartalmazza. Ez a hue. A hue megváltoztatásával könnyen és gyorsan lehet pirosból kéket vagy akármilyen más színt készíteni.


A Grid elem háttérszínének megadásához RGB színösszetevőkre van szükség. Szerencsére a HSB könnyen RGB-re alakítható.

Class: ColorMode
using System;
using System.Windows.Media;

namespace Wpf11_DependencyProperty
{
    public class ColorMode
    {
        public static Color HsbToRgb(double hue, double saturation, double brightness)
        {
            byte r = 0, g = 0, b = 0;
            if (Math.Abs(saturation - 0) < Double.Epsilon)
            {
                r = g = b = (byte)(brightness * 255.0f + 0.5f);
            }
            else
            {
                var h = (hue - Math.Floor(hue)) * 6.0f;
                var f = h - Math.Floor(h);
                var p = brightness * (1.0f - saturation);
                var q = brightness * (1.0f - saturation * f);
                var t = brightness * (1.0f - (saturation * (1.0f - f)));

                switch ((byte)h)
                {
                    case 0:
                        r = (byte)(brightness * 255.0f + 0.5f);
                        g = (byte)(t * 255.0f + 0.5f);
                        b = (byte)(p * 255.0f + 0.5f);
                        break;
                    case 1:
                        r = (byte)(q * 255.0f + 0.5f);
                        g = (byte)(brightness * 255.0f + 0.5f);
                        b = (byte)(p * 255.0f + 0.5f);
                        break;
                    case 2:
                        r = (byte)(p * 255.0f + 0.5f);
                        g = (byte)(brightness * 255.0f + 0.5f);
                        b = (byte)(t * 255.0f + 0.5f);
                        break;
                    case 3:
                        r = (byte)(p * 255.0f + 0.5f);
                        g = (byte)(q * 255.0f + 0.5f);
                        b = (byte)(brightness * 255.0f + 0.5f);
                        break;
                    case 4:
                        r = (byte)(t * 255.0f + 0.5f);
                        g = (byte)(p * 255.0f + 0.5f);
                        b = (byte)(brightness * 255.0f + 0.5f);
                        break;
                    case 5:
                        r = (byte)(brightness * 255.0f + 0.5f);
                        g = (byte)(p * 255.0f + 0.5f);
                        b = (byte)(q * 255.0f + 0.5f);
                        break;
                }
            }

            return Color.FromRgb(r, g, b);
        }
    }
}


Class: GridWithHueProperty
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace Wpf11_DependencyProperty
{
    public class GridWithHueProperty : Grid
    {
        public static readonly DependencyProperty HueProperty =
               DependencyProperty.Register("Hue", typeof(Double),
                                           typeof(GridWithHueProperty),
                                           new FrameworkPropertyMetadata(0.1,
                                               OnHuePropertyChanged,
                                               OnCoerceHueProperty),
                                               OnValidateHueProperty);

        // ********************************************************************

        public Double Hue
        {
            get { return (Double) GetValue(HueProperty); }
            set { SetValue(HueProperty, value); }
        }

        // ********************************************************************

        private static void OnHuePropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
        {
            var sPanel = source as GridWithHueProperty;
            var newValue = (Double)e.NewValue;

            if (sPanel != null)
            {
                sPanel.Background = new SolidColorBrush(ColorMode.HsbToRgb(newValue / 360, 1, 1));
            }

        }

        // ********************************************************************

        private static object OnCoerceHueProperty(DependencyObject d, object basevalue)
        {
            return (double)basevalue % 360;
        }

        // ********************************************************************

        private static bool OnValidateHueProperty(object value)
        {
            return (double)value >= 0;
        }
    }
}


XAML
<Window x:Class="Wpf11_DependencyProperty.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Wpf11_DependencyProperty"
        Title="MainWindow" Height="350" Width="525">
    

    <StackPanel Orientation="Vertical">
        <local:GridWithHueProperty Height="50" Hue="0" x:Name="MyGrid"/>
        
        <StackPanel Orientation="Horizontal">
            <Button Content="150" Width="100" Click="ChangeTo150"/>
            <Button Content="-1" Width="100" Click="ChangeToNegative"/>
            
            <Button Content="Start Anim" Width="100">
                <Button.Triggers>
                    <EventTrigger RoutedEvent="Button.Click">
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation From="0" To="360" Duration="0:0:10" Storyboard.TargetName="MyGrid" Storyboard.TargetProperty="Hue"/>
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </Button.Triggers>
            </Button>
            
            
        </StackPanel>
        
        <TextBlock Text="Error message:"/>
        <TextBox Height="100" Name="ErrorTbox"/>
        
        <TextBlock Text="Info message:"/>
        <TextBox Height="30" Name="InfoTbox"/>
    </StackPanel>
    
</Window>


XAML mögöttes kód
using System;
using System.ComponentModel;
using System.Windows;

namespace Wpf11_DependencyProperty
{

    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();

            var dp = DependencyPropertyDescriptor.FromProperty(GridWithHueProperty.HueProperty, typeof(GridWithHueProperty));
            dp.AddValueChanged(MyGrid, Handler);
        }

        private void Handler(object sender, EventArgs eventArgs)
        {
            var msp = sender as GridWithHueProperty;
            var helper = DependencyPropertyHelper.GetValueSource(msp, GridWithHueProperty.HueProperty);
            InfoTbox.Text = msp.Hue.ToString();
        }


        private void ChangeTo150(object sender, RoutedEventArgs e)
        {
            MyGrid.Hue = 150;
        }

        private void ChangeToNegative(object sender, RoutedEventArgs e)
        {
            try
            {
                MyGrid.Hue = -1;
            }
            catch (Exception exception)
            {
                ErrorTbox.Text = exception.ToString();
            }
        }
    }
}

ColorMode osztály

Amely egyetlen statikus függvénnyel rendelkezik és a megadott HSB értéket RGB-re alakítja.
A bejövő paraméterek 0 és 1 közötti értéket vehetnek fel.

A hue értéke egy 360 fokos körcikk 360-ad része. 
A stauration és a brightness egy 100% skála 0-1 közé konvertált értéke. 

A program csak maximális színekkel foglalkozik, ezért a saturation és a brightness paraméterek mindig 1 értéket kapnak.


GridWithHueProperty osztály.

Amely magát a DependencyProperty-t tartalmazza.
Ez az újonnan létrehozott Grid típus, amely a Grid-ből kerül származtatásra
Minden DependenyProperty publikus, static és  Property végződésű.

public static readonly DependencyProperty HueProperty =
               DependencyProperty.Register("Hue", typeof(Double),
                                           typeof(GridWithHueProperty),
                                           new FrameworkPropertyMetadata(0.1,
                                               OnHuePropertyChanged,
                                               OnCoerceHueProperty),
                                               OnValidateHueProperty);

A tulajdonságot a DependencyProperty.Register statikus metódus használatával kell regisztrálni.
Az első paraméter a tulajdonság neve (immáron Property végződés nélkül).
A második a tulajdonság típusa.
A harmadik paraméter használatával MetaAdatok adhatóak meg, mely segítségével jelenleg az alapérték (0.1), valamint 3 callBack függvény kerül beállításra.

Mind a register mind a FrameworkPropertyMetadata túlterhelt függvények.

Ezt követi egy szokásos .Net tulajdonság, amely egy szimpla warpper, a Hue tulajdonságon keresztül biztosít hozzáférést a DependencyProperty-hez mind az XAML-ben mind a kódban.

public Double Hue
        {
            get { return (Double) GetValue(HueProperty); }
            set { SetValue(HueProperty, value); }
        }
 
A metadatként megadott 3 callBack függvény szolgáltat felületet a tulajdonság változásának észlelésére illetve az ilyenkor szükséges kód lefuttatására.

Az OnHuePropertyChanged metódus, mint ahogy a neve is mutatja az érték megváltozásakor kerül meghívásra. A három függvény közül ez fut le legutoljára (ha eljut idáig a program)
Itt kerül kiszámításra a háttér színe a megadott hue érték alapján.

var sPanel = source as GridWithHueProperty;
var newValue = (Double)e.NewValue;
if (sPanel != null)
{
    sPanel.Background = new SolidColorBrush(ColorMode.HsbToRgb(newValue / 360, 1, 1));
}

Az OnCoerceHueProperty metódus a beérkező paramétert helyezi a megfelelő értéktartományba.
Ezt legegyszerűbben egy Slider elem segítségével lehet demonstrálni, ahol az elemnek van egy minimum és egy maximum értéke. A bejövő értéket itt lehet ellenőrizni és szükség esetén min/max közé helyezni.
Jelen esetben az érték csak 0 és 360 között lehet.

private static object OnCoerceHueProperty(DependencyObject d, object basevalue)
{
    return (double)basevalue % 360;
}

Az OnValidateHueProperty a beérkező elem helyességét vizsgálja. 
Amennyiben false értékkel tér vissza, úgy futás közben kivétel keletkezik.
Jelen esetben csak pozitív érték a megengedett, negatív esetén hibát generál.

private static bool OnValidateHueProperty(object value)
{
    return (double)value >= 0;
}

XAML

 Az XAML-ben elsőként a szükséges NameSpace deklarálására van szükség.
xmlns:local="clr-namespace:Wpf11_DependencyProperty"

Nálam a program a Wpf11_DependencyProperty namespace alatt található, ide a te programod által használt namespace nevét kell írni.

A <local:GridWithHueProperty Height="50" Hue="0" x:Name="MyGrid"/> elhelyezi az újonnan létrehozott  Grid típust a stackpanelben és a hue értékét 0-ra állítja (piros lesz a háttérszín)

Az elhelyezett 3 gomb funkciói:
A hue értékét -1-re állítja
A hue értékét 150-re állítja

A harmadik gomb egy animációt indít:
<Button.Triggers>
    <EventTrigger RoutedEvent="Button.Click">
        <BeginStoryboard>
            <Storyboard>
                <DoubleAnimation From="0" To="360" Duration="0:0:10" Storyboard.TargetName="MyGrid" Storyboard.TargetProperty="Hue"/>
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</Button.Triggers>

Gombnyomás hatására 0-tól(From) 360-ig(To) indít egy animációt amely 10 másodpercig tart (Duration="0:0:10").
A Storyboard.TargetName="MyGrid" az előbb létrehozott Grid nevére mutat
A Storyboard.TargetProperty="Hue" pedig arra a tulajdonságra, amelyet animálni kell.

MainWindow osztály

A MainWindow osztályban két függvény nem több mint a gombokra elhelyezett Click esemény feldolgozása.
Az egyik esetében 150-re állítódik a hue értéke, ami rögtön látható is.
A másik esetében -1 értéket próbál felvenni, ezt viszont az előbb említett OnValidateHueProperty nem engedi, és kivétel történik.


A konstruktorban található még az alábbi két sor
var dp = DependencyPropertyDescriptor.FromProperty(GridWithHueProperty.HueProperty, typeof(GridWithHueProperty));

dp.AddValueChanged(MyGrid, Handler);

Használatával a DependencyProperty osztályon kívül is értesítés szerezhető a tulajdonság értékének megváltozásáról.

Amennyiben az érték változik, úgy meghívásrakerül az alábbi függvény, amely a tulajdonság aktuális értékét kiírja.
private void Handler(object sender, EventArgs eventArgs)
{
    var msp = sender as GridWithHueProperty;
    InfoTbox.Text = msp.Hue.ToString();

2013. április 23., kedd

Wpf #10 Trigger

Egy program fejlesztése során számos alkalommal fordul elő, hogy egy adott tulajdonság megváltoztatása csak bizonyos esemény hatására kell, hogy megtörténjen. Az adott esemény lehet az oldal betöltődése, de lehet akár egy tetszőlegesen tárolt változó egy előre meghatározott értékének felvétele.

A Wpf alatt erre szolgál a trigger, amelyből három féle létezik.
Property trigger: amely akkor aktivizálódik, ha egy DependencyProperty értéke megváltozik.
Data trigger: amely akkor aktivizálódik, ha egy normál .Net tulajdonság értéke megváltozik
Event trigger: amely tetszőleges Routed Event hatására aktivizálódik

A DependencyProperty hasonló a normál tulajdonságokhoz. Egy Wpf elem számos DependencyProperty-vel rendelkezik. Pl. Width, Height.
Mivel egy adott elem megjelenítését számos tulajdonság határozza meg, könnyen belátható, hogy néhány elem kirajzolása szinte  felfalná az összes memóriát.
A DependencyProperty ennek megakadályozására lett kitalálva.
Az adott elem összes tulajdonsága egy alapértékkel rendelkezik, amely csak egyszer kerül eltárolásra egy programban. Minden esetben, amikor egy elem tulajdonsága felülírásra kerül (pl Width="100") akkor  csak ez az érték kerül külön tárolásra.
Számos események látszó tulajdonság is DependencyProperty-ként kerül eltárolásra. Ilyen pl. az IsMouseOver amely a nevéből adódóan a kurzort figyeli, és amennyiben az adott elem fölött található, akkor true értéket vesz fel.

A Routed Event nagyon leegyszerűsítve megfelel egy tetszőleges eseménynek. Pl: Loaded vagy Click.

A triggerek elhelyezése az adott elemhez definiált stíluson belül történik. A beállításokat továbbra is setter elemek végzik, akárcsak egy normál stílus esetén. Mivel az adott elem tulajdonságának triggerel történő beállítása a stíluson keresztül történik, ezért az elem azonos tulajdonságát is kötelezően a stílusban kell megadni.

Egy trigger definíciója az alábbihoz hasonló:
<Grid>
 <Grid.Resources>
  <Style TargetType="{x:Type Button}" x:Key="MyButton">
   <Style.Triggers>
    <trigger>
       trigger által megkívánt teendők
    </trigger>
   </Style.Triggers>
  </Style>
 </Grid.Resources>
 
 <Button Style="{StaticResource MyButton}"/>
 
</Grid>

Egy tetszőleges Resource-ban deklarálásra kerül az adott elemhez tartozó stílus. Jelen esetben egy Button. A stíluson belül deklarálásra kerül a trigger, amely a három különöző trigger esetén más néven kerül bevezetésre.

Az adott elem stílusát beállítva a trigger élesítésre kerül, és a feltételeknek megfelelő pillanatban kifejti a hatását.

Event trigger

Egy Wpf elem számos esemény kezelésére képes. Ilyen pl.: Loaded vagy Click.
Amennyiben egy esemény meghívásra kerül a trigger aktiválódik.  
Az event triggereket animációk irányítására lehet használni.
Az alábbi példa a Button betöltésekor indít egy animációt, amely a Button szélességét változtatja meg 100-ról 200-ra 2 másodperc alatt.

<Grid>
 <Grid.Resources>
  <Style TargetType="{x:Type Button}" x:Key="MyButton">
   <Style.Triggers>
    <EventTrigger RoutedEvent="Loaded">
     <BeginStoryboard>
      <Storyboard>
       <DoubleAnimation From="100" To="200"  Duration="0:0:2" Storyboard.TargetProperty="Width"/>
      </Storyboard>
     </BeginStoryboard>
    </EventTrigger>
   </Style.Triggers>

   <Setter Property="Width" Value="100"/>
   <Setter Property="Height" Value="20"/>
  </Style>
 </Grid.Resources>
 
 <Button Style="{StaticResource MyButton}"  Content="Click"/>
 
</Grid>

Az event trigger a stíluson belül az EventTrigger deklarációval határozható meg. A RoutedEvent attributum segítségével adható meg a figyelni kívánt esemény (Loaded).
Mivel az event trigger animációk irányítására szolgál, ezért egy storyboard megadása szükséges.
Ebben a storyboard-ban kell minden animációt deklarálni amelyet az adott elem meghatározott eseményekor le kell játszani.

Property trigger

A Wpf-ben a property trigger az alapértelmezésű trigger. Bármely DependencyProperty megfigyelhető vele.

Az alábbi példa ezt szemlélteti
<Grid>
 <Grid.Resources>
  <Style x:Key="MyButton" TargetType="{x:Type Button}">
   <Style.Triggers>
    <Trigger Property="IsMouseOver" Value="True">
     <Setter Property="Width" Value="200"/>
    </Trigger>
   </Style.Triggers>
   
   <Setter Property="Width" Value="100"/>
   <Setter Property="Height" Value="20"/>
  </Style>
 </Grid.Resources>
 
 <Button Style="{StaticResource MyButton}" Content="Click"/>
 
</Grid>

Az előző példától eltérően a stíluson belül egy szimpla Trigger kerül deklarálásra.
A Property tulajdonság határozza meg, hogy mely DependencyProperty-t kell figyelni, a Value pedig azt az értéket tartalmazza, amely esetén a triggernek aktiválódnia kell.

Data trigger

Segítségével tetszőleges tulajdonság értéke megfigyelhető. A megfigyelt tulajdonságnak olyan osztályban kell lennie, amely megvalósítja az INotifyPropertyChanged Interface-t, valamint a tulajdonságnak elérhetőnek kell lennie a DataContext-en keresztül.

Az alábbi példa egy boolean típusú tulajdonság felhasználásával változtatja meg a Button szélességét.
Ebben az esetben kódot is kell írni a megjelenítés mögé.

<Grid>
 <Grid.Resources>
  <Style x:Key="MyButton" TargetType="{x:Type Button}">
   <Style.Triggers>
    <DataTrigger Binding="{Binding ChangeWidth}" Value="True">
     <Setter Property="Width" Value="200"></Setter>
    </DataTrigger>
   </Style.Triggers>

   <Setter Property="Width" Value="100"/>
   <Setter Property="Height" Value="20"/>
  </Style>
 </Grid.Resources>

 <Button Style="{StaticResource MyButton}" Content="Just a button"/>

 <Button VerticalAlignment="Bottom" Height="20" Content="Change Width" Click="ButtonBase_OnClick"/>
</Grid>

A trigger-t a stíluson belül a DataTrigger deklarációval kell megkezdeni.  A Binding értékadásával lehet beállítani a figyelni kívánt tulajdonságot, a Value szerepe itt is az itt megadott érték figyelése.

public partial class DataTrigger : Window, INotifyPropertyChanged
{
 private bool _changeWidth;
 public bool ChangeWidth
 {
  get { return _changeWidth; }
  set { _changeWidth = value; OnPropertyChanged("ChangeWidth"); }
 }

 // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

 public event PropertyChangedEventHandler PropertyChanged;
 public void OnPropertyChanged(string propertyName)
 {
  if (PropertyChanged != null)
   PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }

 // --------------------------------------------------------------------

 public DataTrigger()
 {
  InitializeComponent();
  ChangeWidth = false;
  DataContext = this;
 }

 private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
 {
  ChangeWidth = !ChangeWidth;
 }
}

Fordítás után az alul található gomb a ChangeWidth tulajdonság értékét változtatja, és a trigger ennek függvényében változtatja a gomb szélességét.

MultiTrigger - VAGY kapcsolat

Egy trigger nem feltétlenül csak egy bizonyos feltétel meglététől aktiválódhat. Előfordulhat, hogy ugyanaz a trigger több más - egymástól teljesen független - feltétel megléte esetén is kell, hogy végrehajtsa a megkívánt beállításokat.

Ezt nevezik MultiTrigger-nek.
MultiTrigger-ből is többféle létezik (MultiTrigger, MultiDataTrigger), azonban a logikai VAGY kapcsolathoz az előbb megismert triggerek bármelyike felhasználható.

Az alábbi példa egy egyszerű VAGY kapcsolatot mutat be, amely során a Button szélessége megváltozik, ha az egérrel a kurzor felé kerül vagy ha a Button magassága 30 lesz.
Fordítás után a Button magasságát az alul található gomb segítségével lehet növelni.

<Grid>
 <Grid.Resources>
  <Style x:Key="MyButton" TargetType="{x:Type Button}">
   <Style.Triggers>
    <Trigger Property="IsMouseOver" Value="True">
     <Setter Property="Width" Value="200"/>
    </Trigger>
    
    <Trigger Property="Height" Value="30">
     <Setter Property="Width" Value="200"/>
    </Trigger>
   </Style.Triggers>

   <Setter Property="Width" Value="100"/>
   <Setter Property="Height" Value="20"/>
  </Style>
 </Grid.Resources>

 <Button Name="Btn" Style="{StaticResource MyButton}" Content="Click"/>

 <Button VerticalAlignment="Bottom" Height="20" Content="Increment Height" Click="ButtonBase_OnClick"/>

</Grid>

A hozzá tartozó kód, amely az alsó gomb click eseményét kezeli.
public partial class MultiTrigger_Or : Window
{
 public MultiTrigger_Or()
 {
  InitializeComponent();
 }

 private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
 {
  Btn.Height += 10;
 }
}

A gomb megnyomására a középső Button magassága 10-el növekszik. Amint eléri a 30-at, a szélessége a triggerben megadott 200-ra változik. Majd miután 40-re növekszik visszaáll az eredeti értékre.

Itt fontos megjegyezni, hogy az eredeti értékre történő visszaállítással nem kell foglalkozni, azt a keretrendszer automatikusan elvégzi.

Egy trigger logikai VAGY kapcsolata számos további triggert tartalmazhat, ezáltal tetszőleges számú feltételhez köthető a kívánt beállítás aktiválása.

MultiTrigger - ÉS kapcsolat

Előfordulhat, hogy egy adott trigger aktiválódására csak több feltétel együttes megléte esetén van szükség. Erre szolgál a MultiTrigger.
A MutiTrigger csak DependencyProperty vizsgálatára jó.
Amennyiben .Net tulajdonságok vizsgálatára van szükség, úgy a MultiDataTrigger használata kell.

Az alábbi példában a Button szélessége csak abban az esetben változik meg, ha a Button magassága 30 és az egér is a gomb felett van.

<Grid>
 <Grid.Resources>
  <Style x:Key="MyButton" TargetType="{x:Type Button}">
   <Style.Triggers>
    <MultiTrigger>
     <MultiTrigger.Conditions>
      <Condition Property="IsMouseOver" Value="True"/>
      <Condition Property="Height" Value="30"/>
     </MultiTrigger.Conditions>

     <Setter Property="Width" Value="200"/>
    </MultiTrigger>

   </Style.Triggers>

   <Setter Property="Width" Value="100"/>
   <Setter Property="Height" Value="20"/>
  </Style>
 </Grid.Resources>

 <Button Name="Btn" Style="{StaticResource MyButton}" Content="Click"/>

 <Button VerticalAlignment="Bottom" Height="20" Content="Increment Height" Click="ButtonBase_OnClick"/>

</Grid>

A hozzá tartozó kód:
public partial class MultiTrigger_And : Window
{
 public MultiTrigger_And()
 {
  InitializeComponent();
 }

 private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
 {
  Btn.Height += 10;
 }
}

A MultiTrigger feltételeit egyesével meg kell adni a MultiTrigger Conditions tulajdonságában. Amennyiben minden feltétel teljesül a triger aktiválódik.

2013. április 18., csütörtök

Wpf #9 Stílusok

Mint sokmás deklaratív nyelv, az XAML se kerülte el a stílusok létrehozásának lehetőségét. Bár használata hosszútávon felbecsülhetetlen ez nem jelenti azt, hogy kötelező. Egy Wpf program nélküle is fut, segítségével viszont szép és jól elosztott kódot lehet készíteni.

Hagyományos módszer használatával az adott elem egyes tulajdonságai direkt módon kerülnek beállításra. Több azonos tulajdonságú elem esetén ugyanazokat a beállításokat mindenhol újra jelölni kell.

Az alábbi kód 3 gombot tesz egymás mellé.
<StackPanel Orientation="Horizontal">
 <Button Content="Click" FontSize="15" Height="30"/>
 <Button Content="Click" FontSize="15" Height="30"/>
 <Button Content="Click" FontSize="15" Height="30"/>
</StackPanel>

Mindhárom nyomógomb ugyanazt a két beállítást tartalmazza. A betűméret 15  a magasság pedig 30. Amennyiben valamelyik paramétert változtatni kell, akkor mindhárom nyomógomb esetében meg kell ezt tenni. Belátható, hogy minél több helyen szerepelenek ezek a konstans értékek, annál több módosítást kell elvégezni.

Stílusok használatával az egyes tulajdonságok csoportba rendezhetőek amelyet tetszőleges számú elem elérhet. A stílusban történő változtatás az összes olyan elemet érinti, amely használja.

Az elöbbi kód stílusokkal:
<StackPanel Orientation="Horizontal">
 <StackPanel.Resources>
  <Style x:Key="MyButton">
   <Setter Property="Button.FontSize" Value="15"/>
   <Setter Property="Button.Height" Value="30"/>
  </Style>
 </StackPanel.Resources>

 <Button Content="Click" Style="{StaticResource MyButton}"/>
 <Button Content="Click" Style="{StaticResource MyButton}"/>
 <Button Content="Click" Style="{StaticResource MyButton}"/>
</StackPanel>

Az első és legfontosabb, hogy a stílusokat un. Resource-ban kell tárolni. Ez lehet egy külön resource file is. Jelen esetben a StackPanel Resource-ban található.

A stílus hatáskörét megszabja, hogy mely elem Resource-ában lett definiálva. A példában a stílus csak a stackpanel-en belül érvényes. Amennyiben az egész oldalra érvényes stílus deklarálása a cél, úgy azt a Window.Resources alatt érdemes elhelyezni.

A <Style x:Key="MyButton">  létrehoz egy stílust amelyre az x:Key attributumban megadott névvel lehet a továbbiakban hivatkozni. Minden stílus Setter elemekből épül fel. Egy Setter elem két attributummal kell, hogy rendelkezzen.
A Property, amely meghatározza, hogy mely tulajdonság értéket kell beállítani.
A Value, amely magát az értéket tartalmazza.

A stílusra az elem Style attributumán keresztül lehet hivatkozni: 
Style="{StaticResource MyButton}".

Stílusok öröklése

Számos esetben előfordulhat, hogy egy stílus mindössze egy-két tulajdonságában tér el, egy már létező stílustól. Esetleg egy már létező stílust kell kibővíteni. Erre szolgál az öröklés lehetősége.

<StackPanel Orientation="Horizontal">
 <StackPanel.Resources>
  <Style x:Key="MyButton">
   <Setter Property="Button.FontSize" Value="15"/>
   <Setter Property="Button.Height" Value="30"/>
  </Style>
  
  <Style x:Key="MyBoldButton" BasedOn="{StaticResource MyButton}">
   <Setter Property="Button.FontWeight" Value="Bold"/>
  </Style>
 </StackPanel.Resources>

 <Button Content="Click" Style="{StaticResource MyButton}"/>
 <Button Content="Click" Style="{StaticResource MyBoldButton}"/>
 <Button Content="Click" Style="{StaticResource MyButton}"/>
</StackPanel>

Az újonnan létrehozott stílus egyfelől örökli a szülő minden beállítását (BasedOn="{StaticResource MyButton}"), másfelől kiegészíti az egy új beállítással, amely a szöveg vastagságát bold-ra állítja.

A három nyomógomb közül, a középső ezt az új stílust használja. Minden egyéb beállításban egyezik a másik kettővel, kivéve a betűvastagságot.

A Stílus elem megadása

Amennyiben egy stílusról ismert, hogy csak egyféle elem típushoz tartozik, akkor célszerű azt a stílus létrehozásakor jelölni. Az előbbi esetekben a stílusok mind nyomógombok megjelenítéséhez készültek, ezért a Property beállításnál szükség volt egy Button prefixre. Ha már a stílus deklarálásakor jelezzük, hogy az adott stílus mely típusú elemhez tartozik, akkor a prefix elhagyható.

<StackPanel Orientation="Horizontal">
 <StackPanel.Resources>
  <Style x:Key="MyStyle" TargetType="{x:Type Button}">
   <Setter Property="FontSize" Value="15"/>
   <Setter Property="Height" Value="30"/>
   <Setter Property="Background" Value="Yellow"/>
  </Style>
 </StackPanel.Resources>

 <Button Content="Click" Style="{StaticResource MyStyle}"/>
 <Button Content="Click" Style="{StaticResource MyStyle}"/>
</StackPanel>

A TargetType="{x:Type Button}" attributum használtával egyértelműen jelzésre került, hogy az adott stílus csak Button-hoz kapcsolható. Amennyiben valami más elem szeretné használni, akkor futásidejű hiba keletkezik.
Mivel a TargetType egyértelműen megadja annak az elemnek a típusát, amely az adott stílust fel fogja használni, ezért a Setter elem Property attributumában már nem kell külön Button prefixet használni.
<Setter Property="FontSize" Value="15"/>

Stílus név nélkül.

Abban az esetben, amennyiben ismert, hogy egy adott elem mindig ugyanúgy néz ki célszerű implicit stílusokat használni.

<StackPanel Orientation="Horizontal">
 <StackPanel.Resources>
  <Style TargetType="{x:Type Button}">
   <Setter Property="FontSize" Value="15"/>
   <Setter Property="Height" Value="30"/>
   <Setter Property="Background" Value="Yellow"/>
  </Style>
 </StackPanel.Resources>

 <Button Content="Click"/>
 <Button Content="Click"/>
</StackPanel>

Ebben az esetben a a Style nem tartlamaz x:Key attributumot, a TargetType megadása viszont kötelező. Hatására a StackPanelen található összes Button megkapja ezt a stílust. Használatával a Button elemnek nem kell külön jelezni, hogy melyik stílust kell használnia.

Noha egy-egy tulajdonság beállítása miatt a stílus használata eléggé felesleges többletgépelésnek látszik, a végső kódban célszerű a direkt értékadásokat megszüntetni.  A stílusok általában külön Resource file-ban kerülnek tárolásra, nem pedig az adott elemen belül
Ehhez egy új Resource Dictionary-t kell a projekthez adni, amely üresen így néz ki

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    
</ResourceDictionary>

Minden stílus deklarációt ezen belül kell elhelyezni.

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Style x:Key="MyStyle" TargetType="{x:Type Button}">
        <Setter Property="FontSize" Value="15"/>
        <Setter Property="Height" Value="30"/>
        <Setter Property="Background" Value="Yellow"/>
    </Style>

</ResourceDictionary>

Legvégül a XAML kódban csak a Resource File-ra kell hivatkozni.

<StackPanel Orientation="Horizontal">
 <StackPanel.Resources>
  <ResourceDictionary Source="Dictionary1.xaml"/>
 </StackPanel.Resources>

 <Button Content="Click" Style="{StaticResource MyStyle}"/>
 <Button Content="Click" Style="{StaticResource MyStyle}"/>
</StackPanel>

Magát a hivatkozást szintén az adott elem Resource  tulajdonságában kell elhelyezni. Azonban itt már csak egy hivatkozás kell a külső file nevére.

A stílusok megfontolt használatával a leíró kód rövidebbé és átláthatóbbá válik, hosszútávon rengeteg felesleges és redundáns adat begépelése kerülhető el vele.

2013. április 15., hétfő

HLSL #2 Textura

Folytatva az előzőekben megkezdett HLSL ismertetőt, ebben a részben a texturák használatával még látványosabb lesz mindaz amit a képernyőn meg kívánunk jeleníteni.
Továbbra is az FxComposer használatával, egy új üres effect-re lesz szükség.

Egy tetszőleges kép megjelenítéséhez az alábbiakra van szükség:
- model
- kép
- UV koordináták
- Sampler

A model továbbra is az előző részben beillesztett négyzet marad.
A kép tetszőleges lehet, akár saját vagy akár az internetről letöltött
Az UV koordinátákat jelen esetben a program adja a modellhez

A sampler definiáláshoz az alábbi kódsorok szükségeltetnek:
texture diffuseMap
<
    string UIName = "Diffuse Texture";
>;

sampler2D diffuseMapSampler = sampler_state
{
 Texture = <diffuseMap>;
};

Első lépésben egy texture kerül bevezetésre. Ez látható a properties panelen "Diffuse Texture" néven.

A sampler2D definiál minden egyéb paramétert a shader számára, hogy az adott texturát milyen módon kell megjeleníteni.(pl hogyan kell sokszorosítani, stb)

Egyenlőre egyetlen hivatkozás bőven elég, amely meghatározza, hogy az adott sampler melyik képhez tartozik: Texture = <diffuseMap>;
Ahol a diffuseMap az előbb létrehozott texture neve

Fontos megjegyezni, hogy ahány texture kerül bevezetésre, pontosan annyi sampler kell.

A kép megjelenítéséhez szükség van UV koordinátákra, ezért a vertexshader bemenete megváltozik.
struct app2vertex {
 float4 position  : POSITION;
 float2 texCoord  : TEXCOORD0;
};

A vertexsahder minden esetben elvárja a vertex pozícióját: float4 position : POSITION;

A textura koordináták a struktura texCoord elemén keresztül érhetőek el:
float2 texCoord : TEXCOORD0;
A TEXCOORD0 SEMANTICS utasítja az FXComposert, hogy a model első UV koordinátáit adja át.

Amennyiben egy modellhez több UV koordináta is tartozik, úgy azokat a sorszámok növelésével lehet elérni. (TEXCOORD1, TEXCOORD2...)

Mivel az UV koordinátákra a pixelshader-ben is szükség lesz, ezért annak a bemenetét is változtatni kell.
struct vertex2pixel {
  float4 position  : POSITION;
  float2 texCoord  : TEXCOORD0;  
};

Noha jelen esetben a két struktúra megegyezik, ez a legritkább esetben fordul csak elő.

Általánosságban elmondható, hogy mindig vagy egy struktúra amely a programtól a vertexshader felé tartalmaz bemenetet (app2vertex) és van egy struktúra amely a vertexshadertól a pixelshader felé tartalmazza a szükséges adatokat (vertex2pixel)

Mivel a pixelshader a vertexshadertól várja a már előkészített paramétereket, ezért logikus, hogy a vertexshader kimenete a vertex2pixel struktúra.
Bemenete pedig az app2vertex struktúra.

A végső vertexshader az alábbi:
vertex2pixel mainVS(app2vertex In)
{
  vertex2pixel Out; 
  Out.position = mul(In.position, WorldViewProj); 
  Out.texCoord = In.texCoord;
  return Out;
}

A pozíció a már megismert módon kiszámításra kerül.
A textura UV koordináta pedig minden  változtatás nélkül továbbadásra kerül a pixelshader felé.

Mivel a vertexshader kimenete egy vertex2pixel struktúra, ebből következőleg a pixelshader bemenetének is muszáj annak lennie.

float4 mainPS(vertex2pixel In) : COLOR
{
 float4 diffuseTexture = tex2D(diffuseMapSampler, In.texCoord);
 return diffuseTexture;
}

A tex2D függvény készíti el a végső képet és 2 paramétert vár.
Az első a sampler neve, a második a modellhez tartozó UV koordináta, amely immáron a vertexshader kimeneteként jön mint paraméter: vertex2pixel In

A végső shader az alábbi:
/*****************************************************/
// **                AUTO GENERATED
/*****************************************************/

float4x4 WorldViewProj : WorldViewProjection;


/*****************************************************/
// **                TEXTURES
/*****************************************************/

texture diffuseMap
<
    string UIName = "Diffuse Texture";
>;

sampler2D diffuseMapSampler = sampler_state
{
 Texture = <diffuseMap>;
};

/*****************************************************/
// **                IO STRUCTURES
/*****************************************************/

// input from application
struct app2vertex {
 float4 position  : POSITION;
 float2 texCoord  : TEXCOORD0;
};

// output to pixelshader
struct vertex2pixel {
 float4 position  : POSITION;
 float2 texCoord  : TEXCOORD0;  
};


/*****************************************************/
// **                PROGRAMS
/*****************************************************/

vertex2pixel mainVS(app2vertex In)
{
 vertex2pixel Out; 

 Out.position = mul(In.position, WorldViewProj);    
 Out.texCoord = In.texCoord;

 return Out;
}


float4 mainPS(vertex2pixel In) : COLOR
{
 float4 diffuseTexture = tex2D(diffuseMapSampler, In.texCoord);
 return diffuseTexture;
}

/*****************************************************/
// **
// **                TECNIQUES
// **
/*****************************************************/

technique technique0 {
 pass p0 {
  CullMode = none;
  VertexShader = compile vs_3_0 mainVS();
  PixelShader = compile ps_3_0 mainPS();
 }
}



2013. április 12., péntek

Wpf #8 A konstruktor elött

Egy tetszőleges Wpf alkalmazás fejlesztésekor hamar felmerülhet a kérdés, vajon a konstruktor hol hívódik meg? Átnézve a file-okat, amelyeket a VisualStudio hozott létre, az alkalmazáshoz sehol se lehet a konstruktor meghívására utaló kódot találni.

A VisualStudio által készített projektben ilyen nincs is. Ez persze nem azt jelenti, hogy nem lehet.

A projektet közelebbről megvizsgálva található benne egy App.xaml én annak a mögöttes kódja.
Az XAML file tartalma hasonló az alábbihoz (leszámítva az osztály elnevezést)
<Application x:Class="Wpf08_BeforeConstructor.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
         
    </Application.Resources>
</Application>


A program ablak megjelenítés céljából a fontos  értékadás a StartupUri attributumban találaható.
A megatott érték annak a Window-nak a neve amely a program indulásakor látható lesz.

Amennyiben a projekthez hozzáadásra kerül egy új Window (add/new item/Window(WPF)), akkor annak nevét megadva immáron az lesz a kezdőablak.

A konstruktor meghívását a keretrendszer a háttérben elvégzi.

A legritkább  esetek közé tartozik, hogy egy komplett rendszer rögtön az UI megmutatásával indul.
Valójában előtte számos inicializálás és vizsgálat lezajlik, sőt jónéhány paramétert is átadásra kerülhet.

Ahhoz, hogy a konstuktor meghívása már az általunk felügyelt kódból történjen néhány apró változtatásra van szükség.
A legelső, hogy az előbbi StartupUri="MainWindow.xaml" attributumot ki kell törölni és helyette az alábbi attributum kell: Startup="ApplicationStartup"

Az App.xaml új tartalma:
<Application x:Class="Wpf08_BeforeConstructor.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             Startup="ApplicationStartup">
    <Application.Resources>
         
    </Application.Resources>
</Application>

A Startup attributum meghatározza az induláskor lefuttatandó metódus nevét. 
Ebből következőleg valahol kell egy ApplicationStartup nevű metódus.

Az App.xaml.cs file-t megnyitva egy teljesen üres osztály látható. A számunkra szükséges kódot itt kell elhelyezni.

public partial class App : Application
{
 public void ApplicationStartup(Object sender, StartupEventArgs e)
 {
 }
}

A program indításakor immáron ez az eljárás hívódik meg. A Wpf ablak konstruktorának a meghívását is ebből (vagy valamely ebből meghívott eljárásból) célszerű végrehajtani.

A konstruktor jelenleg az alábbi kinézetű
public partial class MainWindow : Window
{
 public MainWindow()
 {
  InitializeComponent();
 }
}

Mivel a konstrukor meghívása immáron kódból történik, ezért tetszőleges paraméter is átadható.
Tételezzük fel, hogy egy string paraméter átadása szükséges.
Ebben az esetben a konstrukor: public MainWindow(string someString)

A meghívása az App.xaml.cs ApplicationStratup-ból történik
public partial class App : Application
{
 public void ApplicationStartup(Object sender, StartupEventArgs e)
 {
  var window = new MainWindow("hello world");
  // itt még tetszőleges, a Window-ban található általad megírt metódus meghívható
  // pl Init()
  window.Show();
 }
}

Ezzel a módszerrel immáron számos ellenőrzés elvégezhető mielőtt az UI meghívásra kerülne.
Számos szerviz injektálható a konstruktoron keresztül (pl Log), amelyek létrehozásához a Wpf ablaknak semmi köze nincs, és illik már készen átadni.

2013. április 9., kedd

Wpf #7 MultiBinding, MultiConverter

Az adatkötés használata során elég gyakran kell egy tetszőleges elem értékét több - egymástól különböző típusú, vagy különböző helyen deklarált - tulajdonság értéke alapján megállapítani.
Noha erre lehet mögöttes kódot írni, ami mindig figyeli mi történik éppen a felhasználói felületen, sokkal egyszerűbb megoldás is létezik a probléma megoldására.

Legyen adott egy cég különböző termékekkel és azok éves bevételivel.
A program ezen adatokat egy listában jelenítse meg.
Kérje be továbbá az éppen aktuális Euro árfolyamot és az összbevételt számítsa ki Euroban.

A felhasznált Model tartalmazza a termék nevét és az éves bevételt.
class Model : INotifyPropertyChanged
{
 private string _productName;
 public string ProductName
 {
  get { return _productName; }
  set { _productName = value; OnPropertyChanged("ProductName"); }
 }

 private decimal _income;
 public decimal Income
 {
  get { return _income; }
  set { _income = value; OnPropertyChanged("Income"); }
 }

 // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

 public event PropertyChangedEventHandler PropertyChanged;
 public void OnPropertyChanged(string propertyName)
 {
  if (PropertyChanged != null)
   PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }

 // ---------------------------------------------------------------------

 public Model(string productName, decimal income)
 {
  ProductName = productName;
  Income = income;
 }

}

A kapcsolódó ViewModel egy gyűjteményben tárolja a cég termékeit, valamint a ViewModel-ben kerül tárolásra az éppen aktuális Euro értéke.
A gyűjtemény 3 termékkel kerül feltöltésre a konstruktorban:
class ViewModel : INotifyPropertyChanged
{
 private ObservableCollection<Model> _myModel;
 public ObservableCollection<Model> MyModel
 {
  get { return _myModel; }
  set { _myModel = value; OnPropertyChanged("MyModel"); }
 }

 private decimal _euroValue;
 public decimal EuroValue
 {
  get { return _euroValue; }
  set { _euroValue = value; OnPropertyChanged("EuroValue"); }
 }

 public event PropertyChangedEventHandler PropertyChanged;
 public void OnPropertyChanged(string propertyName)
 {
  if (PropertyChanged != null)
   PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }

 public ViewModel()
 {
  MyModel = new ObservableCollection<Model>
   {
    new Model("Product1", 10000),
    new Model("Product2", 55891),
    new Model("Product3", 158689)
   };

  EuroValue = 305.23m;
 }
}

A View kódja a már megszokott egyszerűséget hozza
public partial class MainWindow : Window
{
 private readonly ViewModel _viewModel;

 public MainWindow()
 {
  InitializeComponent();
  _viewModel = new ViewModel();
  DataContext = _viewModel;
 }
}

A View-ban megjlenítésre kerül egy lista, amiben az egyes termékek nevei illetve az éves bevételük látható. A Lista megjelenítése a Grid.Resource-ba lévő DataTemplate-ben található.

A View-ban található továbbá az éppen aktuális Euro bevitelére szolgáló input mező, valamint legvégül az átváltott összeg, ami jelen esetben egyenlőre egy nagy kérdőjel.

<Grid>
 <Grid.Resources>
  <DataTemplate x:Key="MyListTemplate">
   <StackPanel Orientation="Horizontal">
    <TextBlock Width="100" Text="{Binding ProductName}"/>
    <TextBlock Width="100" Text="{Binding Income}"/>
   </StackPanel>
  </DataTemplate>
 </Grid.Resources>
 
 <StackPanel Orientation="Vertical">
 
  <ListBox ItemsSource="{Binding MyModel}" ItemTemplate="{StaticResource MyListTemplate}"/>
  
  <StackPanel Orientation="Horizontal">
   <TextBlock Text="Euro: "/>
   <TextBox Text="{Binding EuroValue}" Width="100"/>
  </StackPanel>

  <StackPanel Orientation="Horizontal">
   <TextBlock Text="Teljes bevétel Euroban: "/>
   <TextBlock Text="?"/>
  </StackPanel>

 </StackPanel>
</Grid>

A nyilvánvaló  nagy kérdés az, hogy mi kerül a kérdőjel helyére? A feladat egyértlemű. Add össze a bevételeket majd a kapott értéket az aktuális Euro értékével osztani kell.

Ehhez két input paraméter kell, amelyek a ViewModel osztályban találhatóak.
A gyűjtemény, amely tartalmazza az összes terméket (MyModel)
Valamint az Euro aktuális értéke. (EuroValue)

A fenti számítás egy külön osztályban kerül kiértékelésre.
class EuroConverter : IMultiValueConverter
{
 public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 {
  if (values != null && values.Length == 2)
  {
   var collection = values[0] as ObservableCollection<Model>;
   var euroValue = (decimal)values[1];

   var totalIncome = collection.Sum(model => model.Income);
   var incomeInEuro = totalIncome/euroValue;
   return incomeInEuro.ToString();
  }
  return Binding.DoNothing;
 }

 // ********************************************************************

 public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
 {
  return null;
 }

}

Az osztálynak muszáj megvalósítania az IMultiValueConverter interface-t.
A számításhoz fontos műveletek a Convert metódusban találhatóak.
A bejövő pareméter immáron egy tömb (object[] values) amely tartalmazza a gyűjteményt, valamint az Euro értékét.
A kódot közelebbről megvizsgálva látható, hogy a tömb eleminek feltöltésekor ügyelni kell a sorrendre. Jelen esetben  a program elvárja, hogy a gyűjtemény legyen a tömb első és az Euro értéke a tömb második eleme.

Az összeg kiszámítása után azt stringként vissza kell adni a TextBlock részére.

A View több ponton is változik.
Be kell vezetni egy új namespace-t, ami attól függ, hogy a projekt (de leginkább az EuroConverter osztály) milyen namespace alatt található.
az új sor így kell, hogy kinézzen:
xmlns:multiConverter="clr-namespace:A TE NAMESPACE ELNEVEZÉSED"

A Grid.Resource is bővítésre szorul, végső formája az alábbiakban található
<Grid.Resources>
 <DataTemplate x:Key="MyListTemplate">
  <StackPanel Orientation="Horizontal">
   <TextBlock Width="100" Text="{Binding ProductName}"/>
   <TextBlock Width="100" Text="{Binding Income}"/>
  </StackPanel>
 </DataTemplate>
 
 <multiConverter:EuroConverter x:Key="EuroMultiConverter"/>
</Grid.Resources>

A Konverterre a Grid-en belül a továbbiakban az EuroMultiConverter névvel lehet hivatkozni.

Legvégül maga az adatkötés:
<StackPanel Orientation="Horizontal">
 <TextBlock Text="Teljes bevétel Euroban: "/>
 <TextBlock Width="100">
  <TextBlock.Text>
   <MultiBinding Converter="{StaticResource EuroMultiConverter}">
    <Binding Path="MyModel"/>
    <Binding Path="EuroValue"/>
   </MultiBinding>
  </TextBlock.Text>
 </TextBlock>
</StackPanel>

Mivel a végső érték több bemeneti érték kiszámításával kerül kiértékelésre, ezért MultiBinding adatkapcsolást kell végrehajtani. A konverter az előbb meghatározott  EuroMultiConverter.

A paraméterek pedig abban a sorrendben kerülnek átadásra, ahogy azt az EuroConverter osztály elvárja. Az első a gyűjtemény, a második az Euro értéke.

A végső View az alábbi:
<Window x:Class="Wpf07_MuliBinding.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:multiConverter="clr-namespace:Wpf07_MuliBinding"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.Resources>
            <DataTemplate x:Key="MyListTemplate">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Width="100" Text="{Binding ProductName}"/>
                    <TextBlock Width="100" Text="{Binding Income}"/>
                </StackPanel>
            </DataTemplate>
            
            <multiConverter:EuroConverter x:Key="EuroMultiConverter"/>
        </Grid.Resources>
        
        <StackPanel Orientation="Vertical">
            <ListBox ItemsSource="{Binding MyModel}" ItemTemplate="{StaticResource MyListTemplate}"/>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Euro: "/>
                <TextBox Text="{Binding EuroValue, UpdateSourceTrigger=PropertyChanged}" Width="100"/>
            </StackPanel>

            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Teljes bevétel Euroban: "/>
                <TextBlock Width="100">
                    <TextBlock.Text>
                        <MultiBinding Converter="{StaticResource EuroMultiConverter}">
          <Binding Path="MyModel"/>
          <Binding Path="EuroValue"/>
         </MultiBinding>
                    </TextBlock.Text>
                </TextBlock>
            </StackPanel>

        </StackPanel>
    </Grid>
</Window>


Az Euro érték megadására szolgáló TextBox adatkapcsolásának frissítését átállítottam (UpdateSourceTrigger=PropertyChanged), ezáltal abban a pillanatban kiszámításra kerül az új érték, ahogy egy billentyű lenyomásra kerül.

2013. április 8., hétfő

HLSL #1 Alapok

Egy tetszőleges grafika megjelenítése a képernyőn a mai programokkal szinte gyerekjáték. Legyen szó egy képről, vektorgrafikáról vagy 3D modellről, keretrendszerek tucatjai állnak rendelkezésre, amelyek a probléma nagy részének a megoldását a háttérben elvégzik.

Mindazonáltal, ha valami speciálisat - és főleg egyedit - kell a képernyőre kivarázsolni, akkor a legtöbb esetben még mindig a hardver alapszintjére kell visszanyúlni. A shader programok segítségével, és némi fantáziával egyszerű elemekből felépített grafikát is fantasztikusan fel lehet turbózni. Ehhez persze türelem, és rengeteg tapasztalat kell, mint minden máshoz.

A blogban található shader-ek elkészítéséhez az nvdia ingyenesen letölthető fxcomposer programjának 2.5-ös verziója ajánlott, de bármely más shader editor is tökéletesen megfelel. A Composer használatához érdemes rögtön a manual-t is letölteni.

Installálás után első lépésben hozz létre egy új projektet (File/New).
A jelenleg teljesen üres projekthez minimum kell egy shader és egy modell.
Az első shader létrehozása: Create/Effect
A megjelenő menüben válaszd a HLSL FX-et, majd a Next gomb megnyomásával már előre beállított effektek közül lehet válogatni.
Egyenlőre válaszd a "empty" lehetőséget. File névnek tetszőlegesen találj ki valamit, pl: "01_basic.fx", majd kattints a next gombra.
A megjelenő panelen láthatod a létrehozásra kerülő effect nevét. Ezzel el is készült a shader alapja.

Alapbeállítás esetén a bal felső "Materials" ablakban egy fehér kör látható. Ha mégsem, akkor a View/Materials ki/be kapcsolásával ez tetszőlegesen állíthatod.

A modell hozzáadás lényegesen egyszerűbb. Create/Plane
A jobb alsó sarokban, a "Render" ablakban láthatónak kell lennie az imént hozzáadott plane.

A fehér kört drag-drop használatával húzd rá a plane-re, aminek hatására az fehérre változik. Ezzel a plane mostantól ezt a shadert használja, bármi változtatás esetén a plane követni fogja azt.

A shader kód középen látható, a kommentek kiszedése után így kell kinéznie:
float4x4 WorldViewProj : WorldViewProjection;

float4 mainVS(float3 pos : POSITION) : POSITION{
 return mul(float4(pos.xyz, 1.0), WorldViewProj);
}

float4 mainPS() : COLOR {
 return float4(1.0, 1.0, 1.0, 1.0);
}

technique technique0 {
 pass p0 {
  CullMode = None;
  VertexShader = compile vs_3_0 mainVS();
  PixelShader = compile ps_3_0 mainPS();
 }
}

A fenti kód tartalmaz mindent, ami egy shader minimális működéséhez kell.

A modell térbeli elhelyezését, transzformációit pontosan tudnia kell a programnak, erre szolgál a
WorldViewProjection. Ez tulajdonképp 3 mátrix szorzata.
WorldViewProjection = World * View * Projection
A World határozza meg a modell eltolását, elforgatását, nagyítását.
A View mondja meg, hogy kamera hol található a térben és épp merre néz
A Projection határozza meg a látószöget, a torzítás valamint a legközelebbi és a legtávolabbi látható távolságot.

Ezt a paramétert az FXComposer automatikusan adja a shader részére. A Saját programodban ezt neked kell kiszámolnod. Ahhoz, hogy az FXComposer tudja, hogy neki ezt a paramétert adni kell, a programban ez muszáj jelezni.
float4x4 WorldViewProj : WorldViewProjection;
A float 4x4 egy mátrix-ot definiál, a WoldViewProj pedig tetszőleges változó elnevezés
Az ezt követő kettőspont és a WolrdViewProjection úgynevezett SEMANTICS.
Ez határozza meg, hogy egy adott változó/paraméter milyen koncepció alapján kerül felhasználásra.
Jelen esetben a composer ebből tudja, hogy ennek a változónak kell átadni a már előre kiszámított mátrixot.
Fontos megjegyezni, hogy ez a SEMANTICS csak az fxcomposer számára érthető.
Ha ez most elsőre nem világos, ne ragadj le itt, a legtöbb példában  elegendő lesz, hogy ott van. Ettől függetlenül érdemes a neten pontosan utánaolvasni.

A shader két függvényből áll.
VertexShader: amely minden számítást egyszer végez el a modell összes vertexén.
PixelShader: amely minden pixelen, ahol  a model látható lefut.
Ebből következik, hogy mindaz, amit ki lehet számolni a VertexShader-ben, célszerű ott megtenni, hiszen az jóval kevesebbszer fog valószínűleg lefutni, mint a pixelshader.

A VertexShader a modell pozícióját várja input paraméterként, amit a POSTION SEMATICS jelöl.
float3 pos : POSITION
És a shader kimenete a model egy vertexének a pozíciója. Ezt szintén a POSTION SEMATICS jelöli.
float4 mainVS(float3 pos : POSITION) : POSITION
A függvény kiszámítja a vertex pontos helyzetét, majd átadja PixelShader-nek.

A PixelShader visszatérési értéke az a szín, amit a képernyőn meg kell jeleníteni. Ez a COLOR SEMATICS jelöli
float4 mainPS() : COLOR  
A függvény pedig jelen esetben egy teljesen fehér értékkel tér vissza.
return float4(1.0, 1.0, 1.0, 1.0);
Az RGB értékek 0-1 között tetszőlegesek lehetnek. Az utolsó paraméter az Alpha áttetszőség értéke.
Ezek a paraméter értékek tetszőlegesen változtathatóak.
pl: return float4(1.0, 0.0, 0.0, 1.0);
Ebben az esetben a plane piros színű lesz.
Minden változtatás után a programot le kell fordítani. Addig nem látható a hatása. Erre szolgál a Compille button vagy a Build/Compille utasítás.

Megjegyzés: Az utolsó paraméter (Alpha) értéke szintén tetszőleges lehet, de ez jelen esetben nem befolyásolja a plane áttetszőségét. Ahhoz, hogy ez megtörténjen további beállításokra van szükség.

A shader következő eleme a technique.
Egy shader tetszőleges számú technique-t tartalmazhat. Ez határozza meg a shader lefutáshoz szükséges opciókat. Pl. az áttetszőséget is itt lehet beállítani.

Minden techniqe tetszőleges számú pass elemet tartalmaz.
A pass határozza meg, hogy melyik VertexShader és melyik PixelShader függvényeket kell lefuttatni.

A CullMode = None; hatására a kirajzolandó plane minden irányból látható.
A plane kirajzolása vagy óramutató járásával megegyezik vagy ellentétes.
A render ablakban az ALT+bal egérgomb használatával lehet az aktuális nézetet változtatni.
Jelen esetben a plane mindkét oldala látható (no culling)
A CullMode felveheti a "CW" és a "CCW" értékeket.
CullMode = CW; (ne felejtsd el lefordítani a shadert)
Ebben az esetben a plane egyik irányból nem látható.
Valós helyzetben célszerű CW vagy CCW értékre állítani, mert egy adott polygon nagy valószínűséggel úgyis csak az egyik irányból lesz látható.

A megjelenítendő szín kódból történő változtatása elég lassú folyamat. Amennyiben a plane-t zöld színnel szeretnénk kirajzolni, a megfelelő sort át kéne írni, majd újrafordítani az egész kódot.
Ennek egyszerűsítésére az alábbi változtatások szükségesek.
Add hozzá a következő sorokat a program elejéhez.
float4 customColor :Color
<
 string UIName = "Diffuse Color";
> = {1.0, 1.0, 0.0, 1.0};

Illetve a PixelShader visszatérési értékét írd át az alábbira, majd fordítsd le:
return customColor;

A Jobb felső sarokban (default nézet) látható a properties ablak.
Az ablakban immáron látható egy Diffuse Color elem, ami egy színkiválasztót is tartalmaz. Válassz ki egy tetszőleges színt. Látható, hogy a render ablakban a plane realtime követi.

A Color SEMATICS jelzi az fxcomposer számára, hogy ez a változó egy színt tartalmaz.
A kacsacsőrökben elhelyezett string UIName = "Diffuse Color";  úgynevezett Annotation.
Ez szintén csak az fxComposer számára fontos. Jelen esetben megmondja, hogy a User Interface Panelen milyen névvel szerepeljen ez a változó.
A későbbiekben még számos Annotation használatára lesz példa, egyenlőre bőven elég, hogy a változót értelmesen olvasható névvel el lehet látni.

A leírásban található idegen szavak - főleg a sematics és az annotation - bővebb leírására érdemes a neten rákeresni. Gyakorlásképpen hozz létre még egy Color változót és a PixelShader visszatérési értéke legyen a kettőnek az összege.

Az alábbiakban látható a megoldás:
float4x4 WorldViewProj : WorldViewProjection;

float4 customColor :Color
<
 string UIName = "Diffuse Color";
> = {1.0, 1.0, 0.0, 1.0};

float4 newColor :Color
<
 string UIName = "Another Color";
> = {1.0, 1.0, 0.0, 1.0};

float4 mainVS(float3 pos : POSITION) : POSITION{
 return mul(float4(pos.xyz, 1.0), WorldViewProj);
}

float4 mainPS() : COLOR {
 return customColor + newColor;
}

technique technique0 {
 pass p0 {
  CullMode = none;
  VertexShader = compile vs_3_0 mainVS();
  PixelShader = compile ps_3_0 mainPS();
 }
}