WPF custom panel scaling to fit window - c#

I have a WPF custom panel which arranges its child elements in a spiral shape as per my requirements. The problem I am having is to do with scaling of the items when the window is resized - at the moment it does not scale. Can anyone provide a solution? Thanks - Ben
Custom panel
public class TagPanel : Panel
{
protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)
{
Size resultSize = new Size(0, 0);
foreach (UIElement child in Children)
{
child.Measure(availableSize);
resultSize.Width = Math.Max(resultSize.Width, child.DesiredSize.Width);
resultSize.Height = Math.Max(resultSize.Height, child.DesiredSize.Height);
}
resultSize.Width = double.IsPositiveInfinity(availableSize.Width) ?
resultSize.Width : availableSize.Width;
resultSize.Height = double.IsPositiveInfinity(availableSize.Height) ?
resultSize.Height : availableSize.Height;
return resultSize;
}
protected class InnerPos
{
public UIElement Element { get; set; }
public Size Size { get; set; }
}
private Point GetSpiralPosition(double theta, Size windowSize)
{
double a = 5.0;
double n = 1.0;
double r = a * (Math.Pow(theta, 1.0 / n));
double x = r * Math.Cos(theta);
double y = r * Math.Sin(theta);
x += windowSize.Width / 2.0;
y += windowSize.Height / 2.0;
return new Point(x,y);
}
private Rect CreateRectangleCenteredAtPoint(Point pt, double width, double height)
{
return new Rect(new Point(pt.X - (width / 2.0), pt.Y - (height / 2.0)),
new Size(width, height));
}
protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)
{
//double startPos = 0.0;
List<InnerPos> positions = new List<InnerPos>();
foreach (UIElement ch in Children)
{
// If this is the first time
// we've seen this child, add our transforms
//if (ch.RenderTransform as TransformGroup == null)
//{
// ch.RenderTransformOrigin = new Point(0, 0.5);
// TransformGroup group = new TransformGroup();
// ch.RenderTransform = group;
// group.Children.Add(new ScaleTransform());
// group.Children.Add(new TranslateTransform());
//}
positions.Add(new InnerPos()
{
Element = ch,
Size = ch.DesiredSize
});
}
//double currentTopMax
List<Rect> alreadyUsedPositions = new List<Rect>();
foreach (InnerPos child in positions.OrderByDescending(i => i.Size.Width))
{
for (double theta = 0.0; theta < 100.0; theta += 0.1)
{
Point spiralPos = GetSpiralPosition(theta, finalSize);
Rect centeredRect = CreateRectangleCenteredAtPoint(spiralPos,
child.Element.DesiredSize.Width,
child.Element.DesiredSize.Height);
bool posIsOk = true;
foreach (Rect existing in alreadyUsedPositions)
{
bool positionClashes = existing.IntersectsWith(centeredRect);
if (positionClashes == true)
{
posIsOk = false;
break;
}
}
if (posIsOk)
{
alreadyUsedPositions.Add(centeredRect);
child.Element.Arrange(centeredRect);
break;
}
}
}
return finalSize;
}
}

Is the HorizontalAlignment and VerticalAlignment of the Panel set to Stretch and the Width/Height to Auto (double.NaN)?
It looks like you are passing the finalSize into GetSpiralPosition(), which is the function where all the true work happens. Without seeing the code to this function it is difficult to say more, but I assume you are calculating correctly off of the finalSize.
Have you debugged to see if Arrange is getting called with the updated size when the window is resized? There would be two ways to test this: First, put a trace point instead of a breakpoint. Dump finalSize to the output window and see if it changes when the window is resized. Second, add a handler in the window to SizeChanged. Put a breakpoint in the handler. When the breakpoint is hit, put a breakpoint in the arrange method of the custom panel and then run.

Related

How to optimize draw area in pixel art editor

I have pixel art creator program, and I have rectangles on canvas that are one field (pixel?). And this is good solution on not huge amount of it (for example 128x128). if i want to create 1024x1024 rectangles on canvas this process is very long, ram usage is about 1-2 gb and after that program runs very slowly. How to optimize this, or create better solution?
Using a Rectangle to represent each pixel is the wrong way to do this. As a FrameworkElement, every rectangle participates in layout and input hit testing. That approach is too heavy weight to be scalable. Abandon it now.
I would recommend drawing directly to a WriteableBitmap and using a custom surface to render the bitmap as the user draws.
Below is a minimum proof of concept that allows simple drawing in a single color. It requires the WriteableBitmapEx library, which is available from NuGet.
public class PixelEditor : FrameworkElement
{
private readonly Surface _surface;
private readonly Visual _gridLines;
public int PixelWidth { get; } = 128;
public int PixelHeight { get; } = 128;
public int Magnification { get; } = 10;
public PixelEditor()
{
_surface = new Surface(this);
_gridLines = CreateGridLines();
Cursor = Cursors.Pen;
AddVisualChild(_surface);
AddVisualChild(_gridLines);
}
protected override int VisualChildrenCount => 2;
protected override Visual GetVisualChild(int index)
{
return index == 0 ? _surface : _gridLines;
}
private void Draw()
{
var p = Mouse.GetPosition(_surface);
var magnification = Magnification;
var surfaceWidth = PixelWidth * magnification;
var surfaceHeight = PixelHeight * magnification;
if (p.X < 0 || p.X >= surfaceWidth || p.Y < 0 || p.Y >= surfaceHeight)
return;
_surface.SetColor(
(int)(p.X / magnification),
(int)(p.Y / magnification),
Colors.DodgerBlue);
_surface.InvalidateVisual();
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.LeftButton == MouseButtonState.Pressed && IsMouseCaptured)
Draw();
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
CaptureMouse();
Draw();
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
ReleaseMouseCapture();
}
protected override Size MeasureOverride(Size availableSize)
{
var magnification = Magnification;
var size = new Size(PixelWidth* magnification, PixelHeight * magnification);
_surface.Measure(size);
return size;
}
protected override Size ArrangeOverride(Size finalSize)
{
_surface.Arrange(new Rect(finalSize));
return finalSize;
}
private Visual CreateGridLines()
{
var dv = new DrawingVisual();
var dc = dv.RenderOpen();
var w = PixelWidth;
var h = PixelHeight;
var m = Magnification;
var d = -0.5d; // snap gridlines to device pixels
var pen = new Pen(new SolidColorBrush(Color.FromArgb(63, 63, 63, 63)), 1d);
pen.Freeze();
for (var x = 1; x < w; x++)
dc.DrawLine(pen, new Point(x * m + d, 0), new Point(x * m + d, h * m));
for (var y = 1; y < h; y++)
dc.DrawLine(pen, new Point(0, y * m + d), new Point(w * m, y * m + d));
dc.Close();
return dv;
}
private sealed class Surface : FrameworkElement
{
private readonly PixelEditor _owner;
private readonly WriteableBitmap _bitmap;
public Surface(PixelEditor owner)
{
_owner = owner;
_bitmap = BitmapFactory.New(owner.PixelWidth, owner.PixelHeight);
_bitmap.Clear(Colors.White);
RenderOptions.SetBitmapScalingMode(this, BitmapScalingMode.NearestNeighbor);
}
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
var magnification = _owner.Magnification;
var width = _bitmap.PixelWidth * magnification;
var height = _bitmap.PixelHeight * magnification;
dc.DrawImage(_bitmap, new Rect(0, 0, width, height));
}
internal void SetColor(int x, int y, Color color)
{
_bitmap.SetPixel(x, y, color);
}
}
}
Just import it into your Xaml, preferably inside a ScrollViewer:
<Window x:Class="WpfTest.PixelArtEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:WpfTest"
Title="PixelArtEditor"
Width="640"
Height="480">
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<l:PixelEditor />
</ScrollViewer>
</Window>
Obviously, this is a far cry from being a fully-featured pixel art editor, but it's functional, and it's enough to get you on the right track. The difference in memory usage between editing a 128x128 image vs. 1024x1024 is about ~30mb. Fire it up and see it in action:
Hey, that was fun! Thanks for the diversion.
Just to improve Mike Strobel solution to snap gridlines to device pixels.
var d = -0.5d; // snap gridlines to device pixels
using (DrawingContext dc = _dv.RenderOpen())
{
GuidelineSet guidelineSet = new GuidelineSet();
guidelineSet.GuidelinesX.Add(0.5);
guidelineSet.GuidelinesY.Add(0.5);
dc.PushGuidelineSet(guidelineSet);
// Draw grid
}

How to enable zoom in and zoom out on a contentpage in xamarin forms?

How can I enable zoom on a contentpage in xamarin forms? Is it possible to enable it on the entire contentpage? Or is it only possible to zoom in on images?
You can use the pinch gesture inside a ContentPage, here is the official page:
https://developer.xamarin.com/guides/xamarin-forms/user-interface/gestures/pinch/
and for the entire project sample:
https://github.com/xamarin/xamarin-forms-samples/tree/master/WorkingWithGestures/PinchGesture
Here is an example of what you can achieve:
Xamarin.Forms Pinch Example
Try this Class , this solution do not scroll when you zoom .
Source found here:
Source Code
public class PinchToZoomContainer : ContentView {
double currentScale = 1;
double startScale = 1;
double xOffset = 0;
double yOffset = 0;
public PinchToZoomContainer ()
{
var pinchGesture = new PinchGestureRecognizer ();
pinchGesture.PinchUpdated += OnPinchUpdated;
GestureRecognizers.Add (pinchGesture);
}
void OnPinchUpdated (object sender, PinchGestureUpdatedEventArgs e)
{
if (e.Status == GestureStatus.Started) {
// Store the current scale factor applied to the wrapped user interface element,
// and zero the components for the center point of the translate transform.
startScale = Content.Scale;
Content.AnchorX = 0;
Content.AnchorY = 0;
}
if (e.Status == GestureStatus.Running) {
// Calculate the scale factor to be applied.
currentScale += (e.Scale - 1) * startScale;
currentScale = Math.Max (1, currentScale);
// The ScaleOrigin is in relative coordinates to the wrapped user interface element,
// so get the X pixel coordinate.
double renderedX = Content.X + xOffset;
double deltaX = renderedX / Width;
double deltaWidth = Width / (Content.Width * startScale);
double originX = (e.ScaleOrigin.X - deltaX) * deltaWidth;
// The ScaleOrigin is in relative coordinates to the wrapped user interface element,
// so get the Y pixel coordinate.
double renderedY = Content.Y + yOffset;
double deltaY = renderedY / Height;
double deltaHeight = Height / (Content.Height * startScale);
double originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight;
// Calculate the transformed element pixel coordinates.
double targetX = xOffset - (originX * Content.Width) * (currentScale - startScale);
double targetY = yOffset - (originY * Content.Height) * (currentScale - startScale);
// Apply translation based on the change in origin.
Content.TranslationX = targetX.Clamp (-Content.Width * (currentScale - 1), 0);
Content.TranslationY = targetY.Clamp (-Content.Height * (currentScale - 1), 0);
// Apply scale factor
Content.Scale = currentScale;
}
if (e.Status == GestureStatus.Completed) {
// Store the translation delta's of the wrapped user interface element.
xOffset = Content.TranslationX;
yOffset = Content.TranslationY;
}
}
}
Helper DoubleExtensions
public static class DoubleExtensions
{
public static double Clamp (this double self, double min, double max)
{
return Math.Min (max, Math.Max (self, min));
}
}
You can try using the Scale Api on the Content page. This seemed to work for me on a small test app.
public App ()
{
// The root page of your application
var scaleUp = new Button {
Text = "Scale Up"
};
scaleUp.Clicked += (sender, e) => {
this.MainPage.Scale += 1;
};
var scaleDown = new Button {
Text = "Scale Down"
};
scaleDown.Clicked += (object sender, EventArgs e) => {
this.MainPage.Scale -= 1;
};
MainPage = new ContentPage {
Content = new StackLayout {
VerticalOptions = LayoutOptions.Center,
Children = {
scaleUp,
scaleDown
}
}
};
}
Trying to use this in conjunction with other controls that take the Pan/scroll gesture seems to fail. ScrollView and ListView for example grab the Pinch gesture even though they themselves don't Pinch but they do scroll/pan. So it seems like Pinch inherits/uses that gesture so it is grabbed by the wrapped control. This seems to make Pinch a very niche feature that can only work on very static views of Label and Image for example, but nothing more complex.
You can use gestures.
Sample here : http://arteksoftware.com/gesture-recognizers-with-xamarin-forms/

Best way to draw a line without using a canvas C#

I am wondering what the best way to draw a line, on a maximum value, from a list is without using a canvas?
I have identified the Max, Min and Median I'm wondering what the best way to draw a line/point without using a canvas would be?
public partial class SpectrumControl : UserControl
{
private double Highest;
private double Minimum;
private double Median;
private int Total;
private int CellWidth;
public int Width { get; set; }
public SpectrumControl()
{
InitializeComponent();
}
public void Bind(KLayer klayer)
{
if (Width == 0)
{
Width = 300;
}
Highest = klayer.Values.Max();
Minimum = klayer.Values.Min();
Median = ((Highest - Minimum) / 2) + Minimum;
Total = klayer.Values.Count;
CellWidth = Width / Total;
int rowNumber = 0;
foreach (var item in klayer.Values)
{
var label = CreateLabel(item, rowNumber);
Color backgroundColour = GetColour(item);
stk1.Children.Add(label);
rowNumber++;
}
}
private Label CreateLabel(double item, int rowNumber)
{
var label = new Label()
{
Background = new SolidColorBrush(GetColour(item)),
Width = CellWidth
};
return label;
}
private Color GetColour(double item)
{
byte a = Convert.ToByte(GetTransparency(item)*255);
Color backgroundColour;
if (item < Median)
{
backgroundColour = Color.FromArgb(a, 128, 128, 255);
}
else if (item > Median)
{
backgroundColour = Color.FromArgb(a, 255, 128, 128);
}
else
{
backgroundColour = Colors.White;
}
return backgroundColour;
}
private double GetTransparency(double item)
{
double x = Highest - Minimum;
double difference;
if (item > Median)
{
difference = item - Median;
}
else
{
difference = Median - item;
}
var fraction = difference / x;
return fraction;
}
}
Well, assuming you are going to use something like GridPanel or any other panel, really, you could do this:
var line = new Line();
line.Stroke = System.Windows.Media.Brushes.LightSteelBlue;
line.X1 = 1;
line.X2 = 50;
line.Y1 = 1;
line.Y2 = 50;
line.HorizontalAlignment = HorizontalAlignment.Left;
line.VerticalAlignment = VerticalAlignment.Center;
line.StrokeThickness = 2;
grid.Children.Add(line);
Same thing may be achieved in XAML, but it looks like you prefer to work in code-behind, so that is what I posted here.
Reference: https://msdn.microsoft.com/en-us/library/system.windows.shapes.line%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396
I'm not sure why you'd avoid canvas, though (well, why someone told you to do that). I've created plenty of plots using canvas.

Move a control in a circle at runtime?

I know you can change a control's x/y location at runtime and I can use a timer to move it up/down/left/right/diagonally but how can you programatically move it in a circle?
For example, if I had a PictureBox control at the 12 o'clock position on my main form, can I move that picture box in a circle, finishing at its start position, on a button click?
Use sinus and cosinus functions.
Look at that for example.
A concrete C# example exists here.
In case that the link will not exist some day, here is the source code for drawing 25 increasing radius circles on a form:
void PutPixel(Graphics g, int x, int y, Color c)
{
Bitmap bm = new Bitmap(1, 1);
bm.SetPixel(0, 0, Color.Red);
g.DrawImageUnscaled(bm, x, y);
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics myGraphics = e.Graphics;
myGraphics.Clear(Color.White);
double radius = 5;
for (int j = 1; j <= 25; j++)
{
radius = (j + 1) * 5;
for (double i = 0.0; i < 360.0; i += 0.1)
{
double angle = i * System.Math.PI / 180;
int x = (int)(150 + radius * System.Math.Cos(angle));
int y = (int)(150 + radius * System.Math.Sin(angle));
PutPixel(myGraphics, x, y, Color.Red);
}
}
myGraphics.Dispose();
}
Result:
I've written a small class deriving from PictureBox which should let you achieve your result easily enough. Everytime you call RotateStep its location will change accordingly. Angle and speed are expressed in radians, distance in pixels.
class RotatingPictureBox : PictureBox
{
public double Angle { get; set; }
public double Speed { get; set; }
public double Distance { get; set; }
public void RotateStep()
{
var oldX = Math.Cos(Angle)*Distance;
var oldY = Math.Sin(Angle)*Distance;
Angle += Speed;
var x = Math.Cos(Angle)*Distance - oldX;
var y = Math.Sin(Angle)*Distance - oldY;
Location += new Size((int) x, (int) y);
}
}
Sample usage:
public Form1()
{
InitializeComponent();
var pictureBox = new RotatingPictureBox
{
Angle = Math.PI,
Speed = Math.PI/20,
Distance = 50,
BackColor = Color.Black,
Width = 10,
Height = 10,
Location = new Point(100, 50)
};
Controls.Add(pictureBox);
var timer = new Timer {Interval = 10};
timer.Tick += (sender, args) => pictureBox.RotateStep();
timer.Start();
}

Zoom in and out to the center of the viewport in a canvas

I need to zoom in and out a canvas. But the zoom should always be centered to the center of the screen (not the canvas). And this doesnt work with my current code! It always zooms to the center of the canvas and not the current screen.
The canvas should also be moveable, but should not be aloweded to move away from it's borders.
This is what I have so far:
XAML
<Grid>
<Canvas HorizontalAlignment="Left" Name="canvas1" VerticalAlignment="Top">
</Canvas>
</Grid>
C#
// x & y arent the position, it's how many pixel the object should move to the left/right etc
public void setPosition(double x, double y)
{
Thickness t = new Thickness(canvas1.Margin.Left + x, canvas1.Margin.Top + y, 0, 0);
if (t.Left > 0)
{
t.Left = 0;
}
else if(t.Left < -(canvas1.Width - System.Windows.SystemParameters.PrimaryScreenWidth))
{
t.Left = -(canvas1.Width - System.Windows.SystemParameters.PrimaryScreenWidth);
}
if (t.Top > 0)
{
t.Top = 0;
}
else if (t.Top < -(canvas1.Height - System.Windows.SystemParameters.PrimaryScreenHeight))
{
t.Top = -(canvas1.Height - System.Windows.SystemParameters.PrimaryScreenHeight);
}
canvas1.Margin = t;
}
public void setZoom(double zoom)
{
double tempW = canvas1.Width;
double tempH = canvas1.Height;
canvas1.Width *= (1 + zoom);
canvas1.Height *= (1 + zoom);
Debug.WriteLine("tempW: " + tempW + " tempH: " + tempH + " canvas1.Width: " + canvas1.Width + " canvas1.Height: " + canvas1.Height);
setPosition((tempW - canvas1.Width) / 2, ((tempH - canvas1.Height) / 2));
}
And yes I know there is stuff like ScaleTransform but for some reason this is more performant.. dont ask me why!
Can someone help me please?
Here is my solution for Pan and Zoom, both works fine in my complex application (12000px x 12000px).
You need to have a Zoom dependency property (double) and a ScaleTransform property
private double m_dCurZoom = 1.0;
public double Zoom
{
get { return m_dCurZoom; }
set
{
double oldzoom = m_dCurZoom;
if (m_dCurZoom != value)
{
m_dCurZoom = value;
OnPropertyChanged("Zoom");
UpdateZoom(oldzoom);
}
}
}
private ScaleTransform m_transZoom;
public ScaleTransform TransZoom
{
get { return m_transZoom; }
}
private TranslateTransform m_transPan;
/// <summary>
/// Gets or sets the Panning in X axis.
/// </summary>
public double PanX
{
get { return m_transPan.X; }
set
{
if (m_transPan.X != value)
{
m_transPan.X = value;
ResizeElementContents();
}
}
}
/// <summary>
/// Gets or sets the Panning in Y axis.
/// </summary>
public double PanY
{
get { return m_transPan.Y; }
set
{
if (m_transPan.Y != value)
{
m_transPan.Y = value;
ResizeElementContents();
}
}
}
When you update the Zoom, you call this method to calculate the center point:
public void UpdateZoom(double oldzoom)
{
// Are we visible?
if (m_root == null)
return;
// Get parent of VirtualPanel
FrameworkElement parent = GetRootParent();
if (parent == null)
return;
// Center point of the window
Point ptCenter = new Point(parent.RenderSize.Width / 2, parent.RenderSize.Height / 2);
// Translate into canvas coordinates
ptCenter = m_root.TranslatePoint(ptCenter, m_canvas);
// Update the zoom
m_transZoom.ScaleX = m_dCurZoom;
m_transZoom.ScaleY = m_dCurZoom;
m_transPan.X -= ptCenter.X * m_dCurZoom - ptCenter.X * oldzoom;
m_transPan.Y -= ptCenter.Y * m_dCurZoom - ptCenter.Y * oldzoom;
ResizeElementContents();
OnPropertyChanged("Zoom");
}
/// <summary>
/// Calculate the transform for given zoom & region-to-display for the given root's render-size
/// </summary>
public bool ResizeElementContents()
{
FrameworkElement parent = GetRootParent();
if (parent == null || m_transPan == null)
return false;
// Calculate the total region of the root
Rect? region = WtoHelper.CalcElementRect(Root);
if (region == null || region.HasValue == false)
return false;
// Scale the region to get the actal size with the zoom transformation.
double rgnWid = region.Value.Width * m_dCurZoom;
double rgnHei = region.Value.Height * m_dCurZoom;
// Image fits within our window width?
if (parent.RenderSize.Width > rgnWid)
m_transPan.X = (parent.RenderSize.Width - rgnWid) / 2;
else
{
// Image needs to be resized
if (m_transPan.X < -(rgnWid - parent.RenderSize.Width))
m_transPan.X = -(rgnWid - parent.RenderSize.Width);
else if (m_transPan.X > 0)
m_transPan.X = 0;
}
// Image fits within our window height?
if (parent.RenderSize.Height > rgnHei)
m_transPan.Y = (parent.RenderSize.Height - rgnHei) / 2;
else
{
// Image needs to be resized
if (m_transPan.Y < -(rgnHei - parent.RenderSize.Height))
m_transPan.Y = -(rgnHei - parent.RenderSize.Height);
else if (m_transPan.Y > 0)
m_transPan.Y = 0;
}
return true;
}
I don't provide all the code but it's a base if you can understand what I wrote.
If you need more information (I know that this question is old, this is why I'm not detailing everything) let me know.

Categories