I'm trying to follow the UIScrollView And Autolayout Mixed approach.
I've seen other similar questions but with no accepted answer.
I'm not sure that this is what I need.
Please understand that I'm neither working with Storyboards nor XIBs.
All my views are created programmatically using c# in Xamarin.iOS. This is very similar to objective C code.
Like many others I can't find a way to have my scroll view to actually scroll.
So in my main View Controller I have the following in ViewDidLoad():
public override void ViewDidLoad()
{
base.ViewDidLoad();
_scrollView = new UIScrollView
{
ShowsHorizontalScrollIndicator = false,
TranslatesAutoresizingMaskIntoConstraints = false,
AlwaysBounceVertical = true,
Bounces = true
};
_refreshControl = new UIRefreshControl { TranslatesAutoresizingMaskIntoConstraints = false };
_refreshControl.Enabled = true;
_refreshControl.ValueChanged -= RefreshControl_ValueChanged;
_refreshControl.ValueChanged += RefreshControl_ValueChanged;
if (UIDevice.CurrentDevice.CheckSystemVersion(10, 0))
{
_scrollView.RefreshControl = _refreshControl;
}
else
{
_scrollView.AddSubview(_refreshControl);
}
this.View.AddSubview(_scrollView);
_scrollViewContainer = new UIView { TranslatesAutoresizingMaskIntoConstraints = true };
_scrollView.AddSubview(_scrollViewContainer);
_scrollView.Anchor(top: this.View.SaferAreaLayoutGuide().TopAnchor, leading: this.View.LeadingAnchor, bottom: this.View.SaferAreaLayoutGuide().BottomAnchor, trailing: this.View.TrailingAnchor);
}
Nothing really fancy, I create my UIScrollView add it as a sub view of the main View. Create a UIRefreshControl and add it to the scroll view. Create and add a plain UIView to the UIScrollView in order to put all my subviews.
Because I'm using AutoLayout the ScrollView is anchored within constraints of the main view.
Now later in ViewWillAppear(), I'm creating Card Views that are added to the plain UIView. These card views are using autoloayout to be stacked on top of each other, the first card is anchored to the container view's top, leading and trailing anchors.
Everything in these cards is using autolayout, to basically stack everything.
public UIView BuildQOLCard()
{
CardView qolCard = new CardView();
_scrollViewContainer.AddSubview(qolCard);
qolCard.TranslatesAutoresizingMaskIntoConstraints = false;
qolCard.CornerRadius = 5f;
qolCard.ShadowOffsetHeight = 0;
qolCard.ShadowOffsetWidth = 0;
qolCard.BackgroundColor = UIColor.White;
...
qolCard.Anchor(leading: _scrollViewContainer.LeadingAnchor, trailing: _scrollViewContainer.TrailingAnchor, top: _scrollViewContainer.TopAnchor, padding: new UIEdgeInsets(10f, 10f, 10f, 10f));
return qolCard;
}
Of course the container view won't magically know how to size itself. So I'll have to give it a size otherwise I won't see anything.
So I mage a helper like this:
public void SizeScrollViewContentSize()
{
nfloat scrollViewHeight = 0.0f;
foreach (UIView view in this._scrollViewContainer.Subviews)
{
scrollViewHeight += view.Frame.Size.Height;
}
this._scrollViewContainer.Frame = new CGRect(0, 0, this._scrollView.Frame.Width, scrollViewHeight);
}
But I can't seem to figure out when to call it because the subviews are here but their sizes are unknown. I tried calling it from ViewDidLayoutSubviews().
Any help appreciated.
EDIT : Applying te recommandations of the accepted answer, led to scrolling being enabled but the scroll view always bounces back to top.
Here is the code right now:
public override void ViewDidLoad()
{
base.ViewDidLoad();
_scrollView = new UIScrollView
{
ShowsHorizontalScrollIndicator = false,
TranslatesAutoresizingMaskIntoConstraints = false
};
if (!UIDevice.CurrentDevice.CheckSystemVersion(10, 0))
{
_scrollView.AlwaysBounceVertical = true;
_scrollView.Bounces = true;
}
_refreshControl = new UIRefreshControl { TranslatesAutoresizingMaskIntoConstraints = false };
_refreshControl.Enabled = true;
_refreshControl.ValueChanged -= RefreshControl_ValueChanged;
_refreshControl.ValueChanged += RefreshControl_ValueChanged;
if (UIDevice.CurrentDevice.CheckSystemVersion(10, 0))
{
_scrollView.RefreshControl = _refreshControl;
}
else
{
_scrollView.AddSubview(_refreshControl);
}
this.View.AddSubview(_scrollView);
_scrollViewContainer = new UIView { TranslatesAutoresizingMaskIntoConstraints = false };
_scrollView.AddSubview(_scrollViewContainer);
_scrollView.Anchor(top: this.View.SaferAreaLayoutGuide().TopAnchor, leading: this.View.LeadingAnchor, bottom: this.View.SaferAreaLayoutGuide().BottomAnchor, trailing: this.View.TrailingAnchor);
// Only define the width of the container
_scrollViewContainer.TopAnchor.ConstraintEqualTo(_scrollView.TopAnchor).Active = true;
_scrollViewContainer.LeadingAnchor.ConstraintEqualTo(_scrollView.LeadingAnchor).Active = true;
_scrollViewContainer.WidthAnchor.ConstraintEqualTo(_scrollView.WidthAnchor).Active = true;
}
Card one:
CardView qolCard = new CardView();
_scrollViewContainer.AddSubview(qolCard);
qolCard.TranslatesAutoresizingMaskIntoConstraints = false;
qolCard.CornerRadius = 5f;
qolCard.ShadowOffsetHeight = 0;
qolCard.ShadowOffsetWidth = 0;
qolCard.BackgroundColor = UIColor.White;
...
qolCard.Anchor(leading: _scrollViewContainer.LeadingAnchor, trailing: _scrollViewContainer.TrailingAnchor, top: _scrollViewContainer.TopAnchor, padding: new UIEdgeInsets(10f, 10f, 10f, 10f));
card 2:
CardView goalsCard = new CardView();
_scrollViewContainer.AddSubview(goalsCard);
goalsCard.TranslatesAutoresizingMaskIntoConstraints = false;
goalsCard.CornerRadius = 5f;
goalsCard.ShadowOffsetHeight = 0;
goalsCard.ShadowOffsetWidth = 0;
goalsCard.BackgroundColor = UIColor.White;
...
// Top should be constrained to the previous CardView's Bottom
goalsCard.Anchor(leading: _scrollViewContainer.LeadingAnchor, trailing: _scrollViewContainer.TrailingAnchor, top: qolCard.BottomAnchor,padding: new UIEdgeInsets(10f, 10f, 10f, 10f));
Final anchor:
// constraint the last card bottom to the bottom of the scrollview container
goalsCard.Anchor(bottom: context._scrollViewContainer.BottomAnchor);
The Anchor Helper:
internal static void Anchor(this UIView uIView, NSLayoutYAxisAnchor top = null, NSLayoutXAxisAnchor leading = null, NSLayoutYAxisAnchor bottom = null, NSLayoutXAxisAnchor trailing = null, UIEdgeInsets padding = default, CGSize size = default)
{
uIView.TranslatesAutoresizingMaskIntoConstraints = false;
if (top != null)
{
uIView.TopAnchor.ConstraintEqualTo(top, padding.Top).Active = true;
}
if (leading != null)
{
uIView.LeadingAnchor.ConstraintEqualTo(leading, padding.Left).Active = true;
}
if (bottom != null)
{
uIView.BottomAnchor.ConstraintEqualTo(bottom, -padding.Bottom).Active = true;
}
if (trailing != null)
{
uIView.TrailingAnchor.ConstraintEqualTo(trailing, -padding.Right).Active = true;
}
if (size.Width != 0)
{
uIView.WidthAnchor.ConstraintEqualTo(size.Width).Active = true;
}
if (size.Height != 0)
{
uIView.HeightAnchor.ConstraintEqualTo(size.Height).Active = true;
}
}
A scroll view needs its Frame defined and it needs its Content defined. If the content is constrained properly, scrolling is automatic.
I'll go through this step-by-step - using Storyboard just so we can see what's happening. Absolutely no different when we do it through code.
Start with a View Controller, add a scroll view, and constrain it to all four sides (I gave it a green background so we can see it):
Now we add a "container" view (cyan background), and constrain all 4 sides to the scroll view:
If we run this, we see:
That's because the "container" has no width or height. Its Leading / Trailing / Top / Bottom constraints are defining the scroll view's content.
So, let's constrain it Equal Width and Equal Height to the scroll view:
run it, and we see this:
Which is fine, except... it will never scroll, because it is the same height as the scroll view.
So, I delete the Equal Height - because I want the container's content to determine its height.
I add a "CardView" and set its Top / Leading / Trailing constraints to Top / Leading / trailing of "container", with 10-pts "padding" and a Height of 100. I also give it a Bottom Constraint to the Bottom of "container" (+10) - which will "pull the bottom of the container" up to 10-pts below the bottom of the CardView:
resulting in:
Now I add a second CardView, constrain Leading / Trailing constraints to Leading / trailing of container, with 10-pts "padding" and a Height of 100. And I set its Top constraint to the Bottom of CardView-1 (+10), and I constrain its Bottom to the Bottom of "container" (+10):
Unfortunately, we now have a 10-pt constraint from Bottom of CardView-1 and a 10-pt constraint from Bottom of CardView-2 - which, of course, won't work.
So, let's delete the "Bottom to Superview" constraint from CardView-1
and we get this:
Let's repeat those steps for a few more card views, each time constraining Leading / Trailing to "container", Height of 100, Top to Bottom of previous CardView... and then constrain only the Bottom of the last CardView to the Bottom of "container":
and Yay! We can scroll:
Doing this via code is just a matter of remembering that each CardView's Top should be constrained to the previous CardView's Bottom -- except for the first card, which you constrain to the Top of "container" and the last card, which you constrain to the Bottom of "container".
All done with auto-layout. No need to do any "calculate height and set frame" code.
As a side note, you can use a StackView and get rid of most of that:
forget the "container"
add a stack view to the scroll view
axis: vertical, spacing: 10
constrain all 4 sides of the stack view to the scroll view with 10-pts padding
give the stack view a Width constraint equal to scroll view width -20 (for the 10-pts padding on each side)
Then, just add your CardViews as arrangedSubviews of the stack view.
And.... Done!
Related
I'm trying to get this c# function from within a asp.netcore razor project using itext7 (7.1.7) to output a div containing text that scales up to within the constraints given with height and width. However, for some reason it seems there's some kind of hidden margin that will not scale the text to the boundaries of the given width and height, even though the margins are set to 0.
At the moment the maximum text i get is only half of what it should be.
Also, the paragraph, nor the div is actually set to the min width and min height.
Any help how to fix this is appreciated
public Div MakeScaledTxtDiv(string _displayTxt ,PdfFont _Pdffont, float _maxHeight,
float _maxWidth, Canvas _canvas, bool _showBorder)
{
Div nameDiv = new Div();
iText.Kernel.Geom.Rectangle posRect = new iText.Kernel.Geom.Rectangle(_maxWidth,_maxHeight);
Paragraph txtPara = new Paragraph()
.SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetTextAlignment(iText.Layout.Properties.TextAlignment.CENTER);
if (_showBorder)
txtPara.SetBorder(new DashedBorder(1));
if (_maxHeight > 0)
txtPara.SetMaxHeight(_maxHeight);
if (_maxWidth > 0)
txtPara.SetMaxWidth(_maxWidth);
txtPara
.SetMargin(0f)
.SetPadding(0f)
.SetFont(_Pdffont)
.Add(_displayTxt);
float fontSizeToSet = 1;
float fontSizeMax = 42;
float curFontSize = fontSizeMax;
bool m_bGoodfit = false;
while (!m_bGoodfit)
{
txtPara.SetFontSize(curFontSize);
ParagraphRenderer renderer = (ParagraphRenderer)txtPara.CreateRendererSubTree().SetParent(_canvas.GetRenderer());
LayoutContext context = new LayoutContext(new LayoutArea(1, posRect));
var fits = renderer.Layout(context).GetStatus();
if (fits == LayoutResult.FULL)
{
fontSizeToSet = curFontSize;
m_bGoodfit = true;
}
else
{
curFontSize -= 1;
if (curFontSize == 1)
{
fontSizeToSet = curFontSize;
m_bGoodfit = true;
}
}
}
txtPara.SetFontSize(fontSizeToSet);
if (_maxHeight > 0)
{
nameDiv.SetMinHeight(_maxHeight)
.SetMaxHeight(_maxHeight)
.SetHeight(_maxHeight);
}
if (_maxWidth > 0)
{
nameDiv.SetMinWidth(_maxWidth)
.SetMaxWidth(_maxWidth)
.SetWidth(_maxWidth);
}
nameDiv
.SetMargin(0f)
.SetPadding(0f)
.SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetTextAlignment(iText.Layout.Properties.TextAlignment.CENTER);
if (_showBorder)
nameDiv.SetBorder(new DottedBorder(1));
nameDiv.Add(txtPara).SetVerticalAlignment(VerticalAlignment.MIDDLE);
return nameDiv;
}
What worked for me was to change the multiplier number 0.7f slightly bigger in the code below (code referenced from https://stackoverflow.com/a/57929067/5490413).
It seems as that multiplier gets bigger/smaller, the space between the text and field bottom border grows/shrinks accordingly. Maybe start with 1.2f and find one that fits.
Paragraph linePara = new Paragraph().Add(lineTxt)
.SetTextAlignment(iText.Layout.Properties.TextAlignment.CENTER).SetBorder(new DottedBorder(1))
.SetMultipliedLeading(0.7f);
lineDiv.Add(linePara);
FYI I came across the exact issue you were facing: How to scale text within a fixed rectangle with itext7?
I tried with the answer posted there https://stackoverflow.com/a/57929067/5490413 and faced the same hidden bottom margin issue here.
I have a UWP app, which I should start by pointing out that it uses very little XAML. The views are built from JSON object recieved from an API. This means that the vast majority of everything is done in C#, and therefore adds a little complexity to my problem.
I basically want to have a panel (e.g. Grid) that can have rounded corners and have a drop shadow applied to it. The drop shadow should also have the rounded corners, this can be seen in the sample below.
I have looked at the DropShadowPanel as part of the Windows Community Toolkit, but this from what I can tell doesn't do the rounded corners unless I change the content to be a rectangle or some other shape.
To use this as a solution would mean the XAML equivalent of something like:
<Grid>
<toolkit:DropShadowPanel>
<Rectangle />
<toolkit:DropShadowPanel>
<Grid CornerRadius="30">
<!-- My Content -->
</Grid>
</Grid>
To me, this seems like an inefficient use of XAML!
I have also discovered the Composition Pro Toolkit, which to me looks bery interesting as it is all code behind. In particular the ImageFrame control looks to achieve the basis of what I require - although far more advanced than my needs.
The below has been based on the ImageFrame, but doesn't work (content is my grid):
protected FrameworkElement AddDropShadow(FrameworkElement content)
{
var container = new Grid { HorizontalAlignment = content.HorizontalAlignment, VerticalAlignment = content.VerticalAlignment, Width = content.Width, Height = content.Height };
var canvas = new Canvas { HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch };
content.Loaded += (s, e) =>
{
var compositor = ElementCompositionPreview.GetElementVisual(canvas).Compositor;
var root = compositor.CreateContainerVisual();
ElementCompositionPreview.SetElementChildVisual(canvas, root);
var shadowLayer = compositor.CreateSpriteVisual();
var frameLayer = compositor.CreateLayerVisual();
var frameContent = compositor.CreateShapeVisual();
root.Children.InsertAtBottom(shadowLayer);
root.Children.InsertAtTop(frameLayer);
frameLayer.Children.InsertAtTop(frameContent);
var rectangle = root.Compositor.CreateRoundedRectangleGeometry();
rectangle.Size = new Vector2((float)content.ActualWidth, (float)content.ActualHeight);
rectangle.CornerRadius = new Vector2(30f);
var shape = root.Compositor.CreateSpriteShape(rectangle);
shape.FillBrush = root.Compositor.CreateColorBrush(Colors.Blue);
//var visual = root.Compositor.CreateShapeVisual();
frameContent.Size = rectangle.Size;
frameContent.Shapes.Add(shape);
//create mask layer
var layerEffect = new CompositeEffect
{
Mode = Microsoft.Graphics.Canvas.CanvasComposite.DestinationIn,
Sources = { new CompositionEffectSourceParameter("source"), new CompositionEffectSourceParameter("mask") }
};
var layerEffectFactory = compositor.CreateEffectFactory(layerEffect);
var layerEffectBrush = layerEffectFactory.CreateBrush();
//CompositionDrawingSurface
var graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, new Microsoft.Graphics.Canvas.CanvasDevice(forceSoftwareRenderer: false));
var frameLayerMask = graphicsDevice.CreateDrawingSurface(new Size(0, 0), Windows.Graphics.DirectX.DirectXPixelFormat.B8G8R8A8UIntNormalized, Windows.Graphics.DirectX.DirectXAlphaMode.Premultiplied);
layerEffectBrush.SetSourceParameter("mask", compositor.CreateSurfaceBrush(frameLayerMask));
frameLayer.Effect = layerEffectBrush;
var shadow = root.Compositor.CreateDropShadow();
//shadow.SourcePolicy = CompositionDropShadowSourcePolicy.InheritFromVisualContent;
shadow.Mask = layerEffectBrush.GetSourceParameter("mask");
shadow.Color = Colors.Black;
shadow.BlurRadius = 25f;
shadow.Opacity = 0.75f;
shadow.Offset = new Vector3(0, 0, 0);
shadowLayer.Shadow = shadow;
content.Opacity = 0; //hiding my actual content to see the results of this
};
container.Children.Add(canvas);
container.Children.Add(content);
return container;
}
In these tests, I am doing the same inefficient use of object, creating another container that has both the composition canvas, and also the grid. If possible, I'd like to apply the composition directly to the original content grid.
I am completely new to composition, so any thoughts, pointers, glaring errors or solutions would be most welcomed.
A Hack Solution?
I have changed my method to the following, visually it works - but is it right?
protected FrameworkElement AddDropShadow(FrameworkElement content)
{
var container = new Grid { HorizontalAlignment = content.HorizontalAlignment, VerticalAlignment = content.VerticalAlignment };
var rectangle = new Rectangle { Fill = new SolidColorBrush(Colors.Transparent) };
content.Loaded += (s, e) =>
{
rectangle.Fill = new SolidColorBrush(Colors.Black);
rectangle.Width = content.ActualWidth;
rectangle.Height = content.ActualHeight;
rectangle.RadiusX = 30;
rectangle.RadiusY = 30;
var compositor = ElementCompositionPreview.GetElementVisual(rectangle).Compositor;
var visual = compositor.CreateSpriteVisual();
visual.Size = new Vector2((float)content.ActualWidth, (float)content.ActualHeight);
var shadow = compositor.CreateDropShadow();
shadow.BlurRadius = 30f;
shadow.Mask = rectangle.GetAlphaMask();
shadow.Opacity = 0.75f;
visual.Shadow = shadow;
ElementCompositionPreview.SetElementChildVisual(rectangle, visual);
};
container.Children.Add(rectangle);
container.Children.Add(content);
return container;
}
The concept here is that my container grid holds a rectangle and my content grid (or other element).
The first error of this method is that is assumes my input FrameworkElement will be rectangular. I imagine that this could be improved upon by creating a bitmap render of the content as highlighted in this blog - but this will likely be quite costly. I also have to ensure that the rectangle size and shape exactly matches that of my main content!
It feels very wrong that there is a rectangle drawn on the screen (even though hidden by my main content). The rectangle is purely there to create the alpha mask so I guess it could be scrapped if the mask is created from the renderof the content.
I've tried setting the visibility of the rectangle to collapsed to remove it from the visual tree. This means that I can attach the visual to the container instead:
ElementCompositionPreview.SetElementChildVisual(container, visual)
However, doing this means that the shadow displays in front of the main content, which means I need some other ui element to attach it too - may as well be the rectangle!
Your solution to use Rectangle is my current workaround everywhere I need rounded shadow under Grid or Border. It's simple and it's plain, why should I complain :)
But if it's not your choice you can draw a rounded rectangle and blur it:
GraphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(Compositor, CanvasDevice.GetSharedDevice());
var roudRectMaskSurface = GraphicsDevice.CreateDrawingSurface(new Size(SurfaceWidth + BlurMargin * 2, SurfaceHeight + BlurMargin * 2), DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);
using (var ds = CanvasComposition.CreateDrawingSession(roudRectMaskSurface))
{
ds.Clear(Colors.Transparent);
ds.FillRoundedRectangle(new Rect(BlurMargin, BlurMargin, roudRectMaskSurface.Size.Width + BlurMargin, roudRectMaskSurface.Size.Height + BlurMargin), YourRadius, YourRadius, Colors.Black);
}
var rectangleMask = Compositor.CreateSurfaceBrush(roudRectMaskSurface);
Now you can apply this surface in the EffectBrush with blur effect to obtain custom shadow.
BlurMargin - corresponds to the blur amount, you need it because your blurred surface will be bigger than initial source rectangle (to avoid blur clip).
I'm new to ios development. I am trying to create a vertical ScrollView in a Xamarin iOS application.
Below is my code for a horizontal ScrollView
using System;
using UIKit;
using Foundation;
using CoreGraphics;
using System.Collections.Generic;
namespace TestApp
{
public partial class ViewController : UIViewController
{
public ViewController (IntPtr handle) : base (handle)
{
ScrollingButtonsController ();
}
UIScrollView scrollView;
List<UIButton> buttons;
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
// Perform any additional setup after loading the view, typically from a nib.
//MyScrollView.scrollEnabled=YES;
//MyScrollView.contentSize= CGSizeMake(CGFloat.width, CGFloat.height);
//end
nfloat h = 50.0f;
nfloat w = 50.0f;
nfloat padding = 10.0f;
nint n = 100;
scrollView = new UIScrollView {
Frame = new CGRect (0, 100, View.Frame.Height, h + 2 * padding),
ContentSize = new CGSize ((w + padding) * n, h),
BackgroundColor = UIColor.Red,
AutoresizingMask = UIViewAutoresizing.FlexibleWidth
};
for (int i=0; i<n; i++) {
var button = UIButton.FromType (UIButtonType.RoundedRect);
button.SetTitle (i.ToString (), UIControlState.Normal);
button.Frame = new CGRect (padding * (i + 1) + (i * w), padding, w, h);
scrollView.AddSubview (button);
buttons.Add (button);
}
View.AddSubview (scrollView);
//UIScrollView scrollView;
//scrollView = new UIScrollView (
// new CGRect (0, 0, View.Frame.Width, View.Frame.Height));
// View.AddSubview (scrollView);
}
public void ScrollingButtonsController ()
{
buttons = new List<UIButton> ();
}
public override void DidReceiveMemoryWarning ()
{
base.DidReceiveMemoryWarning ();
// Release any cached data, images, etc that aren't in use.
}
}
}
I want to create a vertical scrollview and add some scrollable Text Views. Any idea on how can to do this?
I would be grateful if you can specify in detail how can I get the elements in my Main.storyboard file with structure scheme as per below to scroll:
-Scroll View
--label
---Image View
---Text View
--label
---Image View
---Text View
--label
---Image View
---Text View
--label
---Image View
---Text View
--label
---Image View
---Text View
and more...
You need to set ScrollView Content Size Height, larger than it's frame height.So it will scroll up/down. Width of scroll view should be the same as its content width, so it won't scroll right/left.
I was searching for this. I used many tutorials both with and without code. The best way to do the scrolling is the following:
Add the scrollview (left, right, up, down constraints)
Add whatever you want
The tricky part is to set 2 constraints:
1) horizontal space to container, where container is the scroll view (do this with you wider element)
2) vertical space to container, where container is the scroll view (do this with your last element)
While your screen becomes taller, the vertical constraint will increase your scroll view. No code needed, just the proper constraints.
In your example you have to:
set up, left and right constraints to your first label
add vertical constraints amongst all your labels
add the final vertical constrain between your final textview and the scrollview (the textview is possible to need a height constraint)
If your elements don't fit the screen in Xcode, use freeform size to make your controller bigger (this applies only to xcode, not on your actual program)
In Dynamic Data Display, there is a class called MarkerPointsGraph. This derives from FrameWorkElement (through a series of other classes along the way, the most immediate parent being PointsGraphBase) and overrides the OnRenderMethod to draw a set of markers on the chart.
It does this by calling a render method in the appropriate marker type (say, Triangle, Circle and so on) once for each point rendered on the screen. I need to find a way to identify when the mouse hovers on one of these markers so that I can set the tooltip for that marker.
I have a set of methods that allow me to convert a point from the screen position to the viewport position and to the data position and back. i.e. converts a screen value to a corresponding data value or viewport value and vice versa.
I also have the tooltip opening event on this framework element and the pixel size of each of these markers. As long as the user hovers on a particular marker, I need to identify which point he has hovered on and let the markerpointsgraph set the tooltip value.
However, the transforms and methods for converting the values don't seem to be working fine, especially in the x direction. THe y - direction seems to be fine.
Below is some sample code that will explain the idea:
double selectedPointX = 0;
double selectedPointY = 0;
CoordinateTransform transformLocal = this.primaryPlotter.Transform;
if (series.SeriesDescription.YAxisAffinity == AxisAffinity_Y.Y1)
{
selectedPointX = Mouse.GetPosition(this.primaryPlotter).ScreenToViewport(transformLocal).X; //Getting the mouse positions
selectedPointY = Mouse.GetPosition(this.primaryPlotter).ScreenToViewport(transformLocal).Y;
}
else if (series.SeriesDescription.YAxisAffinity == AxisAffinity_Y.Y2 && injSecondaryAxis != null)
{
transformLocal = injSecondaryAxis.Transform;
selectedPointX = Mouse.GetPosition(this.injSecondaryAxis).ScreenToViewport(transformLocal).X;
selectedPointY = Mouse.GetPosition(this.injSecondaryAxis).ScreenToViewport(transformLocal).Y;
}
else if (series.SeriesDescription.YAxisAffinity == AxisAffinity_Y.Y3 && injTertiaryAxis != null)
{
transformLocal = injTertiaryAxis.Transform;
selectedPointX = Mouse.GetPosition(this.injTertiaryAxis).ScreenToViewport(transformLocal).X;
selectedPointY = Mouse.GetPosition(this.injTertiaryAxis).ScreenToViewport(transformLocal).Y;
}
foreach (var item in SeriesList)
{
if (item.Key == GraphKey)
{
for (int i = 0; i < item.Value.Collection.Count; i++)
{
//Calculate the size of the marker on the screen and allow for some level of inaccuracy in identifying the marker i.e anywhere within the marker is allowed.
double xlowerBound = item.Value.Collection[i].DataToViewport(transformLocal).X - series.MarkerSize;
double xUpperBound = item.Value.Collection[i].DataToViewport(transformLocal).X + series.MarkerSize;
double ylowerBound = item.Value.Collection[i].DataToViewport(transformLocal).Y - series.MarkerSize;
double yUpperBound = item.Value.Collection[i].DataToViewport(transformLocal).Y + series.MarkerSize;
//If point is within bounds
if (!(selectedPointX < xlowerBound || selectedPointX > xUpperBound || selectedPointY < ylowerBound || selectedPointY > yUpperBound))
{
strToolTip = item.Value.Collection[i].X + ", " + item.Value.Collection[i].Y; //This point is set as the tooltip
break;
}
}
break;
}
}
Here, injSecondary and injTertiary are two injected plotters that provide two vertical Y axes. They behave pretty similar to the primary chart plotter.
Is there anything wrong occurring here? For some reason, points well ahead of the actual clicked point are passing the buffer clause.
Hmm, looks like you're going about this the wrong way to me. If you dig into the source code of D3, you can open one of the marker classes and directly edit the tooltip there. Every System.Windows.Shapes element has a Tooltip property which you can assign right in the marker class. All you need to do is decide what data the tooltip holds.
Example - The CircleElementPointMarker Class :
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Shapes;
namespace Microsoft.Research.DynamicDataDisplay.PointMarkers
{
/// <summary>Adds Circle element at every point of graph</summary>
public class CircleElementPointMarker : ShapeElementPointMarker {
public override UIElement CreateMarker()
{
Ellipse result = new Ellipse();
result.Width = Size;
result.Height = Size;
result.Stroke = Brush;
result.Fill = Fill;
if (!String.IsNullOrEmpty(ToolTipText))
{
ToolTip tt = new ToolTip();
tt.Content = ToolTipText;
result.ToolTip = tt;
}
return result;
}
public override void SetMarkerProperties(UIElement marker)
{
Ellipse ellipse = (Ellipse)marker;
ellipse.Width = Size;
ellipse.Height = Size;
ellipse.Stroke = Brush;
ellipse.Fill = Fill;
if (!String.IsNullOrEmpty(ToolTipText))
{
ToolTip tt = new ToolTip();
tt.Content = ToolTipText;
ellipse.ToolTip = tt;
}
}
public override void SetPosition(UIElement marker, Point screenPoint)
{
Canvas.SetLeft(marker, screenPoint.X - Size / 2);
Canvas.SetTop(marker, screenPoint.Y - Size / 2);
}
}
}
You can see in this class we have code built in to handle tooltips. The ToolTipText is a property that derives from the parent class - ShapeElementPointMarker. All you have to do is assign that property with the data that you need to show.
Use a cursorcoordinategraph on the plotter in place of the Mouse.GetPosition to get the correct screen position and avoid the ScreenToViewport transform. Also, use to DataToScreen while creating the bounds.
I'm learning WPF and have a specific goal.
Imagine you have a grid (3 rows by 3 columns), and a control, say a simple blue rectangle fills the middle cell. When I click on the cell I want the square to rotate smoothly through 180 degrees.
The specific issue I have at the moment is; as the rectangle rotates, it won't change its dimensions, so it will extend beyond the boundary of the cell. I don't want it to clip, i want it to appear on top, partially obscuring surrounding cells.
The second part of this is, if there is one cell that fills the entire window, and I click on the blue rectangle in that cell, can I make the rectangle rotate and extend beyond the sides of the form?
If that doesn't make sense, please ask. I'm finding it hard to google because I don't know the exact terms I should be using.
Thank you
The first part can be acomplished by using the attached property Panel.ZIndex, set it to a high value when you start the animation and a lower value when the animation is complete. The second part (having a control outside of the window) is more complicated. I tried a few things and this method seemed to be the best. It uses a full screen window instead of a Popup as I encountered cliping issues. A copy of the element is made using RenderTargetBitmap this is then placed in the same position. The original element is hidden whilst the copy is animated.
public void PopupAnimation(UIElement element)
{
double w = element.RenderSize.Width,h = element.RenderSize.Height;
var screen = new Canvas();
var pos = element.PointToScreen(new Point(0, 0));
var rtb = new RenderTargetBitmap((int)w,(int)h, 96, 96, PixelFormats.Pbgra32);
rtb.Render(element);
Image i = new Image { Source = rtb, Width = w, Height = h,Stretch=Stretch.Fill};
Canvas.SetLeft(i, pos.X);
Canvas.SetTop(i, pos.Y);
screen.Children.Add(i);
var window = new Window() {
Content = screen, AllowsTransparency = true,
Width=SystemParameters.PrimaryScreenWidth,Height=SystemParameters.PrimaryScreenHeight,
WindowStyle=WindowStyle.None,ShowInTaskbar=false,Topmost=true,
Background=Brushes.Transparent,ShowActivated=false,Left=0,Top=0
};
var transform = new RotateTransform();
i.RenderTransformOrigin = new Point(0.5, 0.5);
i.RenderTransform = transform;
var anim = new DoubleAnimation { To = 360 };
anim.Completed += (s,e) =>
{
element.Visibility = Visibility.Visible;
var delay = new Storyboard { Duration = TimeSpan.FromSeconds(0.1) };
delay.Completed += (s2, e2) => window.Close();
delay.Begin();
};
window.ContentRendered += (s, e) =>
{
transform.BeginAnimation(RotateTransform.AngleProperty, anim);
element.Visibility = Visibility.Hidden;
};
window.Show();
}