Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 7 years ago.
Improve this question
I'm using .Net drawing to draw a diagram. It is essentially a stacked bar chart.
The issue I have is that I want to reduce the amount of lines in the hatch style so in a way scale it up to make it clearer. I've looked around but didn't come across anything that could help me.
I draw a rectangle and then use a hatchbrush to fill it but due to image size the hatchfill becomes less clearer. Thank you for any suggestions.
The hatchStyles and brush types are stored in the db and I use a helper function to return them. So I draw the rectangle and after getting the brush I fill the rectangle. Essentially I want to scale up the hatch fill if that can be done.
g.DrawRectangle(gridpen, startX, startY, BOREHOLE_RECT_WIDTH, layerRectHeight);
brush = GetBoreholeBrush(l.SoilTypeMatrixLevel1Id.PrimaryBrushType,
l.SoilTypeMatrixLevel1Id.PrimaryFillStyle,
l.SoilTypeMatrixLevel1Id.PrimaryColour);
g.FillRectangle(brush, startX, startY, BOREHOLE_RECT_WIDTH, layerRectHeight);
And the getBrush function; the brush type, hatch style and colour are stored in the db and used to create the returned brush:
//===================================
private Brush GetBoreholeBrush(string BrushType, string HatchStyle, string Colour)
//===================================
{
//Decide on what brush type has been chosen.
Brush brush;
if (BrushType.ToLower() == BrushTypes.HatchBrush.ToString().ToLower())
{
brush = new HatchBrush(GetHatchStyle(HatchStyle),
Color.Black, ColorTranslator.FromHtml(Colour));
}
else if (BrushType.ToLower() == BrushTypes.SolidBrush.ToString().ToLower())
{
brush = new HatchBrush(GetHatchStyle(HatchStyle),
Color.Black, ColorTranslator.FromHtml(Colour));
}
else if (BrushType.ToLower() == BrushTypes.TextureBrush.ToString().ToLower())
{
brush = new HatchBrush(GetHatchStyle(HatchStyle),
Color.Black, ColorTranslator.FromHtml(Colour));
}
else
{
brush = new HatchBrush(GetHatchStyle(HatchStyle),
Color.Black, ColorTranslator.FromHtml(Colour));
}
return brush;
}
Function to return the hatch style:
//===================================
private HatchStyle GetHatchStyle(string FillStyle)
//===================================
{
//Loop through each hatch tyle and return the correct one.
foreach (HatchStyle style in Enum.GetValues(typeof(HatchStyle)))
{
if (style.ToString().ToLower() == FillStyle.ToLower())
{
return style;
}
}
return HatchStyle.Vertical;
}
As you can see in the image below the hatch style is not clear.
The most direct but probably not very helpful answer is : No you can't scale the hatch pattern of a HatchBrush.
It is meant to always look sharp at the pixel level and is not even affected by scaling the Graphics object.
Looking at your question I wonder: Are you sure you are really using a HatchBrush? You get the brush from a function GetBoreholeBrush. If you really have stored indices into the 50 HatchStyle then I guess you really use a HatchBrush.
Now as using a HatchBrush won't work I guess you could use a TextureBrush instead..
You could transform the hatch patterns to larger versions by scaling them up; this is not exactly a simple conversion. The direct approach of drawing the larger by an integer factor and without anti-aliasing is simple and may be good enough.
But you may need to fine-tune them, as this way all pixels, that is both line pixels and background pixels get enlarged and also all diagonals will look jagged.
So you would need to balance the hatch size and the stroke width and recreate all patterns you need from scratch in larger sizes.
Here is an example that illustrates the problems with the simple solution; the first row is the original hatch pattern the others are simple texture brush results, scaled by 1x, 2x and 3x..:
First a function to transform a HatchBrush to a TextureBrush
TextureBrush TBrush(HatchBrush HBrush)
{
using (Bitmap bmp = new Bitmap(8,8))
using (Graphics G = Graphics.FromImage(bmp))
{
G.FillRectangle(HBrush, 0, 0, 8, 8);
TextureBrush tb = new TextureBrush(bmp);
return tb;
}
}
Note that the hatch pattern is 8x8 pixels.
Now the Paint code used for the above image:
private void panel1_Paint(object sender, PaintEventArgs e)
{
var hs = (HatchStyle[])Enum.GetValues(typeof(HatchStyle));
for (int i = 0; i < hs.Length; i++)
using (HatchBrush hbr = new HatchBrush(hs[i], Color.GreenYellow))
using (HatchBrush hbr2 = new HatchBrush(hs[i], Color.LightCyan))
{
e.Graphics.FillRectangle(hbr, new Rectangle(i * 20, 10,16,60));
using (TextureBrush tbr = TBrush(hbr2))
{
e.Graphics.FillRectangle(tbr, new Rectangle(i * 20, 80, 16, 60));
tbr.ScaleTransform(2, 2);
e.Graphics.FillRectangle(tbr, new Rectangle(i * 20, 150, 16, 60));
tbr.ResetTransform();
tbr.ScaleTransform(3,3);
e.Graphics.FillRectangle(tbr, new Rectangle(i * 20, 220, 16, 60));
}
}
}
Note that while the TextureBrush has nice methods to modify the texture, the HatchBrush has nothing like that at all..
Related
I want to create a linear gradient with 7 step colors and custom size - from black, blue, cyan, green, yellow, red to white. My problem is that the final bitmap has a black stripe on the right side. Anyone have an idea what's the matter?
public static List<Color> interpolateColorScheme(int size)
{
// create result list with for interpolated colors
List<Color> colorList = new List<Color>();
// use Bitmap and Graphics from bitmap
using (Bitmap bmp = new Bitmap(size, 200))
using (Graphics G = Graphics.FromImage(bmp))
{
// create empty rectangle canvas
Rectangle rect = new Rectangle(Point.Empty, bmp.Size);
// use LinearGradientBrush class for gradient computation
LinearGradientBrush brush = new LinearGradientBrush
(rect, Color.Empty, Color.Empty, 0, false);
// setup ColorBlend object
ColorBlend colorBlend = new ColorBlend();
colorBlend.Positions = new float[7];
colorBlend.Positions[0] = 0;
colorBlend.Positions[1] = 1 / 6f;
colorBlend.Positions[2] = 2 / 6f;
colorBlend.Positions[3] = 3 / 6f;
colorBlend.Positions[4] = 4 / 6f;
colorBlend.Positions[5] = 5 / 6f;
colorBlend.Positions[6] = 1;
// blend colors and copy them to result color list
colorBlend.Colors = new Color[7];
colorBlend.Colors[0] = Color.Black;
colorBlend.Colors[1] = Color.Blue;
colorBlend.Colors[2] = Color.Cyan;
colorBlend.Colors[3] = Color.Green;
colorBlend.Colors[4] = Color.Yellow;
colorBlend.Colors[5] = Color.Red;
colorBlend.Colors[6] = Color.White;
brush.InterpolationColors = colorBlend;
G.FillRectangle(brush, rect);
bmp.Save("gradient_debug_image_sarcus.png", ImageFormat.Png);
for (int i = 0; i < size; i++) colorList.Add(bmp.GetPixel(i, 0));
brush.Dispose();
}
// return interpolated colors
return colorList;
}
Here is my gradient:
I took your code and tried every size from 2 to ushort.MaxValue, generating the gradient and scanning from the right edge to determine how many black pixels there were.
For many sizes, there are no black pixels. However, for certain consecutive runs of sizes, as the size increases, the number of black pixels also increases. There are approximately 2140 such runs in the tested range. This implies that there is a rounding error in the gradient drawing.
This bug has been encountered before (http://www.pcreview.co.uk/threads/error-on-lineargradientbrush.2165794/). The two solutions that link recommends are to
draw the gradient larger than you need it or
use WrapMode.TileFlipX.
What that link gets wrong is that the rounding error is not just 1 pixel at all times; at large image sizes it can be as large as 127 pixels (in the range I tested). Drawing the gradient larger than you need it requires you to know (or estimate) how much bigger you need to make the gradient. You could try scaling by (size + Math.Ceiling(size / 512.0)) / size, which is an upper bound on the error for the range of image sizes I have tested.
If you're looking for a simpler solution, specifying brush.WrapMode = WrapMode.TileFlipX will cause the brush to draw normally up to the (incorrect) edge of the gradient, then repeat the gradient in reverse until the actual edge of the specified rectangle. Since the rounding error is small compared to the size of the rectangle, this will look like the final color of the gradient has been extended to the edge of the rectangle. Visually, it looks good, but it may be unsuitable if you require very precise results.
In relation to this question that I asked a few weeks ago now
LinearGradientBrush does not render correctly
Consider the following code:
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Rectangle rect = new Rectangle(100, 100, 200, 100);
using(LinearGradientBrush brush = new LinearGradientBrush(rect, Color.Red, Color.Blue, 90))
{
using(Pen pen = new Pen(brush, 1))
{
pen.Alignment = PenAlignment.Inset;
e.Graphics.DrawRectangle(pen, rect);
}
}
}
Produces this result...
Why is there a red line where there should be a blue line, and how do I fix this?
Pen.Alignment = PenAlignment.Inset didn't work!
Pens (or the Graphics DrawMethods) have a tendency to draw outside their boundaries. I think Microsoft considered that a feature, but I never understood it.
Try using a smaller rectangle for the pen:
using (var brush = new LinearGradientBrush(rect, Color.Red, Color.Blue, 90)) {
using (Pen pen = new Pen(brush, 1)) {
e.Graphics.DrawRectangle(pen, new Rectangle(rect.X, rect.Y,
rect.Width - 1, rect.Height - 1));
}
}
OK, to sum it up: DrawRectangle always overdraws by 1 pixel (*) and the LinearGradientBrush therefore restarts with the 1st color.
Yes, Lars got one solution, but here is an interesting alternative:
brush.WrapMode = WrapMode.TileFlipXY;
This makes sure that the gradient doesn't start at the beginning when it has to overdraw but reverses in both directions, so the extra pixel is drawn in the right Color and you don't need to manipulate one of the two the Rectangle Sizes..
Well ok, if you need total precision it is probably second best, as the last row and column will have the same color as their neighbours..but not having to deal with two sizes may be worth it..
(*) Which is why you can't use it to draw a 1x1 square/pixel. Instead you need to use FillRectangle..
private static Bitmap[] renders = new Bitmap[characters];
public static void initBitmaps()
{
fontWidth = TextRenderer.MeasureText("c", font).Width;
fontHeight = TextRenderer.MeasureText("c", font).Height;
for (int i=0; i<characters; i++)
{
renders[i] = new Bitmap(fontWidth, fontHeight);
using (Graphics g = Graphics.FromImage(renders[i]))
{
g.DrawString(Convert.ToChar(i + 32).ToString(), font, new SolidBrush(Color.Black), new PointF(0, 0));
}
}
}
After executing this bit of code, all bitmaps are empty (RawData are null). What am I doing wrong?
(the font in question is fixed-width, so size shouldn't be a problem)
DrawString works fine and the bitmaps aren't empty, you just can't see the text because you are drawing with a black brush on a black background.
You'll need to initialize the bitmap; use g.Clear(Color.White). Also note that you are mixing TextRenderer with Graphics.DrawString, which is a bad idea. See DrawString vs. TextRenderer for more information.
If you try proportional fonts, you are going to be disappointed how W and M will fit because you're only measuring the dimensions of lower case c which (in most fonts) would be smaller than a upper case W.
Can anybody tell me how to get a rectangle back from GetBounds in any units OTHER than pixels? The following code - lifted directly off the MSDN documentation for this function - returns a rectangle that is pretty obviously in pixels rather than points (1/72 of an inch). (Unless icons come in a size of 32/72"x32/72" rather than 32x32 pixels like I think). I am most interested in working with a rectangle in inches, but I would settle for simply seeing the GetBounds pageUnit parameter cause a change in the returned rectangle.
Bitmap bitmap1 = Bitmap.FromHicon(SystemIcons.Hand.Handle);
Graphics formGraphics = this.CreateGraphics();
GraphicsUnit units = GraphicsUnit.Point;
RectangleF bmpRectangleF = bitmap1.GetBounds(ref units);
Rectangle bmpRectangle = Rectangle.Round(bmpRectangleF);
formGraphics.DrawRectangle(Pens.Blue, bmpRectangle);
formGraphics.Dispose();
The Information is a little sparse on this, I was able to find this MSDN Forum posting that suggests since the Bitmap is already created the units have already been set and are not changable. Since the GraphicsUnit is being passed by a reference, it you look at it after the call you will find it set back to Pixel from Inch. If you actually want to change the size that the rectangle is drawn at set the Graphics.PageUnit Property on formGraphics to the GraphicsUnit you want to draw the Rectangle at.
From above Link:
In this sample, the parameters of Image.GetBounds method don’t change the result, because the bound of Bitmap has been decided. The parameters only determine the unit length to deal with the range, inch by inch or point by point. But the parameters will not influence the result.
emphasis mine
A bit late answering this one, but I thought I would do so because I found it in Google when trying to answer the question "how many mm can I fit in my picture box?", it would have saved me a lot of time not having to work out how to do it!. GetBounds is useless (if you wanted it in pixels...) but it is possible to find the relation between drawing units and display pixels using the Graphics.TransformPoints method:
private void Form1_Load(object sender, EventArgs e)
{
Bitmap b;
Graphics g;
Size s = pictureBox1.Size;
b = new Bitmap(s.Width, s.Height);
g = Graphics.FromImage(b);
PointF[] points = new PointF[2];
g.PageUnit = GraphicsUnit.Millimeter;
g.PageScale = 1.0f;
g.ScaleTransform(1.0f, 1.0f);
points[0] = new PointF(0, 0);
points[1] = new PointF(1, 1);
g.TransformPoints(CoordinateSpace.Device, CoordinateSpace.Page, points);
MessageBox.Show(String.Format("1 page unit in {0} is {1} pixels",g.PageUnit.ToString(),points[1].X));
points[0] = new PointF(0, 0);
points[1] = new PointF(1, 1);
g.TransformPoints(CoordinateSpace.Page, CoordinateSpace.World, points);
MessageBox.Show(String.Format("1 page unit in {0} is {1} pixels",g.PageUnit.ToString(),points[1].X));
g.ResetTransform();
pictureBox1.Image = b;
SolidBrush brush = new SolidBrush(Color.FromArgb(120, Color.Azure));
Rectangle rectangle = new Rectangle(10, 10, 50, 50);
// Fill in the rectangle with a semi-transparent color.
g.FillRectangle(brush, rectangle);
pictureBox1.Invalidate();
}
This will display the basic mm to display pixels (3.779527 in my case) - the world coordinates are 1 mm per pixel, this would change if you applied graphics.ScaleTransform.
Edit: Of course, it helps if you assign the bitmap to the pictureBox image property (and keep the Graphics object to allow changes as required).
Add label
In class Form1 Add field
PointF[] cooridates;
Form1.cs [design] look for lighting bolt in properties double click Paint create handler
Form1_Paint(object sender,PaintEventArgs)
{
e.Graphics.PageUnit = GraphicsUnit.Inch;
if (cooridates != null)
e.Graphics.TransformPoints(CoorinateSpace.World,
CoorinateSpace.Device,cooridates);
}
Create handler again for Form1.MouseMove
Form1_MouseMove(object sender,MouseEventArgs e
{
cooridates[0].X = e.Location.X;
cooridates[0].Y = e.Location.Y;
this.Refresh();
label1.Text = $"X = {cooridates[0].X} Y = {
{ cooridates[0].Y } ";
}
Form1_Load(object sender,MouseEventArgs)
{
cooridates = new PointF[1] { new PointF(0f,0f) };
}
Move mouse to get cooridates in Inches
Im drawing a linear gradient manually by drawing lines with changing colors. However, this is very slow, and i seems to update, when i resize the window. How do i make it faster? The color scale is linear in this example, but later i wan't to make non-linear gradients.
protected override void OnPaintBackground(PaintEventArgs paintEvnt)
{
SuspendLayout();
// Get the graphics object
Graphics gfx = paintEvnt.Graphics;
// Create a new pen that we shall use for drawing the line
// Loop and create a horizontal line 10 pixels below the last one
for (int i = 0; i <= 500; i++)
{
Pen myPen = new Pen(Color.FromArgb(i/2,0,0));
gfx.DrawLine(myPen, 0, i, 132, i);
}
ResumeLayout();
}
The problem is that GDI+ is incredibly slow.
You should use high level constructs with GDI+ which are relatively fast (relative to drawing lines like you do now). See http://msdn.microsoft.com/en-us/library/system.drawing.drawing2d.lineargradientbrush.aspx for more information about e.g. the LinearGradientBrush. There are much more of these brushes and pens which should help you increase your performance.
One more thing: the Suspend/ResumeLayout doesn't do anything in your example. These methods only apply when you are doing layout by e.g. adding Controls to the current form or changing properties on existing Controls like the Dock property or the Height and Width.
If you want to paint it once and only once, without resizing, I suggest you write this to a Bitmap object once, and then draw this bitmap to the background. Also, you can enable double buffering on the form. this should be a property called DoubleBuffering, or something similar. This should reduce the flashing you get when redrawing your form.
You could pre-compute the color values so you won't have to do it on every redraw. Other than that, there's not much more you can do without resorting to more lowlevel APIs, like XNA.
Update: it is perfectly feasible to host XNA within WinForms controls. There's some nice links forward in this question.
Perhaps specifying a ColorBlend to use with the LinearGradientBrush suggested by Pieter will address your concerns about being able to paint non-linear gradients in the future?
You can create a ColorBlend object that specifies the colors of your choice and an arbitrary position for each. By setting the InterpolationColors property of the LinearGradientBrush to your ColorBlend object, you should be able to get any effect that you want.
MSDN gives the following sample:
protected override void OnPaint(PaintEventArgs e)
{
//Draw ellipse using ColorBlend.
Point startPoint2 = new Point(20, 110);
Point endPoint2 = new Point(140, 110);
Color[] myColors = {Color.Green, Color.Yellow, Color.Yellow, Color.Blue, Color.Red, Color.Red};
float[] myPositions = {0.0f,.20f,.40f,.60f,.80f,1.0f};
ColorBlend myBlend = new ColorBlend();
myBlend.Colors = myColors;
myBlend.Positions = myPositions;
LinearGradientBrush lgBrush2 = new LinearGradientBrush(startPoint2, endPoint2, Color.Green, Color.Red);
lgBrush2.InterpolationColors = myBlend;
Rectangle ellipseRect2 = new Rectangle(20, 110, 120, 80);
e.Graphics.FillEllipse(lgBrush2, ellipseRect2);
}