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.
Related
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!
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>
I'm currently trying to display a Dictionary (which is held in a Dictionary itself).
I started at first using a UniformGrid as ItemsPanelTemplate, but realized pretty fast, the items to display can have individual heights.
I've got so far, that I can display all content using the UniformGrid, but can't seem to get it working using a Grid or StackPanel as ItemsPanelTemplate.
The code below is working fine with the downside that each Operation-block is given the same height though their height can be variable.
After given some thought I came to the conclusion that a StackPanel would be best to use, as the Operations would be shown bleow each other taking the height they needed. But when I tried, I relaized they take only a fraction of the ListView's height.
Worth to mention:
The Operation-UserControl in itself does evaluate its height and build its layout accordingly. So it doesn't take the space needed to display all content, but displays the content which does fit in the available space.
So how can I achieve that the ListViewItems (=operations) take the ListView's full height?
EDIT: clarification
if the described behaviour isn't possible with any above mentioned control, but any other could provide the needed funtionality, let me know...
EDIT2: some examples
Total available space: 500
No Scrollbar.
Sidenote: there is no maxItemLimit, but it's highly unlikely that the ItemCount would exceed 10.
Given 1 item: (needed space to display all content 300)
This single item would take 300.
Given 2 items: (these would need 150 and 200 of space)
Both items would be displayed in there full size: 150, 200.
(Presumably only working with a StackPanel.)
Given 10 items:
Those 10 would be squeezed equally or relative to full-desired size in the 500 (so 50 per item).
Both behaviours would be fine.
UniformGrid vs StackPanel
<UserControl x:Name="vDay" x:Class="RefServiceClient.Day"
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"
xmlns:local="clr-namespace:RefServiceClient"
mc:Ignorable="d"
d:DesignHeight="60" d:DesignWidth="120"
MinHeight="40">
<Grid x:Name="gridDay"
Width="{Binding ActualWidth, ElementName=vDay, Mode=OneWay}"
Height="{Binding ActualHeight, ElementName=vDay, Mode=OneWay}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid x:Name="DayHeader" Grid.Row="0">
<!--containing header info: several textboxes, which are irrelevant for the question-->
<TextBox x:Name="dayShortname"
Grid.Row="0" Grid.Column="1"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
Margin="1"
Text="{Binding Path=Value.DayShortname}"/>
</Grid>
<ListView x:Name="operations" Grid.Row="1" Background="Aqua"
ItemsSource="{Binding Path=Value.OperationList}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<ListView.ItemTemplate>
<DataTemplate>
<local:Operation Background="Crimson" VerticalContentAlignment="Stretch" VerticalAlignment="Stretch"/>
<!--<local:Operation Background="Crimson" />-->
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<!--<Grid Grid.IsSharedSizeScope="True"/>-->
<!--<StackPanel/>-->
<!--<VirtualizingStackPanel Orientation="Vertical"/>-->
<UniformGrid Columns="1"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
</Grid>
You could create a custom panel that arranges your item according to your rules. Then you just have to design your items in a way that they display nicely for whatever size they are allowed to take.
A rough sketch of the panel could look as follows:
public class SqueezeStackPanel : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
var desiredHeight = 0.0;
foreach (UIElement child in InternalChildren)
{
child.Measure(availableSize);
desiredHeight += child.DesiredSize.Height;
}
if (availableSize.Height < desiredHeight)
{
// we will never go out of bounds
return availableSize;
}
return new Size(availableSize.Width, desiredHeight);
}
protected override Size ArrangeOverride(Size finalSize)
{
// measure desired heights of children in case of unconstrained height
var size = MeasureOverride(new Size(finalSize.Width, double.PositiveInfinity));
var startHeight = 0.0;
var squeezeFactor = 1.0;
// adjust the desired item height to the available height
if (finalSize.Height < size.Height)
{
squeezeFactor = finalSize.Height / size.Height;
}
foreach (UIElement child in InternalChildren)
{
var allowedHeight = child.DesiredSize.Height * squeezeFactor;
var area = new Rect(new Point(0, startHeight), new Size(finalSize.Width, allowedHeight));
child.Arrange(area);
startHeight += allowedHeight;
}
return new Size(finalSize.Width, startHeight);
}
}
This panel can be used in the ItemsPanelTemplate of your ListView with disabled scrollbars.
It depends. I'll give you an options. First. Implement, let say, Boolean AttachedProperty, marking whether this particular instance should be of certain size. In case 0/1 is not sufficient, declare appropriate enumeration. Second. Extend existing StackPanel, override appropriate protected members. At least, MeasureOverride/ArrangeOverride. There you can read the value of corresponding attached property and decide how big or small it has to be. Does it sound like a solution? In case it does, I can provide some examples.
I programm an universal Windows platform app with Visual Studio and I want to get a simple Blur effect on my main grid layout, but I don't know how to apply a GaussianBlurEffect object on my grid. I searched for a long time and I've readed the Microsoft documentation but I don't understand the Visual Layer part.
If anyone can give me a little explaination about visuals, it would be nice :)
Sorry if my English is bad, I'm french.
You will find a lot of good samples on the Windows UI DevLabs repository
The idea of Visuals is to provide a low level API (but not as low as DirectX) to handle a lot of GPU accelerated effects on the UI. It allows you to draw what you want or create some effects on the rendering.
Here is a very basic sample to show how to apply a blur effect on a Grid. (It works the same for any other UIElement).
This code is adding a layer over the one used by the XAML renderer to render the grid. This newest layer will apply an effect on top of the image rendered by the XAML renderer.
The XAML of the page:
<Page
x:Class="BlurSample.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:BlurSample"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" x:Name="MainGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Rectangle Fill="Red" />
<Rectangle Fill="Green" Grid.Column="1" />
<Rectangle Fill="Blue" Grid.Row="1" Grid.Column="1" />
<Rectangle Fill="Yellow" Grid.Row="1" />
</Grid>
And the code behind:
public sealed partial class MainPage : Page
{
private CompositionEffectBrush brush;
private Compositor compositor;
public MainPage()
{
this.InitializeComponent();
MainGrid.SizeChanged += OnMainGridSizeChanged;
compositor = ElementCompositionPreview.GetElementVisual(MainGrid).Compositor;
// we create the effect.
// Notice the Source parameter definition. Here we tell the effect that the source will come from another element/object
var blurEffect = new GaussianBlurEffect
{
Name = "Blur",
Source = new CompositionEffectSourceParameter("background"),
BlurAmount = 100f,
BorderMode = EffectBorderMode.Hard,
};
// we convert the effect to a brush that can be used to paint the visual layer
var blurEffectFactory = compositor.CreateEffectFactory(blurEffect);
brush = blurEffectFactory.CreateBrush();
// We create a special brush to get the image output of the previous layer.
// we are basically chaining the layers (xaml grid definition -> rendered bitmap of the grid -> blur effect -> screen)
var destinationBrush = compositor.CreateBackdropBrush();
brush.SetSourceParameter("background", destinationBrush);
// we create the visual sprite that will hold our generated bitmap (the blurred grid)
// Visual Sprite are "raw" elements so there is no automatic layouting. You have to specify the size yourself
var blurSprite = compositor.CreateSpriteVisual();
blurSprite.Size = new Vector2((float) MainGrid.ActualWidth, (float) MainGrid.ActualHeight);
blurSprite.Brush = brush;
// we add our sprite to the rendering pipeline
ElementCompositionPreview.SetElementChildVisual(MainGrid, blurSprite);
}
private void OnMainGridSizeChanged(object sender, SizeChangedEventArgs e)
{
SpriteVisual blurVisual = (SpriteVisual) ElementCompositionPreview.GetElementChildVisual(MainGrid);
if (blurVisual != null)
{
blurVisual.Size = e.NewSize.ToVector2();
}
}
}
Update: Animation
To animate the blur effect, you will have to do two things:
declare the property you want to animate
create the animation
To declare the property, you will have to change the blurEffectFactory creation. Notice the declaration of the Blur.BlurAmount property :
// we convert the effect to a blur that can be used to paint the visual layer
var blurEffectFactory = compositor.CreateEffectFactory(blurEffect, new[] { "Blur.BlurAmount" });
brush = blurEffectFactory.CreateBrush();
Once declared, you can use the Blur.BlurAmount property in any animation you want. Here, I'm declaring a continuous animation of 3 seconds which will blur/unblur the image:
var blurAnimation = compositor.CreateScalarKeyFrameAnimation();
blurAnimation.InsertKeyFrame(0.0f, 100.0f);
blurAnimation.InsertKeyFrame(0.5f, 0.0f);
blurAnimation.InsertKeyFrame(1.0f, 100.0f);
blurAnimation.Duration = TimeSpan.FromSeconds(3);
blurAnimation.IterationBehavior = AnimationIterationBehavior.Forever;
brush.StartAnimation("Blur.BlurAmount", blurAnimation);
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.