How to draw an animated rectangle? - c#

How to draw a rectangle that could move to the edge of the PictureBox and when it reached the edge, it would unfold. And there could be several such rectangles
Here is the code of the class with the Draw function (Pike.cs):
using System.Drawing;
namespace course_project
{
internal class Pike : Fish
{
private Point coordinates;
private Size size = new Size(40, 40);
private Size speed;
public Pike(Point data, AquariumForm aquariumForm) : base(Color.Green, aquariumForm)
{
Data = data;
speed = new Size();
}
public Pike Next { get; set; }
public override void Draw(Graphics graphics)
{
graphics.FillEllipse(brush, coordinates.X, coordinates.Y, size.Width, size.Height);
}
public void UpdateLocation(Rectangle bounds)
{
if (!bounds.Contains(coordinates + speed))
{
if (coordinates.X + speed.Width < bounds.Left || coordinates.X + speed.Width > bounds.Right - size.Width)
{
speed.Width *= -1;
}
if (coordinates.Y + speed.Height < bounds.Top || coordinates.Y + speed.Height > bounds.Bottom - size.Height)
{
speed.Height *= -1;
}
}
coordinates += speed;
}
}
}
Linked List (PikeFlock.cs):
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
namespace course_project
{
internal class PikeFlock : IEnumerable<Pike>
{
Pike head;
Pike tail;
int count;
public void Add(Point data, AquariumForm aquariumForm)
{
Pike pike = new Pike(data, aquariumForm);
if (head == null)
head = pike;
else
tail.Next = pike;
tail = pike;
count++;
}
public bool Remove(Point data)
{
Pike current = head;
Pike previous = null;
while (current != null)
{
if (current.Data.Equals(data))
{
if (previous != null)
{
previous.Next = current.Next;
if (current.Next == null)
{
tail = previous;
}
}
else
{
head = head.Next;
if (head == null)
tail = null;
}
count--;
return true;
}
previous = current;
current = current.Next;
}
return false;
}
public int Count { get { return count; } }
public bool IsEmpty { get { return count == 0; } }
public void Clear()
{
head = null;
tail = null;
count = 0;
}
public bool Contains(Point data)
{
Pike current = head;
while (current != null)
{
if (current.Data.Equals(data))
return true;
current = current.Next;
}
return false;
}
public void AppendFirst(Point data, AquariumForm aquariumForm)
{
Pike pike = new Pike(data, aquariumForm)
{
Next = head
};
head = pike;
if (count == 0)
{
tail = head;
}
count++;
}
public IEnumerator<Pike> GetEnumerator()
{
Pike current = head;
while (current != null)
{
yield return current;
current = current.Next;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)this).GetEnumerator();
}
}
}
AquariumForm.cs (The form itself):
using System;
using System.Drawing;
using System.Windows.Forms;
namespace course_project
{
public partial class AquariumForm : Form
{
readonly Aquarium aquarium;
public AquariumForm()
{
InitializeComponent();
DoubleBuffered = true;
aquarium = new Aquarium(ClientRectangle);
aquarium_timer.Interval = 100;
aquarium_timer.Tick += Timer_Tick;
aquarium_timer.Enabled = true;
}
private void Timer_Tick(object sender, EventArgs e)
{
foreach (Pike pike in aquarium.pikeFlock)
{
pike.UpdateLocation(ClientRectangle);
}
foreach (Carp carp in aquarium.carpFlock)
{
carp.UpdateLocation(ClientRectangle);
}
Invalidate();
}
private void Add_carp_button_Click(object sender, EventArgs e)
{
aquarium.carpFlock.Add(new Point(), this);
}
private void Add_pike_button_Click(object sender, EventArgs e)
{
aquarium.pikeFlock.Add(new Point(), this);
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
aquarium.Paint(e.Graphics);
}
}
}
Aquarium.cs:
using System.Drawing;
namespace course_project
{
internal class Aquarium
{
readonly public PikeFlock pikeFlock;
readonly public CarpFlock carpFlock;
readonly Color waterColor;
public Aquarium(Rectangle clientRectangle)
{
waterColor = Color.LightSkyBlue;
pikeFlock = new PikeFlock();
carpFlock = new CarpFlock();
}
public void Paint(Graphics graphics)
{
graphics.Clear(waterColor);
foreach (Pike pike in pikeFlock)
pike.Draw(graphics);
foreach (Carp carp in carpFlock)
carp.Draw(graphics);
}
}
}
Fish.cs
using System.Drawing;
namespace course_project
{
internal class Fish
{
private protected Brush brush;
private protected AquariumForm aquarium_form;
public Fish(Color color, AquariumForm aquariumForm)
{
aquarium_form = aquariumForm;
brush = new SolidBrush(color);
}
public virtual void Draw(Graphics graphics) { }
public Point Data { get; set; }
}
}
Confused just at the point where you need to animate it all. By clicking the button everything is created as it should be, but animation does not work, even I do not know how :(

You didn't post fish.cs so without testing I would suggest the following changes:
Do not use PictureBox, it just complicates things if you perform custom animations this way. So instead of extracting the Graphics from its assigned Image property, which is backed by a Bitmap that has the same size as your form (btw. what happens if you resize your form?) simply just use the Graphics that is already there when your form is redrawn.
Unlike PictureBox, a Form does not use double buffering by default so you might want to set DoubleBuffered = true in the form constructor to avoid flickering.
Override OnPaint in the form and initiate the whole repaint session from there:
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
aquarium.Paint(e.Graphics); // instead of your Init() method
}
It means you don't need bitmap, graphics and aquarium_form fields in your Aquarium class. Instead, you can pass the form's Graphics to a Paint method:
// in Aquarium.cs:
public void Paint(Graphics graphics)
{
// Please note that graphics can have any size now so it works even if you
// resize the form. Please also note that I treat pikeFlock as a Pike
// enumeration instead of just coordinates
g.Clear(waterColor);
foreach (Pike pike in pikeFlock) // instead of int[] items
pike.Draw(graphics); // instead of Draw(int[])
}
Your PikeFlock is just IEnumerable, I would change it to IEnumerable<Pike> to make it strongly typed.
It also means that PikeFlock.GetEnumerator should yield Pike instances instead of int arrays: yield return current instead of current.Data
Pike has now int[] as coordinates. I would change it to Point. And do not pass new random coordinates in every drawing iteration because every fish will just randomly 'teleport' here and there. Instead, maintain the current horizontal and vertical speed.
// in Pike.cs:
private Point coordinates;
private Size size = new Size(40, 40);
// speed is declared as Size because there is an operator overload for Point + Size
// Do not forget to initialize speed in constructor
private Size speed;
public override void Draw(Graphics graphics)
{
// no Refresh is needed because graphics comes from the form's OnPaint now
graphics.FillEllipse(brush, coordinates.X, coordinates.Y, size.Width, size.Height);
}
public void UpdateLocation(Rectangle bounds)
{
// if a fish would go out of bounds invert its vertical or horizontal speed
// TODO: if you shrink the form an excluded fish will never come back
// because its direction will oscillate until you enlarge the form again.
if (!bounds.Contains(coordinates + speed))
{
if (coordinates.X + speed.Width < bounds.Left
|| coordinates.X + speed.Width > bounds.Right - size.Width)
{
speed.Width *= -1;
}
if (coordinates.Y + speed.Height < bounds.Top
|| coordinates.Y + speed.Height > bounds.Bottom - size.Height)
{
speed.Height *= -1;
}
}
coordinates += speed;
}
Now the only thing is missing is the animation itself. You can add a Timer component to the form from the Toolbox.
// form:
public AquariumForm()
{
InitializeComponent();
DoubleBuffered = true;
// No need to pass the Form anymore. Pass the bounds instead to
// generate the initial coordinates within the correct range.
// No Random field is needed here, use one in Aquarium constructor only.
aquarium = new Aquarium(ClientRectangle);
timer.Interval = 100;
timer.Tick += Timer_Tick;
timer.Enabled = true;
}
private void Timer_Tick(object sender, EventArgs e)
{
// Updating coordinates of all fish. We just pass the current bounds.
foreach (Pike pike in aquarium.pikeFlock)
pike.UpdateLocation(ClientRectangle);
// Invalidating the form's graphics so a repaint will be automatically
// called after processing the pending events.
Invalidate();
}

Related

How do I associate a mouse click with a drawn object in C#?

I have a picturebox with a bunch of rectangles drawn over it (highlighting some features of the image). I want to determine if my user clicked within a given rectangle, and add an action specific to that rectangle (i.e. show additional information). How do I do this?
I can provide more information if desired, I'm just not sure what information would be useful at this point.
Current code to draw rectangles. rectX, rectY, rectRot, rectColor are all currently arrays. rectW and rectH are constants.
private void pbPicture_Paint(object sender, PaintEventArgs e)
{
for(int i = 0; i < rectX.Length; i++)
{
e.Graphics.ResetTransform();
e.Graphics.TranslateTransform(rectX[i], rectY[i]);
e.Graphics.RotateTransform(rectRot[i]);
e.Graphics.DrawRectangle(new Pen(rectColor[i], penWidth), 0, 0, rectW, rectH);
}
e.Graphics.ResetTransform();
}
Edit: added link to picture, additional code.
It'll be easier if you apply and keep the transform data for each shape, then you can use that in your implementation to draw the shapes, interact with the mouse inputs...etc. without doing any additional transform calls to draw the main shapes, nor math routines to find out whether a shape/rectangle contains a given Point.
Consider the Shape class here which encapsulates the relevant data and functionalities that you'll need in your implementation. Using the GraphicsPath class to keep the shape and apply the transform, as well as to use the GraphicsPath.IsVisible method to determine whether the shape contains a given point so you can take an action accordingly. Keeping and exposing the Matrix instance is to use it to transform the graphics in case you need to do more drawings over the shape like drawing text, image...etc.
using System;
using System.Drawing;
using System.Drawing.Text;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
using System.Collections.Generic;
public class Shape : IDisposable
{
private bool disposedValue;
private Matrix mx;
private GraphicsPath gp;
private Size sz;
private Point loc;
private float rot;
public string Text { get; set; }
public Size Size
{
get => sz;
set
{
if (sz != value)
{
sz = value;
CleanUp();
}
}
}
public Point Location
{
get => loc;
set
{
if (loc != value)
{
loc = value;
CleanUp();
}
}
}
public float Rotation
{
get => rot;
set
{
if (rot != value)
{
rot = value;
CleanUp();
}
}
}
public Matrix Matrix
{
get
{
if (mx == null)
{
mx = new Matrix();
// According to your code snippet, you don't need to offset here.
// mx.Translate(Location.X, Location.Y);
mx.RotateAt(Rotation, Center);
}
return mx;
}
}
public GraphicsPath GraphicsPath
{
get
{
if (gp == null)
{
gp = new GraphicsPath();
gp.AddRectangle(Rectangle);
gp.Transform(Matrix);
}
return gp;
}
}
public Point Center
{
get
{
var r = Rectangle;
return new Point(r.X + r.Width / 2, r.Y + r.Height / 2);
}
}
public Rectangle Rectangle => new Rectangle(Location, Size);
public bool Selected { get; set; }
public Color BorderColor { get; set; } = Color.Black;
// Add more, ForeColor, BackColor ...etc.
public bool Contains(Point point) => GraphicsPath.IsVisible(point);
private void CleanUp()
{
gp?.Dispose();
gp = null;
mx?.Dispose();
mx = null;
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing) CleanUp();
disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
Having that, your implementation should be as simple as:
private readonly List<Shape> shapes = new List<Shape>();
private const int recW = 100;
private const int recH = 20;
// A method to create the list...
private void SomeMethod()
{
shapes.ForEach(s => s.Dispose());
shapes.Clear();
// In some loop...
var shape = new Shape
{
Text = "Shape...",
Size = new Size(recW, recH),
Location = new Point(some.X, some.Y),
Rotation = someAngle
};
shapes.Add(shape);
// Add the reset...
pbox.Invalidate();
}
// And to dispose of them...
protected override void OnFormClosed(FormClosedEventArgs e)
{
base.OnFormClosed(e);
shapes.ForEach(x => x.Dispose());
}
The drawing part:
private void pbox_Paint(object sender, PaintEventArgs e)
{
var g = e.Graphics;
using (var sf = new StringFormat(StringFormat.GenericTypographic))
{
sf.Alignment = sf.LineAlignment = StringAlignment.Center;
shapes.ForEach(s =>
{
using (var pnBorder = new Pen(s.BorderColor))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.PixelOffsetMode = PixelOffsetMode.Half;
if (s.Selected) g.FillPath(Brushes.DarkOrange, s.GraphicsPath);
g.DrawPath(pnBorder, s.GraphicsPath);
if (!string.IsNullOrEmpty(s.Text))
{
g.SmoothingMode = SmoothingMode.None;
g.PixelOffsetMode = PixelOffsetMode.Default;
g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
g.Transform = s.Matrix;
g.DrawString(s.Text, Font, Brushes.Black, s.Rectangle, sf);
g.ResetTransform();
}
}
});
}
}
Interacting with the mouse events:
private void pbox_MouseDown(object sender, MouseEventArgs e)
{
foreach (var shape in shapes)
shape.Selected = shape.Contains(e.Location);
pbox.Invalidate();
}
I've created rectangles (Shape objects) with random values to demo.
Note: Some offset (Matrix.Translate(...)) also applied here to have some space between the shapes.
\You can use the event argument e to get the mouse co ordinates
private void pictureBox1_Click(object sender, EventArgs e)
{
MouseEventArgs me = (MouseEventArgs)e;
Point coordinates = me.Location;
}
There is one more link to help you identify the click events on the shape -
https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2015/ide/tutorial-3-create-a-matching-game?view=vs-2015&redirectedfrom=MSDN

how do I make beautiful flipping images?

When I opened the game launcher, I noticed how the news was implemented there.
And I really liked this idea, so I decided to do it in my project, first of all I made a panel, stuffed a few images into it, and then actually made two buttons with which I plan to flip through the images. BUT how to do it smoothly? Here is where the problem is, I do not understand how to make a smooth flipping of
the image
I can't tell how the images are sliding, flipping, stretching or whatever, but I think WinForms with GDI+ isn't the best choice. I think WPF would be better. I would also recommend using a suitable library for those kind of image manipulations.
However, if you want it very(!) simple you could use this class:
public class SlideAnimation
{
public event EventHandler<AnimationEventArgs> AnimationFinished;
private readonly Control Control;
private readonly Timer Timer;
private float fromXPosition;
public SlideAnimation(Control ctrl)
{
Control = ctrl;
Timer = new Timer();
Timer.Interval = 10;
Timer.Tick += Timer_Tick;
Control.Paint += Control_Paint;
}
public float Speed { get; set; }
public Image From { get; set; }
public Image To { get; set; }
public AnimationDirection Direction { get; set; }
public bool IsRunning
{
get
{
return Timer.Enabled;
}
}
public void StartAnimation()
{
// maybe move those checks into the setter of the corresponding property
if (this.From == null)
throw new InvalidOperationException();
if (this.To == null)
throw new InvalidOperationException();
if (this.Speed <= 0)
throw new InvalidOperationException();
fromXPosition = 0;
Timer.Enabled = true;
}
protected void OnAnimationFinished(AnimationEventArgs e)
{
AnimationFinished?.Invoke(this, e);
}
private void Timer_Tick(object sender, EventArgs e)
{
// increase or decrease the position of the first image
fromXPosition = fromXPosition + (this.Speed * this.Direction);
Control.Invalidate();
if (Math.Abs(fromXPosition) >= this.From.Width)
{
Timer.Enabled = false;
OnAnimationFinished(new AnimationEventArgs(this.Direction));
}
}
private void Control_Paint(object sender, PaintEventArgs e)
{
if (!Timer.Enabled)
return;
// draw both images next to each other depending on the direction
e.Graphics.DrawImage(this.From, new PointF(fromXPosition, 0));
e.Graphics.DrawImage(this.To, new PointF(fromXPosition - (this.From.Width * this.Direction), 0));
}
}
public enum AnimationDirection
{
Forward = -1,
Backward = 1
}
public class AnimationEventArgs : EventArgs
{
public AnimationEventArgs(AnimationDirection direction)
{
Direction = direction;
}
public AnimationDirection Direction { get; }
}
This class will only draw the images while the animation is active. Every other invalidation will not trigger the Control_Paint method.
Use following code for your Form:
public class Form1
{
private List<Image> imgList = new List<Image>();
private int currentIndex = 0;
private SlideAnimation animation;
public Slideshow()
{
InitializeComponent();
imgList.Add(Image.FromFile("pic1.bmp"));
imgList.Add(Image.FromFile("pic2.bmp"));
imgList.Add(Image.FromFile("pic3.bmp"));
imgList.Add(Image.FromFile("pic4.bmp"));
imgList.Add(Image.FromFile("pic5.bmp"));
animation = new SlideAnimation(this.Panel1);
animation.Speed = 20;
animation.AnimationFinished += AnimationFinished;
}
private void btnPrev_Click(object sender, EventArgs e)
{
if (currentIndex == 0)
return;
if (animation.IsRunning)
return;
animation.Direction = AnimationDirection.Backward;
animation.From = imgList[currentIndex];
animation.To = imgList[currentIndex - 1];
animation.StartAnimation();
}
private void btnNext_Click(object sender, EventArgs e)
{
if (currentIndex == imgList.Count - 1)
return;
if (animation.IsRunning)
return;
animation.Direction = AnimationDirection.Forward;
animation.From = imgList[currentIndex];
animation.To = imgList[currentIndex + 1];
animation.StartAnimation();
}
private void AnimationFinished(object sender, AnimationEventArgs e)
{
currentIndex = currentIndex - (1 * e.Direction);
}
private void Panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.DrawImage(imgList[currentIndex], 0, 0);
}
}
Since there are a lot of drawing operations you may use a panel which supports DoubleBuffer.
public class DoubleBufferedPanel : Panel
{
public DoubleBufferedPanel()
{
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);
}
}
Keep in mind that this example is very simple and far from "fancy".

How to drag and move shapes in C#

In a C# WindoeFormsApplication, is it possible to select, hence to move or delete a plotted shape with mouse? Like the windows paint program.
The shape plotting works totally fine, all points are stored in some array. As this line drawing example
Point Latest { get; set; }
List<Point> _points = new List<Point>();
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
// Save the mouse coordinates
Latest = new Point(e.X, e.Y);
// Force to invalidate the form client area and immediately redraw itself.
Refresh();
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
base.OnPaint(e);
if (_points.Count > 0)
{
var pen = new Pen(Color.Navy);
var pt = _points[0];
for(var i=1; _points.Count > i; i++)
{
var next = _points[i];
g.DrawLine(pen, pt, next);
pt = next;
}
g.DrawLine(pen, pt, Latest);
}
}
private void Form1_MouseClick(object sender, MouseEventArgs e)
{
Latest = new Point(e.X, e.Y);
_points.Add(Latest);
Refresh();
}
I can let it to calculate the shortest distance between mouse position and each line by basic linear algebra, and set a threshold distance, if it's shorter than the threshold, make this line selected, and can be dragged or edited by mouse. But, just wondering, is there any way that's more manageable for such task? Mainly the selection part.
Any suggestion will be appreciated, thank you!
To hit test shapes you don't need linear algebra. You can create GraphicsPath for your shapes and then using GraphicsPath.IsVisible method or GraphicsPath.IsOutlineVisible method perform hit-testing.
To check if a point is in the area of your path, for example a filled shape, use IsVisible.
To hit-test for lines or curves or empty shapes, you can use IsOutlineVisible.
Example
As an example, you can create a base IShape interface that contains methods for hit-testing, drawing and moving. Then in classes implement those methods. Also you can create a DrawingSurface control which can handle hit-testing, drawing and moving IShape objects.
In the below example, we create IShape interface, Line and Circle classes. Also we create a DrawingSurface control. To test the example, its enough to put a DrawingSurface control on a Form and handle Load event of form and add some shapes, then run application and try to move shapes.
IShape
This interface contains some useful methods which if any class implements them, can be used for drawing, hit-testing and moving. At the end of this example, you can see a DrawingSurface control which can work with IShape implementations simply:
public interface IShape
{
GraphicsPath GetPath();
bool HitTest(Point p);
void Draw(Graphics g);
void Move(Point d);
}
Line
Here is a line class which implements IShape interface. When hit-testing if you click on line, the HitTest returns true. Also to let you choose line more simply, I added 2 points for hit-testing:
public class Line : IShape
{
public Line() { LineWidth = 2; LineColor = Color.Black; }
public int LineWidth { get; set; }
public Color LineColor { get; set; }
public Point Point1 { get; set; }
public Point Point2 { get; set; }
public GraphicsPath GetPath()
{
var path = new GraphicsPath();
path.AddLine(Point1, Point2);
return path;
}
public bool HitTest(Point p)
{
var result = false;
using (var path = GetPath())
using (var pen = new Pen(LineColor, LineWidth + 2))
result = path.IsOutlineVisible(p, pen);
return result;
}
public void Draw(Graphics g)
{
using (var path = GetPath())
using (var pen = new Pen(LineColor, LineWidth))
g.DrawPath(pen, path);
}
public void Move(Point d)
{
Point1 = new Point(Point1.X + d.X, Point1.Y + d.Y);
Point2 = new Point(Point2.X + d.X, Point2.Y + d.Y);
}
}
Circle
Here is a circle class which implements IShape interface. When hit-testing if you click in circle, the HitTest returns true:
public class Circle : IShape
{
public Circle() { FillColor = Color.Black; }
public Color FillColor { get; set; }
public Point Center { get; set; }
public int Radious { get; set; }
public GraphicsPath GetPath()
{
var path = new GraphicsPath();
var p = Center;
p.Offset(-Radious, -Radious);
path.AddEllipse(p.X, p.Y, 2 * Radious, 2 * Radious);
return path;
}
public bool HitTest(Point p)
{
var result = false;
using (var path = GetPath())
result = path.IsVisible(p);
return result;
}
public void Draw(Graphics g)
{
using (var path = GetPath())
using (var brush = new SolidBrush(FillColor))
g.FillPath(brush, path);
}
public void Move(Point d)
{
Center = new Point(Center.X + d.X, Center.Y + d.Y);
}
}
DrawingSurface
The control, draws a list of shapes. Also it performs hit-testing in MouseDown and moves the shape if you drag it. You should add some shapes like Line or Circle to Shapes collection of the control.
public class DrawingSurface : Control
{
public List<IShape> Shapes { get; private set; }
IShape selectedShape;
bool moving;
Point previousPoint = Point.Empty;
public DrawingSurface() { DoubleBuffered = true; Shapes = new List<IShape>(); }
protected override void OnMouseDown(MouseEventArgs e)
{
for (var i = Shapes.Count - 1; i >= 0; i--)
if (Shapes[i].HitTest(e.Location)) { selectedShape = Shapes[i]; break; }
if (selectedShape != null) { moving = true; previousPoint = e.Location; }
base.OnMouseDown(e);
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (moving) {
var d = new Point(e.X - previousPoint.X, e.Y - previousPoint.Y);
selectedShape.Move(d);
previousPoint = e.Location;
this.Invalidate();
}
base.OnMouseMove(e);
}
protected override void OnMouseUp(MouseEventArgs e)
{
if (moving) { selectedShape = null; moving = false; }
base.OnMouseUp(e);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
foreach (var shape in Shapes)
shape.Draw(e.Graphics);
}
}

Draw rectangle with text inside

I want to make an application in which the user could add rectangle with customizable text inside it. The rectangle also can have another rectangles inside. Just as you can see on these picture:
I read about DrawingVisual, Shapes etc. So far I did it using DrawingVisual + Host, which derivies from FrameworkElement. DrawingVisual has FormattedText field, and list of Children elements; Host maintain drawing all elements.
The main problem is that, everytime user changes text in any child element I need to calculate new coordinates, width, height of all child elements. Maybe there is any method to do that automatically?
Also, DrawingVisual doesn't have any mouse events. So how to make all elements selectable / hoverable? Or should I derive from some other class?
Later I will post some code...
EDIT:
public class VisualHost: FrameworkElement
{
private VisualCollection _children;
private List<MyElement> _list;
public VisualHost(List<MyElement> list)
{
_children = new VisualCollection(this);
_list = list;
}
protected override int VisualChildrenCount
{
get { return _children.Count; }
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= _children.Count)
{
throw new ArgumentOutOfRangeException();
}
return _children[index];
}
private void CheckSize(MyElement element)
{
double sw = 0;
double mh = 0;
if (element.GetChildCount() > 0)
{
for (int i = 0; i < element.GetChildCount(); i++)
{
CheckSize(element.GetChild(i));
sw += element.GetChild(i).Width;
mh = Math.Max(mh, element.GetChild(i).Height);
}
}
element.Width = Math.Max(element.Formatted.Width, sw);
element.Height = element.Formatted.Height + mh;
}
private void DrawElement(double top, double left, MyElement element)
{
CheckSize(element);
var context = element.RenderOpen();
context.DrawRectangle( null, new Pen(Brushes.Black, 2d), new Rect(new Point(left, top), new Size(element.Width, element.Height)));
context.DrawText(element.Formatted, new Point(left, top));
top += element.Formatted.Height;
if (element.GetChildCount() > 0)
{
for (int i = 0; i < element.GetChildCount(); i++)
{
context.DrawRectangle(null, new Pen(Brushes.Black, 2d), new Rect(new Point(left, top), new Size(element.GetChild(i).Width, element.GetChild(i).Height)));
context.DrawText(element.GetChild(i).Formatted, new Point(left, top));
left += element.GetChild(i).Width;
}
}
context.Close();
_children.Add(element);
}
public void Redraw()
{
if (_list != null)
{
double top = 0, left = 0;
foreach (MyElement element in _list)
{
DrawElement(top, left, element);
top += element.Height + 10d;
}
}
}
}
public class MyElement: DrawingVisual
{
private string _text;
public string Text
{
get { return _text; }
set {
if (_text != value)
{
Typeface typeface = new Typeface(new FontFamily("Arial"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
Formatted = new FormattedText(value, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 12, Brushes.Red);
_text = value;
}
}
}
public FormattedText Formatted { get; private set; }
public double Height { get; set; }
public double Width { get; set; }
private List<MyElement> _children;
public MyElement GetChild(int i)
{
if (i < 0 || i >= _children.Count)
{
throw new ArgumentOutOfRangeException();
}
return _children[i];
}
public int GetChildCount()
{
return _children.Count;
}
public void AddChild(MyElement child)
{
_children.Add(child);
}
public MyElement(string Text)
{
this.Text = Text;
this._children = new List<MyElement>();
}
}
MainWindow.xaml.cs
public MainWindow()
{
InitializeComponent();
_list = new List<MyElement>();
_list.Add(new MyElement("text"));
var e = new MyElement("text 2");
e.AddChild(new MyElement("a"));
e.AddChild(new MyElement("b"));
e.AddChild(new MyElement("c"));
_list.Add(e);
_host = new VisualHost(_list);
MyCanvas.Children.Add(_host);
_host.Redraw();
}
This is my code for now. I wrote it only to check if idea is correct.
well I'm not sure if you would like this approach but you can actually do it very simple... I'm thinking maybe you can use blend to create a user control and design a label and a listbox in a stackpanel and set 'em all on autosizing.
or design 2 stack panels set 1 to do vertical orientation and the other one to do horizontal and add textblocks or something to the horizontal one.

WPF Custom FrameworkElement / IScrollInfo

I've got a simple test app with a basic custom FrameworkElement implementation (TestElement below). The TestElement creates a couple of drawing visuals and draws some stuff in the constructor to a width of 600. It also implements the necessary bits of IScrollinfo; The Window containing the element has got a scrollviewer and a max size of 300x300. The scrollbar appears but does not scroll the content of the TestElement.
Can anyone suggest whether what I am trying to do is possible and if so what I am doing wrong. I could re-render the drawing visuals in SetHorizontalOffset but don't want to for performance reasons as I have already drawn all I need to.
I hope the question makes some sense - let me know if not and I can clarify.
Many thanks - Karl
public class TestElement : FrameworkElement, IScrollInfo
{
DrawingVisual visual;
DrawingVisual visual2;
public TestElement()
{
Draw();
this.MaxWidth = 600;
this.MaxHeight = 300;
}
public void Draw()
{
if(visual == null)
{
visual = new DrawingVisual();
base.AddVisualChild(visual);
base.AddLogicalChild(visual);
}
if (visual2 == null)
{
visual2 = new DrawingVisual();
base.AddVisualChild(visual2);
base.AddLogicalChild(visual2);
}
Random rand = new Random();
var pen = new Pen(Brushes.Black, 1);
using(var dc = visual.RenderOpen())
{
for (int i = 0; i < 400; i++)
{
var r = rand.Next(10, 200);
dc.DrawLine(pen, new Point(i, r), new Point(i, 0));
}
}
using (var dc = visual2.RenderOpen())
{
for (int i = 0; i < 200; i++)
{
var r = rand.Next(10, 200);
dc.DrawLine(pen, new Point(i, r), new Point(i, 0));
}
visual2.Offset = new Vector(400, 0);
}
}
protected override int VisualChildrenCount
{
get { return 2; }
}
protected override Visual GetVisualChild(int index)
{
return index == 0 ? visual : visual2;
}
protected override Size MeasureOverride(Size availableSize)
{
viewport = availableSize;
owner.InvalidateScrollInfo();
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
var value = base.ArrangeOverride(finalSize);
return base.ArrangeOverride(finalSize);
}
Point offset = new Point(0,0);
public void SetHorizontalOffset(double offset)
{
this.offset.X = offset;
this.InvalidateArrange();
}
public void SetVerticalOffset(double offset)
{
this.offset.Y = offset;
}
public Rect MakeVisible(Visual visual, Rect rectangle)
{
throw new NotImplementedException();
}
public bool CanVerticallyScroll { get; set; }
public bool CanHorizontallyScroll { get; set; }
Size extent = new Size(600, 300);
private Size viewport = new Size(0, 0);
public double ExtentWidth
{
get { return extent.Width; }
}
public double ExtentHeight
{
get {return extent.Height; }
}
public double ViewportWidth
{
get { return viewport.Width; }
}
public double ViewportHeight
{
get { return viewport.Height; }
}
public double HorizontalOffset
{
get { return offset.X; }
}
public double VerticalOffset
{
get { return offset.Y; }
}
private ScrollViewer owner;
public ScrollViewer ScrollOwner
{
get { return owner; }
set { owner = value; }
}
}
the xaml:
<Window x:Class="TestWpfApp.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l ="clr-namespace:TestWpfApp"
Title="TestWpfApp" Height="300" Width="300" >
<Grid>
<ScrollViewer CanContentScroll="True" HorizontalScrollBarVisibility="Visible">
<l:TestElement CanHorizontallyScroll="True" />
</ScrollViewer>
</Grid>
Just had a beer too and it helps to find a solution. ;-)
It's not the ultimative "all well done" solution, but for sure it will help you further:
In your implementation of ArrangeOverride, try:
protected override Size ArrangeOverride(Size finalSize)
{
this.visual.Offset = new Vector(-this.HorizontalOffset, -this.VerticalOffset);
var value = base.ArrangeOverride(finalSize);
return base.ArrangeOverride(finalSize);
}
Basically you have to move your objects yourself.
For more information see this article too: iscrollinfo tutorial
.
Normally you would have to use Transformations to move the objects there where you scrolled them.

Categories