Hi WPF users and developers.
I am new to WPF, I am actually a little bit confused by the adorners. so far I understand that the Adorner are rendered on Top of the element in a specific layer (AdornerLayer) which is defined in higher level of the visual tree.
Firstly
How can I create an AdornerLayer for a specific element, for example a shape.
Secondly
I guess I need to add the AdornerLayer to the visual children of the Adorned element in order to get valid hit testing on the Adorners in the AdornerLayer. How to do It?
Thank you in advance.
first of all you have to crate a class derived from Adorner like below. you can use thumb or thumbs to be rendered. here is code that I have implement before but I have changed a little bit for simplicity so there may be mistakes.
public class ResizingAdorner : Adorner
{
Thumb topLeft;
// To store and manage the adorner's visual children.
VisualCollection visualChildren;
// Initialize the ResizingAdorner.
public ResizingAdorner(UIElement adornedElement)
: base(adornedElement)
{
visualChildren = new VisualCollection(this);
// Call a helper method to initialize the Thumbs
BuildAdornerCorner(ref topLeft, Cursors.SizeNWSE);
//these are events for the thumbs you may want it or not according to your needs//
topLeft.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(topLeft_PreviewMouseLeftButtonDown);
topLeft.DragDelta += new DragDeltaEventHandler(HandleTopLeft);
}
//override this method for rendering
protected override void OnRender(DrawingContext drawingContext)
{
Rect adornedElementRect = new Rect(this.AdornedElement.DesiredSize);
// Some arbitrary drawing implements.
SolidColorBrush RectBrush = new SolidColorBrush(Colors.Green);
RectBrush.Opacity = 0.3;
Pen renderPen = new Pen(RectBrush, 1.5);
// Draw Rectangle
drawingContext.DrawRectangle(null, renderPen, adornedElementRect);
}
// Arrange the Adorners.
protected override Size ArrangeOverride(Size finalSize)
{
Rect adornedElementRect = new Rect(this.AdornedElement.DesiredSize);
// desiredWidth and desiredHeight are the width and height of the element that's being adorned.
// These will be used to place the ResizingAdorner at the corners of the adorned element.
double desiredWidth = adornedElementRect.BottomRight.X;
double desiredHeight = adornedElementRect.BottomRight.Y;
// adornerWidth & adornerHeight are used for placement as well.
double adornerWidth = adornedElementRect.TopLeft.X;
double adornerHeight = adornedElementRect.TopLeft.Y;
//Arrange PathPoints with the helper Methods
topLeft.Arrange(new Rect(new Point(adornerWidth, adornerHeight), new Point(desiredWidth, desiredHeight)));
// Return the final size.
return finalSize;
}
// set some appearance properties, and add the elements to the visual tree//
void BuildAdornerCorner(ref Thumb cornerThumb, Cursor customizedCursor)
{
Path adornered = AdornedElement as Path;
if (cornerThumb != null) return;
cornerThumb = new Thumb();
// Set some arbitrary visual characteristics.
cornerThumb.Cursor = customizedCursor;
cornerThumb.Height = cornerThumb.Width = 10;
cornerThumb.Opacity = 1;
cornerThumb.Background = new SolidColorBrush(Colors.Black);
visualChildren.Add(cornerThumb);
}
// UnScale Adorners. this part is optional
public override GeneralTransform GetDesiredTransform(GeneralTransform transform)
{
if (this.visualChildren != null)
{
foreach (var thumb in this.visualChildren.OfType<Thumb>())
{
thumb.RenderTransform
= new ScaleTransform(1 / ScaleXC, 1 / ScaleYC);
thumb.RenderTransformOrigin = new Point(0.5, 0.5);
}
}
return base.GetDesiredTransform(transform);
}
}
after that use this class as shown below to adorn element
AdornerLayer ADL; //consider this is as a public field somewhere in your class
...
...
ResizingAdorner A = new XamlEditor.ResizingAdorner(p); // p is an UIElement I my case
ADL = AdornerLayer.GetAdornerLayer(p);
ADL.Add(A);
Related
I have a custom control that renders a bezier line from the top-left to the bottom-right corner. I would like to modify the hit test behavior so that the control is only "hit", if hovering over or near the bezier curve, but FillContainsWithDetail doesn't return the expected results.
What am I missing?
Here's a derived example class:
public class BezierControl : FrameworkElement
{
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
if (_geometry == null) return null;
var p = hitTestParameters.HitPoint;
EllipseGeometry expandedHitTestArea = new EllipseGeometry(p, 10.0, 10.0);
var intersection = expandedHitTestArea.FillContainsWithDetail(_geometry);
if (intersection > IntersectionDetail.Empty)
{
return new PointHitTestResult(this, hitTestParameters.HitPoint);
}
return null;
}
private StreamGeometry _geometry;
private StreamGeometry getGeometry()
{
var result = new StreamGeometry();
using (var context = result.Open())
{
var start = new Point(0, 0);
var startCp = new Point(10, 0);
var end = new Point(this.ActualWidth, this.ActualHeight);
var endCp = new Point(this.ActualWidth - 10, this.ActualHeight);
context.BeginFigure(start, false, false);
context.BezierTo(startCp, endCp, end, true, false);
}
result.Freeze();
return result;
}
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
if (_geometry == null) _geometry = getGeometry();
dc.DrawGeometry(null, _pen, _geometry);
}
private Pen _pen = new Pen(Brushes.Red, 1.0);
}
EDIT: The approved answer below is fine, but it also lead me to another alternative - or showed me why my attempted solution was failing: I was creating the geometry like this:
context.BeginFigure(bezier.Start, false, false);
The first bool parameter is called isFilled. Setting this to true allows intersecting between _geometry and expandedHitTestArea.
Before noticing this, I implemented the accepted answer below and decided to create the widenedGeometry lazily for performance reasons. Which now gets me wondering: from a performance perspective, which approach is better? Or, is this negligible in this case, because geometries are inexpensive anyway?
You don't need that EllipseGeometry. Just check if the hit point is inside a widened path geometry of the original geometry (which may of course also be created once when _geometry is created):
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
if (_geometry != null)
{
var widenedGeometry = _geometry.GetWidenedPathGeometry(new Pen(null, 20));
if (widenedGeometry.FillContains(hitTestParameters.HitPoint))
{
return new PointHitTestResult(this, hitTestParameters.HitPoint);
}
}
return null;
}
I want to take a snapshot of my UserControl, which has not been shown yet.
That's my code:
public Screenshot(MyViewModel viewModel)
{
if (viewModel == null)
return;
// Create a TabControl, where View is hosted in
var visualTab = new TabControl();
visualTab.Width = FrameworkAdjustments.VisualSize.Width;
visualTab.Height = FrameworkAdjustments.VisualSize.Height;
visualTab.TabStripPlacement = Dock.Left;
// Tabs should only be shown, if there are more than one 'SubView'
Visibility tabVisibility = Visibility.Collapsed;
if (viewModel.SubViews.Count > 1)
tabVisibility = Visibility.Visible;
foreach (var subView in viewModel.SubViews)
{
var tab = new TabItem();
tab.Header = subView.TranslatedId; // multilanguage header
tab.Visibility = tabVisibility;
if (subView.Id == viewModel.ActiveSubView.Id)
{
tab.IsSelected = true;
// Without the following line my UI works, but my TabControl is empty.
tab.Content = ViewManager.GetViewById(subView.Id);
// ViewManager.GetViewById(subView.Id); returns a UserControl
}
tab.Measure(FrameworkAdjustments.VisualSize);
tab.Arrange(new Rect(FrameworkAdjustments.VisualSize));
visualTab.Items.Add(tab);
}
_ContentCtrl = new ContentControl() { Width = FrameworkAdjustments.VisualSize.Width, Height = FrameworkAdjustments.VisualSize.Height };
_ContentCtrl.Content = visualTab;
_ContentCtrl.Measure(FrameworkAdjustments.VisualSize);
_ContentCtrl.Arrange(new Rect(FrameworkAdjustments.VisualSize));
RenderTargetBitmap bmp = new RenderTargetBitmap((int)FrameworkAdjustments.VisualSize.Width, (int)FrameworkAdjustments.VisualSize.Height, 96, 96, PixelFormats.Pbgra32);
bmp.Render(_ContentCtrl);
this.ItemBrush = new ImageBrush(bmp);
}
This code runs for each 'MyViewModel' when I start my App.
'MyViewModel' contains a List of 'SubViews' which are the content of the Tabs and they contain a 'FunctionKeyBar' which's buttons can be activated by using 'F1' to 'F12'. But after creating my screenshot I can't use the F1 to F12 anymore. Also there are other problems, like switch language.
Is there an other way to create a snapshot of a control which has not came into view?
Thanks for all replys.
Greetings Benny
Approach : Set Margin to negative to keep it hidden, Add the control to the Grid / any other container.
Follow these steps :
1) Create a Task to create and add your ContentControl to the Grid.
2) Call user-define CaptureScreen () function.
3) Visibility must not be Hidden/Collapsed. Margin can be negative to hide the control.
In this example, I have done this in a Button.Click.
async private void Button_Click(object sender, RoutedEventArgs e)
{
Task<ContentControl> t = AddContentControl();
ContentControl ctrl = await t;
RenderTargetBitmap bmp = CaptureScreen(ctrl, 5000, 5000);
Img.Source = bmp;
}
/* Add the ContentControl to the Grid, and keep it hidden using neg. Margin */
private Task<ContentControl> AddContentControl()
{
Task<ContentControl> task = Task.Factory.StartNew(() =>
{
ContentControl ctrl = null;
Dispatcher.Invoke(() =>
{
ctrl = new ContentControl() { Content = "Not shown", Width = 100, Height = 25, Margin = new Thickness(-8888, 53, 0, 0) };
Grd.Children.Add(ctrl);
});
return ctrl;
});
return task;
}
/* Important , wont work with Visibility.Collapse or Hidden */
private static RenderTargetBitmap CaptureScreen(Visual target, double dpiX, double dpiY)
{
if (target == null)
{
return null;
}
Rect bounds = VisualTreeHelper.GetDescendantBounds(target);
RenderTargetBitmap rtb = new RenderTargetBitmap((int)(bounds.Width * dpiX / 96.0),
(int)(bounds.Height * dpiY / 96.0),
dpiX,
dpiY,
PixelFormats.Pbgra32);
DrawingVisual dv = new DrawingVisual();
using (DrawingContext ctx = dv.RenderOpen())
{
VisualBrush vb = new VisualBrush(target);
ctx.DrawRectangle(vb, null, new Rect(new Point(), bounds.Size));
}
rtb.Render(dv);
return rtb;
}
Now I found a solution, thanks to AnjumSKan.
I took his code and changed it a little bit. As I said, I need to create the snapshot on application startup or culture changed and I have more than one view. in my Function AddContentControl, I add the Content to my TabControl which has negative Margin. Add the end I call HiddenTab.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Render, new Action(CreateSnapshotOnRender));
('HiddenTab' is my TabControl which is not shown to user). This calls a function called CreateSnapshotOnRender, where I call AnjumSKhan's CaptureScreen-method from. After that I call again the function AddContentControl with my next content. This looks as following:
private void CreateScreenshotOnRender()
{
HiddenTab.Measure(ViewSize);
HiddenTab.Arrange(new Rect(ViewSize));
var snapshot = CaptureScreen(HiddenTab, dpiX, dpiY);
/* Do anything with snapshot */
_Index++; // counter to know thich view is next
CreateAllScreenshots();
}
Thanks again to AnjumSKan, because you lead me to this. Thats why I marked your Answer as the correct one.
I have class, that creates Shapes for me (I tried to create some kind of "class factory" but im not sure if this is correct term for that I have created.
Problem is described in comments in my code.
public static Ellipse SomeCircle()
{
Ellipse e = new Ellipse();
double size = 10;
e.Height = size;
e.Width = size;
e.Fill = new SolidColorBrush(Colors.Orange);
e.Fill.Opacity = 0.8;
e.Stroke = new SolidColorBrush(Colors.Black);
// i want to have something like this here:
// canvas1.Children.Add(e);
// but I cant access non-static canvas1 from here
// I need this to place my ellipse in desired place
// (line below will not work if my Ellipse is not placed on canvas
// e.Margin = new Thickness(p.X - e.Width * 2, p.Y - e.Height * 2, 0, 0);
return e;
}
I have no idea how to workaround this.
I don't want to pass that canvas by parameter in my whole application...
Since you do not want to pass your Canvas around as a parameter, you could try creating an Extension Method which would act on your Canvas Object.
namespace CustomExtensions
{
public static class Shapes
{
public static Ellipse SomeCircle(this Canvas dest)
{
Ellipse e = new Ellipse();
double size = 10;
e.Height = size;
e.Width = size;
e.Fill = new SolidColorBrush(Colors.Orange);
e.Fill.Opacity = 0.8;
e.Stroke = new SolidColorBrush(Colors.Black);
dest.Children.Add(e);
return e;
}
}
}
Usage remember to add the CustomExtensions Namespace to your usings.
canvas1.SomeCircle();
I'm working on a Adorner for a Line in a drawing program using WPF. The Line is drawn in code-behind and then adorned with my custom Adorner called LineAdorner. I've managed to use a Thumb for the start and end point of the Line. My problem is the arrangement of the Thumbs regarding the start and end points. I think the problem is in the method ArrangeOverride, where is supposed to arrange the Thumbs with the start and end points. I can't find the right amount to subtract or add in the Rect X and Y parameters. How can i find these values to always arrange the Thumbs with the points of the Line?
The code from my custom Adorner is this:
public class LineAdorner : Adorner
{
private Point start;
private Point end;
private Thumb startThumb;
private Thumb endThumb;
private Line selectedLine;
private VisualCollection visualChildren;
// Constructor
public LineAdorner(UIElement adornedElement) : base(adornedElement)
{
visualChildren = new VisualCollection(this);
startThumb = new Thumb { Cursor = Cursors.Hand, Width = 10, Height = 10, Background = Brushes.Green };
endThumb = new Thumb { Cursor = Cursors.Hand, Width = 10, Height = 10, Background = Brushes.BlueViolet };
startThumb.DragDelta += StartDragDelta;
endThumb.DragDelta += EndDragDelta;
visualChildren.Add(startThumb);
visualChildren.Add(endThumb);
selectedLine = AdornedElement as Line;
}
// Event for the Thumb Start Point
private void StartDragDelta(object sender, DragDeltaEventArgs e)
{
Point position = Mouse.GetPosition(this);
selectedLine.X1 = position.X;
selectedLine.Y1 = position.Y;
}
// Event for the Thumb End Point
private void EndDragDelta(object sender, DragDeltaEventArgs e)
{
Point position = Mouse.GetPosition(this);
selectedLine.X2 = position.X;
selectedLine.Y2 = position.Y;
}
protected override int VisualChildrenCount { get { return visualChildren.Count; } }
protected override Visual GetVisualChild(int index) { return visualChildren[index]; }
protected override void OnRender(DrawingContext drawingContext)
{
if (AdornedElement is Line)
{
selectedLine = AdornedElement as Line;
start = new Point(selectedLine.X1, selectedLine.Y1);
end = new Point(selectedLine.X2, selectedLine.Y2);
}
}
protected override Size ArrangeOverride(Size finalSize)
{
var startRect = new Rect(selectedLine.X1, selectedLine.Y1, ActualWidth, ActualHeight);
startThumb.Arrange(startRect);
var endRect = new Rect(selectedLine.X2, selectedLine.Y2, ActualWidth, ActualHeight);
endThumb.Arrange(endRect);
return finalSize;
}
}
Try this in your ArrangeOverride. You can get rid of the "start" and "end" variables, and you don't need to override OnRender since your Thumbs will render themselves if you tell them where they need to be.
protected override Size ArrangeOverride(Size finalSize)
{
selectedLine = AdornedElement as Line;
double left = Math.Min(selectedLine.X1, selectedLine.X2);
double top = Math.Min(selectedLine.Y1, selectedLine.Y2);
var startRect = new Rect(selectedLine.X1 - (startThumb.Width / 2), selectedLine.Y1 - (startThumb.Width / 2), startThumb.Width, startThumb.Height);
startThumb.Arrange(startRect);
var endRect = new Rect(selectedLine.X2 - (endThumb.Width / 2), selectedLine.Y2 - (endThumb.Height / 2), endThumb.Width, endThumb.Height);
endThumb.Arrange(endRect);
return finalSize;
}
You are setting an explicit size on the Thumbs, so that had to be maintained in the Arrange. Also, you need to subtract half the width and height of the Thumbs to center on the end points.
Due to the nature of the Canvas and Shapes, you need to subtract the "real" left and top values of the line, since unlike the Line, the Adorners are not going to draw themselves from the top-left on the Canvas. This shouldn't be required outside of using Canvasses.
I'm attempting to create an application that looks much like the Windows 8 Metro UI with the tiles.. right now if you click on a tile I have an animation that increases the width and reveals more info on that tile.
I have the tiles laid out in a WrapPanel which is nice because if I resize the tile the other tiles next to it move and keep it's margin perfectly. However I've been researching this for awhile, I would like to if possible limit the number of items in the wrap panel to two wide (Right now it's three wide) as if you select one of the tiles it resizes itself and pushes a tile next to it (or if it's the end tile it will push itself) to the next row, while my animations are smooth it doesn't look the best presentation-wise..
Could someone point me towards how I might specify my wrappanel to only have a width of two items across?
Any help is very much appreciated.
Try making your own wrap panel deriving from standard wrap panel as described in this post in detail. Post addresses the same issue which you are trying to solve.
public class MyWrapPanel : WrapPanel
{
public int MaxRows
{
get { return (int)GetValue(MaxRowsProperty); }
set { SetValue(MaxRowsProperty, value); }
}
public static readonly DependencyProperty MaxRowsProperty =
DependencyProperty.Register("MaxRows", typeof(int), typeof(MyWrapPanel), new UIPropertyMetadata(4));
protected override Size ArrangeOverride(Size finalSize)
{
Point currentPosition = new Point();
double ItemMaxHeight = 0.0;
int RowIndex = 0;
foreach (UIElement child in Children)
{
ItemMaxHeight = ItemMaxHeight > child.DesiredSize.Height ? ItemMaxHeight : child.DesiredSize.Height;
if (currentPosition.X + child.DesiredSize.Width > this.DesiredSize.Width)
{
currentPosition = new Point(0, currentPosition.Y + ItemMaxHeight);
ItemMaxHeight = 0.0;
RowIndex++;
}
if (RowIndex < MaxRows)
{
child.Visibility = System.Windows.Visibility.Visible;
Rect childRect = new Rect(currentPosition, child.DesiredSize);
child.Arrange(childRect);
}
else
{
Rect childRect = new Rect(currentPosition, new Size(0,0));
child.Arrange(childRect);
}
currentPosition.Offset(child.DesiredSize.Width, 0);
}
return finalSize;
}
protected override Size MeasureOverride(Size availableSize)
{
return base.MeasureOverride(availableSize);
}
}