views:

734

answers:

2

I was wondering if anyone knew how to duplicate the 9-slice functionality of Flex/Flash in WPF and VB.Net. I have used 9-slice scaling many times in Flex and it would be a great asset in WPF. I would like to be able to have an image as my background of a Canvas and have it stretch without ruining the rounded corners. Please, does anyone know how to do this?

A: 

I'm not aware of any built-in functionality that can do this, but you could write a custom control to do so.

The salient part of such a control would be a 9 part grid in which 4 parts were of fixed size (the corners), two parts had fixed heights and variable widths (center top and center bottom), two parts had fixed widths and variable heights (left center and right center), and the final part had variable height and width (the middle). Stretching in only one direction (e.g. making a button that only grows horizontally) is as simply as limiting the middle portion's height.

In XAML, that would be:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="20"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="20"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="20"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="20"/>
    </Grid.RowDefinitions>
</Grid>

Then you'd add objects to paint the images on to (I'll use Rectangles), as well as an object to put content into (ContentPresenter):

<Rectangle Grid.Row="0" Grid.Column="0" x:Name="TopLeft"/>
<Rectangle Grid.Row="0" Grid.Column="1" x:Name="TopCenter"/>
<Rectangle Grid.Row="0" Grid.Column="2" x:Name="TopRight"/>

<Rectangle Grid.Row="1" Grid.Column="0" x:Name="CenterLeft"/>
<Rectangle Grid.Row="1" Grid.Column="2" x:Name="CenterRight"/>

<Rectangle Grid.Row="2" Grid.Column="0" x:Name="BottomLeft"/>
<Rectangle Grid.Row="2" Grid.Column="1" x:Name="BottomCenter"/>
<Rectangle Grid.Row="2" Grid.Column="2" x:Name="BottomRight"/>

<Grid Grid.Row="2" Grid.Column="1" x:Name="Middle">
  <Rectangle/>
  <ContentPresenter x:Name="MiddleContent"/>
</Grid>

Each of the Rectangles can be painted using an ImageBrush so that they show the correct portion of your source image:

<Rectangle>
    <Rectangle.Fill>
        <ImageBrush ImageSource="Image.png" TileMode="None" 
                    <!-- Add the settings necessary to show the correct part of the image --> />
    </Rectangle.Fill>
</Rectangle>

Wrapping all of that up into a custom control, you could produce a pretty usable 9-slice image control:

<local:NineSliceImage Image="Source.png" Slice="20,20">
    <TextBox Text="Nine Slice Image TextBox!"/>
</local:NineSliceImage>

Where Slice is a property of type System.Windows.Size, so that you can use it like the Margin/Padding/etc. properties for setting the position of the slices.

You'll also want to set SnapToDisplayPixels to True on all of the Rectangles; otherwise, you'll see small gaps between the pieces of the image at certain resolutions as WPF tries to interpolate the in-between pixels.

An alternative, slightly faster way of doing this if you plan to use a lot of these controls is to override OnRender and do it there; I've done this in the past for a 3-slice image control, but it's quite a bit more tricky.

That should get you most of the way there - if there's anything I'm missing, leave a comment.

Nicholas Armstrong
A: 

Here is what I ended up getting after some toiling:

This is the SlicedImage.xaml file:

    <Rectangle Grid.Row="1" Grid.Column="0" SnapsToDevicePixels="True">
        <Rectangle.Fill>
            <ImageBrush x:Name="CenterLeft" Stretch="Fill" ViewboxUnits="Absolute" AlignmentX="Left"  AlignmentY="Center" />
        </Rectangle.Fill>
    </Rectangle>
    <Rectangle Grid.Row="1" Grid.Column="1" SnapsToDevicePixels="True">
        <Rectangle.Fill>
            <ImageBrush x:Name="CenterCenter" Stretch="Fill" ViewboxUnits="Absolute" AlignmentX="Center"  AlignmentY="Center" />
        </Rectangle.Fill>
    </Rectangle>
    <Rectangle Grid.Row="1" Grid.Column="2" SnapsToDevicePixels="True">
        <Rectangle.Fill>
            <ImageBrush x:Name="CenterRight" Stretch="Fill" ViewboxUnits="Absolute" AlignmentX="Right"  AlignmentY="Center" />
        </Rectangle.Fill>
    </Rectangle>

    <Rectangle Grid.Row="2" Grid.Column="0" SnapsToDevicePixels="True">
        <Rectangle.Fill>
            <ImageBrush x:Name="BottomLeft" Stretch="Fill" ViewboxUnits="Absolute" AlignmentX="Left"  AlignmentY="Bottom" />
        </Rectangle.Fill>
    </Rectangle>
    <Rectangle Grid.Row="2" Grid.Column="1" SnapsToDevicePixels="True">
        <Rectangle.Fill>
            <ImageBrush x:Name="BottomCenter" Stretch="Fill" ViewboxUnits="Absolute" AlignmentX="Center"  AlignmentY="Bottom" />
        </Rectangle.Fill>
    </Rectangle>
    <Rectangle Grid.Row="2" Grid.Column="2" SnapsToDevicePixels="True">
        <Rectangle.Fill>
            <ImageBrush x:Name="BottomRight" Stretch="Fill" ViewboxUnits="Absolute" AlignmentX="Right"  AlignmentY="Bottom" />
        </Rectangle.Fill>
    </Rectangle>

</Grid>

</UserControl>

And the SlicedImage.xaml.vb for those who want this in VB.Net:

Partial Public Class SlicedImage
Public imageSource As ImageSource
Public sliceTop As Double
Public sliceRight As Double
Public sliceLeft As Double
Public sliceBottom As Double

Public Sub New()

    InitializeComponent()

End Sub

Public Sub SetViewboxes()
    Dim RealHeight As Double = TopLeft.ImageSource.Height
    Dim RealWidth As Double = TopLeft.ImageSource.Width

    ColumnLeft.Width = New GridLength(sliceLeft)
    ColumnRight.Width = New GridLength(RealWidth - sliceRight)
    RowTop.Height = New GridLength(sliceTop)
    RowBottom.Height = New GridLength(RealHeight - sliceBottom)


    TopLeft.Viewbox = New Rect(0, 0, sliceLeft, sliceTop)
    TopCenter.Viewbox = New Rect(sliceLeft, 0, sliceRight - sliceLeft, sliceTop)
    TopRight.Viewbox = New Rect(sliceRight, 0, RealWidth - sliceRight, sliceTop)

    CenterLeft.Viewbox = New Rect(0, sliceTop, sliceLeft, sliceBottom - sliceTop)
    CenterCenter.Viewbox = New Rect(sliceLeft, sliceTop, sliceRight - sliceLeft, sliceBottom - sliceTop)
    CenterRight.Viewbox = New Rect(sliceRight, sliceTop, RealWidth - sliceRight, sliceBottom - sliceTop)

    BottomLeft.Viewbox = New Rect(0, sliceBottom, sliceLeft, RealHeight - sliceBottom)
    BottomCenter.Viewbox = New Rect(sliceLeft, sliceBottom, sliceRight - sliceLeft, RealHeight - sliceBottom)
    BottomRight.Viewbox = New Rect(sliceRight, sliceBottom, RealWidth - sliceRight, RealHeight - sliceBottom)

End Sub
Public Property ImageLocation() As ImageSource
    Get
        Return Nothing
    End Get
    Set(ByVal value As ImageSource)
        TopLeft.ImageSource = value
        TopCenter.ImageSource = value
        TopRight.ImageSource = value
        CenterLeft.ImageSource = value
        CenterCenter.ImageSource = value
        CenterRight.ImageSource = value
        BottomLeft.ImageSource = value
        BottomCenter.ImageSource = value
        BottomRight.ImageSource = value
    End Set
End Property

Public Property Slices() As String
    Get
        Return Nothing
    End Get
    Set(ByVal value As String)
        Dim sliceArray As Array = value.Split(" ")
        sliceTop = sliceArray(0)
        sliceRight = sliceArray(1)
        sliceBottom = sliceArray(2)
        sliceLeft = sliceArray(3)
        SetViewboxes()
    End Set
End Property

End Class

The User Control would be used like this:

<my:SlicedImage ImageLocation="Images/left_bubble.png" Slices="18 25 19 24" />

where ImageLocation is the image location, and Slices is "top right bottom left" for how the image should be sliced. All dimensions should be based from the top left corner.

Daniel