Creating a control that covers the whole area except a particular area - c#

I want to create a control that covers the whole area except a particular area. This particular area will be in an ellipse/rectangle shape. Something like the below concept
My idea is to use paths. Below is my code for the path.
<Path Name="ShowcasePath" StrokeThickness="1" IsHitTestVisible="True">
<Path.Data>
<GeometryGroup>
<EllipseGeometry RadiusX="100" RadiusY="50"/>
<RectangleGeometry/>
</GeometryGroup>
</Path.Data>
</Path>
Centre of the EllipseGeometry and Rect of the RectangleGeometry will be set in code behind.
This method has two problems
The fill colour is semi-transparent even with no alpha value. The final colour will be transparent white. So it's not a real problem.
The item inside the ellipse should be fully functional and items outside the ellipse should be non-functional.
Any idea for solving the above issues?
or Any different idea for this control?

I think you were already pretty close with your example, at least I can't find anything wrong with it. If you try the following code, you will see it does exactly what you described:
<Grid>
<Button Content="Clickable" HorizontalAlignment="Left"
Margin="113,90,0,0" VerticalAlignment="Top" Width="75"/>
<Button Content="Non-Clickable" HorizontalAlignment="Left"
Margin="272,90,0,0" VerticalAlignment="Top" Width="75"/>
<Path Name="ShowcasePath" StrokeThickness="1" Fill="#88ff0000" IsHitTestVisible="True">
<Path.Data>
<GeometryGroup FillRule="EvenOdd">
<EllipseGeometry Center="150,100" RadiusX="100" RadiusY="50"/>
<RectangleGeometry Rect="0,0,500,500"/>
</GeometryGroup>
</Path.Data>
</Path>
</Grid>
Keep in mind however, that users could still change the focused element with Tab and activate elements with Space or Return, even if you set IsHitTestVisible to false.
The "focus the ellipse on an element" functionality can easily be taken care of like this:
public void FocusOnElement(FrameworkElement element)
{
Point center = new Point(element.ActualWidth / 2, element.ActualHeight / 2);
Point newPosition = element.TranslatePoint(center, ShowcasePath);
ellipseGeometry.Center = newPosition;
}
The enable/disable all other elements functionality would best be taken care of in your viewmodel, or if you don't have one, kind of like this:
private void DiableAllButThisAndItsParents(FrameworkElement thisElement)
{
List<FrameworkElement> hierarchy = FindParents(thisElement).ToList();
foreach (FrameworkElement element in hierarchy)
{
element.IsEnabled = true;
if (ReferenceEquals(element, thisElement)) continue;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
{
var child = VisualTreeHelper.GetChild(element, i);
if (!(child is FrameworkElement childElement)) continue;
childElement.IsEnabled = hierarchy.Contains(childElement);
}
}
}
private IEnumerable<FrameworkElement> FindParents(FrameworkElement element)
{
DependencyObject current = element;
while (current != null)
{
if (current is FrameworkElement)
yield return (FrameworkElement) current;
current = VisualTreeHelper.GetParent(current);
}
}
Put that all together and it should look something like this:

Instead of trying to get a element which is "Click-through" you can simply bring the Element (in your case the togglebutton) to the front (basically above everything else so that it is clickable).
You can do this by either of the following:
change the element's ZIndexProperty so that it comes to the front.
YourElement.SetValue(Canvas.ZIndexProperty, index);
Remove the element and add it again to the parent grid(thus it will basically be the last element to be added)
parentGrid.Children.Remove(YourElement);
parentGrid.Children.Add(YourElement);
parentGrid.UpdateLayout();
After the user has successfully interacted with the control you can switch it back to the way it was before.. Hope this helps..

Related

Subclassing Path causes rendering issues

I'm trying to draw Microsoft.UI.Xaml.Shapes.Path objects to a Microsoft.UI.Xaml.Controls.Canvas control. These objects are configured in their own coordinate system, and transformed to the pixel-space coordinate system using the Transform property of the Geometry objects.
However, I'm getting some odd behaviour that I'm struggling to make sense of. Here is code that replicates the issue:
XAML
<Window
x:Class="TransformRenderBug.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:TransformRenderBug"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
SizeChanged="Window_SizeChanged">
<TabView >
<TabViewItem Header="Blank tab"/>
<TabViewItem Header="Drawing tab">
<local:NumberSpace x:Name="NumberSpace" >
<Path Stroke="Red" StrokeThickness="1">
<Path.Data>
<EllipseGeometry RadiusX="1" RadiusY="1" Transform="{x:Bind Transform}"/>
</Path.Data>
</Path>
<Path Stroke="Green" StrokeThickness="1">
<Path.Data>
<EllipseGeometry RadiusX="0.9" RadiusY="0.9" Transform="{x:Bind NumberSpace.Transform}"/>
</Path.Data>
</Path>
<local:NumberSpacePath Stroke="Blue" StrokeThickness="1">
<local:NumberSpacePath.Data>
<EllipseGeometry RadiusX="0.8" RadiusY="0.8" Transform="{x:Bind Transform}"/>
</local:NumberSpacePath.Data>
</local:NumberSpacePath>
<local:NumberSpacePath Stroke="Black" StrokeThickness="1">
<local:NumberSpacePath.Data>
<EllipseGeometry RadiusX="0.7" RadiusY="0.7" Transform="{x:Bind NumberSpace.Transform}"/>
</local:NumberSpacePath.Data>
</local:NumberSpacePath>
</local:NumberSpace>
</TabViewItem>
</TabView>
</Window>
C#
public sealed partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private CompositeTransform Transform { get; } = new();
private void Window_SizeChanged(object sender, WindowSizeChangedEventArgs args)
{
NumberSpace.SetTransform(Transform, args.Size.Width, args.Size.Height - 40);
NumberSpace.Width = args.Size.Width;
NumberSpace.Height = args.Size.Height - 40;
}
}
public class NumberSpace : Canvas
{
public CompositeTransform Transform { get; } = new();
public NumberSpace()
{
SizeChanged += (o, e) => SetTransform(Transform, ActualWidth, ActualHeight);
}
public static void SetTransform(CompositeTransform transform, double width, double height)
{
// Figure out the transform required for the shapes to fill the space
double pixelScaleX = width / 2;
double pixelScaleY = height / 2;
transform.ScaleX = pixelScaleX;
transform.ScaleY = pixelScaleY;
transform.TranslateX = pixelScaleX;
transform.TranslateY = pixelScaleY;
}
}
public class NumberSpacePath : Path
{
}
This code produces a window with two tabs. The first is blank. The second has four concentric ellipses drawn on it which take their transforms from either the Window or the Canvas. The ellipses are drawn as either Path objects or NumberSpacePath objects. NumberSpacePath inherits from Path but adds and changes nothing.
The two outer rings draw and resize without issue. However, when the second tab is first switched to, the innermost black ring is not rendered. Switching back to the first tab and again to the second tab causes it to be rendered. Additionally, enlarging the window causes the innermost two rings to be clipped, like on the image below. Again, switching and returning tabs causes them to be rendered correctly.
What is going on? Why does subclassing Path make a difference?

How do I get a Rectangle drawn on an adorner to scale with the Image element its bound to when the window size is changed?

I'm using a rectangle drawn on an adorner to mark a region of interest on an image. The issue is that if I resize the window, the rectangle doesn't change size.
I'm new to WPF, so I've done a bunch of research, googling what I can with multiple different search terms. I actually just learned adorners that way, and I've gotten this far on that, but I've hit a wall on how to finish this last piece. I know that my problem is based in the size of the rectangle, but I don't know what to capture/look for to adjust it, since wpf resizes the actual image object on window resize, so there's no scale factor to look at.
Here's the XAML for the application I'm testing things in.
<Window x:Class="TestingAdorners.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:TestingAdorners"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid ClipToBounds="True">
<AdornerDecorator>
<Image Name="Btn" Source="nyan.png" Stretch="Uniform"/>
</AdornerDecorator>
</Grid>
</Window>
The adorner class:
class RoiAdorner : Adorner
{
public Rect rectangle = new Rect();
public RoiAdorner(UIElement adornedElement) : base(adornedElement)
{
rectangle.Height = 30;
rectangle.Width = 100;
IsHitTestVisible = false;
}
protected override void OnRender(DrawingContext drawingContext)
{
Pen pen = new Pen(Brushes.Green, 5);
drawingContext.DrawRectangle(null, pen, rectangle);
}
}
And the Xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
AdornerLayer.GetAdornerLayer(Btn).Add(new RoiAdorner(Btn));
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
}
}
The desired result is that the rectangle scales with the image object so that it always covers the same region of the image. The problem is I don't know how to capture a scale factor to scale it up and down as the window resizes.
Update: After thinking through Frenchy's suggestion I realized the answer is simply: "Normalize your coordinates"
you just adapt your render method like this:
class RoiAdorner : Adorner
{
public double factorX = 0d;
public double factorY = 0d;
public Rect rectangle = new Rect();
public RoiAdorner(UIElement adornedElement) : base(adornedElement)
{
rectangle.Height = 30;
rectangle.Width = 100;
IsHitTestVisible = false;
}
protected override void OnRender(DrawingContext drawingContext)
{
if (factorY == 0)
factorY = rectangle.Height / AdornedElement.DesiredSize.Height;
if (factorX == 0)
factorX = rectangle.Width / AdornedElement.DesiredSize.Width;
var r = new Rect(new Size(AdornedElement.DesiredSize.Width * factorX, AdornedElement.DesiredSize.Height * factorY));
//Rect adornedElementRect = new Rect(this.AdornedElement.DesiredSize);
drawingContext.DrawRectangle(null, new Pen(Brushes.Red, 5), r);
}
this.AdornedElement.DesiredSize gives you the size of image.
The approach I would use is to render the picture and rectangle to the same thing. Then that one thing is stretched, scaled or whatever.
One way to do this would be to use a DrawingImage. Drawing methods are extremely efficient if rather low level.
<Grid ClipToBounds="True">
<AdornerDecorator>
<Image Name="img" Stretch="Uniform">
<Image.Source>
<DrawingImage PresentationOptions:Freeze="True">
<DrawingImage.Drawing>
<DrawingGroup>
<ImageDrawing Rect="0,0,595,446" ImageSource="DSC00025.jpg"/>
<GeometryDrawing Brush="Green">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,100,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</AdornerDecorator>
</Grid>
Another is with a visualbrush. Controls inherit from visual - this is somewhat higher level coding.
<Grid ClipToBounds="True">
<AdornerDecorator>
<Rectangle Name="rec">
<Rectangle.Fill>
<VisualBrush Stretch="Uniform">
<VisualBrush.Visual>
<Grid Height="446" Width="595">
<Image Source="DSC00025.jpg" Stretch="Fill"/>
<Rectangle Height="30" Width="100" Fill="Green"
VerticalAlignment="Top" HorizontalAlignment="Left"/>
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
</AdornerDecorator>
</Grid>
Note that both of these are quick and dirty illustrations to give you the idea. The image I picked at random off my hard drive is sized 446 * 595. You could calculate sizes or bind or stretch as suits your requirement best.

how to check if an element is going out of its parent in c# wpf

I am creating shapes at run time on canvas in my app and except all the shapes, ellipse is going out of canvas. How do I restrict it to canvas? All other shapes are contained in canvas because of the control points at their vertices. How do I keep a check as to not let ellipse go out of canvas without clipping. I have used ClipToBounds and it doesn't meet my needs.
Also, an alternate solution is if I can add a controlpoint at the left side of ellipse of radiusX property. I can't add a controlpoint to left side of radiusX on ellipse. If you could help me with either of that?
radiusXcp = new ControlPoint(this, EllipseGeom, EllipseGeometry.RadiusYProperty, 1, true, false);
radiusXcp.RelativeTo = EllipseGeometry.CenterProperty;
shape_ControlPoints.Add(radiusXcp);
radiusXcp = new ControlPoint(this, EllipseGeom, EllipseGeometry.RadiusXProperty, 0, true, false);
radiusXcp.RelativeTo = EllipseGeometry.CenterProperty;
shape_ControlPoints.Add(radiusXcp);
//EllipseGeom.RadiusX = -EllipseGeom.RadiusX;
//radiusXcp = new ControlPoint(this, EllipseGeom, EllipseGeometry.RadiusXProperty, 0, true, false);
//radiusXcp.RelativeTo = EllipseGeometry.CenterProperty;
//shape_ControlPoints.Add(radiusXcp);
//EllipseGeom.RadiusX = -EllipseGeom.RadiusX;
Here is a quick example of what i would do. It could be improved on and the code is mainly written to be easy to read and follow. It also does not handle the possibility to if the shape's size is bigger than the Canvas (not sure if that is a use case in your project).
For the example I used the "Loaded" event on the Canvas, to reset the position before drawing. You would want this check before you draw the Ellipse object.
private void TestCanvas_Loaded(object sender, RoutedEventArgs e)
{
//canvas = 450 x 800
Ellipse test_ellipse = new Ellipse();
test_ellipse.Width = 100;
test_ellipse.Height = 100;
test_ellipse.Fill = Brushes.Red;
Canvas.SetLeft(test_ellipse, 700);
Canvas.SetTop(test_ellipse, -500);
Reset_Ellipse_Bounds(TestCanvas, ref test_ellipse);
TestCanvas.Children.Add(test_ellipse);
}
private void Reset_Ellipse_Bounds(Canvas myCanvas, ref Ellipse myEllipse)
{
var left = Canvas.GetLeft(myEllipse);
var top = Canvas.GetTop(myEllipse);
//handle too far right
if (left + myEllipse.Width > myCanvas.ActualWidth)
Canvas.SetLeft(myEllipse, myCanvas.ActualWidth - myEllipse.Width);
//handle too far left
if(left < 0)
Canvas.SetLeft(myEllipse, 0);
//handle too far up
if (top < 0)
Canvas.SetTop(myEllipse, 0);
//handle too far down
if (top + myEllipse.Height > myCanvas.ActualHeight)
Canvas.SetTop(myEllipse, myCanvas.ActualHeight - myEllipse.Height);
}
For Completeness the XAML:
<Grid>
<Canvas x:Name="TestCanvas" Loaded="TestCanvas_Loaded" />
</Grid>
The idea is to check the bounding box against the Canvas edges. There are ways to improve this, but i figured the simplest solution is easier to follow.
Within each if statement you could add more logic or a method to do further processing, but this should answer the general question of knowing if it is outside the parent.
Just set ClipToBounds="true" to its father control, it avoids the canvas to be drawn outside of it.
In my case I set it to Grid as followed :
<Grid x:Name="MainGrid" Background="WhiteSmoke" ClipToBounds="true">
<Canvas Margin="10" Background="Transparent"
SizeChanged="ViewportSizeChanged"
MouseLeftButtonDown="ViewportMouseLeftButtonDown"
MouseLeftButtonUp="ViewportMouseLeftButtonUp"
MouseMove="ViewportMouseMove"
MouseWheel="ViewportMouseWheel">
<Canvas x:Name="canvas" Width="1000" Height="600"
HorizontalAlignment="Left" VerticalAlignment="Top">
<Canvas.RenderTransform>
<MatrixTransform x:Name="transform"/>
</Canvas.RenderTransform>
</Canvas>
<Canvas x:Name="canvas2" Width="1000" Height="600"
HorizontalAlignment="Left" VerticalAlignment="Top">
<Canvas.RenderTransform>
<MatrixTransform x:Name="transform2"/>
</Canvas.RenderTransform>
</Canvas>
</Canvas>
</Grid>

Change the actual position of a shape

On a wpf I have implemented the practice proposed as answer to the question: How to drag a UserControl inside a Canvas in order to drag and move items (shapes, child canvas) on a canvas. However as correctly indicated the next answer of the same question there is a flaw inside the method:
private void Control_MouseMove(object sender, MouseEventArgs e)
{
var draggableControl = sender as UserControl;
if (isDragging && draggableControl != null)
{
Point currentPosition = e.GetPosition(this.Parent as UIElement);
var transform = draggableControl.RenderTransform as TranslateTransform;
if (transform == null)
{
transform = new TranslateTransform();
draggableControl.RenderTransform = transform;
}
transform.X = currentPosition.X - clickPosition.X;
transform.Y = currentPosition.Y - clickPosition.Y;
}
}
It uses the RenderTranform which does not changes the position of an item permanently but its visual position instead. The result is that the item returns to its initial position just on the next mouse event, so the drag and drop does not work properly (you cannot move it actually in this way but only visually).What kind of modification should be done on it to rectify the functionality of the method? Is there alternatively a similar practice that carries out the task properly? Should I use another Transform like Layout Transform?
What you did is a possible implementation but in WPF there exists already a Control which is made for being dragged around: the Thumb.
example:
<Canvas Width="200" Height="200" Background="Yellow">
<Thumb x:Name="DragThumb" DragDelta="Mover_DragDelta" Canvas.Top="20" Canvas.Left="10" Background="Gray" Width="50" Height="50" >
<Thumb.Template>
<ControlTemplate>
<Rectangle Fill="Black" Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" />
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Canvas>
with a very simple code behind:
private void Mover_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
Canvas.SetLeft(DragThumb, Canvas.GetLeft(DragThumb) + e.HorizontalChange);
Canvas.SetTop(DragThumb, Canvas.GetTop(DragThumb) + e.VerticalChange);
}
already moves your canvas. (and it stays as the Canvas.Top / Canvas.Left Positions got set).
You could use Canvas.Left and Canvas.Top property instead of mocking around with the RenderTransform.
Canvas.SetLeft(this, Canvas.GetLeft(this) + delta.X);
Canvas.SetTop(this, Canvas.GetTop(this) + delta.Y);

Adding a line to a textblock programmatically

I know how to add a control to the canvas/grid/layout- simply by calling canvas.Childern.Add(). However, when I want to embed something inside a textblock, I can't seem to find the method for it. A textblock doesn't contain a Add method or anything, so I'm at a bit of a lost.
The XAML I'm trying to turn into C# is:
<TextBlock x:Name="textBlock">
<Line X1="0" Y1="0" X2="100" Y2="0" Stroke="Black" StrokeThickness="4" x:Name="line1"/>
<TextBlock Text="Hello there!" VerticalAlignment="Center" HorizontalAlignment="Center" x:Name="innerTextBlock" />
<Line X1="0" Y1="0" X2="100" Y2="0" Stroke="Black" StrokeThickness="4" x:Name="line2"/>
</TextBlock>
EDIT: I think the best way to do it (besides the answer) is to simply create a WPF User control and reference that.
You have to use inlines property (as stated before) so to reproduce your xaml it is enough to do the following (where LayoutRoot is the name of your parent control):
var t = new TextBlock();
t.Inlines.Add(new Line { X1 = 0, Y1 = 0, X2 = 100, Y2 = 0, Stroke = new SolidColorBrush(Colors.Black), StrokeThickness = 4.0 });
t.Inlines.Add("Hello there!");
t.Inlines.Add(new Line { X1 = 0, Y1 = 0, X2 = 100, Y2 = 0, Stroke = new SolidColorBrush(Colors.Black),StrokeThickness = 4.0});
LayoutRoot.Children.Add(t);
I Believe if you have multiple lines you must use the Inlines property which is a collection that contains a list of inline elements. You can't directly add text to it, you must add it to an Inline object - such as a Run.

Categories