I'm going to draw hundreds of lines in real-time. I have chosen the Visual Layer to do this. But I see that there are two different ways to draw a line here. Which one you suggest to get a better performance and speed?
1. DrawingContext.DrawLine
public class DrawingTypeOne : FrameworkElement
{
private readonly VisualCollection _visuals;
public DrawingTypeOne(double thickness)
{
var myPen = new Pen
{
Thickness = 1,
Brush = Brushes.White,
};
myPen.Freeze();
_visuals = new VisualCollection(this);
var drawingVisual = new DrawingVisual();
using (var dc = drawingVisual.RenderOpen())
{
dc.DrawLine(myPen, new Point(0,0) , new Point(100,100));
_visuals.Add(drawingVisual);
}
}
protected override Visual GetVisualChild(int index)
{
return _visuals[index];
}
protected override int VisualChildrenCount
{
get
{
return _visuals.Count;
}
}
}
2. StreamGeometry
public class DrawingTypeTwo : FrameworkElement
{
private readonly VisualCollection _visuals;
public DrawingTypeTwo()
{
_visuals = new VisualCollection(this);
var geometry = new StreamGeometry();
using (var gc = geometry.Open())
{
gc.BeginFigure(new Point(0, 0), true, true);
gc.LineTo(new Point(100,100), true, false);
}
geometry.Freeze();
var drawingVisual = new DrawingVisual();
using (var dc = drawingVisual.RenderOpen())
{
dc.DrawGeometry(Brushes.Red, null, geometry);
}
_visuals.Add(drawingVisual);
}
protected override Visual GetVisualChild(int index)
{
return _visuals[index];
}
protected override int VisualChildrenCount
{
get
{
return _visuals.Count;
}
}
}
Like I said you only need one visual and inside you can have all your lines.
Take a look at this:
First we define multiple drawings inside our drawing context:
class EllipseAndRectangle : DrawingVisual
{
public EllipseAndRectangle()
{
using (DrawingContext dc = RenderOpen())
{
// Black ellipse with blue border
dc.DrawEllipse(Brushes.Black,
new Pen(Brushes.Blue, 3),
new Point(120, 120), 20, 40);
// Red rectangle with green border
dc.DrawRectangle(Brushes.Red,
new Pen(Brushes.Green, 4),
new Rect(new Point(10, 10), new Point(80, 80)));
}
}
}
This is that one speical visual or element hosting all the drawings:
public class EllAndRectHost : FrameworkElement
{
private EllipseAndRectangle _ellAndRect = new EllipseAndRectangle();
// EllipseAndRectangle instance is our only visual child
protected override Visual GetVisualChild(int index)
{
return _ellAndRect;
}
protected override int VisualChildrenCount
{
get
{
return 1;
}
}
}
And this is how you can use all those things in XAML:
<local:EllAndRectHost Margin="30" ... />
I was talking about the DrawingVisual class you could inherit from instead of creating 100 visuals for 100 lines.
Regarding your question, first approach is faster. Because the second approach is in the end doing the same the first does just its wrapped nicely. DrawLine is the lowest end. You can't go any deeper than DrawLine. DrawGeometry is calling DrawLine and some other internal stuff.
Related
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
I'm creating a custom PictureBox.
As you can see, it's a PictureBox designed for profile photos
Well, this is the class of the CircularPictureBox
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Hector.Framework.Controls
{
public class CircularPictureBox : PictureBox
{
private Color _idlecolor = Color.White;
public CircularPictureBox()
{
this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);
this.DoubleBuffered = true;
this.BackColor = Color.White;
this.SizeMode = PictureBoxSizeMode.StretchImage;
this.Size = new Size(100, 100);
}
protected override void OnPaint(PaintEventArgs pe)
{
base.OnPaint(pe);
using (var gpath = new GraphicsPath())
{
var brush = new SolidBrush(this.IdleBorderColor);
var pen = new Pen(brush, 5);
var outrect = new Rectangle(-1, -1, this.Width + 5, this.Height + 5);
gpath.AddEllipse(outrect);
pe.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
pe.Graphics.DrawPath(pen, gpath);
brush.Dispose();
pen.Dispose();
gpath.Dispose();
}
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
using (var gpath = new GraphicsPath())
{
var rect = new Rectangle(1, 1, this.Width - 1, this.Height - 1);
gpath.AddEllipse(rect);
this.Region = new Region(gpath);
gpath.Dispose();
}
}
public Color IdleBorderColor
{
get => this._idlecolor;
set => this._idlecolor = value;
}
}
}
My problem is that since it is a control that can be used from the designer, I want it to have properties such as edge width or border color.
I started testing with the color, but it is that whenever I change the color,
Visual Studio shows me an error message saying that The value of the property is not valid
I made a few modifications to your code, to highlight some features that can be useful in the design of Custom Control.
The modifications I've made I think are self-explanatory.
However, take a look at the OnPaint event. The e.Graphics.Clip Region lets you hide all graphics parts that are not in the selected region. This implies that when you drag the control in Design Mode, the Image will be clipped and won't be seen outside the region area.
The PixelOffsetMode.HighQuality and SmoothingMode.AntiAlias contributes to the overall quality of the rendering (there are commented out options that can useful in other situations).
The calculation of the Border offset must reference the BorderSize width, and scaled accordingly. The Pen object draws starting from the middle of its size. If a Pen has a size of 3 pixels, 1 Pixel is drawn on the border, one outside the area and one inside (weird? Maybe).
The transparency settings is just a "fake" here.
It might be used effectively in other situations (it should read "Platforms").
public class CircularPictureBox : PictureBox
{
private Bitmap bitmap;
private Color borderColor;
private int penSize;
private Color alphaColor = Color.FromArgb(0, 255,255,255);
private bool enhancedBuffering;
public CircularPictureBox()
{
InitializeComponent();
this.SetStyle(ControlStyles.SupportsTransparentBackColor |
ControlStyles.ResizeRedraw |
ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer, true);
}
private void InitializeComponent()
{
this.enhancedBuffering = true;
this.bitmap = null;
this.borderColor = Color.Silver;
this.penSize = 7;
this.BackColor = alphaColor;
this.SizeMode = PictureBoxSizeMode.StretchImage;
this.Size = new Size(100, 100);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.CompositingMode = CompositingMode.SourceOver;
//e.Graphics.CompositingQuality = CompositingQuality.HighQuality;
//e.Graphics.InterpolationMode = InterpolationMode.Bicubic;
e.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
if (this.Region != null) e.Graphics.Clip = this.Region;
var rect = this.ClientRectangle;
if (bitmap != null) {
e.Graphics.DrawImage(bitmap, rect);
}
rect.Inflate(-penSize / 2 + 1, -penSize / 2 + 1);
using (var pen = new Pen(borderColor, penSize)) {
e.Graphics.DrawEllipse(pen, rect);
}
}
protected override void OnResize(EventArgs e)
{
using (var path = new GraphicsPath()) {
path.AddEllipse(this.ClientRectangle);
path.CloseFigure();
using (Region region = new Region(path)) {
this.Region = region.Clone();
}
}
}
[Description("Gets or Sets the Image displayed by the control"), Category("Appearance")]
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true)]
public Bitmap Bitmap
{
get { return bitmap; }
set { bitmap = value; Invalidate(); }
}
[Description("Gets or Sets the size of the Border"), Category("Behavior")]
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true)]
public int BorderSize
{
get { return penSize; }
set { penSize = value; Invalidate(); }
}
[Description("Gets or Sets the Color of Border drawn around the Image.")]
[Category("Appearance")]
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true)]
public Color BorderColor
{
get { return borderColor; }
set { borderColor = value; Invalidate(); }
}
[Description("Enables or disables the control OptimizedDoubleBuffering feature")]
[Category("Useful Features")] //<= "Useful feature" is a custom category
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true)]
public bool EnhancedBuffering
{
get { return enhancedBuffering; }
set { enhancedBuffering = value;
SetStyle(ControlStyles.OptimizedDoubleBuffer, value);
UpdateStyles();
}
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never), Browsable(false)]
public new Image ErrorImage
{
get { return null; }
set { base.ErrorImage = null; }
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never), Browsable(false)]
public new Image InitialImage
{
get { return null; }
set { base.InitialImage = null; }
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never), Browsable(false)]
public new Image BackgroundImage
{
get { return null; }
set { base.BackgroundImage = null; }
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never), BrowsableAttribute(false)]
public new Image Image {
get { return null; }
set { base.Image = null; }
}
}
Some System.ComponentModel Attributes that can help shaping the Control.
For example, Description and Category attributes:
(These have been inserted in the custom Property BorderColor of your control).
[Description("Gets or Sets the Color of the Border drawn around the Image.")
[Category("Appearance")]
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true)]
Description of course explains the user what the Property is for.
Category is used to give the Properties an organic disposition inside the PropertyGrid. You can use standard names (Appearance, Behavior etc.) or specify anything else.
Give the Category a custom name and it will be listed among the others, when the Categorized view is in use.
The Image property of the Custom Control has been hidden and substituted with a Bitmap Property:
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never), BrowsableAttribute(false)]
The EditorBrowsable Attribute is a hint to Intellisense that lets you determine whether to show a property or method in the Popup menu. It can be Never, Always or Advanced (for those who know how to reach VS Options). Properties and Methods will be Hidden when the Custom Control is deployed (as a dll), not while you are designing it.
The BrowsableAttribute Attribute (or just [Browsable]) allows to specify whether that Property should be shown in the PropertyGrid.
The DesignerSerializationVisibility
With the DesignerSerializationVisibility Attribute, you can indicate
whether the value for a property is Visible, and should be persisted
in initialization code, Hidden, and should not be persisted in
initialization code, or consists of Content, which should have
initialization code generated for each public, not hidden property of
the object assigned to the property.
Also interesting:
TypeConverter(typeof(System.ComponentModel.ExpandableObjectConverter))
With this Attribute, you can instruct to list the Public Properties of a Class Object in the PropertyGrid.
This Class Object can be an internal Class that serializes a complex Property of a Control.
The TypeConverter Class is very interesting itself.
I am relatively new to c# and WPF, maybe that’s why I cannot find the (probably very obvious) answer to my problem. I have been trying and googling but with no success.
I have a custom shape class that returns 3 RectangleGeometries in a GeometryGroup. The 3 corresponding rectangles can be displayed in a Canvas in MainWindow as expected. I would now like to animate each of the rectangles individually, say drop the first one to the bottom of the canvas, rotate the second one and animate the width of the third one.
My own research says the key are Dependency Properties. So I registered them but I couldn’t get them to do any changes on the rectangles.
Preferably, I would do all this in code behind. Only the Canvas
has been added in XAML. Can it be done? Here is some code to work with.
Thank you in advance
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;
namespace Test1
{
public partial class MainWindow : Window
{
CustomShape customShape = new CustomShape();
public MainWindow()
{
InitializeComponent();
customShape.Fill = Brushes.Blue;
cnvMain.Children.Add(customShape);
}
}
class CustomShape : Shape
{
private Rect rect1, rect2, rect3;
private RectangleGeometry rg1, rg2, rg3;
private GeometryGroup allRectangleGeometries = new GeometryGroup();
//Constructor
public CustomShape()
{
makeCustomShape();
}
private void makeCustomShape()
{
rect1 = new Rect(50, 20, 100, 50);
rg1 = new RectangleGeometry(rect1);
allRectangleGeometries.Children.Add(rg1);
rect2 = new Rect(200, 20, 60, 20);
rg2 = new RectangleGeometry(rect2);
allRectangleGeometries.Children.Add(rg2);
rect3 = new Rect(300, 20, 200, 80);
rg3 = new RectangleGeometry(rect3);
allRectangleGeometries.Children.Add(rg3);
}
protected override Geometry DefiningGeometry
{
get
{
return allRectangleGeometries;
}
}
}
}
Looks like I found an answer myself.
I implemented 3 Dependency Properties and a Callback method that is executed every time a property changes.
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace Test1
{
public partial class MainWindow : Window
{
CustomShape customShape = new CustomShape();
public MainWindow()
{
InitializeComponent();
customShape.Fill = Brushes.Blue;
cnvMain.Children.Add(customShape);
}
private void ButtonAnimate_Clicked(object sender, RoutedEventArgs e)
{
DoubleAnimation rec1Animation = new DoubleAnimation(500, TimeSpan.FromSeconds(1));
customShape.BeginAnimation(CustomShape.Rec1YProperty, rec1Animation);
DoubleAnimation rec2Animation = new DoubleAnimation(360, TimeSpan.FromSeconds(1));
customShape.BeginAnimation(CustomShape.Rec2RotateProperty, rec2Animation);
DoubleAnimation rec3Animation = new DoubleAnimation(400, TimeSpan.FromSeconds(1));
customShape.BeginAnimation(CustomShape.Rec3WidthProperty, rec3Animation);
}
}
class CustomShape : Shape
{
private Rect rect1, rect2, rect3;
private RectangleGeometry rg1, rg2, rg3;
private GeometryGroup allRectangleGeometries = new GeometryGroup();
public double Rec1Y
{
get { return (double)GetValue(Rec1YProperty); }
set { SetValue(Rec1YProperty, value); }
}
public static readonly DependencyProperty Rec1YProperty =
DependencyProperty.Register("Rec1Y", typeof(double), typeof(CustomShape), new PropertyMetadata(20d, new PropertyChangedCallback(OnAnyPropertyChanged)));
public double Rec2Rotate
{
get { return (double)GetValue(Rec2RotateProperty); }
set { SetValue(Rec2RotateProperty, value); }
}
public static readonly DependencyProperty Rec2RotateProperty =
DependencyProperty.Register("Rec2Rotate", typeof(double), typeof(CustomShape), new PropertyMetadata(0d, new PropertyChangedCallback(OnAnyPropertyChanged)));
public double Rec3Width
{
get { return (double)GetValue(Rec3WidthProperty); }
set { SetValue(Rec3WidthProperty, value); }
}
public static readonly DependencyProperty Rec3WidthProperty =
DependencyProperty.Register("Rec3Width", typeof(double), typeof(CustomShape), new PropertyMetadata(200d, new PropertyChangedCallback(OnAnyPropertyChanged)));
//Constructor
public CustomShape()
{
makeCustomShape();
}
private void makeCustomShape()
{
rect1 = new Rect(50, Rec1Y, 100, 50);
rg1 = new RectangleGeometry(rect1);
allRectangleGeometries.Children.Add(rg1);
rect2 = new Rect(200, 20, 60, 20);
rg2 = new RectangleGeometry(rect2);
rg2.Transform = new RotateTransform(Rec2Rotate, 230, 30);
allRectangleGeometries.Children.Add(rg2);
rect3 = new Rect(300, 20, Rec3Width, 80);
rg3 = new RectangleGeometry(rect3);
allRectangleGeometries.Children.Add(rg3);
}
private static void OnAnyPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
CustomShape customShape = source as CustomShape;
customShape.allRectangleGeometries.Children.Clear();
customShape.makeCustomShape();
}
protected override Geometry DefiningGeometry
{
get
{
return allRectangleGeometries;
}
}
}
}
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.
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.