Can width of TextBox be set using FormattedText.MinWidth? - wpf

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.

Related

WPF Custom Border - bracket style

I would like to create a custom border around a content, that the upper and lower side of the border is not continuous, it should have a style like bracket as below. (inside the "bracket border" the content should be placed e.g. grid, stackpanel, etc.)
Note that the height of the right and the left border can be changed depending on the content's height, whereas the top and the bottom should have a standard width.
[ sample content ]
In order to achieve this, I separated the view in a 3 columns grid:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="22px"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="22px"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" BorderBrush="Blue" BorderThickness="6px 6px 0px 6px"></Border>
<Border Grid.Column="1" BorderBrush="Transparent" BorderThickness="6px">
<!--Place for the actual content of the border-->
<TextBlock Text="Test" FontSize="15"></TextBlock>
</Border>
<Border Grid.Column="2" BorderBrush="Blue" BorderThickness="0px 6px 6px 6px"></Border>
</Grid>
Is there another approach for achieving this style?
One possible solution is to write your own Border based on a Decorator.
An implementation (for a different border) can be found in How can I draw a border with squared corners in wpf?
One simple trick is try setting some LinearGradientBrush for the BorderBrush. If your text has a fixed width, it will look best at all time. However when the text's width may change, the horizontal lines (at 2 ends) will shrink/extend at some ratio. That's because we set some Offset for the GradientStop and it's pity that this Offset can only be set based on some ratio (from 0 to 1) with the width of the whole Brush (which is exactly the width of the Border when the background is stretched). Note that the MappingMode cannot change this behavior, it just works for StartPoint and EndPoint.
Here is the pure XAML code:
<Border VerticalAlignment="Center" HorizontalAlignment="Center"
BorderThickness="3" Padding="5,0,5,0">
<TextBlock Text="Square bracket border here" FontSize="30"
HorizontalAlignment="Center"/>
<Border.BorderBrush>
<LinearGradientBrush StartPoint="0,1" EndPoint="1,1">
<GradientStop Offset="0.03" Color="Blue"/>
<GradientStop Offset="0.03" Color="Transparent"/>
<GradientStop Offset="0.97" Color="Transparent"/>
<GradientStop Offset="0.97" Color="Blue"/>
</LinearGradientBrush>
</Border.BorderBrush>
</Border>
You can change the Offset of the first 2 GradientStops to what you want, the Offset of the remaining GradentStops should be the subtraction of the first's Offset from 1.
If using some code behind, you can pinpoint exactly the length of the horizontal lines (at the 2 ends). That way we need some Binding between the Offset and the ActualWidth of the Border. Next we need some Converter here, this Converter will convert the ActualWidth and the desired exact length to the correct ratio. So when the text width changes, the length of the horizontal lines will always be some fixed value.
You can try the following XAML code:
<Grid>
<Border BorderBrush="Black" BorderThickness="1"/>
<TextBlock Text="sample content"/>
<Border BorderBrush="White" BorderThickness="0,1,0,1" Margin="8,0,8,0"/>
</Grid>
Second border's color "White" can be replaced with the actual background color. "Transparent" color will not help.
Thanks,
RDV

How to make a multipaged UserControl in WPF?

I'm trying to make a user control which contains three different pages, each displaying different content. My idea was to make the following: create the user control main grid, then create another grid with the width set to three times the width of the user control or the main grid, and then create three columns in it. Then I would create a grid for each of the columns, wrapping each page content. Next, create two buttons to slide the pages, changing them through a translate transform animation.
I did it all right, but the sliding doesn't work as I expected: when the grid is translated, the content of the new page doesn't get displayed, and the other page keeps visible in the side of the user control.
The code is as follows:
.cs
private void TranslateMainGrid(bool right)
{
DoubleAnimation gridTranslateAnimation = new DoubleAnimation(); // Calculations not important
gridTranslateAnimation.From = right ? 0 - (this.SelectedPanel - 1) * 286 : 0 - (this.SelectedPanel + 1) * 286;
gridTranslateAnimation.To = 0 - this.SelectedPanel * 286;
gridTranslateAnimation.Duration
= new Duration(new TimeSpan(0, 0, 0, 0, 500));
TranslateTransform oTransform
= (TranslateTransform)PanelGrid.RenderTransform;
oTransform.BeginAnimation(TranslateTransform.XProperty,
gridTranslateAnimation);
}
.xaml
<Grid x:Name="MainGrid" Height="400" Width="286" Background="#7B9D9D9D" RenderTransformOrigin="0.5,0.5">
<Grid x:Name="PanelGrid" Height="400" Width="858" RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform>
<TranslateTransform X="0"/>
</Grid.RenderTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid x:Name="ChimeraGrid" Grid.Column="0">
<Grid.Background>
<ImageBrush ImageSource="/GameView;component/Resources/arcaneCreature.png"/>
</Grid.Background>
</Grid>
<Grid x:Name="CreatureGrid" Grid.Column="1">
<Grid.Background>
<ImageBrush ImageSource="/GameView;component/Resources/chimeraTest.png"/>
</Grid.Background>
<Label Content="lolololol" Height="81" VerticalAlignment="Top" HorizontalAlignment="Right" Width="164"/>
</Grid>
<Grid x:Name="EquipmentGrid" Grid.Column="2">
<Grid.Background>
<ImageBrush ImageSource="/GameView;component/Resources/tribeCreature.png"/>
</Grid.Background>
</Grid>
</Grid>
</Grid>
The code was simplified, but I guess it ilustrates the whole stuff. How can I deal with this grids? Is there any other way to do what I intended here?
Thanks
Replace your top-level Grid
<Grid x:Name="MainGrid" Width="286" ...>
by a Canvas and set the ClipToBounds property:
<Canvas Name="MainCanvas" Width="286" ClipToBounds="True">
Moreover you have to set the Height property of those Grids in the three columns that don't have any content. Setting only the Background to an ImageBrush will not affect the Grid's size. The result is that the three Grids have Width=286 (resulting from 858 divided by three columns) but the left and right Grid have Height=0, because they have no content. The middle one gets its height from the contained Label and is hence visible.
Instead of setting an ImageBrush you could also put an Image control in each column Grid. Thus the heights of the three Grids would be set automatically.
Of course ClipToBounds also works with a Grid, but it seems that the Grid won't re-render any previously invisible parts of its content when a RenderTransform is applied to that content.
When using a Canvas you may also consider to animate the Canvas.Left property instead of using a TranslateTransform.
EDIT: Here is the XAML from my test program:
<Window x:Class="SlidingGrid.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="500" Width="400">
<Canvas Width="286" ClipToBounds="True" Margin="10">
<Grid Width="858" Name="grid" Canvas.Left="0" Height="400">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RenderTransform>
<TranslateTransform x:Name="slideTransform"/>
</Grid.RenderTransform>
<Grid Grid.Column="0">
<Grid.Background>
<ImageBrush ImageSource="C:\Users\Public\Pictures\Sample Pictures\Desert.jpg" Stretch="UniformToFill"/>
</Grid.Background>
</Grid>
<Grid Grid.Column="1">
<Grid.Background>
<ImageBrush ImageSource="C:\Users\Public\Pictures\Sample Pictures\Penguins.jpg" Stretch="UniformToFill"/>
</Grid.Background>
</Grid>
<Grid Grid.Column="2">
<Grid.Background>
<ImageBrush ImageSource="C:\Users\Public\Pictures\Sample Pictures\Tulips.jpg" Stretch="UniformToFill"/>
</Grid.Background>
</Grid>
</Grid>
</Canvas>
</Window>
and the code:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += (o, e) =>
{
//grid.BeginAnimation(
// Canvas.LeftProperty,
// new DoubleAnimation(-572, TimeSpan.FromSeconds(2)));
slideTransform.BeginAnimation(
TranslateTransform.XProperty,
new DoubleAnimation(-572, TimeSpan.FromSeconds(2)));
};
}
}

Stop WPF ScrollViewer automatically scrolling to perceived content

The Application
I am building an application which includes a range selector. This consists of two custom drawn Slider controls contained within one UserControl derived class. The range selector control is then contained inside a ScrollViewer which has the HorizonalScrollBar visible most of the time.
Sample Application Code: ( appologies for the wall of text )
Window.xaml ( the Window file ):
<Grid>
<ScrollViewer x:Name="ScrollViewer" HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Disabled">
<local:SliderTest x:Name="slider"
LowerValue="0"
UpperValue="10"
Minimum="0"
Maximum="100" Width="900" Height="165" Padding="15,0,15,0" HorizontalAlignment="Left">
</local:SliderTest>
</ScrollViewer>
</Grid>
SliderTest.xaml:
<UserControl x:Class="scrollviewerDemoProblem.SliderTest"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
x:Name="root"
xmlns:local="clr-namespace:scrollviewerDemoProblem"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<ControlTemplate x:Key="simpleSlider" TargetType="{x:Type Slider}">
<Border SnapsToDevicePixels="true" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" MinHeight="{TemplateBinding MinHeight}"/>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Track x:Name="PART_Track" Grid.Row="1">
<Track.Thumb>
<Thumb x:Name="Thumb" FlowDirection="LeftToRight" Width="15">
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Canvas>
<Path x:Name="test1" StrokeThickness="0" Fill="DarkGreen">
<Path.Data>
<GeometryGroup FillRule="NonZero">
<PathGeometry>
<PathGeometry.Figures>
<PathFigure IsClosed="True" StartPoint="0,150" IsFilled="True">
<PathFigure.Segments>
<PathSegmentCollection>
<LineSegment Point="-15,150" />
<LineSegment Point="-15,0" />
<LineSegment Point="0,0" />
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</GeometryGroup>
</Path.Data>
</Path>
</Canvas>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
</Track>
</Grid>
</Border>
</ControlTemplate>
<ControlTemplate x:Key="simpleSliderRight" TargetType="{x:Type Slider}">
<Border SnapsToDevicePixels="true" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" MinHeight="{TemplateBinding MinHeight}"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Track x:Name="PART_Track" Grid.Row="1">
<Track.Thumb>
<Thumb x:Name="Thumb" HorizontalAlignment="Center" HorizontalContentAlignment="Center" Width="15">
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Canvas>
<Path Stroke="Black" StrokeThickness="0" Fill="DarkCyan">
<Path.Data>
<GeometryGroup FillRule="NonZero">
<PathGeometry>
<PathGeometry.Figures>
<PathFigure IsClosed="True" StartPoint="0,150">
<PathFigure.Segments>
<PathSegmentCollection>
<LineSegment Point="15,150" />
<LineSegment Point="15,0" />
<LineSegment Point="0,0" />
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</GeometryGroup>
</Path.Data>
</Path>
</Canvas>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
</Track>
</Grid>
</Border>
</ControlTemplate>
</UserControl.Resources>
<Grid x:Name="Gridd" VerticalAlignment="Top" Height="165" >
<Border x:Name="timeScaleBorder" Width="auto" Height="15" VerticalAlignment="Top" Background="Black">
<Canvas x:Name="timeCanvas" Width="auto" Height="15">
</Canvas>
</Border>
<Border x:Name="background" BorderThickness="1,1,1,1" BorderBrush="Black" VerticalAlignment="Center" Height="150"
Margin="0,15,0,0" Background="White" />
<Slider x:Name="LowerSlider"
Minimum="{Binding ElementName=root, Path=Minimum}"
Maximum="{Binding ElementName=root, Path=Maximum}"
Value="{Binding ElementName=root, Path=LowerValue, Mode=TwoWay}"
Template="{StaticResource simpleSlider}"
Margin="0,15,0,0" />
<Slider x:Name="UpperSlider"
Minimum="{Binding ElementName=root, Path=Minimum}"
Maximum="{Binding ElementName=root, Path=Maximum}"
Value="{Binding ElementName=root, Path=UpperValue, Mode=TwoWay}"
Template="{StaticResource simpleSliderRight}"
Margin="0,15,0,0" />
</Grid>
</UserControl>
SliderText.xaml.cs:
public partial class SliderTest : UserControl
{
public SliderTest()
{
InitializeComponent();
}
#region Dependency properties, values etc.
public static readonly DependencyProperty MinimumProperty =
DependencyProperty.Register("Minimum", typeof(double), typeof(SliderTest), new UIPropertyMetadata(0d));
public double LowerValue
{
get { return (double)GetValue(LowerValueProperty); }
set { SetValue(LowerValueProperty, value); }
}
public static readonly DependencyProperty LowerValueProperty =
DependencyProperty.Register("LowerValue", typeof(double), typeof(SliderTest), new UIPropertyMetadata(0d));
public double UpperValue
{
get { return (double)GetValue(UpperValueProperty); }
set { SetValue(UpperValueProperty, value); }
}
public static readonly DependencyProperty UpperValueProperty =
DependencyProperty.Register("UpperValue", typeof(double), typeof(SliderTest), new UIPropertyMetadata(0d));
public double Maximum
{
get { return (double)GetValue(MaximumProperty); }
set { SetValue(MaximumProperty, value); }
}
public static readonly DependencyProperty MaximumProperty =
DependencyProperty.Register("Maximum", typeof(double), typeof(SliderTest), new UIPropertyMetadata(1d));
public double Minimum
{
get { return (double)GetValue(MinimumProperty); }
set { SetValue(MinimumProperty, value); }
}
#endregion
}
The Problem
Most of the sample code provided is boring and the mechanics of it works pretty good. The problem I am having is a visual problem specifically with the ScrollViewer control that I have in the main Window. The ScrollViewer seems to be automatically adjusting the horizontal offset of the ScrollViewer when either of the Slider's gains focus ( from a mouse click for example ).
Reproducing the behaviour
Run the application, you will see that the horizontal scroll bar of the ScrollViewer is visible.
Click on the Green ( far left ) Slider, you will notice that the ScrollViewer automatically adjusts to shift the horizontal offset to where the perceived 'content' starts.
These symptoms occur at either end of the scroll pane.
Screenshot of application when it is run ( Application is Zoomed in 200% for detail clarity ):
Screenshot of the behavior when the left slider is clicked:
What I want to happen:
When I click on either slider item ( at either end ) when a slider looks to be beyond end of the slider ( slider range is denoted by the black bar at the top ) I don't want the ScrollViewer to automatically adjust it's horizontal offset.
Suspected problem:
I suspect that the problem is that the ScrollViewer perceives the actual 'content' of it's childen starts 15 pixels ( the drawn width of both of my sliders ) in from where the actual drawn content does start. The Canvas only draws because I included a padding of 15 pixels inside of the SliderTest control on the main window, if this padding is removed the ScrollViewer does not show any of the Slider's Canvas.
EDIT : it appears the padding is not the problem, read the comments as to why.
Things I have tried
I have tried looking into overriding the OnPreviewMouseDown event of the main Window. The problem here is that I still want both Slider's to behave normally, setting the event to Handled causes the Slider to stop working completely.
Notes:
The Slider's within the range selector control ( Called SliderTest in this example ) must both have a width of 1 pixel. The slider's must be able to extend 15 pixels past the end of the time selection range ( see the black bar at the top for a reference ).
Thank you for reading this novel-lengthed problem.
By default when a control receives the logical focus, FrameworkElement calls its own BringIntoView method (from within its OnGotFocus method if it has keyboard focus). That results in a RequestBringIntoView event being generated that bubbles up the element tree to allow ancestor elements to bring that portion of the element into view. The ScrollViewer listens for this event and eventually will call MakeVisible on the associated IScrollInfo/ScrollContentPresenter which leaves it up to the panel to bring that portion into view (since the panel would know how it arranges its children). It then takes that returned rect it receives back and asks for that portion of itself to be brought into view (in case you had nested elements that would require some action to ensure the original element was brought into view). So one way to suppress this behavior would be to handle the RequestBringIntoView event on the sliders and mark the event handled.
This may not work in this specific scenario, but a simple, clean solution to prevent a ScrollViewer from scrolling a focused element into view is to make the element unfocusable via Focusable=False. If an element cannot be focused then it will also not be automatically scrolled into view.

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...

9-Slice Images in 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.

Resources