9-Slice Images in WPF - wpf

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?

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>
<!-- 20 is the amount of pixel on your image corner -->
<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="1" 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.

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.

Related

Creating two rectangles on top of each other that resize proportionally

EDIT: I made my example screen grabs more illustrative. They were conflicting with the text description.
I am a complete WPF newbie.
I want to have a thin rectangle with a dividing line that will resize as I expand and contract the window. Since I need to color the rectangle differently above and below the line I thought to do this with one rectangle overlaid on top of the other.
I can get a single rectangle that fills up the xaml control top to bottom ( called tallRectangle ) to resize properly with the window, but I can't figure out how to extend that behavior to the second rectangle:
<Grid>
<Rectangle x:Name="tallRectangle" Fill="#FFF4F4F5" HorizontalAlignment="Left" Margin="23,0,0,0" Stroke="Black" Width="56"/>
<Rectangle x:Name="halfRectangle" Fill="#FF757576" HorizontalAlignment="Left" Margin="84,136,0,0" Stroke="Black" Width="53" StrokeThickness="0" Stretch="Fill" RenderTransformOrigin="0.5,0.5">
<Rectangle.LayoutTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</Rectangle.LayoutTransform>
<Rectangle.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
I wish the second one, halfRectangle, to be half the size of the first, but to also scale larger and smaller if the window is resized. But I don't know how to make it do that.
Here is how it starts out:
When I make the window smaller, the top of the second rectangle stays static:
I would like the second rectangle to resize itself proportionally as well, like the orange outline in this:
This is only a very first step to what I'd like to have eventually, but I hope once I understand how to get that behavior in the second rectangle I'd be able to figure out the remainder of it.
Eventually I'd need to set that second rectangle to be a percentage of the height of the first, and I'd also need to add a variable number of hash marks to the first rectangle dynamically, at variable positions. It seems that once I understand how to get that second rectangle resizing, I'd be able to figure out the hash marks too.
EDIT: Once the grid structure is in place as explained in the answer to this question, the code behind can be modified to dynamically set the ratio, for example:
Dim realDepth = 8000
Dim divideDepth = 2600
Dim realScreenHeight = topRectangle.Height.Value + bottomRectangle.Height.Value
Dim realHeightToScreenRatio = realScreenHeight / realDepth
Dim divideScreenPoint = divideDepth * realHeightToScreenRatio
topRectangle.Height = New GridLength(divideScreenPoint, GridUnitType.Star)
bottomRectangle.Height = New GridLength(realScreenHeight - topRectangle.Height.Value, GridUnitType.Star)
The magic sprinkles is the second parameter to GridLength in order to preserve the dynamic resizing of the row's height.
If you are already using a Grid, why not define two equally sized rows:
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Rectangle Grid.RowSpan="2" Fill="#FFF4F4F5" Stroke="Black" Width="56"/>
<Rectangle Grid.Row="1" Fill="#FF757576" Stroke="Black" Width="56"/>
</Grid>
If you need to set the ratio of the row heights at runtime, you can simply assign a name to each row
<Grid>
<Grid.RowDefinitions>
<RowDefinition x:Name="row1"/>
<RowDefinition x:Name="row2"/>
</Grid.RowDefinitions>
<Rectangle Grid.RowSpan="2" Fill="#FFF4F4F5" Stroke="Black" Width="56"/>
<Rectangle Grid.Column="1" Grid.Row="1" Fill="#FF757576" Stroke="Black" Width="56"/>
</Grid>
and change their relative heights like this:
row1.Height = new GridLength(2, GridUnitType.Star); // 2/5
row2.Height = new GridLength(3, GridUnitType.Star); // 3/5

Can width of TextBox be set using FormattedText.MinWidth?

Banging my head trying to figure this out. The goal is to create a wrapping TextBox at a given location where the width is set to that of the largest word in the string of text. FormattedText.MinWidth calculates the width of the largest word in the string. But passing that MinWidth to the TextBox causes the TextBox to be slightly too narrow. TextBlock does not have this problem.
Evidentally something is happening deep down inside the TextBox causing this behavior. I can't just add a fixed magic number to the TextBox width because the increase in the width needed to correct the problem will always differ based on the pixel width of the character that was wrapped to the next line. The number of pixels will always differ depending on what that character is, the font, and font size.
If someone has more reputation could you please add FormattedText and MinWidth as a tag? Restrictions won't let me, a stupid first post newbe, do this. I also would like to have added an image which would make this sooo much easier to understand but stupid restrictions (did I say that?) prevent me from doing so.
namespace FormattedTextExample
{
public partial class FormattedText1 : Window
{
string noteText =
"We hold these truths to be self-evident, that all men are created equal, that they " +
"are endowed by their Creator with certain unalienable Rights, that among these are " +
"Life, Liberty and the pursuit of Happiness.--That to secure these rights, Governments " +
"are instituted..";
public FormattedText1()
{
InitializeComponent();
myText.Text = noteText;
}
protected override void OnRender(DrawingContext drawingContext)
{
FormattedText ft = new FormattedText(
textToFormat: noteText,
culture: CultureInfo.GetCultureInfo("en-us"),
flowDirection: myText.FlowDirection,
typeface: new Typeface(myText.FontFamily.ToString()),
emSize: myText.FontSize,
foreground: myText.Foreground);
ft.MaxTextWidth = ft.MinWidth;
DrawingContext dc = drawDest.Open();
dc.DrawText(ft, new Point(0, 0));
dc.Close();
myDrawingBrush.Drawing = drawDest;
myText.Width = ft.MinWidth;
}
}
}
<Window x:Class="FormattedTextExample.FormattedText1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="FormattedText" Height="500" Width="500">
<DockPanel>
<Grid DockPanel.Dock="Top" ShowGridLines="True">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Rectangle Grid.Column="0">
<Rectangle.Fill>
<DrawingBrush x:Name="myDrawingBrush" Stretch="None"
AlignmentY="Top" AlignmentX="Left" >
<DrawingBrush.Drawing>
<DrawingGroup x:Name="drawDest" />
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
<StackPanel Grid.Column="1">
<TextBox x:Name="myText" TextWrapping="Wrap" />
<!-- Everything works fine if using TextBlock -->
<!--<TextBlock x:Name="myText" TextWrapping="Wrap" />-->
</StackPanel>
</Grid>
</DockPanel>
</Window>
The reason your textbox needs more space is the way its control template is defined.
This is how the default control template looks like (from MSDN):
<ControlTemplate TargetType="{x:Type TextBoxBase}">
<Border Name="Border"
BorderThickness="1"
CornerRadius="2"
Padding="2">
<Border.Background>
<SolidColorBrush Color="{DynamicResource ControlLightColor}" />
</Border.Background>
<Border.BorderBrush>
<SolidColorBrush Color="{DynamicResource BorderMediumColor}" />
</Border.BorderBrush>
<!-- VisualStateManager code -->
<ScrollViewer x:Name="PART_ContentHost" Margin="0" />
</Border>
</ControlTemplate>
Studying the default control template or defining your own helps determining exactly how much extra space do you need to allocate to your text box.

How to change the foreground of the Thumb of Scrollbar in wpf?

I want to change the foreground of the Scroll bar thumb in my code.
I have applied a style which changes my thumb background, but, i wanted to change the
foreground image at run time. here is my style code for scroll bar.
<ControlTemplate x:Key="MyScrollBar" TargetType="{x:Type ScrollBar}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="178"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="12" />
<RowDefinition Height="*"/>
<RowDefinition Height="12" />
</Grid.RowDefinitions>
<Border Grid.Row="1" CornerRadius="2" BorderThickness="0" >
<Border.Background>
<ImageBrush ImageSource="/HyperVibe;component/Images/Grey_Slider_Background.png" />
</Border.Background>
</Border>
<RepeatButton Grid.Row="0" Command="ScrollBar.LineUpCommand" Content=" ^" />
<!--IsDirectionReversed set to true draws a ScrollBar with a
Track whose lowest value is at the bottom.
The default orientation of a ScrollBar is for the Track
values to decrease from top to bottom.-->
<Track Grid.Row="1" Name="PART_Track" IsDirectionReversed="true">
<Track.Thumb>
<Thumb BorderThickness="1" DataContext="{Binding}" >
<Thumb.OpacityMask>
<ImageBrush ImageSource="/HyperVibe;component/Images/Green%20Slider.png" />
</Thumb.OpacityMask>
<Thumb.Background>
<ImageBrush ImageSource="/HyperVibe;component/Images/Green%20Slider.png" />
</Thumb.Background>
</Thumb>
</Track.Thumb>
</Track>
<RepeatButton Grid.Row="2" Command="ScrollBar.LineDownCommand" Content=" v" />
</Grid>
</ControlTemplate>
any help will be greatly appreciated.
Best Regards,
~Anup
Try if you can load the above control template as a DynamicResource and do the following in C# code. If you load it as a static resource then you will not be able to edit the resource as it will be sealed.
ControlTemplate myCtrlTpl = (ControlTemplate) FindResource("MyScrollBar");
Trigger tgrIsMouseOver = new Trigger { Property = Thumb.IsMouseOverProperty, Value = true };
Trigger tgrIsMouseNotOver = new Trigger { Property = Thumb.IsMouseOverProperty, Value = false };
ImageBrush mImgBrhDefault = (Create what ever brush either imagebrush or solidcolorbrush)
ImageBrush mImgBrhHighlight = (Create what ever brush either imagebrush or solidcolorbrush)
tgrIsMouseOver.Setters.Add(new Setter(Thumb.BackgroundProperty, mImgBrhHighlight));
tgrIsMouseNotOver.Setters.Add(new Setter(Thumb.BackgroundProperty, mImgBrhDefault));

How can I draw a border with squared corners in wpf?

You know, like Battlestar paper! I have given this a few goes but now I'm stumped. I haven't yet gone down the geometery route, so I'll explain this as best as I can.
I'd like the border to be sizable, but contain fixed-size corners, just like CornerRadius does. Instead of rounded corners, I'd like them to be tapered, like:
/---------\
| |
| |
\_________/
I've done two attempts at this:
My first attempt attempts to manipulate a border class. This just doesn't work, as stretching the shape ruins the geometry and scale.
The second attempt was a bit more out the box. Literally. I created a 3x3 grid and filled it with 4 borders, each with a thickness of 2,0,0,0 - 0,2,0,0 - 0,0,2,0 and 0,0,0,2 respectively. The final step, is the join the borders up with a Line. Here where my question lies....
First attempt
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Grid.Resources>
<Style x:Key="MyPoly" TargetType="Polygon">
<Setter Property="Points">
<Setter.Value>
<PointCollection>
<Point X="0.10" Y="0.01"/>
<Point X="0.50" Y="0.01"/>
<Point X="0.60" Y="0.10"/>
<Point X="0.60" Y="0.50"/>
<Point X="0.50" Y="0.60"/>
<Point X="0.10" Y="0.60"/>
<Point X="0.01" Y="0.50"/>
<Point X="0.01" Y="0.10"/>
</PointCollection>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<Border
Width="100"
Height="100"
BorderBrush="Black"
BorderThickness="3"
CornerRadius="5"/>
<Grid Width="400"
Height="300">
<Polygon
Stroke="Purple"
StrokeThickness="2"
Style="{StaticResource MyPoly}" Stretch="Fill">
<Polygon.Fill>
<SolidColorBrush Color="Blue" Opacity="0.4"/>
</Polygon.Fill>
<Polygon.LayoutTransform>
<ScaleTransform ScaleX="1" ScaleY="1"/>
</Polygon.LayoutTransform>
</Polygon>
</Grid>
</Grid>
</Page>
Second attempt
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" SnapsToDevicePixels="True">
<Grid>
<Grid.Resources>
</Grid.Resources>
<Grid Width="200" Height="350" SnapsToDevicePixels="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="10"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="10"/>
<RowDefinition Height="*"/>
<RowDefinition Height="10"/>
</Grid.RowDefinitions>
<Border Grid.Column="0" Grid.Row="1" Margin="0" BorderBrush="Red" BorderThickness="2,0,0,0" Padding="0" SnapsToDevicePixels="True"/>
<Border BorderThickness="1" BorderBrush="Black">
<Line SnapsToDevicePixels="True" Stretch="Fill" Stroke="Red" StrokeThickness="2" X1="0" X2="1" Y1="1" Y2="0">
</Line>
</Border>
<Border Grid.Column="1" Grid.Row="0" BorderBrush="Red" BorderThickness="0,2,0,0" SnapsToDevicePixels="True"/>
<Border Grid.Column="2" Grid.Row="1" BorderBrush="Red" BorderThickness="0,0,2,0" SnapsToDevicePixels="True"/>
<Border Grid.Column="1" Grid.Row="2" BorderBrush="Red" BorderThickness="0,0,0,2" SnapsToDevicePixels="True"/>
</Grid>
</Grid>
</Page>
The Line is set to scale to the grid size. Setting the Line properties to X1="0" X2="1" Y1="1" Y2="0" and using Stretch="Fill" expands the Line to the edges. However, it ends up looking like this:
(Annoyingly, I can't post images, I need to go answer someone elses questions to earn some rep. So instead please go to this link to see the line, or paste the above XAML into Kaxaml.)
http://img375.imageshack.us/img375/1996/border1.png
I drew a magenta border around the Grid element hosting the Line, to make the problem more obvious.
How can I expand the line to really fill the gap (for example by inflating the drawable area within the grid), or, is there a better way?
Also, transformations distort the line, making it thicker. I tried scaling up but there wasn't a consistency to this. Endcaps on the line look just as bad (Triangle for example).
Finally, this method is still flawed, because I want to be able to set the corner size in the future, so having the edge width for the row/column set to 10 seems like a stumbling point. Binding to a property might solve that, I've never done that in a Style though.
Thanks for reading, Tom
The WPF border is inheriting from class Decorator. It is pretty easy to write your own Decorator. Below one draws a border around a child with "tucked in" corners.
class FunkyBorder : Decorator
{
public Brush BorderBrush
{
get { return (Brush)GetValue(BorderBrushProperty); }
set { SetValue(BorderBrushProperty, value); }
}
public static readonly DependencyProperty BorderBrushProperty =
DependencyProperty.Register("BorderBrush",
typeof(Brush),
typeof(FunkyBorder),
new UIPropertyMetadata(Brushes.Transparent));
protected override void OnRender(DrawingContext drawingContext)
{
// TODO, make pen thickness and corner width (currently 10) into dependency properties.
// Also, handle case when border don't fit into given space without overlapping.
if (_pen.Brush != BorderBrush)
{
_pen.Brush = BorderBrush;
}
drawingContext.DrawLine(_pen, new Point(0, 10), new Point(10, 0));
drawingContext.DrawLine(_pen, new Point(10, 0), new Point(ActualWidth - 10, 0));
drawingContext.DrawLine(_pen, new Point(ActualWidth - 10, 0), new Point(ActualWidth, 10));
drawingContext.DrawLine(_pen, new Point(0, 10), new Point(0, ActualHeight - 10));
drawingContext.DrawLine(_pen, new Point(ActualWidth, 10), new Point(ActualWidth, ActualHeight - 10));
drawingContext.DrawLine(_pen, new Point(0, ActualHeight - 10), new Point(10, ActualHeight));
drawingContext.DrawLine(_pen, new Point(10, ActualHeight), new Point(ActualWidth - 10, ActualHeight));
drawingContext.DrawLine(_pen, new Point(ActualWidth - 10, ActualHeight), new Point(ActualWidth, ActualHeight - 10));
}
private Pen _pen = new Pen(Brushes.Transparent, 2);
}
Use like this:
<BorderTest:FunkyBorder BorderBrush="Red">
<TextBlock Text="Hello" />
</BorderTest:FunkyBorder>
To avoid the nasty breaks at the end, you could use a Polygon or PolyLine:
<Polygon
Stroke="Red"
StrokeThickness="2"
Points="
0,1 1,0
1,0 20,0
20,0 21,1
21,1 21,20
21,20 20,21
20,21 1,21
1,21 0,20
0,1 1,0
"
Stretch="Fill"
/>
The width I picked is arbitrary...

XAML Path sizing and positioning

I am trying to design a template for a TabItem using paths and rectangles. The sides of the tab layout are paths to accommodate curves in the design. I am having a problem with getting the sides to line up/resize correctly. Here's my XAML:
<StackPanel Orientation="Horizontal">
<Path Stretch="UniformToFill" Fill="#FF000000" Data="F1 M 97.1985,101.669L 104.824,95.0005C 105.574,94.3338 106.921,93.8627 107.824,93.9171L 107.824,101.667L 97.1985,101.669 Z "/>
<Grid>
<Rectangle Grid.Column="1" Fill="#FF000000"/>
<ContentControl Grid.Column="1" x:Name="HeaderTopSelected" FontSize="{TemplateBinding FontSize}" Foreground="{TemplateBinding Foreground}" IsTabStop="False" Cursor="{TemplateBinding Cursor}" HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="{TemplateBinding VerticalAlignment}"/>
</Grid>
<Path Stretch="UniformToFill" Fill="#FF000000" Data="F1 M 118.714,101.678L 111.088,95.0097C 110.338,94.343 108.991,93.8719 108.088,93.9264L 108.088,101.676L 118.714,101.678 Z "/>
</StackPanel>
(I apologize for the lack of line breaks. I'm new to StackOverflow and don't know how to insert them, nor do I have the time to figure it out :D)
The code snippet posted almost works: The side paths are the correct size, but they do not display in a line, they overlap behind the rectangle/next element. If I set a fixed width they work as well, but I can't set the width as fixed, they need to be fluid in case the content of the tab exceeds the basic height.
This is an idea of what I would like to get
The sides (paths) grow uniformally based on the height of the ContentControl
Take 3 (now we know what the requirement is):
Basically to get the desired effect you need your 2 side parts to resize their width if their height changes, in order to keep a fixed aspect ratio. This will force the parent container to expand as the actual content gets taller and the sides get taller to match. none of the containers (including ViewBox) work exactly this way.
You could use a custom container to do this, but the preferred solution would be to create a behaviour that retains its object's aspect ratio on height changes. We will call it a FixedAspectBehavior:
using System.Windows;
using System.Windows.Interactivity;
namespace SilverlightApplication1
{
public class FixedAspectRatioBehavior : TargetedTriggerAction<FrameworkElement>
{
public double AspectRatio { get; set; }
protected override void OnAttached()
{
base.OnAttached();
FrameworkElement element = this.AssociatedObject as FrameworkElement;
if (element != null)
{
AspectRatio = element.Width/element.Height;
element.SizeChanged += new SizeChangedEventHandler(element_SizeChanged);
}
}
void element_SizeChanged(object sender, SizeChangedEventArgs e)
{
FrameworkElement element = AssociatedObject as FrameworkElement;
element.Width = element.Height * AspectRatio;
}
protected override void Invoke(object parameter)
{
}
}
}
The layout requires some work as self resizing elements (of type path) do some very weird things depending on their parent container. I had to use ViewBoxes as the parent of the paths, combined with the behaviours to get the desired result:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Viewbox>
<Path Stretch="UniformToFill" Fill="#FF000000" Data="F1 M 97.1985,101.669L 104.824,95.0005C 105.574,94.3338 106.921,93.8627 107.824,93.9171L 107.824,101.667L 97.1985,101.669 Z " UseLayoutRounding="False" HorizontalAlignment="Left">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<local:FixedAspectRatioBehavior/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Path>
</Viewbox>
<Grid Width="Auto" Background="#FFFF0D0D" Grid.ColumnSpan="1" Grid.Column="1">
<Rectangle Grid.Column="1" Fill="#FFD21F1F"/>
<ContentControl Grid.Column="1" x:Name="HeaderTopSelected" FontSize="{TemplateBinding Control.FontSize}" Foreground="{TemplateBinding Control.Foreground}" IsTabStop="False" Cursor="{TemplateBinding FrameworkElement.Cursor}" HorizontalAlignment="{TemplateBinding FrameworkElement.HorizontalAlignment}" VerticalAlignment="{TemplateBinding FrameworkElement.VerticalAlignment}"/>
<TextBlock TextWrapping="Wrap" FontSize="16"><Run Text="This is just a very string to see "/><LineBreak/><Run Text="what happens when we get to a size where the container should get larger than the default size"/></TextBlock>
</Grid>
<Viewbox Grid.Column="2">
<Path Stretch="UniformToFill" Fill="#FF000000" Data="F1 M 118.714,101.678L 111.088,95.0097C 110.338,94.343 108.991,93.8719 108.088,93.9264L 108.088,101.676L 118.714,101.678 Z " UseLayoutRounding="False" d:LayoutOverrides="VerticalAlignment">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<local:FixedAspectRatioBehavior/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Path>
</Viewbox>
</Grid>
Take 1 (obsolete now, as is 2 below):
The width of a path does not push along items in a stack panel. In that respect it's width is useless except to scale the item.
To get the effect you wanted, group each path into a grid. The size problem then goes away.
Apologies for turning your rectangle horrible red to show it clearly :)
XAML below:
<StackPanel Orientation="Horizontal" Width="Auto">
<Grid>
<Path Stretch="UniformToFill" Fill="#FF000000" Data="F1 M 97.1985,101.669L 104.824,95.0005C 105.574,94.3338 106.921,93.8627 107.824,93.9171L 107.824,101.667L 97.1985,101.669 Z " UseLayoutRounding="False" d:LayoutOverrides="Width"/>
</Grid>
<Grid Width="100" Background="#FFFF0D0D">
<Rectangle Grid.Column="1" Fill="#FFD21F1F"/>
<ContentControl Grid.Column="1" x:Name="HeaderTopSelected" FontSize="{TemplateBinding Control.FontSize}" Foreground="{TemplateBinding Control.Foreground}" IsTabStop="False" Cursor="{TemplateBinding FrameworkElement.Cursor}" HorizontalAlignment="{TemplateBinding FrameworkElement.HorizontalAlignment}" VerticalAlignment="{TemplateBinding FrameworkElement.VerticalAlignment}"/>
</Grid>
<Grid>
<Path Stretch="UniformToFill" Fill="#FF000000" Data="F1 M 118.714,101.678L 111.088,95.0097C 110.338,94.343 108.991,93.8719 108.088,93.9264L 108.088,101.676L 118.714,101.678 Z " UseLayoutRounding="False" d:LayoutOverrides="Width"/>
</Grid>
</StackPanel>
Take 2 (also obsolete now, see take 3 at the top):
If you require fixed end widths, use a grid instead of a stack panel (XAML below).
If you require something else, please provide a couple of screen shots of the desired effect.
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<Path Stretch="Fill" Fill="#FF000000" Data="F1 M 97.1985,101.669L 104.824,95.0005C 105.574,94.3338 106.921,93.8627 107.824,93.9171L 107.824,101.667L 97.1985,101.669 Z " UseLayoutRounding="False" d:LayoutOverrides="Width"/>
<Grid Width="Auto" Background="#FFFF0D0D" Grid.ColumnSpan="1" Grid.Column="1">
<Rectangle Grid.Column="1" Fill="#FFD21F1F"/>
<ContentControl Grid.Column="1" x:Name="HeaderTopSelected" FontSize="{TemplateBinding Control.FontSize}" Foreground="{TemplateBinding Control.Foreground}" IsTabStop="False" Cursor="{TemplateBinding FrameworkElement.Cursor}" HorizontalAlignment="{TemplateBinding FrameworkElement.HorizontalAlignment}" VerticalAlignment="{TemplateBinding FrameworkElement.VerticalAlignment}" Content="Hello there.... this is a long string to see what happens"/>
</Grid>
<Path Stretch="Fill" Fill="#FF000000" Data="F1 M 118.714,101.678L 111.088,95.0097C 110.338,94.343 108.991,93.8719 108.088,93.9264L 108.088,101.676L 118.714,101.678 Z " UseLayoutRounding="False" d:LayoutOverrides="Width" Grid.Column="2"/>
</Grid>

Resources