Hiding black bars in letterboxed thumbnail - c#

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.

Related

rendering wpf control to bitmap [duplicate]

I'm having a problem with RenderTargetBitmap whenever I render canvas and clear its children and set the rendered bitmap as background of canvas it slide toward bottom right.
can't insert images until 10 reputation :(.
WPF:
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="500" Width="700"
KeyDown="Window_KeyDown">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50"/>
<RowDefinition Height="*"/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Border BorderBrush="Black" BorderThickness="1" Grid.Row="1" Grid.Column="1">
<Canvas x:Name="Pad">
<Rectangle Height="100" Width="100" Fill="Red" Canvas.Left="10" Canvas.Top="10"></Rectangle>
</Canvas>
</Border>
</Grid>
</Window>
c# code:
namespace WpfApp1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_KeyDown(object sender, KeyEventArgs e)
{
RenderTargetBitmap rendrer = new RenderTargetBitmap(Convert.ToInt32(Pad.ActualWidth), Convert.ToInt32(Pad.ActualHeight), 96, 96, PixelFormats.Pbgra32);
rendrer.Render(Pad);
Pad.Background = new ImageBrush(rendrer);
Pad.Children.Clear();
}
}
}
To avoid any offset problems with drawing a Visual into a RenderTargetBitmap, you may use an intermediate DrawingVisual:
var rect = new Rect(Pad.RenderSize);
var visual = new DrawingVisual();
using (var dc = visual.RenderOpen())
{
dc.DrawRectangle(new VisualBrush(Pad), null, rect);
}
var bitmap = new RenderTargetBitmap(
(int)rect.Width, (int)rect.Height, 96, 96, PixelFormats.Default);
bitmap.Render(visual);
Pad.Background = new ImageBrush(bitmap);
Pad.Children.Clear();
Note that without setting any further properties of the ImageBrush (like e.g. its Viewport), it will fill the entire area of the Rectangle. For details, see TileBrush Overview.
Your primary problem stems from the fact that, due to the 1-pixel border around the Canvas, its VisualOffset vector is (1,1). Thus, any visual effect, like the background brush, will be applied at that offset. When you render the visual into a bitmap, it captures the present appearance, and then when you set the bitmap as the brush, it gets shifted.
Ironically, one of the easiest ways to fix this is to insert another <Border/> element into your XAML:
<Border BorderBrush="Black" BorderThickness="1" Grid.Row="1" Grid.Column="1">
<Border>
<Canvas x:Name="Pad">
<Rectangle Height="100" Width="100" Fill="Red" Canvas.Left="10" Canvas.Top="10"/>
</Canvas>
</Border>
</Border>
Then the offset caused by the outer <Border/> element is handled by the new <Border/> element's transform, rather than being applied to the <Canvas/> element.
That change alone will almost fix your code completely. However, there's one other little artifact that you may still notice: every time you render the visual, it gets just a teensy bit blurrier. This is because the default value for the Brush object's Stretch property is Stretch.Fill, and because your <Canvas/> element is not precisely an integral width or height, the bitmap (which necessarily does have integral width and height) gets stretched just a teensy bit when rendered. With each iteration, this becomes more and more apparent.
You can fix that by setting the Stretch property to Stretch.None. At the same time, you'll also want to set the brush's alignment to Left and Top:
private void Window_KeyDown(object sender, KeyEventArgs e)
{
RenderTargetBitmap renderer = new RenderTargetBitmap(
Convert.ToInt32(Pad.ActualWidth), Convert.ToInt32(Pad.ActualHeight), 96, 96, PixelFormats.Pbgra32);
renderer.Render(Pad);
ImageBrush brush = new ImageBrush(renderer);
brush.AlignmentX = AlignmentX.Left;
brush.AlignmentY = AlignmentY.Top;
brush.Stretch = Stretch.None;
Pad.Background = brush;
Pad.Children.Clear();
}
The defaults are Center, which again incurs the rounding error and will cause both movement and blurring of the image after repeated iterations of the process.
With the above changes, I found a perfectly stable image, regardless of the number of iterations.
The "wrap in a border" idea came from here: https://blogs.msdn.microsoft.com/jaimer/2009/07/03/rendertargetbitmap-tips/
On that page you'll find a more general-purpose solution which does not require modification of the actual XAML. In your example above, the "wrap in a border" approach seems like a reasonable work-around, but it is admittedly not as clean as forcing an unadorned context into which you can render the visual, as shown on that blog page.

How to exclude child from layouting

Look at example:
<Grid VerticalAlignment="Top" Background="Yellow">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="textBlock"/>
<Button Grid.Column="1">
<Viewbox>
<Path Height="100" Width="100" Fill="Black" Data="M 0,0 H 100 V 100 H 0 Z" />
</Viewbox>
</Button>
</Grid>
Here is a button with vector graphics and I want it to be as small as it needs to be (restrict vector graphics from exploding).
Here is how it looks like and how I want it to be:
There are several possible solutions how to overcome the problem, to list few:
By binding Width/Height to some other element (this has issues with designer):
...
<Button Height="{Binding ActualHeight, ElementName=textBlock}" ...>
...
This is often used together with the ghost: special invisible element used by others (have no relationship with it) to layout themselves.
By hosting element inside Canvas (which is magical container), but then Canvas itself require layouting.
You can try putting Button inside Canvas. This will cause parent Grid only take height of TextBlock, but then there is another problem: how to position (layout) Canvas itself, so that its children are layout properly, kek.
In example above I don't actually want TextBlock to be a sister of Button, they could overlap (you wouldn't want the button to be hidden if available size is not enough, it should rather overlap something less important), I just want them to have same parent (if it moves - children will move). Confused? Look here:
<Grid>
<TextBlock ... />
<Button HorizontalAlignment="Right" ... />
</Grid>
This layout has same problems and can be solved similarly.
Now try to abstract from concrete examples above.
What I actually want: is to learn how to exclude element from layout of container. Like if element is collapsed, so the parent container will measure children sizes (except this element), layout children and then, after layouting is finished, element suddenly become visible and is restricted by parent container, while can use various alignments.
Does it make sense what I am asking? Maybe custom container is the way? Or do I miss something existing and obvious?
If you want to constrain the Path by the size of the described geometry, it's as simple as setting StretchDirection="DownOnly" on the Viewbox.
If you truly want it to request no vertical space of its own, and have its height determined by its layout 'neighbors' (in this case, the TextBlock), then you'll need to wrap it in a custom layout container. But you can't simply exclude it from layout--at least not completely. If you did, the element would always end up with zero width and height. Instead, you can measure in two passes, with the first pass requesting a child height of zero, and the second pass basing the requested size on the arrange size given after the first pass.
I think the container below will give you what you want, but be warned that I haven't tested it thoroughly. Use at your own risk.
public class ZeroHeightDecorator : Border
{
private Size _lastSize;
private Size _idealSize;
protected override void OnVisualChildrenChanged(DependencyObject added, DependencyObject removed)
{
base.OnVisualChildrenChanged(added, removed);
_idealSize = new Size();
_lastSize = new Size();
}
protected override Size MeasureOverride(Size constraint)
{
var child = this.Child;
if (child == null)
return new Size();
if (child.IsMeasureValid)
child.Measure(new Size(Math.Max(_lastSize.Width, constraint.Width), _lastSize.Height));
else
child.Measure(new Size(constraint.Width, 0d));
_idealSize = child.DesiredSize;
return new Size(_idealSize.Width, 0d);
}
protected override Size ArrangeOverride(Size arrangeSize)
{
var child = this.Child;
if (child != null)
{
if (arrangeSize != _lastSize)
{
// Our parent will assume our measure is the same if the last
// arrange bounds are still available, so force a reevaluation.
this.InvalidateMeasure();
}
child.Arrange(new Rect(arrangeSize));
}
_lastSize = arrangeSize;
return arrangeSize;
}
}
A more flexible container would allow you to specify which dimension(s) to minimize: Width , Height, or Neither, or Both. Feel free to extend it :).
Example in action:
<Grid VerticalAlignment="Top" Background="Yellow">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="textBlock" />
<t:ZeroHeightDecorator Grid.Column="1">
<Button>
<Viewbox>
<Path Fill="Black" Data="M 0,0 H 100 V 100 H 0 Z" />
</Viewbox>
</Button>
</t:ZeroHeightDecorator>
</Grid>

WPF controls and margin proportional to the window size

Currently when I making the UI, I set all the margin and size of the control to a fixed size according to the screen size. Is there some way adjust the size or margin dynamically proportionally to the screen size so there's no need to set the property in the XAML every time.
My current knowledge tells me that I could set the fixed width & height in style or template or in every layout which was used.
I would say re-think your approach. Instead of absolutely positioning your elements using margins, use the correct panel so elements correctly re-position and stretch themselves depending on the size of the container.
There are many panels in WPF supporting different layouts for automatically laying out your elements dynamically, e.g. StackPanel for stacking, WrapPanel for wrapping, see the overview of panels here: http://msdn.microsoft.com/en-us/library/ms754152(v=vs.110).aspx
e.g.
instead of:
<Grid>
<Label Content="Name:" Margin="92,320,0,0"/>
<TextBox Text="enter your name..." Margin="124,320,0,0"/>
</Grid>
use:
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Content="Name:" Grid.Column="0"/>
<TextBox Text="enter your name..." Grid.Column="1"/>
</Grid>
Then use Margin for giving the element space, not position, e.g. a margin of 5.
Not sure if this is what you are looking for. But I what I do is set the size of my master window to be 80% of the client monitor size.
public void SetPage(Page currentPage)
{
currentPage.Tag = this; //Set the new page's Tag to 'this' so we can reference it from within.
_mainFrame.Navigate(currentPage);
double height = System.Windows.SystemParameters.PrimaryScreenHeight;
double width = System.Windows.SystemParameters.PrimaryScreenWidth;
//set the size of the window to 80% of the client monitor
this.Height = (80.0 / 100.0) * height;
this.Width = (80.0 / 100.0) * width;
WindowStartupLocation = System.Windows.WindowStartupLocation.CenterScreen;
}

Windows Phone 8 - ScrollViewer max content size, app crashes

I am creating game for WP 8 and I am using ScrollViewer and Grid with Buttons for game board.
The problem is when game board is too lardge. Then application crashes with no exception.
Output is:
ScrollViewer content size: 2400,2400 //my debug output
The program '[3420] TaskHost.exe' has exited with code -528909961 (0xe0797977).
Here is XAML code:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<ScrollViewer x:Name="ContentPanelScroll" HorizontalScrollBarVisibility="Auto">
<Grid x:Name="GamePanel" ShowGridLines="True">
<Grid.RowDefinitions>
<RowDefinition Height="100"/>
<RowDefinition Height="100"/>
<RowDefinition Height="100"/>
<!-- more RowDefinitions in this grid are addend in C# code-->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
<!-- more ColumnDefinitions in this grid are addend in C# code-->
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Content="..."/>
<!-- more buttons in this grid are addend in C# code-->
</Grid>
</ScrollViewer>
</Grid>
This is how I resize/zoom game board
private void ZoomGameBoard(double delta)
{
// typically delta = 10px;
// 20 columns and 20 rows, each zoomed to 150px is 3000x3000 px
foreach (var item in RowDefinitions)
{
var value = item.Height.Value + delta;
item.Height = new GridLength(value); // make row bigger so its content stretch
}
foreach (var item in ColumnDefinitions)
{
var value = item.Width.Value + delta;
item.Width = new GridLength(value); // make column bigger so its content stretch
}
}
Could you please tell me where is the problem or how to make larger game board?
Thx. :-)
EDIT:
My problem is probably this:
Insufficient memory to continue the execution of the program.
and this
An unhandled exception of type 'System.OutOfMemoryException' occurred in System.Windows.ni.dll
But this exception is not thrown every time app craches (just sometimes) does not solve my problem how to display large game board.
EDIT 2:
Here is test application to demonstrate my problem. You can change size in code or use Buttons in bottom part of phone.
Whole project is here: http://www.stud.fit.vutbr.cz/~xmarec12/shared/2013/TestGameApp.zip

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

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;
}

Categories