Maintaining fixed-thickness lines in WPF with Viewbox scaling/stretching - c#

I have a <Grid> which contains some vertical and horizontal <Line>s. I want the grid to be scalable with the window size, and retain its aspect ratio, so it's contained in a <Viewbox Stretch="Uniform">.
However, I also want the lines to always render with a width of 1 pixel, so I use:
Line line = new Line();
line.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
// other line settings here...
This makes the lines' initial appearance ideal, but as soon as you start resizing the window, the stretching/scaling kicks in, and the lines become a mixture of 1 and 2 pixels thick again.
Is there any way to have the lines always be 1 pixel thick and also allow for resizing of the window/grid?
Update - Using path geometry as per Clemens' suggestion
#Clemens - Thanks for highlighting the rendering differences between lines and paths. As I try to rework my code using your example, I'm getting that sinking feeling that I'm digging more holes for myself and not really grasping the entire concept (entirely my fault, not yours, I'm just new to WPF).
I'll add some screenshots to illustrate the following description:
I'm making a game board (for the game of Go, in case that helps understand the layout at all). I have a 9x9 grid, and I'm planning on placing the game pieces by simply adding an ellipse to a particular grid cell.
To draw the underlying lines on the board, however, I need to draw lines intersecting the middle of the cells across the board (in Go, pieces are placed on the intersections, not the middle of the cells).
It could well be that I'm taking entirely the wrong approach, please feel free to tell me to start again down a different route, rather than hacking around within the current structure.
This is how I've done it so far (I'm adding the paths programatically, due to the way the coordinates are calculated. Not sure if it can all be done in XAML):
XAML:
<Grid MinHeight="400" MinWidth="400" ShowGridLines="False" x:Name="boardGrid">
<Grid.Resources>
<ScaleTransform x:Key="transform"
ScaleX="{Binding ActualWidth, ElementName=boardGrid}"
ScaleY="{Binding ActualHeight, ElementName=boardGrid}" />
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<!-- more rows, 9 in total -->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<!-- more columns, 9 in total -->
</Grid.ColumnDefinitions>
<!-- example game pieces -->
<Ellipse Stroke="Black" Fill="#333333" Grid.Row="3" Grid.Column="2" />
<Ellipse Stroke="#777777" Fill="#FFFFFF" Grid.Row="4" Grid.Column="4" />
</Grid>
C#:
int cols = 9;
int rows = 9;
// Draw horizontal lines
for (int row = 0; row < rows; row++)
{
var path = new System.Windows.Shapes.Path();
path.Stroke = Brushes.Black;
path.StrokeThickness = 1;
path.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
Grid.SetRow(path, row);
Grid.SetColumnSpan(path, cols);
Grid.SetZIndex(path, -1);
double cellWidth = boardGrid.ColumnDefinitions[0].ActualWidth;
double cellHeight = boardGrid.RowDefinitions[0].ActualHeight;
double x1 = (cellWidth / 2) / boardGrid.ActualWidth;
double y1 = (cellHeight / 2) / boardGrid.ActualHeight;
double x2 = ((cellWidth * cols) - (cellWidth / 2)) / boardGrid.ActualWidth;
double y2 = (cellHeight / 2) / boardGrid.ActualHeight;
path.Data = new LineGeometry(new Point(x1, y1),
new Point(x2, y2),
(ScaleTransform)boardGrid.TryFindResource("transform"));
boardGrid.Children.Add(path);
}
// Similar loop code follows for vertical lines...
This is what I get when using the code above
This is pretty much how I want it to look. It's raised 2 more questions for me:
1) Am I taking the right approach where I'm calculating the x1, x2, y1 and y2 values by diving them by the total board width to create a number between 0 and 1, so that the ScaleTransform can then be applied to them?
2) Now that I'm not using a Viewbox any more, how do I accomplish fixed-ratio scaling? If I enlarge my window, the board stretches out of proportion (see image below). (It doesn't anti-alias the lines any more though, which is great.)
I know this is getting to be a bit of a monolithic post. I'm very grateful for your patience and responses.

A Viewbox can only "visually" scale its child element, including the thickness of any rendered stroke. What you need is a scaling that only applies to the geometry of the lines (or other shapes), but leaves the stroke unaffected.
Instead of using Line objects, you could draw your lines by Path objects that use transformed LineGeometries for their Data property. You could create a ScaleTransform that scales from logical coordinates to viewport coordinates by using the Grid's width and height as scaling factors in x and y direction. Each LineGeometry (or any other Geometry) would use logical coordinates in the range 0..1:
<Grid x:Name="grid">
<Grid.Resources>
<ScaleTransform x:Key="transform"
ScaleX="{Binding ActualWidth, ElementName=grid}"
ScaleY="{Binding ActualHeight, ElementName=grid}"/>
</Grid.Resources>
<Path Stroke="Black" StrokeThickness="1">
<Path.Data>
<LineGeometry StartPoint="0.1,0.1" EndPoint="0.9,0.9"
Transform="{StaticResource transform}"/>
</Path.Data>
</Path>
</Grid>
In order to get a uniform scaling you may simply bind both the ScaleTransform's ScaleX and ScaleY properties to either the ActualWidth or ActualHeight of the Grid:
<Grid x:Name="grid">
<Grid.Resources>
<ScaleTransform x:Key="transform"
ScaleX="{Binding ActualWidth, ElementName=grid}"
ScaleY="{Binding ActualWidth, ElementName=grid}"/>
</Grid.Resources>
...
</Grid>
You may also calculate the uniform scaling factor from the minimum value of the width and height, with a bit of code behind:
<Grid x:Name="grid" SizeChanged="grid_SizeChanged">
<Grid.Resources>
<ScaleTransform x:Key="transform"/>
</Grid.Resources>
...
</Grid>
with a SizeChanged handler like this:
private void grid_SizeChanged(object sender, SizeChangedEventArgs e)
{
var transform = grid.Resources["transform"] as ScaleTransform;
var minScale = Math.Min(grid.ActualWidth, grid.ActualHeight);
transform.ScaleX = minScale;
transform.ScaleY = minScale;
}

Related

How to properly align Canvas with the underlying Image?

My application detects a foreign object (blob, cluster etc) in the live webcam image and displays object's outline on top of the image. To achieve that I employ Image and Canvas elements as follows:
<Border x:Name="ViewportBorder" Grid.Column="0" Grid.Row="1" BorderThickness="3" Background="AliceBlue" BorderBrush="Red">
<Grid>
<Image x:Name="videoPlayer" Stretch="Uniform" MouseDown="videoPlayer_MouseDown"></Image>
<Canvas x:Name="ObjectsCanvas"></Canvas>
</Grid>
</Border>
Border element in the above XAML is used just to draw a thick red line border around the Grid containing videoPlayer and ObjectsCanvas. Stretch="Uniform" is set to preserve image aspect ratio while being able it to stretch when application window gets maximized.
Every time the new frame arrives from the camera videoPlayer.Source gets updated with frame's bitmap whereas blob detection method yields a list of coordinates used for drawing a Polyline. The Polyline object is then added to ObjectsCanvas to be shown on top of the actual image frame.
Here's a part which draws the blob and adds it to the ObjectsCanvas.Children:
private void DrawBlob(List<Point> corners)
{
this.Dispatcher.Invoke(() =>
{
var myPolyline = new Polyline();
myPolyline.Stroke = System.Windows.Media.Brushes.Yellow;
myPolyline.StrokeThickness = 4;
myPolyline.FillRule = FillRule.EvenOdd;
myPolyline.Points = corners;
Canvas.SetLeft(myPolyline, 0);
Canvas.SetTop(myPolyline, 0);
ObjectsCanvas.Children.Clear(); // remove any old blob polyline
ObjectsCanvas.Children.Add(myPolyline); // add new polyline
});
}
When running the application I observe imperfect overlap of the blob object (thick yellow polyline), it gets somewhat right-shifted as shown in the image below.
Observed imperfection is not due to blob detection algorithm! I verified that by drawing the polylines of very same coordinates using old-fashion GDI methods on the actual bitmap.
It gets worse when I maximize the application window, an action causing videoPlayer to stretch:
I tried setting HorizontalAlignment and VerticalAlignment properties of ObjectsCanvas to Stretch but that does not help. Is there any method to align canvas exactly with the actual displayed image region?
I could get back to drawing the polylines using GDI, but I think it's a shame doing so in WPF...
I think I found a solution.
So, in order to stretch your canvas up to your image size you could wrap your canvas in ViewvBox control and bind your Canvas.Height and Canvas.Width to the image source's Height and Width like so:
<Grid>
<Image x:Name="MyFrame" Stretch="Uniform" />
<Viewbox Stretch="Uniform">
<Canvas
x:Name="MyCanvas"
Width="{Binding ElementName=MyFrame, Path=Source.Width}"
Height="{Binding ElementName=MyFrame, Path=Source.Height}">
<Canvas.Background>
<SolidColorBrush Opacity="0" Color="White" />
</Canvas.Background>
</Canvas>
</Viewbox>
</Grid>
However you need to set your Canvas.Background (you can make it transparent like in the example above), or ViewvBox will hide all of the canvas children otherwise.
Here are some screenshots of it working with a yellow polyline:
One more thing to note here - the coordinates of your polyline should be relative to image resolution.
Hope that helps!

Hiding black bars in letterboxed thumbnail

I have a user control that shows a thumbnail and some text below it. The API I'm using returns a 480x360 letterboxed thumbnail. I'm trying to hide it so the user only sees the image without the two 45px tall bars on the top an bottom. Below are the dimensions of the thumbnail:
User Control xaml:
<Grid>
<Grid.RowDefinitions>
<RowDefinition x:Name="ThumbnailRow"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Image Source="..." Stretch="UniformToFill" VerticalAlignment="Center"/>
<Grid Grid.Row="1" Background="Gray">
<TextBlock Padding="24" Text="..." HorizontalAlignment="Left"/>
</Grid>
</Grid>
In my codebehind, I tried to modify the height of ThumbnailViewRow to hide the black bars:
private double GetScreenWidth()
{
double scaleFactor = DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel;
double width = scaleFactor * Window.Current.Bounds.Width;
return width;
}
private double GetAdjustedThumbnailRowHeight()
{
// 38 represents 19px left & right margins in ListView
double adjustedWidth = GetScreenWidth() - 38;
double projectedHeight = (360 * adjustedWidth) / 480;
// in a full 480x360 image, I would need to shave 45 px from the top
// and bottom. In some resolutions, the image is scaled so I have
// to find the proportionate amount to trim
double toTrim = (projectedHeight * 90) / 360;
return projectedHeight - toTrim;
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
ThumbnailViewRow.Height = new GridLength(GetAdjustedThumbnailRowHeight());
}
The above code only slightly works; a large portion of the bars are still visible on both ends. On a 480x800 device, I was able to tweak some numbers to get the thumbnail to display correctly. The fix in that case was to multiply toTrim by 1.55 but I have no clue how well this would work out on devices with other resolutions. I don't have another device to test nor a WP emulator.
Could the reason for this problem be an embarrassing math mistake, or a subtlety in the way XAML works? How can I get my approach to work properly in different resolutions?
Here's a quick working example I made in WPF. (Note the explicitly-set height and width).
<Grid Margin="0, 30, 0, 0">
<Grid.RowDefinitions>
<!-- 360 - 45 - 45 = 270 -->
<RowDefinition Height="270"/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<Image Source="..." VerticalAlignment="Center" Width="480" Stretch="UniformToFill"/>
</Grid>
Use the Clip Property of the Image like so
<Image x:Name="myimage" Stretch="None" Source="/Assets/my_image.jpg">
<Image.Clip>
<RectangleGeometry Rect="0, 45, 480, 435"></RectangleGeometry>
</Image.Clip>
</Image>
RECT is the rectangle section of the image you want to be visible.

Textblock dyanmic placement WPF

I have a text data. I would like to place the data dynamically to a certain point [Absolute point, it may be (0,0) (10,10) or (100,100)].
Here the problem comes. My requirement is text block's center point should be the base point. How to make it and place it directly on the canvas? [Which means align the absolute point and center point of the textblock]. Because in winforms we can set the alignment property and place it directly. Please check this question .It describes how to move the textblock after placing it on the canvas since actual width can be found.
I have two specific questions
Is it possible to do it in WPF
Is there anyway to find the actual width before placement, in case I can do it as in the other question
Update (Further explanation)::::: This is my Data Template to place the text, I can use TranslateTransform to translate the text as soon as the text is placed on the canvas by using MoveToPoint function mention below.
The real problem come, how to pass the ActualWidth to MoveToPoint inside datatemplate,
XAML
<DataTemplate DataType="{x:Type local:Text}">
<TextBlock Text="{Binding Description}"
FontSize= "{Binding Thickness}"
RenderTransformOrigin="0.5,0.5"
Foreground="#FFF63AFF"
FontWeight="Bold" >
<TextBlock.RenderTransform>
<TransformGroup>
<TranslateTransform X= "{Binding StartPoint.X}" Y= "{Binding StartPoint.Y}" />
<RotateTransform Angle= "{Binding Angle}" />
</TransformGroup>
</TextBlock.RenderTransform>
</TextBlock>
</DataTemplate>
C#
void MoveToPoint(UIElement sender, Point point)
{
Canvas.SetLeft(sender, point.X - sender.RenderTransformOrigin.X * sender.ActualWidth);
Canvas.SetTop(sender, point.Y - sender.RenderTransformOrigin.Y * sender.ActualHeight);
}

Stretch image to fill available width / height (separately)

Is it any way to fill available width / height with image in xaml?
I need something like UniformToFill, but where I can control stretch direction (width or height)
Assume I have have following code:
<UniformGrid Columns="2" Rows="2">
<Image Source="Desert1.jpg" Stretch="Uniform"/> //
<Image Source="Desert2.jpg" Stretch="UniformToFill"/> //
<Image Source="Desert3.jpg" />
<Image Source="Desert4.jpg" />
</UniformGrid>
EDIT:
for examle (width): if image is half as wide as I want to show, I don't care about height and just scale x2 image height and width. So image must fit width, and don't care about height. It's desired behaviour, but if it's not possible - ok. So you can rethink question as IF it possible, HOW can I do it in xaml.
Also all images may have different width and height
I think that you might be able to get the effect you desire in certain conditions. If your images are all bigger than the size that they will be displayed, you could possibly use this:
<Image Source="Desert.jpg" Stretch="UniformToFill" StretchDirection="DownOnly" />
A ViewBox has the same Stretch properties as an Image and there is a good example of the differences between the different combinations in the How to: Apply Stretch Properties to the Contents of a Viewbox article on MSDN.
This might be what you are looking for...
TransformedBitmap
Here is a static method I made in an ImageUtility class.
public static TransformedBitmap GetScaledBitmapImageSprite(BitmapSource src, double x_scale, double y_scale)
{
return (new TransformedBitmap(src, new ScaleTransform(x_scale, y_scale)));
{
The x_scale and y_scale are doubles in the form of:
desired_width / original_width
Maybe a little different than what you are looking for but I think it can get you started on the right track.
You can store your TransformedBitmap in memory and apply new transforms through:
TransformedBitmap x = new TransformedBitmap();
x.Transform = new ScaleTransform(x,y);
You should use
<Image Source="{Binding Image}" Stretch="Fill"/>
like if you use Stretch="UnifrmtoFill" then it will change both length and width in a ratio or I say both together.
so if you use
Stretch="Fill", it gives you chance to change either height or width at a time whatever is changed.

WPF Jitters with TranslateTransform and Canvas.SetLeft

I'm running into a problem of "jitters" when moving the X, Y coordinates of controls. Basically, I got an animation to work two different ways: 1) TranslateTransform of the X property, and 2) A Timer that calls Canvas.SetLeft. Both of which cause the image to move, but not smoothly.
XAML:
<Canvas Margin="0" Name="CanvasContainer">
<Canvas Margin="0" Name="FirstCanvas" Background="White">
<Image Name="FirstImage" Opacity="1" Margin="0,0,0,0" Canvas.Left="0" Canvas.Top="0" Source="someImage.png" />
</Canvas>
<Canvas Margin="0" Name="SecondCanvas" Background="DarkOrange">
<Image Name="SecondImage" Opacity="1" Margin="0,0,0,0" Canvas.Left="0" Canvas.Top="0" Source="anotherImage.png" />
</Canvas>
</Canvas>
TranslateTransform:
private void StartMovement(double startX, double endX, double milliseconds = 1000)
{
GuiDispatcher.Invoke(DispatcherPriority.Normal, new Action<Canvas, double, double, double>(MoveTo), Canvas, startX, endX, milliseconds);
}
private void MoveTo(Canvas canvas, double startX, double endX, double milliseconds)
{
canvas.RenderTransform = new TranslateTransform();
var animation = new DoubleAnimation(startX, endX, TimeSpan.FromMilliseconds(milliseconds));
canvas.RenderTransform.BeginAnimation(TranslateTransform.XProperty, animation);
}
Is there a better method for accomplishing this, or do I have something set up wrong? Any help would be appreciated.
Either of those methods are generally fine for animations in WPF. If the image isn't moving smoothly, I have a few of questions.
How big is the image?
Large images take longer to render, and will therefore not animate as well.
Are you rendering the image at its native resolution?
Like large images, scaling can slow down the render, as it takes longer to calculate the rendered pixels.
How good is your graphics card? And are your drivers up to date?
WPF uses your graphics card to render, unless it isn't good enough. If it has to fallback to software rendering, everything gets sluggish.
How far is the image moving?
The further the image moves, the fewer frames will be drawn per second, which could leave to the appearance of the animation being jerky.
If it is a framerate issue because the image is moving too far too quickly, you can increase the desired framerate by setting the Timeline.DesiredFrameRate property:
Timeline.SetDesiredFrameRate(animation, 120);;
In WPF, the default target framerate is 60, and is by no means guaranteed. But one of the primary uses for this attached property is to decrease horizontal tearing, so it might help.

Categories