Drawing smooth lines [duplicate] - c#

I have a canvas and draw curve with this code:
using (Graphics g = Graphics.FromImage(canvas.BackgroundImage))
{
g.DrawCurve(pen, points);
points is array that I fill that by mouse location points.
In the result I see some jagged lines that I didn't draw.
You can see them here(in red rectangles):
What can i do about this?

What you is see is the somewhat unlucky combination of the default for Linejoin, which is Miter and the default for MiterLimit, which is 10.
Instead you have a choice of either picking one of the other LineJoin options or reducing the MiterLimit to say less than half the Pen.Width..
using (Pen myPen = new Pen(Color.Blue, 24f))
{
// either another LineJoine;
myPen.LineJoin = System.Drawing.Drawing2D.LineJoin.Round;
// or a reduced MiterLimit:
myPen.MiterLimit = 1+ myPen.Width / 5f;
}

Related

Draw rotated text in c#

I have a radar chart in chart.js in which I want to draw the labels as rotated. As I could see inthe doc, it is not possible, so I am trying to do it myself using basic drawing in c#. Here is the code I use :
Bitmap objBmpImage = new Bitmap(1000, 1000);
System.Drawing.Font objFont = new System.Drawing.Font("Arial", 12,
System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Pixel);
Graphics objGraphics = Graphics.FromImage(objBmpImage);
objGraphics.Clear(Color.Transparent);
float angle = (float)360.0 / (float)competences.Count();
objGraphics.TranslateTransform(500, 450);
objGraphics.RotateTransform(-90 - (angle / 3));
foreach (T_Ref_Competence competence in competences)
{
byte r, g, b;
HexToInt(competence.T_Ref_CompetenceNiveau2.T_Ref_CompetenceNiveau1.Couleur,
out r, out g, out b);
Brush brush = new System.Drawing.SolidBrush(Color.FromArgb(255,r,g,b));
objGraphics.DrawString(competence.Nom,objFont, brush, 255,0);
objGraphics.RotateTransform(angle);
}
string filename = Server.MapPath("..\\TempImage\\") + NomUtilisateur + ".png";
objBmpImage.Save(filename,System.Drawing.Imaging.ImageFormat.Png);
HexToInt is just a simple function that takes a #FF0000 and convert it to 3 bytes variables (255,0 and 0 for this exemple).
Here is the result so far :
As you can see, I can get something quite nice. However, I have one last request, which I am pretty sure is possible, but I'm not certain how.
How can I rotate the text 180 degrees for label on the left section ? In this exemple, from "Organisation" to "Vérification qualité et inspection". Mind you, I know how to decide which one I have to turn around, I don't know how to do it.
Thanks.
Idea:
Add 180 degrees to the angle.
Now the labels appear on the wrong side but have the right orientation. Move them by the diameter + textLength to the left. You can do so by providing a negative X to the DrawString call. No need for another transformation.
You can calculate the text length with the Graphics.MeasureString Method.

How does one set an image as or along a chart axis?

I am trying to use a colored spectrum strip as an axis for a chart. The idea is to match the color on the image with its associated wavelength along the x-axis at the bottom. The strip needs to change in size to match changes of the chart area and expand and contract sections to match scroll-zooming in the chart area.
I have tried using image annotations but as the chart area changes, the annotation dimensions remain fixed. Also, the scroll zooming that focuses in on mouse position obviously has no effect on the annotation.
The approach that came closest was using the image as a background for the chart area. This automatically scaled the image as the chart area changed but scroll-zooming has no effect on the background image. Also, it would be ideal to have the background clear so as to avoid obscuring data plot points. I can edit the image to have a large transparent section and only a colored strip at the bottom but even then, that strip could obscure lower intensity data points.
Spectrum as annotation and background:
Annotation not scaling, background scales well:
Both annotation and background not scaling with zooming:
This is a nice idea.
The simplest way is to draw the image in a Paint event of the Chart, maybe PrePaint.
Let's go to work.. We will use the DrawImage overload that allows us zooming as well as cropping. For this we need two rectangles.
The first challenge is to always get the correct target rectangle.
For this we need to convert the InnerPlotPosition from relative positions to absolute pixels.
These two functions will help:
RectangleF ChartAreaClientRectangle(Chart chart, ChartArea CA)
{
RectangleF CAR = CA.Position.ToRectangleF();
float pw = chart.ClientSize.Width / 100f;
float ph = chart.ClientSize.Height / 100f;
return new RectangleF(pw * CAR.X, ph * CAR.Y, pw * CAR.Width, ph * CAR.Height);
}
RectangleF InnerPlotPositionClientRectangle(Chart chart, ChartArea CA)
{
RectangleF IPP = CA.InnerPlotPosition.ToRectangleF();
RectangleF CArp = ChartAreaClientRectangle(chart, CA);
float pw = CArp.Width / 100f;
float ph = CArp.Height / 100f;
return new RectangleF(CArp.X + pw * IPP.X, CArp.Y + ph * IPP.Y,
pw * IPP.Width, ph * IPP.Height);
}
With these numbers setting the destination rectangle is as simple as:
Rectangle tgtR = Rectangle.Round(new RectangleF(ipr.Left, ipr.Bottom - 15, ipr.Width, 15));
You can chose a height as you like..
The next challenge is the source rectangle.
Without zooming it would simply be:
Rectangle srcR = new Rectangle( 0, 0, bmp.Width, bmp.Height);
But for zooming and panning we need to scale it; for this we can use the x-axis and the ScaleView's Minimum and Maximum values.
We calculate factors for the first and last spot on the axis:
double f1 = ax.ScaleView.ViewMinimum / (ax.Maximum - ax.Minimum);
double f2 = ax.ScaleView.ViewMaximum / (ax.Maximum - ax.Minimum);
now we get the source rectangle maybe like this:
int x = (int)(bmp.Width * f1);
int xx = (int)(bmp.Width * f2);
Rectangle srcR = new Rectangle( x, 0, xx - x, bmp.Height);
Let's put it together:
private void chart_PrePaint(object sender, ChartPaintEventArgs e)
{
// a few short names
Graphics g = e.ChartGraphics.Graphics;
ChartArea ca = chart.ChartAreas[0];
Axis ax = ca.AxisX;
// pixels of plot area
RectangleF ipr = InnerPlotPositionClientRectangle(chart, ca);
// scaled first and last position
double f1 = ax.ScaleView.ViewMinimum / (ax.Maximum - ax.Minimum);
double f2 = ax.ScaleView.ViewMaximum / (ax.Maximum - ax.Minimum);
// actual drawing with the zooming overload
using (Bitmap bmp = (Bitmap)Bitmap.FromFile(imagePath))
{
int x = (int)(bmp.Width * f1);
int xx = (int)(bmp.Width * f2);
Rectangle srcR = new Rectangle( x, 0, xx - x, bmp.Height);
Rectangle tgtR = Rectangle.Round(
new RectangleF(ipr.Left , ipr.Bottom - 15, ipr.Width, 15));
g.DrawImage(bmp, tgtR, srcR, GraphicsUnit.Pixel);
}
}
A few notes:
Of course I would recomend to use an Image resource instead of always loading from disk!
The Drawing will always overlay the data points and also the grids. You can either..
choose a different minimum to make room
make the image smaller
move it below the x-axis labels
make the image semi-transparent
make the x-axis so fat that it can hold the image strip : ax.LineWidth = 10
For the latter solution you would want to offset the y-position depending on the zoom state. Quick and dirty: int yoff = (ax.ScaleView.IsZoomed ? 12 : 5);. To avoid black stripes also make the axis Transparent or chart.BackColor..
Update:
You can also revert to using a StripLine. It can scale its BackgroundImage and you would have to create a suitable image whenever changing the scaleview, i.e. when zooming or panning. For this much of the above code would be used to create the new images. See this post for examples of adding and replacing varying NamedImage to a Chart! (The relevant portion is close to the end about the marker images!)
In fact I found that way to be the best solution and have added a second answer.
Alternative and recommended solution:
I dabbled with the last option I mentioned in my other answer and found it to be rather nice; it is similarily extensive, so I decided to post a second answer.
The idea is to use a StripLine with just the right BackgroundImage.
The advantage is that is will display nicely under all chart elements and never draw over the axis, grid, datapoints or conflict with the zoom tools.
Since the StripLine must be updated repeatedly I put it in a function:
Here is the function; it makes use of the same two helper functions to calculate pixel positions as the other answer does..:
void updateStripLine(Chart chart, ChartArea ca, string name)
{
// find our stripline; one could pass in a class level variable as well
StripLine sl = ca.AxisY.StripLines.Cast<StripLine>()
.Where(s => s.Tag.ToString() == name).FirstOrDefault();
if (sl != null) // either clean-up the resources..
{
var oldni = chart.Images.FindByName(name);
if (oldni != null)
{
oldni.Image.Dispose();
chart.Images.Remove(oldni);
oldni.Dispose();
}
}
else // or, create the line
{
sl = new StripLine();
sl.Tag = name;
ca.AxisY.StripLines.Add(sl);
}
ca.RecalculateAxesScale();
RectangleF ipr = InnerPlotPositionClientRectangle(chart, ca);
Axis ax = ca.AxisX;
Axis ay = ca.AxisY;
double f1 = ax.ScaleView.ViewMinimum / (ax.Maximum - ax.Minimum);
double f2 = ax.ScaleView.ViewMaximum / (ax.Maximum - ax.Minimum);
Bitmap b0 = (Bitmap)chart.Images["spectrum"].Image;
int x = (int)(b0.Width * f1);
int xx = (int)(b0.Width * f2);
Rectangle srcR = new Rectangle( x, 0, xx - x, b0.Height);
Rectangle tgtR = Rectangle.Round(new RectangleF(0,0, ipr.Width , 10));
// create bitmap and namedImage:
Bitmap bmp = new Bitmap( tgtR.Width, tgtR.Height);
using (Graphics g = Graphics.FromImage(bmp))
{ g.DrawImage(b0, tgtR, srcR, GraphicsUnit.Pixel); }
NamedImage ni = new NamedImage(name, bmp);
chart.Images.Add(ni);
sl.BackImageWrapMode = ChartImageWrapMode.Scaled;
sl.StripWidth = ay.PixelPositionToValue(0) - ay.PixelPositionToValue(12);
sl.Interval = 100; // make large enough to avoid another sLine showing up
sl.IntervalOffset = 0;
sl.BackImage = name;
}
Much of the comments and links apply, especially wrt to the NamedImage we use for the StripLine.
A few more notes:
I use one of the (four) axis conversion functions, PixelPositionToValue to calculate a pixel height of 12px; the StripLine takes values, so I use two pixel values to get the right difference value.
To identify the StripLine I use the Tag property. Of course the Name property would be much more natural, but it is read-only. No idea why?!
The function is called from the AxisViewChanged, the Resize event and also the the PrePaint event; this makes sure it will always be called when needed. To avoid invalid calls from the PrePaint there I do it like this: if (ay.StripLines.Count == 0) updateStripLine(chart, ca, "sl"); Of course you should adapt if you use other StripLines on this axis..
The code makes use of the same image as before; but I have put it into a first NamedImage called spectrum. This would be an option in the 1st answer as well.
NamedImage spectrum = new NamedImage("spectrum", Bitmap.FromFile(imagePath);
chart.Images.Add(spectrum);
It also makes sure to dispose of the old images properly, I hope..

Halftone effect in with gdi+

How would I go about mimicking this halftone effect in GDI+?
It almost looks like Floyd–Steinberg dithered version of the image overlaying a full one but I'm not convinced.
I gave this a try and got this result:
It may be a place to start. I did it like this:
Draw the original picture with low saturation (using a color
matrix)
Draw the original image onto 1) with high saturation
using a pattern mask (ie the dots)
I created the pattern mask like this:
using (var g = Graphics.FromImage(bmpPattern))
{
g.Clear(Color.Black);
g.SmoothingMode = SmoothingMode.HighQuality;
for (var y = 0; y < bmp.Height; y += 10)
for (var x = 0; x < bmp.Width ; x += 6)
{
g.FillEllipse(Brushes.White, x, y, 4, 4);
g.FillEllipse(Brushes.White, x + 3, y + 5, 4, 4);
}
}
And then I imposed it over the oversaturated bitmap using this technique.
Update: Elaboration on how the images got merged. Let's talk even a little more general and say that we want to combine two different colorized versions of the same image using a pattern mask, resulting in a new image - we could do it like this:
Create THREE new bitmaps, all with the same size as the original image. Call them bmpA, bmpB and bmpMask.
Draw one colored/effect version into bmpA
Draw the other colored/effect version into bmpB
Create the mask in bmpMask (black and white)
Push one of the R/G/B channels of bmpMask into the alpha channel of bmpB using the
transferOneARGBChannelFromOneBitmapToAnother method.
Draw bmpB over bmpA (since bmpB now has transparent parts in it)
The result is now bmpA. bmpB and bmpMask can be disposed.
Done

Drawing an I-Beam based on two points

I have two Point structures and I need to draw an I-Beam based on those points, where each point represents the cross-section on either side of the I-Beam. The width of the end caps should be fixed and arbitrary.
Basically I need to draw three lines. First I'll DrawLine(Point1, Point2), then I need the math to figure out how to draw the next two lines on perpendicular angles so that they are centered on Point1 and Point2.
The image below shows what I need to draw based on the center line. However, this line can be at any angle. The Point1 and Point2 that connect the line can be anywhere in a 2D space.
You can try playing around with LineCaps:
protected void DrawIBeam(Graphics g, Point fromPoint, Point toPoint)
{
using (GraphicsPath hPath = new GraphicsPath())
{
hPath.AddLine(new Point(-5, 0), new Point(5, 0));
CustomLineCap myCap = new CustomLineCap(null, hPath);
myCap.SetStrokeCaps(LineCap.Round, LineCap.Round);
using (Pen myPen = new Pen(Color.Black, 2))
{
myPen.CustomStartCap = myCap;
myPen.CustomEndCap = myCap;
g.DrawLine(myPen, fromPoint, toPoint);
}
}
}
and call it:
DrawIBeam(e.Graphics, new Point(10, 10), new Point(60, 60));
From CustomLineCap Class
Assuming a width that's half the width of the I part of the I beam, first you find the slope of the first line you drew.
Next, you take the negative inverse of the slope, and draw a line from Point1 of length width in both directions. That's why width is half of the width you want to draw.
Finally you draw a line from Point 2 of length width in both directions.
Here's the mathematical formula for drawing a perpendicular line.

Calculating Text Wrapping in the .NET DrawingContext DrawText method

I'm working on a project that has me approximating text rendered as an image and a DHTML editor for the text. The images are rendered using the .NET 4 DrawingContext object's DrawText method.
The DrawText method will take text along with font information as well as dimensions and calculate the wrapping necessary to get the text to fit as much as possible, placing an ellipsis at the end if the text is too long. So, if I have the following code to draw text in a Rectangle it will abbrevaiate it:
string longText = #"A choice of five engines, although the 2-liter turbo diesel, supposedly good for 48 m.p.g. highway, is not coming to America, at least for now. A 300-horsepower supercharged gasoline engine will likely be the first offered in the United States. All models will use start-stop technology, and fuel consumption will decrease by an average of 19 percent across the A6 lineup. A 245-horsepower A6 hybrid was also unveiled, but no decision has yet been made as to its North America sales prospects. Figure later in 2012, if sufficient demand is detected.";
var drawing = new DrawingGroup();
using (var context = drawing.Open())
{
var text = new FormattedText(longText,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface("Calibri"),
30,
Brushes.Green);
text.MaxTextHeight = myRect.Height;
text.MaxTextWidth = myRect.Width;
context.DrawText(text, new Point(0, 0));
}
var db = new DrawingBrush(drawing);
db.Stretch = Stretch.None;
myRect.Fill = db;
Is there a way to calculate how the text will be wrapped? In this example, the outputted text is wrapped at "2-liter" and "48 m.p.g" etc as seen in the image below:
You can use the Graphics.MeasureString(String, Font, Int32) function. You pass it the string, font, and maximum width. It returns a SizeF with the rectangle it would form. You can use this to get the overall height, and thus the number of lines:
Graphics g = ...;
Font f = new Font("Calibri", 30.0);
SizeF sz = g.MeasureString(longText, f, myRect.Width);
float height = sz.Height;
int lines = (int)Math.round(height / f.Height); // overall height divided by the line height = number of lines
There are many ways to get a Graphics object, and any will do since you are only using it to measure and not to draw (you may have to correct its DpiX, DpiY, and PageUnit fields since those effect measurements.
Ways to get a Graphics object:
Graphics g = e.Graphics; // in OnPaint, with PaintEventArgs e
Graphics g = x.CreateGrahics(); // where x is any Form or Control
Graphics g = Graphics.CreateFrom(img); // where img is an Image.
Not sure if you still need a solution or if this particular solution is appropriate for your application, but if you insert the below snippet just after your using block it will show you the text in each line (and therefore where the text was broken for wrapping).
I arrived at this solution using the very ghetto/guerrilla approach of just browsing properties while debugging, looking for the wrapped text segments - I found 'em and they were in accessible properties...so there you go. There very well may be a more proper/direct way.
// Object heirarchy:
// DrawingGroup (whole thing)
// - DrawingGroup (lines)
// - GlyphRunDrawing.GlyphRun.Characters (parts of lines)
// Note, if text is clipped, the ellipsis will be placed in its own
// separate "line" below. Give it a try and you'll see what I mean.
List<DrawingGroup> lines = drawing.Children.OfType<DrawingGroup>().ToList();
foreach (DrawingGroup line in lines)
{
List<char> lineparts = line.Children
.OfType<GlyphRunDrawing>()
.SelectMany(grd => grd.GlyphRun.Characters)
.ToList();
string lineText = new string(lineparts.ToArray());
Debug.WriteLine(lineText);
}
Btw, Hi David. :-)

Categories