How to overlay items in StackPanel or ListView?

2020-08-25 05:42发布

问题:

I am making a card game and I want to display cards in player's hand half-covered be each other. How can I do that using ListView or StackPanel? Here is an example how I would like to display player hand.

<Grid Background="Green" >
        <Image x:Name="One" Width="100" Height="100" Margin="10,10,250,210"/>
        <Image x:Name="Two" Width="100" Height="100" Margin="10,10,210,210"/>
</Grid>

UPDATE

I set margins for ListView's ItemContainerStyle and it worked, but I have another problem. Width of ListView items doesn't fit the image and there is some spacing. How do I remove that. See image below the XAML code.

<ListView Grid.Row="0" Grid.Column="0">
        <ListView.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Horizontal" />
            </ItemsPanelTemplate>
        </ListView.ItemsPanel>
        <ListView.ItemContainerStyle>
            <Style TargetType="ListViewItem">
                <Setter Property="Margin" Value="0, 0, -80, 0"></Setter>
                <Setter Property="Height" Value="100"></Setter>
                <Setter Property="Width" Value="100"></Setter>
            </Style>
        </ListView.ItemContainerStyle>
        <Image x:Name="One" MaxWidth="100" Height="100" />
        <Image x:Name="Two" MaxWidth="100" Height="100" />
    </ListView>

回答1:

I would use Canvas in the list, and draw your card to the canvas, because things drawn in a canvas are not clipped, and instead managed through the canvas ZIndex etc.

Size the canvas based on your desired spacing, and oversize the contents. I'd also recommend binding to Items-source when using listboxes and using templates.

BTW I'm defining my cards using solidColorBrushes so I can just draw rectangles, replace this with your image source. I've defined my source in the resources, but in reality it would be bound to an ObservableCollection (Say, PlayersCurrentHand or something):

<UserControl.Resources>
    <x:Array Type="{x:Type SolidColorBrush}" x:Key="Cards">
        <SolidColorBrush Color="Blue"/>
        <SolidColorBrush Color="Red"/>
        <SolidColorBrush Color="White"/>
        <SolidColorBrush Color="White"/>
        <SolidColorBrush Color="White"/>
        <SolidColorBrush Color="White"/>
    </x:Array>
</UserControl.Resources>

Now, I presume you are using ListBox because you want to support selection? If so, the way WPF highlights list box items will mess up with this overlap, so we will need to replace it. If you don't want selection, just use an itemsControl and you can skip all the selection stuff.

Here's our basic listbox:

<ListView ItemsSource="{StaticResource Cards}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="112,98,-325,-25" Width="513" Height="227">
    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal" IsItemsHost="True" VerticalAlignment="Top"/>
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
    <ListView.ItemTemplate>
        <DataTemplate>
            <Border BorderBrush="Black" BorderThickness="1">
                <Rectangle Fill="{Binding}" Width="60" Height="100"/>
            </Border>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Which gives us this:

Now, we want to have all the list items to be drawn in a canvas, so let's define our ItemContainerStyle:

    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListViewItem}">
                        <StackPanel>
                            <Canvas Width="15" Height="100">
                                <ContentPresenter />
                            </Canvas>
                        </StackPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ListView.ItemContainerStyle>

See how we've set the canvas Width to 15? That defines the spacing of our cards. All the canvases are stacked at intervals of 15. However, the Rectangles we are drawing in our DateTemplate is Width 60, so these spill off to the right.

We've overridden the messy standard selection and highlighting styles. But no we don't know what's highlighted and selected, so let's add some functionality back in. We can also add things like shadows etc:

<ControlTemplate TargetType="{x:Type ListViewItem}">
    <StackPanel>
        <Canvas Width="15" Height="100">
            <Rectangle x:Name="Highlight"  Width="60" Height="5" Canvas.Top="105"/>
            <Rectangle Fill="#50000000" Width="60" Height="100" Margin="5,0,-5,0"/>
            <ContentPresenter />
        </Canvas>
    </StackPanel>
    <ControlTemplate.Triggers>
        <Trigger Property="IsSelected" Value="True">
            <Setter TargetName="Highlight" Property="Fill" Value="Yellow"/>
        </Trigger>
        <Trigger Property="IsMouseOver" Value="True">
            <Setter Property="Panel.ZIndex" Value="99"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

So now we have this:

Note, the gif didn't render the selection exactly right. The width issue is going to be tricky to fix without some code behind I think. One option is to make an IValueConverter that calculates width given the List of cards, and binding it to the Listview's Width property.

Edit

Found a way to get around the size issue! Padding! Of course. However, I found the scroll viewer clips even the canvas it contains (which makes sense if you think about it) but leaves all our effort hidden:

So you have to overwrite the scroll viewer functionality by setting the ControlTemplate manually:

    <ListBox.Template>
        <ControlTemplate>
            <Border Padding="5,25,55,15" BorderBrush="Gray" BorderThickness="1">
                <ItemsPresenter />
            </Border>
        </ControlTemplate>
    </ListBox.Template>

So now the padding accounts for the last card sticking out an extra 50.

Total code, with some more visual tweaks:

<ListView ItemsSource="{StaticResource Cards}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20" BorderBrush="Black">
    <ListBox.Template>
        <ControlTemplate>
            <Border Padding="5,25,55,15" BorderBrush="Gray" BorderThickness="1">
                <ItemsPresenter />
            </Border>
        </ControlTemplate>
    </ListBox.Template>
    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal" IsItemsHost="True" ClipToBounds="False" />
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListViewItem}">
                        <StackPanel>
                            <Canvas Width="15" Height="100">
                                <Rectangle x:Name="Highlight"  Width="60" Height="5" Canvas.Top="105"/>
                                <ContentPresenter x:Name="CardPresenter"/>
                            </Canvas>
                        </StackPanel>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter Property="Panel.ZIndex" Value="99"/>
                                <Setter TargetName="CardPresenter" Property="Canvas.Top" Value="-5"/>
                            </Trigger>
                            <Trigger Property="IsSelected" Value="True">
                                <Setter TargetName="Highlight" Property="Fill" Value="Yellow"/>
                                <Setter TargetName="CardPresenter" Property="Canvas.Top" Value="-20"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ListView.ItemContainerStyle>
    <ListView.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Border Background="#60000000" BorderThickness="0" CornerRadius="5" Height="100" Margin="5,0,-5,0"/>
                <Border BorderBrush="Black" BorderThickness="1" CornerRadius="5" Background="{Binding}" Width="60" Height="100"/>
            </Grid>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

It's pretty flexible, it was easy to add the "sticking out" functionality. Animations would be the next big step.

Edit 2

I'm just playing now. I'm not sure I like the "jump to the front" functionality, would be better if they just peeked out. Also, fanning them out (using a multi-binding):

Using the following template:

<ControlTemplate TargetType="{x:Type ListViewItem}">
    <StackPanel>
        <Canvas Width="15" Height="100">
            <Rectangle x:Name="Highlight"  Width="60" Height="5" Canvas.Top="105"/>
            <ContentPresenter x:Name="CardPresenter">
                <ContentPresenter.RenderTransform>
                    <TransformGroup>
                        <TranslateTransform x:Name="TranslateTransformHighlight"/>
                        <RotateTransform x:Name="RotateTransformHighlight" CenterY="100"/>
                        <TranslateTransform x:Name="TranslateTransformSelect"/>
                    </TransformGroup>
                </ContentPresenter.RenderTransform>
            </ContentPresenter>
        </Canvas>
    </StackPanel>
    <ControlTemplate.Triggers>
        <Trigger Property="IsMouseOver" Value="True" >
            <Trigger.EnterActions>
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetName="TranslateTransformHighlight" Duration="0:0:0.200" To="-5" Storyboard.TargetProperty="Y" />
                        <DoubleAnimation Storyboard.TargetName="RotateTransformHighlight" Duration="0:0:0.200" To="-5" Storyboard.TargetProperty="Angle" />
                    </Storyboard>
                </BeginStoryboard>
            </Trigger.EnterActions>
            <Trigger.ExitActions>
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetName="TranslateTransformHighlight" Duration="0:0:0.200" To="0" Storyboard.TargetProperty="Y" />
                        <DoubleAnimation Storyboard.TargetName="RotateTransformHighlight" Duration="0:0:0.200" To="0" Storyboard.TargetProperty="Angle" />
                    </Storyboard>
                </BeginStoryboard>
            </Trigger.ExitActions>
        </Trigger>
        <Trigger Property="IsSelected" Value="True">
            <Setter TargetName="Highlight" Property="Fill" Value="Yellow"/>
            <Trigger.EnterActions>
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetName="TranslateTransformSelect" Duration="0:0:0.200" To="-15" Storyboard.TargetProperty="Y" />
                    </Storyboard>
                </BeginStoryboard>
            </Trigger.EnterActions>
            <Trigger.ExitActions>
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetName="TranslateTransformSelect" Duration="0:0:0.200" To="0" Storyboard.TargetProperty="Y" />
                    </Storyboard>
                </BeginStoryboard>
            </Trigger.ExitActions>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>