ScaleTransform transforms non-linearly - wpf

I am using scale transform to allow a user to resize a control. What happens though is that when you start to move the mouse the control jumps to a new size, and then scales oddly. The further you move your mouse from the starting location the larger the increase in size becomes.
I expect its the way I calculate the scale to be applied. Here is the code:
private void ResizeGrip_MouseDown(object sender, MouseButtonEventArgs e)
{
ResizeHandle.CaptureMouse();
//Get the initial coordinate cursor location on the window
initBtmX = e.GetPosition(this).X;
bottomResize = true;
}
private void ResizeGrip_MouseUp(object sender, MouseButtonEventArgs e)
{
bottomResize = false;
ResizeHandle.ReleaseMouseCapture();
}
private void ResizeGrip_MouseMove(object sender, MouseEventArgs e)
{
if( bottomResize == true)
{
//Get the new Y coordinate cursor location
double newBtmX = e.GetPosition(this).X;
//Get the smallest change between the initial and new cursor location
double diffX = initBtmX - newBtmX;
// Let our rectangle capture the mouse
ResizeHandle.CaptureMouse();
double newWidth = e.GetPosition(this).X - diffX;
double scaler = newWidth / ResizeContainer.ActualWidth;
Console.WriteLine("newWidth: {0}, scalar: {1}", newWidth, scaler);
if (scaler < 0.75 || scaler > 3)
return;
ScaleTransform scale = new ScaleTransform(scaler, scaler);
ResizeContainer.LayoutTransform = scale;
}
}
Update: Now with XAML
<wtk:IToolDialog x:Name="VideoPlayer" ParentControl="{Binding ElementName=Stage}" DialogTitle="Video Player" Margin="90,5,0,0">
<Grid>
<Grid x:Name="ResizeContainer" ClipToBounds="True" Width="320" Height="240" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,1">
<!-- The video frame -->
<Image Stretch="Fill" Source="{Binding CurrentFrameImage}" x:Name="VideoImage" />
<Grid>
<pplcontrols:VideoGroundPlane Foreground="Black" GridSize="20" GroundPlane="{Binding GroundPlane}">
</pplcontrols:VideoGroundPlane>
</Grid>
<Grid x:Name="HitMask" IsHitTestVisible="False"/>
</Grid>
<ResizeGrip Cursor="SizeNWSE" x:Name="ResizeHandle" VerticalAlignment="Bottom" HorizontalAlignment="Right" Mouse.MouseDown="ResizeGrip_MouseDown" Mouse.MouseUp="ResizeGrip_MouseUp" Mouse.MouseMove="ResizeGrip_MouseMove"></ResizeGrip>
</Grid>
</wtk:IToolDialog>

Any reason why you are not using ResizeContainer.RenderTransfrom instead of ResizeContainer.LayoutTransform? I.e. use
ResizeContainer.LayoutTransform = scale;
If you want the scale to be linear I think you should use
double scaler = 1 - diff / ResizeContainer.ActualWidth;
EDIT:
There is a bug in the code that causes the scaled control to "jump" in size if you try to resize more than once. I suggest you do the following:
Add a RenderTransform to your ResizeContainer grid:
<Grid.RenderTransform>
<ScaleTransform x:Name="transform" ScaleX="1" ScaleY="1" />
</Grid.RenderTransform>
Change the code in your MouseMove event handler to the following:
if (bottomResize)
{
double newBtmX = e.GetPosition(this).X;
double scaler = -(initBtmX - newBtmX) / grid1.ActualWidth;
initBtmX = newBtmX;
transform.ScaleX += scaler;
transform.ScaleY += scaler;
}
This way you change the scale by whatever small amount the mouse has moved while dragging. All child controls within the grid should scale as well.

Related

SizeToContent with a Listbox

I'm using SizeToContent="WidthAndHeight" so that I can size a window to the size of the currently displayed controls. This works fine, however, I'm running into an issue when displaying and adding items to a listbox. When adding a great number of items to the listbox this will cause the window to extend below the edge of the monitor. I notice that without the SizeToContent="WidthAndHeight" I can easily get the Listbox to limit itself and use a scrollbar. However, this has the negative of displaying all the extra unused space that the listbox with take up before I have made it visible. I have added a small sample solution below to display this behaviour.
NOTE window does not need to be resizable. It just needs to be efficient on space and I would rather not use absolute values if possible.
Explaination of desired behaviour; user only sees the button, window has no unused space around the button. When the button is clicked the Listbox is now visible but has not extended beyond the screen (i.e. a scroll bar has been added) and the button beneath is visible.
...ResizeMode="CanMinimize" SizeToContent="WidthAndHeight">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*"/>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button Content="hello" Click="Button_Click"/>
<ListView Name="lbx" Grid.Row="1" Visibility="Collapsed">
</ListView>
<Button Name="btn" Content="hello" Grid.Row="2" Visibility="Collapsed"/>
</Grid>
private void Button_Click(object sender, RoutedEventArgs e)
{
btn.Visibility = Visibility.Visible;
lbx.Visibility = Visibility.Visible;
//Have tried various combinations of settings in code at runtime
this.SizeToContent = SizeToContent.Width;
this.Height = Double.NaN;
for (int i = 0; i < 1000; i++)
{
lbx.Items.Add("Num:" + i);
}
}
I've also tried this with a dockpanel instead of a Grid;
<Button Content="hello" Click="Button_Click" DockPanel.Dock="Top"/>
<Button Name="btn" Content="hello" DockPanel.Dock="Bottom" Visibility="Collapsed"/>
<ListView Name="lbx" Visibility="Collapsed">
</ListView>
Try swapping SizeToContent="WidthAndHeight" with SizeToContent="Width", the window starts up showing all the empty space that will later be used by the collapsed controls.
You could set the MaxHeight of the window based on the actual height of the ListBox and the screen, e.g.:
private void Button_Click(object sender, RoutedEventArgs e)
{
btn.Visibility = Visibility.Visible;
lbx.Visibility = Visibility.Visible;
for (int i = 0; i < 5; i++)
{
lbx.Items.Add("Num:" + i);
}
Dispatcher.Invoke(() =>
{
MaxHeight = Math.Min(SystemParameters.WorkArea.Height - Top, grid.ActualHeight);
}, System.Windows.Threading.DispatcherPriority.Background);
}
This seems like a good option. One drawback is that with a small number of items it still introduces a scrollbar even when the listbox items should be able to fit.
Introduce an offset then:
private void Button_Click(object sender, RoutedEventArgs e)
{
btn.Visibility = Visibility.Visible;
lbx.Visibility = Visibility.Visible;
for (int i = 0; i < 5; i++)
{
lbx.Items.Add("Num:" + i);
}
grid.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
grid.Arrange(new Rect(new Size(grid.DesiredSize.Width, grid.DesiredSize.Height)));
Height = Math.Min(SystemParameters.WorkArea.Height - Top, grid.ActualHeight + 40);
}

How to exactly center rendered text inside a Border

I have a Border with a Content of TextBlock that I want to be perfectly centered both horizontally and vertically. No matter what I try it never looks centered. What am I missing?
Using the code below the top of the text is 19px below the border, the bottom of the text is 5px above the border. It's also off center left or right depending on the Text value which I assume is related to the font.
The solution should work for varying text (1-31) with any font.
Code
<Grid Width="50" Height="50">
<Border BorderThickness="1" BorderBrush="Black">
<TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
</Border>
</Grid>
Result
Well then, challenge accepted ;-) This solution is based on the following idea:
Fit the TextBlock inside the border and make sure the entire text is rendered, even if not visible.
Render the text into a bitmap.
Detect the glyphs (i.e. characters) inside the bitmap to get the pixel-exact position.
Update the UI layout so the text is centered inside the border.
If possible, allow simple, generic usage.
1. TextBlock inside border / fully rendered
This is simple once you realize that the entire content of a ScrollViewer is rendered, so here is my UserControl XAML:
<UserControl x:Class="WpfApplication4.CenteredText"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<ScrollViewer x:Name="scroll"
IsHitTestVisible="False"
VerticalScrollBarVisibility="Hidden"
HorizontalScrollBarVisibility="Hidden" />
</Grid>
</UserControl>
With the code behind as:
public partial class CenteredText : UserControl
{
public CenteredText()
{
InitializeComponent();
}
public static readonly DependencyProperty ElementProperty = DependencyProperty
.Register("Element", typeof(FrameworkElement), typeof(CenteredText),
new PropertyMetadata(OnElementChanged));
private static void OnElementChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var elem = e.NewValue as FrameworkElement;
var ct = d as CenteredText;
if(elem != null)
{
elem.Loaded += ct.Content_Loaded;
ct.scroll.Content = elem;
}
}
public FrameworkElement Element
{
get { return (FrameworkElement)GetValue(ElementProperty); }
set { SetValue(ElementProperty, value); }
}
void Content_Loaded(object sender, RoutedEventArgs e) /*...*/
}
This control is basically a ContentControlwhich allows to handle the Loaded event of the content generically. There may be a simpler way to do this, I'm not sure.
2. Render to Bitmap
This one is simple. In the Content_Loaded() method:
void Content_Loaded(object sender, RoutedEventArgs e)
{
FrameworkElement elem = sender as FrameworkElement;
int w = (int)elem.ActualWidth;
int h = (int)elem.ActualHeight;
var rtb = new RenderTargetBitmap(w, h, 96, 96, PixelFormats.Pbgra32);
rtb.Render(elem);
/* glyph detection ... */
}
3. Detect the glyphs
This is surprisingly easy since a TextBlock is rendered with fully transparent background by default and we are only interested in bounding rectangle. This is done in a separate method:
bool TryFindGlyphs(BitmapSource src, out Rect rc)
{
int left = int.MaxValue;
int toRight = -1;
int top = int.MaxValue;
int toBottom = -1;
int w = src.PixelWidth;
int h = src.PixelHeight;
uint[] buf = new uint[w * h];
src.CopyPixels(buf, w * sizeof(uint), 0);
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
// background is assumed to be fully transparent, i.e. 0x00000000 in Pbgra
if (buf[x + y * w] != 0)
{
if (x < left) left = x;
if (x > toRight) toRight = x;
if (y < top) top = y;
if (y > toBottom) toBottom = y;
}
}
}
rc = new Rect(left, top, toRight - left, toBottom - top);
return (toRight > left) && (toBottom > top);
}
The above method tries to find the leftmost, rightmost, topmost and bottommost pixel which is not transparent and returns the results as a Rect in the output parameter.
4. Update Layout
This is done later in the Content_Loaded method:
void Content_Loaded(object sender, RoutedEventArgs e)
{
/* render to bitmap ... */
Rect rc;
if (TryFindGlyphs(rtb, out rc))
{
if (rc.Height > this.scroll.ActualHeight || rc.Width > this.scroll.ActualWidth)
{
return; // todo: error handling
}
double desiredV = rc.Top - 0.5 * (this.scroll.ActualHeight - rc.Height);
double desiredH = rc.Left - 0.5 * (this.scroll.ActualWidth - rc.Width);
if (desiredV > 0)
{
this.scroll.ScrollToVerticalOffset(desiredV);
}
else
{
elem.Margin = new Thickness(elem.Margin.Left, elem.Margin.Top - desiredV,
elem.Margin.Right, elem.Margin.Bottom);
}
if (desiredH > 0)
{
this.scroll.ScrollToHorizontalOffset(desiredH);
}
else
{
elem.Margin = new Thickness(elem.Margin.Left - desiredH, elem.Margin.Top,
elem.Margin.Right, elem.Margin.Bottom);
}
}
}
This UI is updated using the following strategy:
Compute the desired offset between the border and the glyph rectangle in both directions
If the desired offset is positive, it means that the text needs to move up (or left in the horizontal case) so we can scroll down (right) by the desired offset.
If the desired offset is negative, it means that the text needs to move down (or right in the horizontal case). This cannot be done by scrolling since the TextBlock is top-left-aligned (by default) and the ScrollViewer is still at the initial (top/left) position. There is a simple solution though: Add the desired offset to the Margin of the TextBlock.
5. Simple Usage
The CenteredText control is used as follows:
<Border BorderBrush="Black" BorderThickness="1" Width="150" Height="150">
<local:CenteredText>
<local:CenteredText.Element>
<TextBlock Text="31" FontSize="150" />
</local:CenteredText.Element>
</local:CenteredText>
</Border>
Results
For border size 150x150 and FontSize 150:
For border size 150x150 and FontSize 50:
For border size 50x50 and FontSize 50:
Note: There is a 1-pixel error where the space to the left of the text is 1 pixel thicker or thinner than the space to the right. Same with the top / bottom spacing. This happens if the border has an even width and the rendered text an odd width (no sub-pixel perfectness is provided, sorry)
Conclusion
The presented solution should work up to a 1-pixel error with any Font, FontSize and Text and is simple to use.
And if you haven't noticed yet, very limited assumptions were made about the FrameworkElement which is used with the Elem property of the CenteredText control. So this should also work with any element which has transparent background and needs (near-)perfect centering.
What you are talking about is related to the specific font (and characters within that font) that you are using. Different fonts will have different baselines, heights and other attributes. In order to combat that, just use Padding on the Border or Margin on the TextBlock to make it fit where you want it:
<Grid Width="50" Height="50">
<Border BorderThickness="1" BorderBrush="Black">
<TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center"
FontSize="50" Margin="0,0,3,14" />
</Border>
</Grid>
Note: You can also use the TextBlock.TextAlignment Property to make adjustments to the horizontal alignment of text content.
I'd add this as a comment but I haven't got enough reputation :P
It looks off center because the height and width you have specified for the grid (50x50) is too small to house a font size of 50. Either increase the size to 100x100 or lower the font size to something smaller.
To demonstrate that they will be perfectly aligned in the center by doing this - view this code in visual studio somewhere. You will see the numbers of these textblocks overlap perfectly.
<Grid Height="100" Width="100">
<Border BorderThickness="1" BorderBrush="Black" >
<TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
</Border>
<Border BorderThickness="1" BorderBrush="Black" >
<TextBlock Text="31" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
</Border>
</Grid>
I hope this helps you out :)

Query about hit testing

I am new to WPF. I want to have several different shapes in a canvas, then show a different context menu depending on which shape user right-clicks in. I am presently trying with just one shape, a rectangle inside a canvas. What came to mind was that in the handler for right click I should do a hit test for the rectangle. If it comes back true, I would show the context menu for this shape. But it does not work. How can I detect that the right click is inside the rectangle and not elsewhere on the canvas?
XAML :
<Canvas Grid.Column ="2" Name="canvas" Background="Transparent" RightButtonDown="show_context_menu" >
<Rectangle Name="myrectangle" Width="1000" Height="500" Fill="LightSteelBlue" Stroke="Black" StrokeThickness="4" Canvas.Left="10" Canvas.Top="100"/>
</Canvas>
C# :
private void show_context_menu(object sender, MouseButtonEventArgs e)
{
Point pt = e.GetPosition((UIElement)sender);
HitTestResult result = VisualTreeHelper.HitTest(myrectangle, pt);
if (result != null)
{
ContextMenu cm = cmCanvas as ContextMenu;
cm.PlacementTarget = sender as Canvas;
cm.IsOpen = true;
}
}
Issue is you are calculating point relative to canvas and passing visual as rectangle.
Calculate point relative to myrectangle and pass it in HitTest method:
Point pt = e.GetPosition(myrectangle);
HitTestResult result = VisualTreeHelper.HitTest(myrectangle, pt);
if (result != null)
{
.......
}

MatrixTransform in combination with Width/Height of a Canvas

When zooming in on a drawing on a Canvas I have the requirement to show scrollbars.
The Canvas is in a ScrollViewer and I increase the Width/Height of the Canvas so that that the scollbars appear (otherwise they don't).
To zoom in with a factor of 1.1 I use this code:
Matrix m = this.LayoutTransform.Value;
if (e.Delta > 0) f = 1.1;
else f = 1.0 / 1.1;
m.Scale(f, f);
this.LayoutTransform = new MatrixTransform(m);
this.Height = this.ActualHeight * f;
this.Width = this.ActualWidth * f;
It turns out that the Canvas becomes much too large. The drawing zooms in 10% but the width seems to become 20% more like the square of 1.1. So I use Math.Sqrt(f); instead of f.
Can anybody explain why it behaves this way?
You should only change the LayoutTransform of the Canvas, like in this simplified example:
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Canvas Width="1000" Height="1000" Background="Transparent"
MouseWheel="Canvas_MouseWheel">
<Canvas.LayoutTransform>
<MatrixTransform/>
</Canvas.LayoutTransform>
</Canvas>
</ScrollViewer>
The MouseWheel event handler:
private void Canvas_MouseWheel(object sender, MouseWheelEventArgs e)
{
var element = (FrameworkElement)sender;
var transform = (MatrixTransform)element.LayoutTransform;
var matrix = transform.Matrix;
var scale = e.Delta >= 0d ? 1.1 : (1d / 1.1);
matrix.Scale(scale, scale);
transform.Matrix = matrix;
e.Handled = true;
}

pixel selection for PanZoomImage

I am new to WPF and am trying to write a clickable zoom-pan image control. I already have a zoom-pan image which seems to work:
<Border Name="border" ClipToBounds="True">
<Canvas>
<Image Name ="image">
Source="{Binding Path=Source}"
MouseLeftButtonDown="image_MouseLeftButtonDown"
MouseLeftButtonUp="image_MouseLeftButtonUp"
MouseMove="image_MouseMove"
MouseWheel="image_MouseWheel">
</Image>
</Canvas>
</Border>
For the mouse and wheel events I used this post: http://www.codeproject.com/Articles/168176/Zooming-and-panning-in-WPF-with-fixed-focus
I am writing the clickable control by inheriting from ZoomPanImage and adding an event for LeftMouseUp.
public class ClickableImage : PanZoomImage
{
public event Action<Point> Click;
//...
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
// ... all sorts of checks to distinguish click from mouse move
if (Click != null)
{
Click(ControlToImage(mouseUpCoordinates));
}
}
protected Point ControlToImage(Point controlPixel)
{
//this is where i am stuck...
}
}
My problem is that I can't seem to calculate the correct image coordinates given the control coordinates. I need to take into account that the image can be zoomed and panned and that the window itself can be resized.
I tried using the rendering transform. When I zoom and pan the image I update the transform. And when I try to convert control coordinates to image coordinates I use the inverse transform:
Point imagePixel = image.RenderTransform.Inverse.Transform(controlPixel);
But this didn't work. One of the problems is that the Transform starts as Identity while in fact the image is stretched uniformly to the control's size.
Thanks,
Dina
Here's how I solved it. As Clemens suggested, I set the image stretch mode to none.
<Image Name="image" RenderOptions.BitmapScalingMode="NearestNeighbor" Stretch="None"
Source="{Binding Path=Source}"
MouseLeftButtonDown="image_MouseLeftButtonDown"
MouseLeftButtonUp="image_MouseLeftButtonUp"
MouseMove="image_MouseMove"
MouseWheel="image_MouseWheel"
Loaded="image_Loaded">
<Image.ContextMenu>
<ContextMenu>
<MenuItem Header="Fit to window" Click="FitToWindow_MenuItem_Click"></MenuItem>
</ContextMenu>
</Image.ContextMenu>
</Image>
This means that when the image is loaded into the window, you can only see part of it - depending on the window size. This is bad, but what's important is that the transform is identity and you can now manually set it such that the image is fully shown in the window.
private void FitViewToWindow()
{
if (Source == null)
throw new InvalidOperationException("Source not set");
BitmapSource bitmapSource = Source as BitmapSource;
if (bitmapSource == null)
throw new InvalidOperationException("Unsupported Image Source Type");
if (border.ActualWidth <= 0 || border.ActualHeight <= 0)
return;
double scaleX = border.ActualWidth / bitmapSource.PixelWidth;
double scaleY = border.ActualHeight / bitmapSource.PixelHeight;
double scale = Math.Min(scaleX, scaleY);
Matrix m = Matrix.Identity;
m.ScaleAtPrepend(scale, scale, 0, 0);
double centerX = (border.ActualWidth - bitmapSource.PixelWidth * scale) / 2;
double centerY = (border.ActualHeight - bitmapSource.PixelHeight * scale) / 2;
m.Translate(centerX, centerY);
image.RenderTransform = new MatrixTransform(m);
}
This function should be called upon loading the image and upon changing the source of the image. As for resizing the window - as long as you keep track of the transform, you will be able to convert coordinate systems correctly. For example, here's what I do for window resize:
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
base.OnRenderSizeChanged(sizeInfo);
//center the image in the new size
if (sizeInfo.PreviousSize.Width <= 0 || sizeInfo.PreviousSize.Height <= 0)
return;
Matrix m = image.RenderTransform.Value;
double offsetX = (sizeInfo.NewSize.Width - sizeInfo.PreviousSize.Width) / 2;
double offsetY = (sizeInfo.NewSize.Height - sizeInfo.PreviousSize.Height) / 2;
m.Translate(offsetX, offsetY);
image.RenderTransform = new MatrixTransform(m);
}

Resources