Query about hit testing - wpf

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)
{
.......
}

Related

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 :)

WPF - Hidden Window Shows In Screenshot

My simple goal is to take a screenshot.
My application has a main window that has multiple purposes and one of them is taking a screenshot.
What I do is hide the window by using MainWindow.Hide(); and then showing a second window that handles the screenshot taking process.
this.Hide();
SnapshotManager snapshotMgr = new SnapshotManager();
snapshotMgr.Closed += SnapshotMgrClosed;
snapshotMgr.Show();
The second window is basically this:
<Grid>
<Image Name="MainImage" Stretch="None" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
</Image>
<Border BorderBrush="Transparent" BorderThickness="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Border.Background>
<SolidColorBrush Color="Black" Opacity="0.4"/>
</Border.Background>
</Border>
</Grid>
with the properties:
AllowsTransparency="True"
Background="Transparent"
WindowStyle="None"
When the user clicks a button to take the screenshot I hide the main window and show this one.
Then I save the screen to the image in my second window like so:
Bitmap snapshot = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
Graphics graphics = Graphics.FromImage(snapshot);
graphics.CopyFromScreen(new System.Drawing.Point(0,0), new System.Drawing.Point(0,0), Screen.PrimaryScreen.Bounds.Size, CopyPixelOperation.SourceCopy);
//-----------------------------------
// Save the screen to MainImage
//-----------------------------------
MemoryStream ms = new MemoryStream();
snapshot.Save(ms, ImageFormat.Bmp);
ms.Position = 0;
BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.StreamSource = ms;
bi.EndInit();
MainImage.Source = bi;
The problem is that for some reason my main window shows up in the screenshot.
The MainWindow.Hide() probably doesn't work like I think it does.
How can I take a screenshot with the main window?
Thank you!
Edit:
The following code works but uses Sleep(200) which I really would like to avoid.
private void OnTakeSnapshot(object sender, RoutedEventArgs e)
{
this.LayoutUpdated += RunSnapshotMgr;
this.Hide();
}
private void RunSnapshotMgr(object sender, EventArgs e)
{
if (IsVisible == true) return;
Thread.Sleep(200);
SnapshotManager snapshotMgr = new SnapshotManager();
snapshotMgr.Closed += SnapshotMgrClosed;
snapshotMgr.Show();
this.LayoutUpdated -= RunSnapshotMgr;
}

scrollable wrap with overflow

I need to display a block of text in a resizable column. The text should wrap with overflow but, for a given column size, the user should be able to scroll horizontally to view the overflown text.
I do not believe this can be achieved w/ out of the box controls; if I'm wrong about that please tell me. I have attempted to achieve this with a custom control:
public class Sizer : ContentPresenter
{
static Sizer()
{
ContentPresenter.ContentProperty.OverrideMetadata(typeof(Sizer), new FrameworkPropertyMetadata(ContentChanged)); ;
}
public Sizer() : base() {}
protected override Size MeasureOverride(Size constraint)
{
var childWidth = Content==null ? 0.0 : ((FrameworkElement)Content).RenderSize.Width;
var newWidth = Math.Max(RenderSize.Width, childWidth);
return base.MeasureOverride(new Size(newWidth, constraint.Height));
}
private static void ContentChanged(DependencyObject dep, DependencyPropertyChangedEventArgs args)
{
var #this = dep as Sizer;
var newV = args.NewValue as FrameworkElement;
var oldV = args.OldValue as FrameworkElement;
if (oldV != null)
oldV.SizeChanged -= #this.childSizeChanged;
if(newV!=null)
newV.SizeChanged += #this.childSizeChanged;
}
private void childSizeChanged(object sender, SizeChangedEventArgs e)
{
this.InvalidateMeasure();
}
}
...and I can test it in a simple WPF application like so:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Auto" VerticalAlignment="Top">
<local:Sizer>
<local:Sizer.Content>
<TextBlock Background="Coral" Text="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaa"
TextWrapping="WrapWithOverflow" />
</local:Sizer.Content>
</local:Sizer>
</ScrollViewer>
<GridSplitter Grid.Column="1" Background="Black" VerticalAlignment="Stretch" HorizontalAlignment="Left" Width="5" />
</Grid>
This works, after a fashion. It wraps and displays the text correctly, and the horizontal scrollbar lets me view the overflown text. If I resize the column to the right (larger) the text re-wraps correctly. However, if I resize the column to the left (smaller) the text will not re-wrap. So, for instance, if I resize the column to the right so that all the text is on one line it will remain all on one line regardless of any subsequent re-sizing. This is an unacceptable bug.
I have tinkered w/ this code a great deal although I haven't had what you'd a call a good strategy for finding a solution. I do not know and have not been able to discover any mechanism for forcing a textblock to re-wrap its contents. Any advice?
I was able to get this working (w/ some limitations) by adding the following to the custom control:
//stores first ScrollViewer ancestor
private ScrollViewer parentSV = null;
//stores collection of observed valid widths
private SortedSet<double> wrapPoints = new SortedSet<double>();
protected override void OnVisualParentChanged(DependencyObject oldParent)
{
parentSV = this.FindParent<ScrollViewer>();
base.OnVisualParentChanged(oldParent);
}
... and editing MeasureOverride list this:
protected override Size MeasureOverride(Size constraint)
{
if (parentSV != null)
{
var childWidth = Content == null ? 0.0 : ((FrameworkElement)Content).RenderSize.Width;
var viewportWidth = parentSV.ViewportWidth;
if (childWidth > viewportWidth + 5)
wrapPoints.Add(childWidth);
var pt = wrapPoints.FirstOrDefault(d => d > viewportWidth);
if (pt < childWidth)
childWidth = pt;
return base.MeasureOverride(new Size(childWidth, constraint.Height));
}
else
return base.MeasureOverride(constraint);
}
I do not like this at all. It doesn't work for WrapPanels (although that isn't a required use case for me), it flickers occasionally and I'm concerned that the wrapPoints collection may grow very large. But it does what I need it to do.

How to make the pivot navigate to right or left?

I put a WebBrowser control inside a pivotItem, and a problem happens, the WebBrowser control take over the flick gesture. So the pivot can't navigate normally. So I put a gestureListner inside the parent container.
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto">
<phone:WebBrowser x:Name="myWB1"
FontSize="{StaticResource PhoneFontSizeExtraLarge}"
wb:WebBrowserHtmlBinding.HtmlString="{Binding MainFloor}"
Foreground="{StaticResource TitleColor}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Top"
Width="Auto" Height="Auto"
Navigating="WebBrowser_Navigating">
</phone:WebBrowser>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener Flick="GestureListener_Flick" />
</toolkit:GestureService.GestureListener>
</ScrollViewer>
private void GestureListener_Flick(object sender, FlickGestureEventArgs e)
{
if (e.Direction.ToString() == "Horizontal")
{
myPivot.SelectedIndex = 1;
}
}
The code above worked, but the problem is it is always navigate in one direction. No matter I flick to right or left. The pivot always navigate from right to left. Why, and How to solve it?
[The pivotitem's SelectedIndex of the WebBrowser control is 0, the next pivot is 1. ]
You are not checking whether the flick was towards the left or right. You are only checking the direction, in case of horizontal it could be either left or right. Try this:
private void GestureListener_Flick(object sender, FlickGestureEventArgs e) {
if (e.Direction == System.Windows.Controls.Orientation.Vertical) return;
var currentIndex = myPivot.SelectedIndex;
var maxIndex = myPivot.Items.Count - 1;
// User flicked towards left
if (e.HorizontalVelocity < 0) {
if (currentIndex < maxIndex) myPivot.SelectedIndex++; else myPivot.SelectedIndex = 0;
// User flicked towards right
} else if (e.HorizontalVelocity > 0) {
if (currentIndex > 0) myPivot.SelectedIndex--; else myPivot.SelectedIndex = maxIndex;
}
}
Alternatively, you could simply set IsHitTestVisible to false on the browser control to disable gestures on it.

ScaleTransform transforms non-linearly

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.

Resources