Graphics bug when dragged off-screen - c#

I have wrote this code that will create a rounded corner border around a panel.
public void DrawRoundRect(Graphics g, Pen p, float X, float Y, float width, float height, float radius)
{
GraphicsPath gp = new GraphicsPath();
//Upper-right arc:
gp.AddArc(X + width - (radius * 2), Y, radius * 2, radius * 2, 270, 90);
//Lower-right arc:
gp.AddArc(X + width - (radius * 2), Y + height - (radius * 2), radius * 2, radius * 2, 0, 90);
//Lower-left arc:
gp.AddArc(X, Y + height - (radius * 2), radius * 2, radius * 2, 90, 90);
//Upper-left arc:
gp.AddArc(X, Y, radius * 2, radius * 2, 180, 90);
gp.CloseFigure();
g.DrawPath(p, gp);
gp.Dispose();
}
private void panel_Paint(object sender, PaintEventArgs e)
{
Graphics v = e.Graphics;
DrawRoundRect(v, Pens.White, e.ClipRectangle.Left, e.ClipRectangle.Top, e.ClipRectangle.Width - 1, e.ClipRectangle.Height - 1, 10);
base.OnPaint(e);
}
It works fine, until it goes off screen and this happens:
Does anyone know what I've done wrong, or how to fix the issue?

You misunderstand what ClipRectangle means.
It doesn't tell you how big the panel is - it tells you which part of the panel to redraw. As you move the form off-screen, it needs to redraw the parts that changed monitors - but not the ones that remain on the original one. So instead of redrawing the whole panel (when ClipRectangle is the whole area of the panel, and your code works), it tells you to only redraw a part of it - and ClipRectangle is much smaller than the panel you're trying to draw.
ClipRectangle is really an optimization feature. If you don't care about that, you can pretty much ignore it completely - just use 0, 0, panel.Width, panel.Height.

You have two problems. First is relying on e.ClipRectangle, that is not the size of the panel. You need to use panel.ClientRectangle instead. That's what causes the smearing when you drag it off the screen and back.
You haven't found the next one yet, it is much more subtle. Panel was designed to be a container control and it optimizes its painting. Redrawing only the parts that get revealed when it is resized. Pretty important, makes resizing a window more bearable. That however will not work well for details like this, the rounded corners will not get properly repainted. Looks like a smearing effect as well, less pronounced than what you have now. It will happen whenever you resize the panel, typically with the Dock or Anchor property.
Proper way to do that is to derive your own class from Panel, set the ResizeRedraw property to true in the constructor. You typically almost always also want to set DoubleBuffered to true to suppress the visible flicker you now have. Or use the panel's Resize event, call panel.Invalidate().

Related

C# winforms unable to paint in middle of screen, even with the correct screen height and width

I'm trying to write a program, that will place a crosshair in the middle of the screen on a transparent background, however the crosshair will always be drawn at an offset even though the coordinates where it's placed is right.
The method which draws the crosshair:
public void drawSquareCrosshair(Graphics gfx, Color color) {
SolidBrush lightChrosshairColor = new SolidBrush(color);
SolidBrush transparentColor = new SolidBrush(Color.Fuchsia);
// Crosshair Bounds
int width = 20;
int height = 20;
int x = (SystemInformation.VirtualScreen.Width / 2)- (width / 2);
int y = (SystemInformation.VirtualScreen.Height / 2) - (height / 2);
Console.WriteLine("X: " + x);
Console.WriteLine("Y: " + y);
gfx.FillRectangle(lightChrosshairColor, x, y, width, height);
}
The logic is, that the width of the screen is divided by two, and minussed by the width of the crosshair divided by two. The same goes for the height. I've set the graphics object to create graphics on a panel which is anchored on all sides within the form, yet the panels height and width is still 1920x1080, just as the form is. The way I've set the form to get maximized is by using form.FormBorderStyle = FormBorderStyle.None; and form.WindowState = FormWindowState.Maximized;
However that doesn't seem to be the case, as the crosshair is still placed at the coordinates that would resemble the middle of the screen (X: 960, Y: 540 on a 1920x1080 screen). I created a desktop background which as a crosshair in the middle of the screen in photoshop (The image is also 1920x1080). Here's how it looks:
The square is the crosshair painted by my application, the red cross is the wallpaper
Has anyone run into this problem before?
This can be fixed by replacing SystemInformation.VirtualScreen with ClientRectangle, see the below code. ClientRectangle returns the bounds of the drawable are of the window.
int x = (ClientRectangle.Width / 2) - (width / 2);
int y = (ClientRectangle.Height / 2) - (height / 2);

Graphics.DrawImage() clips image when using RotateTransform()

I have following code to draw my border2.bmp in 4 direction
private void Form1_Paint(object sender, PaintEventArgs e)
{
Bitmap border = new Bitmap("border2.bmp");
int borderThick = border.Height;
Graphics g = e.Graphics;
Size region = g.VisibleClipBounds.Size.ToSize();
Rectangle desRectW = new Rectangle(0, 0, region.Width - borderThick, borderThick);
// 1. LEFT - RIGHT border
g.TranslateTransform(30, 30);
g.DrawImage(border, desRectW, desRectW, GraphicsUnit.Pixel);
// 2. UP - BUTTOM border
g.ResetTransform();
g.TranslateTransform(50, 50);
g.RotateTransform(90);
g.DrawImage(border, desRectW, desRectW, GraphicsUnit.Pixel);
// 3. RIGHT-LEFT border
g.ResetTransform();
g.TranslateTransform(100, 100);
g.RotateTransform(180);
g.DrawImage(border, desRectW, desRectW, GraphicsUnit.Pixel);
// 4. BOTTOM - UP border
g.ResetTransform();
g.TranslateTransform(150, 150);
g.RotateTransform(270);
g.DrawImage(border, desRectW, desRectW, GraphicsUnit.Pixel);
}
My original image is:
But the result of rotations are not exactly as I expected. 90 degrees is missing the first red line, 270 degrees is missing first black column, and 180 degrees is missing both.
Like image I attached:
PS: you can get border2.bmp at: http://i.imgur.com/pzonx3i.png
Edit:
I tried g.PixelOffsetMode = PixelOffsetMode.HighQuality; as #Peter Duniho comment, but I found it also does't draw correctly.
Example: 4 line is not starting at same position as we expect.
g.TranslateTransform(50, 50);
// LEFT - RIGHT border
g.DrawLine(Pens.Red, 0, 0, 100, 0);
// UP - BOTTOM border
g.RotateTransform(90);
g.DrawLine(new Pen(Color.FromArgb(128, Color.Blue)), 0, 0, 100, 0);
// RIGHT-LEFT border
g.RotateTransform(90);
g.DrawLine(new Pen(Color.FromArgb(128, Color.Green)), 0, 0, 100, 0);
// BOTTOM - UP border
g.RotateTransform(90);
g.DrawLine(new Pen(Color.FromArgb(128, Color.Gray)), 0, 0, 100, 0);
I can't really explain why this happens, except that any graphics API is necessarily going to include optimizations which may lead to imprecise behaviors at times and it seems that you are running into such a situation here.
In your particular example, the problem can be corrected by adding the following statement to the code, before you draw the images:
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
Setting it to Half will also work. It is equivalent to HighQuality (or technically, HighQuality is equivalent to it…but I find HighQuality more descriptive in the code :) ).
This will slow the rendering of the bitmap down somewhat, but probably not in a way that is perceptible to your users.
While the .NET documentation isn't very helpful in terms of describing this setting, the native Win32 docs for the same feature has slightly more detail:
PixelOffsetMode enumeration. From the description, one can infer that with the logical center of the pixel at (0,0), it's possible to lose a pixel on one edge when rotating (and/or gain a pixel on another edge). Switching to Half fixes this.
You need to account some things.
(1) RotateTransforms applies rotation at the current origin (as explained in the Matrix.Rotate documentation
(2) The default MatrixOrder (when not specified by using the overrides with the additional argument) is Prepend. Which means in your case the resulting transformation is rotate, then translate.
For instance, if you put this code inside the paint event:
var g = e.Graphics;
var loc = new Point(128, 128);
var rect = new Rectangle(0, 0, 64, 16);
g.ResetTransform();
g.TranslateTransform(loc.X, loc.Y);
g.FillRectangle(Brushes.Blue, rect);
g.ResetTransform();
g.TranslateTransform(loc.X, loc.Y);
g.RotateTransform(90);
g.FillRectangle(Brushes.Red, rect);
g.ResetTransform();
g.TranslateTransform(loc.X, loc.Y);
g.RotateTransform(180);
g.FillRectangle(Brushes.Green, rect);
g.ResetTransform();
g.TranslateTransform(loc.X, loc.Y);
g.RotateTransform(270);
g.FillRectangle(Brushes.Magenta, rect);
you'll get this
The blue rectangle is not rotated. Now pin virtually the origin (the upper left point) and start rotating clockwise. You'll see that it will exactly match the Red (90), Green (180) and Magenta (270) rectangles.
What all that means is that if you want to form a rectangle, you need to apply additional offset (translation) to the rotated rectangles. It depends how you want to handle the overlapping areas, but for the sample if we want to concat the Red rectangle right to the Blue one, we need to add the Blue rectangle Width + the original rectangle Height in the X direction. For other rotated rectangles you can apply similar additional offset to X, Y or both.
To complete the sample, if we modify the code like this
var g = e.Graphics;
var loc = new Point(128, 128);
var rect = new Rectangle(0, 0, 64, 16);
g.ResetTransform();
g.TranslateTransform(loc.X, loc.Y);
g.FillRectangle(Brushes.Blue, rect);
g.ResetTransform();
g.TranslateTransform(loc.X + rect.Width + rect.Height, loc.Y);
g.RotateTransform(90);
g.FillRectangle(Brushes.Red, rect);
g.ResetTransform();
g.TranslateTransform(loc.X + rect.Width + rect.Height, loc.Y + rect.Width + rect.Height);
g.RotateTransform(180);
g.FillRectangle(Brushes.Green, rect);
g.ResetTransform();
g.TranslateTransform(loc.X, loc.Y + rect.Width + rect.Height);
g.RotateTransform(270);
g.FillRectangle(Brushes.Magenta, rect);
the new result will be
Once you understand all that, hope you can apply the required corrections to your concrete code.

MonoGame - proper SpriteFont scaling

I'm making a GUI for my game and now I'm stuck on animations. I need to scale a font up when the mouse hovers over it and scale it down when it's not. Here's my code:
// Update()
if (!IsDisabled)
{
elapsedSecondsFast = (float)gameTime.ElapsedGameTime.TotalSeconds * 3;
if (Size.Contains(InputManager.MouseRect))
{
scale += elapsedSecondsFast;
if (scale >= 1.05f) scale = 1.05f;
}
else
{
scale -= elapsedSecondsFast;
if (scale <= 1.0f) scale = 1.0f;
}
}
// Draw()
if ((PrimaryFont != null) && (SecondaryFont != null) && (!string.IsNullOrEmpty(Text)))
{
if (IsHovered) TextOutliner.DrawBorderedText(spriteBatch, SecondaryFont, Text, new Vector2(TextRectangle.X, TextRectangle.Y), ForeColor, 0.0f, new Vector2((SecondaryFont.MeasureString(Text).X / 2 * scale - SecondaryFont.MeasureString(Text).X / 2), (SecondaryFont.MeasureString(Text).Y / 2 * scale - SecondaryFont.MeasureString(Text).Y / 2)), scale);
else TextOutliner.DrawBorderedText(spriteBatch, PrimaryFont, Text, new Vector2(TextRectangle.X, TextRectangle.Y), ForeColor, 0.0f, new Vector2(PrimaryFont.MeasureString(Text).X / 2 * scale - PrimaryFont.MeasureString(Text).X / 2, (PrimaryFont.MeasureString(Text).Y / 2 * scale - PrimaryFont.MeasureString(Text).Y / 2)), scale);
}
The above is a GUIElement class which is inherited by my Button class. Let me explain my code briefly:
PrimaryFont and SecondaryFont are 2 SpriteFonts that use the same
font but a different size. This gives me the scale up/down animation I need without blurring my PrimaryFont.
TextRectangle and Size are 2 different Rectangles. Since my button has a texture and text I decided not to draw text on the texture file but have the game position my text over the texture to "fake" the effect. So TextRectangle is the size and location of button text and Size is size and location of button texture. TextRectangle has its center point in the center of the Button texture. So far I have been using magic numbers to achieve this. This is the core of the problem here.
You can see my origin, I passed it to the DrawBorderedText method of my TextOutliner class. The attributes are in the same order as if it were a spriteBatch.DrawString() call, only without SpriteEffects and layerDepth.
The Problem
Since I'm scaling the font (origin = center I think) it will no longer be in the center of the button. And since I have been using magic numbers to position the un-scaled text over the center of the button texture, I don't want to be forced to do the same thing for scaled text. I'm looking for an algorithm that would always position the text in the middle of my 270x72 texture, no matter if the text is scaled or not, while keeping the scale animation shown above, for each instance of the Button class. Preferably to have its origin point in the center.
Edit
So should I draw like this:
if (IsHovered) TextOutliner.DrawBorderedText(spriteBatch, SecondaryFont, Text, new Vector2(TextRectangle.X, TextRectangle.Y), ForeColor, 0.0f, new Vector2((SecondaryFont.MeasureString(Text).X / 2), (SecondaryFont.MeasureString(Text).Y / 2)), scale);
else TextOutliner.DrawBorderedText(spriteBatch, PrimaryFont, Text, new Vector2(TextRectangle.X, TextRectangle.Y), ForeColor, 0.0f, new Vector2(PrimaryFont.MeasureString(Text).X / 2, (PrimaryFont.MeasureString(Text).Y / 2)), scale);
and then draw the button's text at btn.Size.Width / 2, btn.Size.Height / 2, (int)MainGame.GameFontLarge.MeasureString("Play").X / 2, (int)MainGame.GameFontLarge.MeasureString("Play").Y / 2
So I eventually found the algorithm by myself and finally eliminated the need of using magic numbers for position of the text. Here's my technique:
TextOutliner.DrawBorderedText(spriteBatch, Font, Text, new Vector2(Size.X + ((Size.Width - Font.MeasureString(Text).X) / 2), Size.Y + ((Size.Height - Font.MeasureString(Text).Y)) / 2), ForeColor, 0.0f, new Vector2((Font.MeasureString(Text).X / 2 * scale - Font.MeasureString(Text).X / 2), (Font.MeasureString(Text).Y / 2 * scale - Font.MeasureString(Text).Y / 2)), scale);
I couldn't take the scale out of the origin equation as #LibertyLocked suggested, because the font was scaling from top-left point upon Mouse Hover and not the center as I want it to.

Can a Pen or Brush paint a Point?

I know that I can draw a filled circle and I can draw many simple and complicated things with a Graphics. But I couldn't get it to draw a single point (not a single pixel).
I'm playing with a Paint program and the user can draw fine but not plot a dot. I can add a dummy point really close or can draw a filled cirlce but sometimes I miss the obvious.
So is there a way to draw a single point with a given Brush or Pen?
And no, of course I don't mean to draw a single Pixel. I want to use the Properties like color and width. Like a DrawLine with only one Point or with the same Point twice. But that renders nothing.
public void DrawPoint(Graphics G, Pen pen, Point point)
{
// add more LineCaps as needed..
int pw2 = (int ) Math.Max(1, pen.Width / 2);
using(var brush = new SolidBrush(pen.Color))
{
if (pen.EndCap == LineCap.Square)
G.FillRectangle(brush, point.X - pw2, point.Y - pw2, pen.Width, pen.Width);
else
G.FillEllipse(brush, point.X - pw2, point.Y - pw2, pen.Width, pen.Width);
}
}

Border in DrawRectangle

Well, I'm coding the OnPaint event for my own control and it is very nescessary for me to make it pixel-accurate.
I've got a little problem with borders of rectangles.
See picture:
removed dead ImageShack link
These two rectangles were drawn with the same location and size parameters, but using different size of the pen. See what happend? When border became larger it has eaten the free space before the rectangle (on the left).
I wonder if there is some kind of property which makes border be drawn inside of the rectangle, so that the distance to rectangle will always be the same. Thanks.
You can do this by specifying PenAlignment
Pen pen = new Pen(Color.Black, 2);
pen.Alignment = PenAlignment.Inset; //<-- this
g.DrawRectangle(pen, rect);
If you want the outer bounds of the rectangle to be constrained in all directions you will need to recalculate it in relation to the pen width:
private void DrawRectangle(Graphics g, Rectangle rect, float penWidth)
{
using (Pen pen = new Pen(SystemColors.ControlDark, penWidth))
{
float shrinkAmount = pen.Width / 2;
g.DrawRectangle(
pen,
rect.X + shrinkAmount, // move half a pen-width to the right
rect.Y + shrinkAmount, // move half a pen-width to the down
rect.Width - penWidth, // shrink width with one pen-width
rect.Height - penWidth); // shrink height with one pen-width
}
}
This isn't a direct answer to the question, but you might want to consider using the ControlPaint.DrawBorder method. You can specify the border style, colour, and various other properties. I also believe it handles adjusting the margins for you.
I guess not... but you may move the drawing position half the pen size to the bottom right

Categories