I am writing an Xamarin.iOS app, and found some Swift code for a loading spinner that I would like to use. So I am trying to convert the Swift code to C#. I hope this question is ok because I've been trying to convert this code for hours. This is the Swift code:
public class CircularProgressView: UIView {
public dynamic var progress: CGFloat = 0 {
didSet {
progressLayer.progress = progress
}
}
fileprivate var progressLayer: CircularProgressLayer {
return layer as! CircularProgressLayer
}
override public class var layerClass: AnyClass {
return CircularProgressLayer.self
}
override public func action(for layer: CALayer, forKey event: String) -> CAAction? {
if event == #keyPath(CircularProgressLayer.progress),
let action = action(for: layer, forKey: #keyPath(backgroundColor)) as? CAAnimation {
let animation = CABasicAnimation()
animation.keyPath = #keyPath(CircularProgressLayer.progress)
animation.fromValue = progressLayer.progress
animation.toValue = progress
animation.beginTime = action.beginTime
animation.duration = action.duration
animation.speed = action.speed
animation.timeOffset = action.timeOffset
animation.repeatCount = action.repeatCount
animation.repeatDuration = action.repeatDuration
animation.autoreverses = action.autoreverses
animation.fillMode = action.fillMode
animation.timingFunction = action.timingFunction
animation.delegate = action.delegate
self.layer.add(animation, forKey: #keyPath(CircularProgressLayer.progress))
}
return super.action(for: layer, forKey: event)
}
}
fileprivate class CircularProgressLayer: CALayer {
#NSManaged var progress: CGFloat
let startAngle: CGFloat = 1.5 * .pi
let twoPi: CGFloat = 2 * .pi
let halfPi: CGFloat = .pi / 2
override class func needsDisplay(forKey key: String) -> Bool {
if key == #keyPath(progress) {
return true
}
return super.needsDisplay(forKey: key)
}
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
UIGraphicsPushContext(ctx)
//Light Grey
UIColor.lightGray.setStroke()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let strokeWidth: CGFloat = 4
let radius = (bounds.size.width / 2) - strokeWidth
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: twoPi, clockwise: true)
path.lineWidth = strokeWidth
path.stroke()
//Red
UIColor.red.setStroke()
let endAngle = (twoPi * progress) - halfPi
let pathProgress = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle , clockwise: true)
pathProgress.lineWidth = strokeWidth
pathProgress.lineCapStyle = .round
pathProgress.stroke()
UIGraphicsPopContext()
}
}
let circularProgress = CircularProgressView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut, animations: {
circularProgress.progress = 0.76
}, completion: nil)
This is what I have so far:
public class CircularProgressView : UIView
{
// I doubt I made these properties correctly?
public float Progress
{
get
{
return ProgressLayer.Progress;
}
set
{
ProgressLayer.Progress = value;
}
}
public CircularProgressLayer ProgressLayer { get; set; }
public override Foundation.NSObject ActionForLayer(CALayer layer, string eventKey)
{
// I don't understand this part
return base.ActionForLayer(layer, eventKey);
}
}
public class CircularProgressLayer : CALayer
{
public float Progress { get; set; }
private nfloat startAngle = (nfloat)(1.5f * Math.PI);
private nfloat twoPi = (nfloat)(2 * Math.PI);
private double halfPi = Math.PI / 2;
public override bool NeedsDisplay
{
get
{
return base.NeedsDisplay;
}
}
public override void DrawInContext(CoreGraphics.CGContext ctx)
{
base.DrawInContext(ctx);
UIGraphics.PushContext(ctx);
UIColor.LightGray.SetStroke();
var center = new CGPoint(x: Bounds.GetMidX(), y: Bounds.GetMidY());
float strokeWidth = 4;
nfloat radius = (Bounds.Size.Width / 2) - strokeWidth;
var path = UIBezierPath.FromArc(center, radius: radius, startAngle: 0, endAngle: twoPi, clockwise: true);
path.LineWidth = strokeWidth;
path.Stroke();
UIColor.Red.SetStroke();
nfloat endAngle = (nfloat)((twoPi * Progress) - halfPi);
var pathProgress = UIBezierPath.FromArc(center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true);
pathProgress.LineWidth = strokeWidth;
pathProgress.LineCapStyle = CGLineCap.Round;
pathProgress.Stroke();
UIGraphics.PopContext();
}
}
There's a few parts that I do not understand, like this one:
if event == #keyPath(CircularProgressLayer.progress)
and this one:
fileprivate var progressLayer: CircularProgressLayer {
return layer as! CircularProgressLayer
}
I'm very good at C# but am new to Swift. I hope this question is ok because I've been trying to convert this code for hours.
Update:
I needed to create #dynamic / NSManaged in a Xamarin.iOS prototype and ended up being able to do this by creating them at runtime, see my comment here:
https://bugzilla.xamarin.com/show_bug.cgi?id=38823#c5
And related gist:
https://gist.github.com/sushihangover/327ba6282b2d41d8e10001b7e461bb21
So while Xamarin.iOS does not support them out-of-the-box, you can create them at runtime if needed.
Original:
Currently you can not generate dynamic properties (#dynamic / NSManaged) using Xamarin.iOS so the code you posted will not work even if converted.
Issue: https://bugzilla.xamarin.com/show_bug.cgi?id=38823
If manually constructing CAAction/CAAnimation objects is a valid option for your needs, you can review the Xamarin.iOS CustomPropertyAnimation example app:
https://github.com/xamarin/ios-samples/tree/master/CustomPropertyAnimation
This is the compiler friendly way for expressing key paths in Swift. Keypath is a series of properties in the consecutive objects (eg. company.manager.fullName). If you want to learn more, read about the KVC. This expression produces a String, in your case, it will be "progress". I think you can replace it with a simple comparison.
#keyPath(CircularProgressLayer.progress)
This is the read-only property which exposes the layer property as an instance of the CircularProgressLayer. The x as! y means: cast x to y or crash if not possible.
fileprivate var progressLayer: CircularProgressLayer {
return layer as! CircularProgressLayer
}
Related
I have the following code:
public class Test : UnityEngine.MonoBehaviour
{
[Range(0.0f, 1.0f)] // draws a slider restricted to 0.0 <-> 1.0 range in UI
public float RangeMin = 0.0f;
[Range(0.0f, 1.0f)] // draws a slider restricted to 0.0 <-> 1.0 range in UI
public float RangeMax = 1.0f;
private void OnValidate() // called at every update in UI to validate/coerce
{
RangeMin = math.min(RangeMin, RangeMax);
RangeMax = math.max(RangeMin, RangeMax);
}
}
Currently, it does the following:
Changing minimum does never influence maximum: (desired behavior)
Changing maximum does influence minimum: (unwanted behavior)
That very simple piece of code works for minimum slider but not for maximum slider.
Note, please don't suggest using EditorGUI.MinMaxSlider as it doesn't show the values:
Your problem is the order:
private void OnValidate() // called at every update in UI to validate/coerce
{
RangeMin = math.min(RangeMin, RangeMax);
RangeMax = math.max(RangeMin, RangeMax);
}
it "works" for the RangeMin because you immediately check and limit it in the moment you changed it.
However, while changing RangeMax you already influence immediately the RangeMin, before it gets a chance to limit the RangeMax!
As suggested you should check which of the two values you are currently changing e.g. like
[HideInInspector] private float lastMin;
[HideInInspector] private float lastMax;
private void OnValidate()
{
if(!Mathf.Approximately(lastMin, RangeMin))
{
RangeMin = Mathf.Min(RangeMin, RangeMax);
lastMin = RangeMin;
}
if(!Mathf.Approximately(lastMax, RangeMax))
{
RangeMax = Mathf.Max(RangeMin, RangeMax);
lastMax = RangeMax;
}
}
Another alternative also already mentioned in the comments is using local variables to store the clamped values but wait with the assignment until all values are finished clamping like e.g.
private void OnValidate()
{
var newMin = Mathf.Min(RangeMin, RangeMax);
var newMax = Mathf.Max(RangeMin, RangeMax);
RangeMin = newMin;
RangeMax = newMax;
}
Alternatively back to the
Note, please don't suggest using EditorGUI.MinMaxSlider as it doesn't show the values.
I guess you could simply make it like it was already done by Naughty Attributes
namespace NaughtyAttributes
{
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class MinMaxSliderAttribute : DrawerAttribute
{
public float MinValue { get; private set; }
public float MaxValue { get; private set; }
public MinMaxSliderAttribute(float minValue, float maxValue)
{
MinValue = minValue;
MaxValue = maxValue;
}
}
}
And the drawer
namespace NaughtyAttributes.Editor
{
[CustomPropertyDrawer(typeof(MinMaxSliderAttribute))]
public class MinMaxSliderPropertyDrawer : PropertyDrawerBase
{
protected override float GetPropertyHeight_Internal(SerializedProperty property, GUIContent label)
{
return (property.propertyType == SerializedPropertyType.Vector2)
? GetPropertyHeight(property)
: GetPropertyHeight(property) + GetHelpBoxHeight();
}
protected override void OnGUI_Internal(Rect rect, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(rect, label, property);
MinMaxSliderAttribute minMaxSliderAttribute = (MinMaxSliderAttribute)attribute;
if (property.propertyType == SerializedPropertyType.Vector2)
{
EditorGUI.BeginProperty(rect, label, property);
float indentLength = NaughtyEditorGUI.GetIndentLength(rect);
float labelWidth = EditorGUIUtility.labelWidth + NaughtyEditorGUI.HorizontalSpacing;
float floatFieldWidth = EditorGUIUtility.fieldWidth;
float sliderWidth = rect.width - labelWidth - 2.0f * floatFieldWidth;
float sliderPadding = 5.0f;
Rect labelRect = new Rect(
rect.x,
rect.y,
labelWidth,
rect.height);
Rect sliderRect = new Rect(
rect.x + labelWidth + floatFieldWidth + sliderPadding - indentLength,
rect.y,
sliderWidth - 2.0f * sliderPadding + indentLength,
rect.height);
Rect minFloatFieldRect = new Rect(
rect.x + labelWidth - indentLength,
rect.y,
floatFieldWidth + indentLength,
rect.height);
Rect maxFloatFieldRect = new Rect(
rect.x + labelWidth + floatFieldWidth + sliderWidth - indentLength,
rect.y,
floatFieldWidth + indentLength,
rect.height);
// Draw the label
EditorGUI.LabelField(labelRect, label.text);
// Draw the slider
EditorGUI.BeginChangeCheck();
Vector2 sliderValue = property.vector2Value;
EditorGUI.MinMaxSlider(sliderRect, ref sliderValue.x, ref sliderValue.y, minMaxSliderAttribute.MinValue, minMaxSliderAttribute.MaxValue);
sliderValue.x = EditorGUI.FloatField(minFloatFieldRect, sliderValue.x);
sliderValue.x = Mathf.Clamp(sliderValue.x, minMaxSliderAttribute.MinValue, Mathf.Min(minMaxSliderAttribute.MaxValue, sliderValue.y));
sliderValue.y = EditorGUI.FloatField(maxFloatFieldRect, sliderValue.y);
sliderValue.y = Mathf.Clamp(sliderValue.y, Mathf.Max(minMaxSliderAttribute.MinValue, sliderValue.x), minMaxSliderAttribute.MaxValue);
if (EditorGUI.EndChangeCheck())
{
property.vector2Value = sliderValue;
}
EditorGUI.EndProperty();
}
else
{
string message = minMaxSliderAttribute.GetType().Name + " can be used only on Vector2 fields";
DrawDefaultPropertyAndHelpBox(rect, property, message, MessageType.Warning);
}
EditorGUI.EndProperty();
}
}
}
which in the end looks like
[SerializeField] [MinMaxSlider(0f; 100f)] private float _minMaxSlider;
Now before copying that code, note that Naughty Attributes Package is available in the Unity Asset Store for free and has a lot more nice enhancements for the editor (ReorderableList, Button, ShowIf, etc) ;)
I'm creating GMap.NET app and added some custom markers on it. Is it possible to make them scale depending on the zoom level of the map?
You will need to create a custom GMapMarker and use the zoom level in the OnRender() to scale the image. Each zoom level has a different ratio but luckily for you, I have mesured them all. Not sure if the ratio changes with different map providers?
You can set the x/y scale and heading using a seperate thread and it will update in real time.
The "defaultZoomLevel" will set the zoom level at which the image has a 1:1 ratio. I have only tried the value 16, not sure it will work for other values you may need to calculate all the ratios for each value or extract the curve from the data and use that to scale the ratio.
Use this to set the zoom level when it changes:
private void Gmap_OnMapZoomChanged()
{
//trackBar_Zoom.Value = (int)Gmap.Zoom;
//label_Zoom.Text = "Zoom: " + trackBar_Zoom.Value.ToString();
SelectedOverlay?.SetZoomLevel(Gmap.Zoom);
}
Use it like:
SelectedOverlay = new GMapMarkerImageOverlay(Gmap.Position, 0, 1, 1, Gmap.Zoom, Resources.GreySquare);
Overlay_ImageOverlays.Markers.Add(SelectedOverlay);
And this is the MapMarker:
public class GMapMarkerImageOverlay : GMapMarker
{
private readonly double deg2rad = 3.14159265 / 180.0;
private readonly double rad2deg = 180 / 3.14159265;
private readonly float defaultZoomLevel = 16;
private float zoomRatio = 1f;
private float heading = 0;
private double zoom = 0;
private float scaleX = 0;
private float scaleY = 0;
private Bitmap overlayImage;
private void SetZoomRatio()
{
if (zoom < 12)
{
zoomRatio = 0.045f;
}
else if (zoom == 12)
{
zoomRatio = 0.08f;
}
else if (zoom == 13)
{
zoomRatio = 0.155f;
}
else if (zoom == 14)
{
zoomRatio = 0.285f;
}
else if (zoom == 15)
{
zoomRatio = 0.53f;
}
else if (zoom == 16)
{
zoomRatio = 1f;
}
else if (zoom == 17)
{
zoomRatio = 1.88f;
}
else if (zoom == 18)
{
zoomRatio = 3.55f;
}
else if (zoom == 19)
{
zoomRatio = 6.75f;
}
else if (zoom == 20)
{
zoomRatio = 11.5f;
}
else
{
zoomRatio = 11.5f;
}
}
public GMapMarkerImageOverlay(PointLatLng p, float heading, float scaleX, float scaleY, double zoom, Bitmap image)
: base(p)
{
overlayImage = image;
this.heading = heading;
this.scaleX = scaleX;
this.scaleY = scaleY;
this.zoom = zoom;
SetZoomRatio();
}
internal void SetPosition(PointLatLng position)
{
//Position = position;
//LocalPosition = position;
}
public void SetHeading(float h)
{
heading = h;
}
public void SetZoomLevel(double z)
{
zoom = z;
SetZoomRatio();
//Util.Log($"Zoom level: {z}");
}
public void SetScaleX(float x)
{
scaleX = x;
}
public void SetScaleY(float y)
{
scaleY = y;
}
public void SetRatio(float r)
{
zoomRatio = r;
}
public override void OnRender(Graphics g)
{
try
{
var temp = g.Transform;
g.TranslateTransform(LocalPosition.X, LocalPosition.Y);
float ratio = (float)zoom / defaultZoomLevel;
ratio *= zoomRatio;
g.ScaleTransform(scaleX*ratio, scaleY*ratio);
base.ToolTipMode = MarkerTooltipMode.OnMouseOver;
base.ToolTipText = $"Ratio:{ratio}";
// anti NaN
try
{
g.RotateTransform(heading);
}
catch
{
}
var sIcon = overlayImage;
sIcon = new Bitmap(sIcon, sIcon.Width / 1, sIcon.Height / 1);
g.DrawImageUnscaled(sIcon, sIcon.Width / -2, sIcon.Height / -2);
g.Transform = temp;
}
catch (Exception ex)
{
//Util.Log(ex);
}
}
}
I'm writing a small graph writing program as a personal project.
Each point is supposed to get drawn on a wpf canvas but I'm having trouble translating the points from the graph's coordinate system, example: x = -8 to 4 y=-4 to 4, to the canvas' coordinate system example: x = 600 to 0 y = 400 to 0.
I'm using the method outlined here to precompute the transformation equations.
However I'm having trouble with the result of the multiplication v=M^-1*u.
My expected result is:
[0.2 ]
[0.0 ]
[-8.0]
[4.0 ]
But the result I'm getting is:
[4.0 ]
[-4.0 ]
[0.01 ]
[-0.006]
I've verified that my transformation matrix is correct and when I do the calculation by hand I'm getting the expected result.
The method to calculate the transformation equations:
private void CalculateTransformationFunctions()
{
// Define the transformation matrix
var transformationMatrix = new Matrix4x4
{
M11 = _destArea.XMin,
M12 = _destArea.YMin,
M13 = 1,
M14 = 0,
M21 = -_destArea.YMin,
M22 = _destArea.XMin,
M23 = 0,
M24 = 1,
M31 = _destArea.XMax,
M32 = _destArea.YMax,
M33 = 1,
M34 = 0,
M41 = -_destArea.YMax,
M42 = _destArea.XMax,
M43 = 0,
M44 = 1
};
// Define the source vector
var srcVector = new Vector4
{
X = _srcArea.XMin,
Y = _srcArea.YMax,
Z = _srcArea.XMax,
W = _srcArea.YMin
};
// Invert the transformationmatrix before the multiplication
Matrix4x4 invertedTransformationMatrix;
if(!Matrix4x4.Invert(transformationMatrix,out invertedTransformationMatrix))
throw new Exception();
// Returns the wrong value
var transformResult = Vector4.Transform(srcVector, invertedTransformationMatrix);
float a = transformResult.X,
b = transformResult.Y,
c = transformResult.Z,
d = transformResult.W;
_xTransformationFunction = (x, y) => (a*x + b*y - b*d - a*c)/(a*a + b*b);
_yTransformationFunction = (x, y) => (b*x - a*y - b*c + a*d)/(a*a + b*b);
}
Which is called in the constructor of its parent class.
My question:
Am I misunderstanding what Vector4.Transform() does here? Or am I completely blind and missing something very obvious?
Full source of the class:
using System;
using System.Numerics;
using System.Windows;
using System.Windows.Media;
using Grapher.Control.Grapher;
namespace Grapher.GraphingMath
{
public class Translator
{
private GraphingArea _srcArea;
private GraphingArea _destArea;
public GraphingArea SourceArea
{
get
{
return _srcArea;
}
set
{
_srcArea = value;
CalculateTransformationFunctions();
}
}
public GraphingArea DestinationArea
{
get { return _destArea; }
set
{
_destArea = value;
CalculateTransformationFunctions();
}
}
private Func<double, double, double> _xTransformationFunction;
private Func<double, double, double> _yTransformationFunction;
public Translator(GraphingArea sourceArea, GraphingArea destArea)
{
_destArea = destArea;
_srcArea = sourceArea;
CalculateTransformationFunctions();
}
public Point TranslatePoint(Point point)
{
var x = point.X;
var y = point.Y;
return new Point
{
X = _xTransformationFunction(x, y),
Y = _yTransformationFunction(x, y)
};
}
/*
x1 y1 1 0
-y1 x1 0 1
M= x2 y2 1 0
-y2 x2 0 1
x1,y1 = dest_min
x2,y2 = dest_max
*/
private void CalculateTransformationFunctions()
{
// Define the transformation matrix
var transformationMatrix = new Matrix4x4
{
M11 = _destArea.XMin,
M12 = _destArea.YMin,
M13 = 1,
M14 = 0,
M21 = -_destArea.YMin,
M22 = _destArea.XMin,
M23 = 0,
M24 = 1,
M31 = _destArea.XMax,
M32 = _destArea.YMax,
M33 = 1,
M34 = 0,
M41 = -_destArea.YMax,
M42 = _destArea.XMax,
M43 = 0,
M44 = 1
};
// Define the source vector
var srcVector = new Vector4
{
X = _srcArea.XMin,
Y = _srcArea.YMax,
Z = _srcArea.XMax,
W = _srcArea.YMin
};
// Invert the transformationmatrix before the multiplication
Matrix4x4 invertedTransformationMatrix;
if(!Matrix4x4.Invert(transformationMatrix,out invertedTransformationMatrix))
throw new Exception();
// Returns the wrong value
var transformResult = Vector4.Transform(srcVector, invertedTransformationMatrix);
float a = transformResult.X,
b = transformResult.Y,
c = transformResult.Z,
d = transformResult.W;
_xTransformationFunction = (x, y) => (a*x + b*y - b*d - a*c)/(a*a + b*b);
_yTransformationFunction = (x, y) => (b*x - a*y - b*c + a*d)/(a*a + b*b);
}
}
}
And for the graphing area struct:
using System;
namespace Grapher.Control.Grapher
{
public struct GraphingArea
{
public float XMin { get; set; }
public float YMin { get; set; }
public float XMax { get; set; }
public float YMax { get; set; }
public float Width => Math.Abs(XMax - XMin);
public float Height => Math.Abs(YMax - YMin);
}
}
In my main method I call the Translator class like this:
Point testPoint = new Point {X = 0, Y = 0};
var srcArea = new GraphingArea
{
XMax = 4,
XMin = -8,
YMax = 4,
YMin = -4
};
var destArea = new GraphingArea
{
XMax = 600,
XMin = 0,
YMax = 400,
YMin = 0
};
var translator = new Translator(srcArea, destArea);
var translatedPoint = translator.TranslatePoint(testPoint);
Edit
Ended up just writing my own matrix multiplication method. I must be misunderstanding what Vector4.Transform() does...
Code here, for anyone interested:
using System.Numerics;
namespace Grapher.GraphingMath.MatrixAndVectorMath
{
public static class Matrix4x4Multiply
{
public static Vector4 Vector4Multiply(Matrix4x4 matrix, Vector4 vector)
{
var mat = new float[4, 4]
{
{matrix.M11, matrix.M12, matrix.M13, matrix.M14},
{matrix.M21, matrix.M22, matrix.M23, matrix.M24},
{matrix.M31, matrix.M32, matrix.M33, matrix.M34},
{matrix.M41, matrix.M42, matrix.M43, matrix.M44}
}; // We'll just wrap the matrix in a float so we can index it.
var vec = new float[4] {vector.X, vector.Y, vector.Z, vector.W}; // And the same with the vector
var result = new float[4] {0, 0, 0, 0};
for (var row = 0; row < mat.GetLength(0); row++)
{
for (var col = 0; col < mat.GetLength(1); col++)
{
result[row] += mat[row, col]*vec[col];
}
}
return new Vector4
{
X = result[0],
Y = result[1],
Z = result[2],
W = result[3]
};
}
}
}
I don't know if this will help but I do something along those lines in one of my projects. I don't use matrices though so it may not be what you are looking for. I simply store the extents of coordinates for the graph and the container (canvas) width and height. I then provide two extensions functions:
public static System.Windows.Point ConvertToScreen(this System.Windows.Point point, CartesianExtents2D extents, double containerWidth, double containerHeight)
{
var x = (point.X - extents.XMinimum) * containerWidth / (extents.XMaximum - extents.XMinimum);
var y = (extents.YMaximum - point.Y) * containerHeight / (extents.YMaximum - extents.YMinimum);
return new System.Windows.Point(x, y);
}
public static System.Windows.Point ConvertToReal(this System.Windows.Point point, CartesianExtents2D extents, double containerWidth, double containerHeight, )
{
var x = extents.XMinimum + (point.X * (extents.XMaximum - extents.XMinimum)) / containerWidth;
var y = extents.YMaximum - (point.Y * (extents.YMaximum - extents.YMinimum)) / containerHeight;
return new System.Windows.Point(x, y);
}
Call thus:
Point p = new Point();
p.ConvertToReal(...);
I'm hoping the contents of CartesianExtents2D is obvious - just min and max for x and y
I want to create a own Control with the form of a pie without the tip of it like in the picture afterwards. I just dont get how to get this working.
http://www.directupload.net/file/d/3563/a3hvpodw_png.htm
//EDIT:
Ok I forgot to mention that i want to fill it afterwards. So if I'm right I need a region for that but i don't know how to do this. And to be honest i didn't think about your idea so far. I just used a Pie so far, like this:
Graphics gfx = pe.Graphics;
Pen p = new Pen(Color.Red);
gfx.DrawPie(p, 0, 0, 200, 200, 0, 45);
base.OnPaint(pe);
It's my first time with custom controls, so sorry if it is a little bit goofy what I'm asking.
Try this:
class ShapedControl : Control
{
private float startAngle;
private float sweepAngle;
private float innerRadius;
private float outerRadius;
public ShapedControl()
{
InnerRadius = 30;
OuterRadius = 60;
StartAngle = 0;
SweepAngle = 360;
}
[DefaultValue(0)]
[Description("The starting angle for the pie section, measured in degrees clockwise from the X-axis.")]
public float StartAngle
{
get { return startAngle; }
set
{
startAngle = value;
Invalidate();
}
}
[DefaultValue(360)]
[Description("The angle between StartAngle and the end of the pie section, measured in degrees clockwise from the X-axis.")]
public float SweepAngle
{
get { return sweepAngle; }
set
{
sweepAngle = value;
Invalidate();
}
}
[DefaultValue(20)]
[Description("Inner radius of the excluded inner area of the pie")]
public float InnerRadius
{
get { return innerRadius; }
set
{
innerRadius = value;
Invalidate();
}
}
[DefaultValue(30)]
[Description("Outer radius of the pie")]
public float OuterRadius
{
get { return outerRadius; }
set
{
outerRadius = value;
Invalidate();
}
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics g = e.Graphics;
g.Clear(this.BackColor);
GraphicsPath gp1 = new GraphicsPath();
GraphicsPath gp2 = new GraphicsPath();
float xInnerPos = -innerRadius / 2f + this.Width / 2f;
float yInnerPos = -innerRadius / 2f + this.Height / 2f;
float xOuterPos = -outerRadius / 2f + this.Width / 2f;
float yOuterPos = -outerRadius / 2f + this.Height / 2f;
if (innerRadius != 0.0)
gp1.AddPie(xInnerPos, yInnerPos, innerRadius, innerRadius, startAngle, sweepAngle);
gp2.AddPie(xOuterPos, yOuterPos, outerRadius, outerRadius, startAngle, sweepAngle);
Region rg1 = new System.Drawing.Region(gp1);
Region rg2 = new System.Drawing.Region(gp2);
g.DrawPath(Pens.Transparent, gp1);
g.DrawPath(Pens.Transparent, gp2);
rg1.Xor(rg2);
g.FillRegion(Brushes.Black, rg1);
this.Region = rg1;
}
//Just for testing purpose. Place a breakpoint
//in here and you'll see it will only get called when
//you click inside the "pie" shape
protected override void OnClick(EventArgs e)
{
base.OnClick(e);
}
}
EDIT: made the code better by centering the shape and adding properties for the VS Designer , stolen from another answer ;-)
MORE EDITS: taking care of the case where Inner Radius == 0
Try this code sample of a complex shaped control.
You can control its shape using StartAngle, SweepAngle and InnerPercent properties.
public partial class PathUserControl : UserControl
{
private readonly GraphicsPath outerPath = new GraphicsPath();
private readonly GraphicsPath innerPath = new GraphicsPath();
private float startAngle;
private float sweepAngle = 60;
private float innerPercent = 30;
public PathUserControl()
{
base.BackColor = SystemColors.ControlDark;
}
[DefaultValue(0)]
[Description("The starting angle for the pie section, measured in degrees clockwise from the X-axis.")]
public float StartAngle
{
get { return startAngle; }
set
{
startAngle = value;
SetRegion();
}
}
[DefaultValue(60)]
[Description("The angle between StartAngle and the end of the pie section, measured in degrees clockwise from the X-axis.")]
public float SweepAngle
{
get { return sweepAngle; }
set
{
sweepAngle = value;
SetRegion();
}
}
[DefaultValue(30)]
[Description("Percent of the radius of the excluded inner area of the pie, measured from 0 to 100.")]
public float InnerPercent
{
get { return innerPercent; }
set
{
if (value < 0 || value > 100f)
throw new ArgumentOutOfRangeException("value", "Percent must be in the range 0 .. 100");
innerPercent = value;
SetRegion();
}
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
SetRegion();
}
private void SetRegion()
{
if (Region != null)
{
Region.Dispose();
Region = null;
}
if (ClientSize.IsEmpty)
return;
float innerCoef = 0.01f * InnerPercent;
outerPath.Reset();
innerPath.Reset();
outerPath.AddPie(0, 0, ClientSize.Width, ClientSize.Height, StartAngle, SweepAngle);
innerPath.AddPie(ClientSize.Width * (1 - innerCoef) / 2, ClientSize.Height * (1 - innerCoef) / 2, ClientSize.Width * innerCoef, ClientSize.Height * innerCoef, StartAngle, SweepAngle);
Region region = new Region(outerPath);
region.Xor(innerPath);
Region = region;
}
}
EDIT The #LucMorin idea with XOR is great, I've stolen it.
so i have drawn a few objects , circles squares or even lines. This is the code i use to draw the images:
Graphics surface = this.splitContainer1.Panel2.CreateGraphics();
Pen pen1 = new Pen(ColorR.BackColor, float.Parse(boxWidth.Text));
switch (currentObject)
{
case "line":
if (step == 1)
{
splitContainer1.Panel2.Focus();
one.X = e.X;
one.Y = e.Y;
boxX.Text = one.X.ToString();
boxY.Text = one.Y.ToString();
step = 2;
}
else
{
two.X = e.X;
two.Y = e.Y;
boxX2.Text = two.X.ToString();
boxY2.Text = two.Y.ToString();
surface.DrawLine(pen1, one, two);
step = 1;
}
break;
case "circle":
if (step == 1)
{
boxX.Text = e.X.ToString();
boxY.Text = e.Y.ToString();
step = 2;
}
else
{
int tempX = int.Parse(boxX.Text);
int tempY = int.Parse(boxY.Text);
int tempX2 = e.X;
int tempY2 = e.Y;
int sideX, sideY;
if (tempX > tempX2)
{
sideX = tempX - tempX2;
}
else
{
sideX = tempX2 - tempX;
}
if (tempY > tempY2)
{
sideY = tempY - tempY2;
}
else
{
sideY = tempY2 - tempY;
}
double tempRadius;
tempRadius = Math.Sqrt(sideX * sideX + sideY * sideY);
tempRadius *= 2;
bWidth.Text = bHeight.Text = Convert.ToInt32(tempRadius).ToString();
surface.DrawEllipse(
pen1,
int.Parse(boxX.Text) - int.Parse(bWidth.Text) / 2,
int.Parse(boxY.Text) - int.Parse(bHeight.Text) / 2,
float.Parse(bWidth.Text), float.Parse(bHeight.Text));
step = 1;
}
break;
case "square":
if (step == 1)
{
boxX.Text = e.X.ToString();
boxY.Text = e.Y.ToString();
step = 2;
}
else if (step == 2)
{
int tempX = e.X;
if (tempX > int.Parse(boxX.Text))
{
bWidth.Text = (tempX - int.Parse(boxX.Text)).ToString();
}
else
{
bWidth.Text = (int.Parse(boxX.Text) - tempX).ToString();
}
step = 3;
}
else
{
int tempY = e.Y;
if (tempY > int.Parse(boxY.Text))
{
bHeight.Text = (tempY - int.Parse(boxY.Text)).ToString();
}
else
{
bHeight.Text = (int.Parse(boxY.Text) - tempY).ToString();
}
surface.DrawRectangle(
pen1,
int.Parse(boxX.Text),
int.Parse(boxY.Text),
int.Parse(bWidth.Text),
int.Parse(bHeight.Text));
step = 1;
}
break;
}
So after I draw the images, I want to be able to select a figure and--for example--change the color or rotate it. But I cant seem to figure it out how to do it.
I suggest defining a base abstract shape class that has methods all shapes should provide, such as a method to draw itself on a graphics object, a method that says whether a point is within it / should could as selecting it, a method to rotate it by a given amount and a method to change the color.
Once you've got your shape class then you've got to work out how to fill in the methods for each derived shape. For drawing you've already got the code. For selecting it, that will be dependent on the shape. For something like a circle it's fairly easy, just calculate the distance between the center of the circle, and the point clicked, for something like a line it's harder as you don't want the user to have to click it exactly.
That leaves rotating and changing the colour. Changing the colour is easy, just have a Color property on the Shape class, then when you draw your shapes, use that colour to create a brush or pen.
As for rotation, take a look at Graphics.RotateTransform.
public abstract class Shape
{
public Color Color { get; set; }
public float Rotation { get; set; }
public Point Position { get; set; }
public Shape(Color color, float rotation, Point position)
{
Color = color;
Rotation = rotation;
Position = position;
}
public void ChangeRotation(float amount)
{
Rotation += amount;
}
public abstract void Draw(Graphics graphics);
public abstract bool WithinBounds(Point point);
}
public class Circle : Shape
{
public float Radius { get; set; }
public Circle(Color color, float rotation, Point position)
:base(color, rotation, position)
{
}
public override void Draw(Graphics graphics)
{
}
public override bool WithinBounds(Point point)
{
if (Math.Sqrt(Math.Pow(point.X - Position.X, 2) + Math.Pow(point.Y - Position.Y, 2)) <= Radius)
return true;
else
return false;
// Note, if statement could be removed to become the below:
//return Math.Sqrt(Math.Pow(point.X - Position.X, 2) + Math.Pow(point.Y - Position.Y, 2)) <= Radius;
}
}
Have a look at the RotateTransform method of the Graphics object. There is a TranslateTransform method too.