Elemek nagyítása ListBox-ban

Nemrégiben felmerült egy kérdés egy portálon azzal kapcsolatban, hogyan lehet a Silverlight alatt ListBox elemeit nagyítani úgy, hogy a ListBox a kilógó részeket ne vágja le, illetve az “előemelkedő” elemet ne takarja ki a háttérben maradó elem.

Mivel nem vagyok Silverlight guru, a kérdésre nem válaszoltam, ugyanakkor elég érdekes a probléma ahhoz, hogy megvizsgáljuk, mit lehet tenni WPF alatt.

Először is, észre kell venni, hogy nagyítás tekintetében két különböző feladattal állunk szemben, és a két feladat más megoldási stratégiát kíván. Az egyik feladat egy síma vizuális visszajelzés, ekkor az elem csak “szimplán megnő”, mint egy nagyító alatt. Valamit nagyobbá tenni több módon lehet, de talán a legegyszerűbb egy transzformációt elvégezni rajta.

A másik típusú probléma esetében az elemen plusz információkat szeretnénk megjeleníteni, emiatt az információhoz nagyobb terület szükséges, és ez indokolja az elem növelését. Itt egy transzformáció
nyilván nem megoldás, az információkat magábafoglaló konténert kell megnövelni, hogy nagyobb felületet adjon a több megjelenítendő adatnak.

Szimpla nagyítás

Legyen az a feladat, hogy egy ListBox-ban a kurzorral egy elem fölé állva az emelkedjen ki, de úgy, hogy részben elfedje a körülötte levő elemeket, és lógjon ki a ListBox-ból. Kiművelt körökben ezt VSM-et használva oldják meg, így előre elnézést kérek, hogy én triggereket fogok használni.

Szükség van tehát egy ListBox-ra

<Window x:Class="WpfApplication1.Window1"
    ...
    Background="Black">
    
    <Grid>
        <ListBox x:Name="listbox" 
                 Margin="30" 
                 MaxWidth="150"
                 HorizontalContentAlignment="Stretch"
                 Background="Black">

        ...

        </ListBox>
    </Grid>
</Window>

A ListBox elemeihez készítünk egy saját template-et, ez a template kezeli azt, hogy megnövekedjen, ha az egér felette áll. Kell tehát a ListBox-hoz egy ItemTemplate:

<ListBox.ItemTemplate>
    <DataTemplate>
      
    ...
      
    </DataTemplate>               
</ListBox.ItemTemplate>  

Ez a template három fő részből áll. Egyrészt ad egy kis formát a megjelenítő adatoknak. A ListBox most stringekkel lesz feltöltve, egy minimális formázással a stringek egy lekerekített kereteben ülnek:

<Border                        
    Margin="3"
    Padding="3"
    BorderThickness="2" 
    CornerRadius="5"
    BorderBrush="#608080FF"                          
    RenderTransformOrigin="0.5, 0.5">
    
    <Label Foreground="#FFA0A0FF" Content="{Binding}"/>
    
    <Border.Background>
        <SolidColorBrush x:Name="BackColor" Color="#308080FF"/>
    </Border.Background>
    
    <Border.RenderTransform>
        <ScaleTransform 
            x:Name="ScaleTransform" 
            ScaleX="1" 
            ScaleY="1"/>
    </Border.RenderTransform>    
</Border>

Mivel az elemeknek meg kell majd nőni a kurzor hatására, a Border egy render transzformációt tartalmaz, amely azonban egyelőre nincs hatással a megjelenésre. Azért van a transzformációnak neve (lásd x:Name), mert egy animációból később a ScaleX és ScaleY tulajdonságok módosítva lesznek. A háttér színe szintén meg lett nevezve, nagyításkor a háttérszínt is módosítjuk, hogy az átfedések jobban látszódjanak.

Az item template az alap kinézeten felül még tartalmazza a növekedést megvalósító logikát. Ehhez kellenek az animációk:

<DataTemplate.Resources>
    <Storyboard x:Key="Increase">
        <DoubleAnimation 
            Storyboard.TargetName="ScaleTransform"  
            Storyboard.TargetProperty="ScaleX" 
            To="1.8"
            Duration="00:00:00.2"/>
        <DoubleAnimation 
            Storyboard.TargetName="ScaleTransform"  
            Storyboard.TargetProperty="ScaleY" 
            To="1.8"
            Duration="00:00:00.2"/>
        <ColorAnimation                                
            Storyboard.TargetName="BackColor"  
            Storyboard.TargetProperty="Color" 
            To="#FFFFFFFF"
            Duration="00:00:00.2"/>
    </Storyboard>
    <Storyboard x:Key="Decrease">
        <DoubleAnimation 
            Storyboard.TargetName="ScaleTransform"  
            Storyboard.TargetProperty="ScaleX"                                 
            Duration="00:00:00.2"/>
        <DoubleAnimation 
            Storyboard.TargetName="ScaleTransform"  
            Storyboard.TargetProperty="ScaleY" 
            Duration="00:00:00.2"/>
        <ColorAnimation                                
            Storyboard.TargetName="BackColor"  
            Storyboard.TargetProperty="Color" 
            Duration="00:00:00.2"/>
    </Storyboard>                        
</DataTemplate.Resources>

A kódban semmi különös nincs, két storyboard, amelyből az egyik a ScaleTransform-on keresztül megnöveli a ListBox elemét, ezenkívül átszinezi, a másik storyboard pedig visszatér az eredeti értékekhez.

A storyboardokat a következő események aktivizálják:

<DataTemplate.Triggers>
    <Trigger Property="IsMouseOver" Value="True">
        <Trigger.EnterActions>
            <BeginStoryboard Storyboard="{StaticResource Increase}"/>                                
        </Trigger.EnterActions>
        <Trigger.ExitActions>
            <BeginStoryboard Storyboard="{StaticResource Decrease}"/>
        </Trigger.ExitActions>                            
    </Trigger>
</DataTemplate.Triggers> 

Próbáljuk ki, hogyan működik az alkalmazás:

Két probléma látszik. Az egyik, hogy az elemek rosszul fedik át egymást. A gond az, hogy minden elem azonos ZIndex-szel rendelkezik, és ekkor a takarási sorrendet az dönti el, hogy milyen sorrendben lettek az
elemek a ListBox-hoz adva. A számunkra előnyös működés az lenne, ha mindig annak az elemnek lenne nagyobb a ZIndex-e, amelyik felett az egér áll. Az ember hajlamos első ötlenek módosítani az elem template-jében a legkülső kontrol ZIndex-ét, jelen esetben a Border-t. (Neeem, én neeem…) Ez azonban nem működik. Azért nem, mert minden elem egy ListBoxItem-ben van. Ha a Border ZIndex-ét módosítjuk, akkor a ListBoxItem-en belül ő lesz ugyan legfelső elem (igaz, más elem nincs is ott), de a ListBoxItem-ek még mindig egy szinten lesznek. Emiatt kirajzoláskor a sorrend ugyanaz marad, mint eddig: a ListBoxItem-ek hozzáadás sorrendjében lesznek megjelenítve, és mivel a ListBoxItem megjelenítése közben rajzolódik ki a Border, a Border-ek sorrendjén nem változtat az, ha az egyik Border magasabb ZIndex-szel rendelkezik, mint a másik.

Amit meg kell hát változtatni, az a ListBoxItem ZIndex-e. Ez megtehető azáltal, hogy a ListBoxItem-ekre egy triggert állítunk, ami megváltoztatja a ZIndex property-t, ha az IsMouseOver property igaz:

<ListBox.Resources>
    <Style TargetType="{x:Type ListBoxItem}">
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Panel.ZIndex" Value="1"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</ListBox.Resources>

A fenti stílus minden, a ListBox-ban levő LisBoxItem-re ráhúzza a triggert. Nézzük hol állunk most:

Látszik, hogy az átfedés problémáját ez megoldotta. Ami hátravan, hogy a ListBox ne vágja meg a kiemelkedő elemet. Ez a probléma lehet bonyolult vagy egyszerű, attól függően, hogy milyen végső működést várunk a ListBox-tól.

A bonyolult eset az, amikor ki szeretnénk használni, hogy a ListBox egy ScrollViewer-t használ, így tud scrollozni. A ScrollViewer-en belül egy ScrollContentPresenter van, ami működésének logikája miatt mindenképpen vágni fog, és nem lehet úgy konfigurálni, hogy ezt ne tegye. Erre a cikk végén még visszatérünk, most az egyszerűség kedvéért mondjunk le a scrollozásról, és használjunk a ListBox-on belül egy egyszerű StackPanel-t:

<ListBox.Template>
   <ControlTemplate>
       <Border                         
           BorderThickness="2"                         
           CornerRadius="5"
           BorderBrush="#708080FF"  
           Background="#308080FF">
            
           <StackPanel IsItemsHost="True"/>                        
            
        </Border>
    </ControlTemplate>
</ListBox.Template>

StackPanel, ha csak nincs beállítva rajta a ClipToBounds property, nem fogja megvágni a kilógó elemeket. Ekkor az eredmény az, amit várunk:

Természetesen használható más panel is, például WrapPanel

Elem nagyítása helynövelés céljából

Az egyszerűség kedvéért legyen az a feladat, hogy ha az egér az adott elem fölé áll, akkor az növekedjen meg helyet adva új információkank. Hogy mik ezek az információk, most nem érdekes, egyszerűen csak az elem templétjében levő Border-t fogjuk megnövelni nagyobbra úgy, hogy a Width property-t egy magasabb
értékre animáljuk.

A Width property értékével azonban van egy kis gond. Az előző példában ez nem volt megadva, hogy a Border akkora méretet vegyen fel, hogy kitöltse StackPanel szélességét. Általában WPF alatt nem illik explicit méreteket megadni, hogy a layout jól alkalmazkodjon a változó ablakméretekhez. Ekkor viszont az animáció a Width property-re nem fog működni, mert nincs kiinduló érték. Ennek a problémának a megoldása megtalálható ezen a linken

Most egy ennél egyszerűbb és sokkal csúnyább megoldást választunk, megadunk egy kiinduló értéket a Border Width property-nek, hogy működjön az animáció. Ezenkívül el is kell nevezni a Border-t, hogy hivatkozni lehessen az animációkból. A xaml kód tehát így változik:

<ListBox.ItemTemplate>
    <DataTemplate>
        <Border x:Name="Border"
            Width="120"
            ...

Az animációkat szintén módosítani kell, mivel most már nem a ScaleTransform-ot kell animálni, hanem a Border Width property-jét:

<DataTemplate.Resources>
    <Storyboard x:Key="Increase">
        <DoubleAnimation 
            Storyboard.TargetName="Border"  
            Storyboard.TargetProperty="Width" 
            To="250"
            Duration="00:00:00.2"/>
        
        <ColorAnimation                                
            Storyboard.TargetName="BackColor"  
            Storyboard.TargetProperty="Color" 
            To="#FFFFFFFF"
            Duration="00:00:00.2"/>
    </Storyboard>
    <Storyboard x:Key="Deccrease">
        <DoubleAnimation 
            Storyboard.TargetName="Border"  
            Storyboard.TargetProperty="Width"                                 
            Duration="00:00:00.2"/>
        
        <ColorAnimation                                
            Storyboard.TargetName="BackColor"  
            Storyboard.TargetProperty="Color" 
            Duration="00:00:00.2"/>
    </Storyboard>
</DataTemplate.Resources>

A ListBox templétjének ugyanúgy a szimpla StackPanel-t kell tartalmaznia, mint korábban, hiszen a ScrollViewer megint megvágná a nagyra nőtt Border-t. Nézzük meg, mi történik.

Hiába nincs már ScrollViewer, a Border akkor is vágva van. Mi ennek az oka? Az ok az, hogy az elemek rajzolása közben, miután a Layout információk kiszámításra kerültek, a rendszer körbekérdezi az összes UI elemet, hogy ha adott méretűre kell őt kirajzolni, akkor milyen területre kell vágni. Az ekkor meghívott függvény a GetLayoutClip(). Hogy melyik elem mit ad vissza, az változó. A viselkedést általában lehet befolyásolni a ClipToBounds property segítségével, ezt false értékre állítva az elemek egy része null-t ad vissza geometriának, ami annyit jelent, hogy nem kíván semmilyen vágást. Sajnos az elemek másik része ezt a property-t nem veszi figyelembe, ilyen volt a ScrollViewer, és láthatólag ilyen a Border is. Szerencsére a GetLayoutClip() függvény felülírható, emiatt a Border osztályból származtatva a vágás megszűntethető:

public class OverfloatingBorder : Border
{
    protected override Geometry GetLayoutClip(Size layoutSlotSize)
    {
        return null;
    } // GetLayoutClip
} // class OverfloatingBorder

Ezután a datatemplate-et is módosítani kell, hogy az új típusú border-t használja:

<ListBox.ItemTemplate>
     <DataTemplate>
         <c:OverfloatingBorder x:Name="Border"

Természetesen a megfelelő namespace-t is be kell állítani, mint xmlns:c=”clr-namespace:WpfApplication1″

Az eredmény pedig:

Ha mindenképpen ScrollViewer kell?

Sajnos a ScrollViewer problémájára nem sikerült elegáns megoldást találni. Amit a ScrollViewer-től szeretnénk, az valójában ellentétes a ScrollViewer természetével. A ScrollViewer arra készült, hogy egy területre be nem férő elemet (elemeket) úgy lehessen megjeleníteni, hogy változtatható legyen a túlméretes kép éppen szemlélt részlete (azaz, lehessen scrollozni). Most pedig azt szeretnénk, hogy a túlméretes kép lógjon túl a számára kijelölt területen. A scrollozás és a túllógás két teljesen különböző stratégia, és szembe mennek egymással.

A ScrollViewer belül egy ScrollContentPresenter osztályt használ. Ez az osztály sealed, szóval nincs mód rá, hogy származtatással felülírjuk a működését. Maga a működése is olyan összetett, hogy delegációval sem könnyű felhasználni a ScrollContentPresenter már megvalósított funkcióit.

Egy viszonylag egyszerű “vészmegoldás” lehet, ha a ScrollViewer templétjét módosítva a belül levő ScrollContentPresenter-nek nagyobb méretet adunk vízszintes irányba, mint amekkora a ListBox, így nem fogja megvágni az elemeket, mivel azok így már nem fognak kilógni.

Amit még módosítani kell a template-en, hogy a függőleges scrollbar átkerüljön a baloldalra, különben a megnövekedett elemek bele fognak lógni. A template összességében így néz ki:

<ScrollViewer 
    Focusable="false" 
    MaxWidth="{TemplateBinding MaxWidth}" 
    Padding="{TemplateBinding Padding}">
    
    <ScrollViewer.Template>
        <ControlTemplate TargetType="{x:Type ScrollViewer}">
            <Grid x:Name="Grid" Background="{TemplateBinding Background}">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="*"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                
                <ScrollContentPresenter                    
                    Width="{TemplateBinding MaxWidth}" 
                    CanContentScroll="{TemplateBinding CanContentScroll}" 
                    CanHorizontallyScroll="False" 
                    CanVerticallyScroll="False" 
                    ContentTemplate="{TemplateBinding ContentTemplate}" 
                    Content="{TemplateBinding Content}" 
                    Grid.Column="1" 
                    Grid.Row="0"
                    Margin="{TemplateBinding Padding}"/>
                
                <ScrollBar 
                    x:Name="PART_VerticalScrollBar" 
                    AutomationProperties.AutomationId="VerticalScrollBar" 
                    Cursor="Arrow" Grid.Column="0" 
                    Grid.Row="0" 
                    Maximum="{TemplateBinding ScrollableHeight}" 
                    Minimum="0"                     
                    Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" 
                    Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" 
                    ViewportSize="{TemplateBinding ViewportHeight}"/>
                  
                <ScrollBar 
                    x:Name="PART_HorizontalScrollBar" 
                    AutomationProperties.AutomationId="HorizontalScrollBar" 
                    Cursor="Arrow" 
                    Grid.Column="0" 
                    Grid.Row="1"
                    Maximum="{TemplateBinding ScrollableWidth}" 
                    Minimum="0" Orientation="Horizontal" 
                    Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" 
                    Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" 
                    ViewportSize="{TemplateBinding ViewportWidth}"/>
            </Grid>
        </ControlTemplate>
    </ScrollViewer.Template>
    
    <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                            
</ScrollViewer>

Az eredmény így:

Összefoglalás

A két nagyítási feladat közül az egyiket sikerült jól megoldani, a másik viszont csak szükségmegoldásnak jó. Ha egy ügyfél nagyon ragaszkodik, hát tessék itt van. Én azonban nem szívesen használnám. Az ilyen feladatokra egyébként is a master-details mintát szokták alkalmazni.

  1. Leave a comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: