How can I preserve the painting I drew on a picturebox?
I draw a circle, and fill it via the ExtFloodFill API.
This works fine.
When I resize the form (or minimize it) and resize it back to its original size part of the painting is gone.
When I refresh the picturebox the painting will be gone completely
I tried to repaint it in the Paint event, but this caused it to be repainted continuously as the painting itself triggered the Paint event as well.
See below for a test project.
When you click on the picturebox the painting will be drawn.
When you doubleclick the picturebox will be refreshed.
[1 form with 1 picturebox named pictureBox1]
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace FloodFill
{
public partial class Form1 : Form
{
[DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateSolidBrush(int crColor);
[DllImport("gdi32.dll")]
public static extern bool ExtFloodFill(IntPtr hdc, int nXStart, int nYStart, int crColor, uint fuFillType);
[DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll")]
public static extern int GetPixel(IntPtr hdc, int x, int y);
public static uint FLOODFILLSURFACE = 1;
public Form1()
{
InitializeComponent();
}
private void pictureBox1_Click(object sender, EventArgs e)
{
DrawCircle();
FillGreen();
}
private void DrawCircle()
{
Graphics graBox = Graphics.FromHwnd(pictureBox1.Handle);
graBox.DrawEllipse(Pens.Red, 10, 10, 100, 100);
}
private void FillGreen()
{
Graphics graBox = Graphics.FromHwnd(pictureBox1.Handle);
IntPtr ptrHdc = graBox.GetHdc();
IntPtr ptrBrush = CreateSolidBrush(ColorTranslator.ToWin32(Color.Green));
SelectObject(ptrHdc, ptrBrush);
ExtFloodFill(ptrHdc, 50, 50, GetPixel(ptrHdc, 50, 50), FLOODFILLSURFACE);
DeleteObject(ptrBrush);
graBox.ReleaseHdc(ptrHdc);
}
private void pictureBox1_DoubleClick(object sender, EventArgs e)
{
pictureBox1.Refresh();
}
}
}
How can I preserve the painting I make when my form or the picturebox is resized or in any other way to be refreshed?
[EDIT]
I changed my Paint event to the following:
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
DrawCircle();
FillGreen();
}
And now the circle is being redrawn after a resized, but the FloodFill isn't
(I also gave the picturebox a lightblue background for another test)
[EDIT2]
I changed the Paint event to use the Graphics g as follows:
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
DrawCircle(g);
FillGreen(g);
}
private void DrawCircle(Graphics g)
{
g.DrawEllipse(Pens.Red, 10, 10, 100, 100);
}
private void FillGreen(Graphics g)
{
IntPtr ptrHdc = g.GetHdc();
IntPtr ptrBrush = CreateSolidBrush(ColorTranslator.ToWin32(Color.Green));
SelectObject(ptrHdc, ptrBrush);
ExtFloodFill(ptrHdc, 50, 50, GetPixel(ptrHdc, 50, 50), FLOODFILLSURFACE);
DeleteObject(ptrBrush);
g.ReleaseHdc(ptrHdc);
}
But when I resize back to the original size some lines of the FloodFill are skipped, especially when I resize slowly
Using GDI+ methods for drawing the code is simply:
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
Rectangle rect = new Rectangle(10, 10, 100, 100);
e.Graphics.FillEllipse(Brushes.Green, rect);
using (Pen pen = new Pen(Color.Red, 2f))
{
e.Graphics.DrawEllipse(pen , rect);
}
}
None of your DllImport or constants is needed at all.
Note that I chose to use a Pen with a width of 2f to demonstrate the use of the using clause to correctly create and dispose of the GDI+ (non-standard) object Pen
This will persist as it is drawn whenever needed. To draw it initially you must call pictureBox1.Invalidate(); once!
If you want to change the coordinates you should move the variable rect to class level, set it to new data and the call Invalidate on the PictureBox! The same goes for Colors or the Pen.Width: use class level variables and after each change trigger the Paint event by calling Invalidate()..
Once you understand that, the most important part of learning drawing in GDI+ is done..
For filling any drawings you have three options:
simple shapes draw with DrawXXX methods all have a FillXXX method.
complex shapes can be created with a GraphicsPath, which also has both a DrawXXX methods and a FillXXX method.
areas not created by shapes but by drawn pixels must be filled with a floodfill method. There is non buit-in in GDI+, but can simply write your own. maybe like the Fill4 in this answer..
Update: If you feel better using the FloodFill from gdi32.dll you can do so, but please change the code to use the Graphics object from the Paint event:
FillGreen(e.Graphics);
private void FillGreen(Graphics graBox)
{
IntPtr ptrHdc = graBox.GetHdc();
IntPtr ptrBrush = CreateSolidBrush(ColorTranslator.ToWin32(Color.Green));
SelectObject(ptrHdc, ptrBrush);
ExtFloodFill(ptrHdc, 50, 50, GetPixel(ptrHdc, 50, 50), FLOODFILLSURFACE);
DeleteObject(ptrBrush);
graBox.ReleaseHdc(ptrHdc);
}
for better flexibility you can probably make the params all dynamic, but I'm not used to that old library..
Note that the order of calls matters: FillXXX wil cover some of the pixels drawn by DrawXXX, so it must come first. FloodFill however depends on the bounding lines, in your case the circle, being drawn first, so it must come after..
Related
I have a program which is drawing lots of lines onto a screen (once per frame, not all in one Paint() call), using the LinearGradientBrush class. The endpoints and colors may be different every time. The code is below:
using (Brush brush = new LinearGradientBrush(oldLoc, newLoc, oldColor, newColor))
{
using (Pen pen = new Pen(brush))
{
g.DrawLine(pen, oldLoc, newLoc);
pen.Dispose();
}
brush.Dispose();
}
GC.Collect();
This works for a while but invariably runs out of memory and crashes. I've tried it with and without the Dispose() calls, and with and without the GC.Collect(), and it doesn't seem to make a difference. I would have thought that between the using blocks, Dispose() and GC.Collect(), there would be something that clears the memory being allocated, but it doesn't seem to work.
I can't create the brush in advance and reuse it because the endpoints and colors will be different every time. I've tried saving one LinearGradientBrush and just changing its parameters each time I want to use it - this works for the colors, but not for the endpoints. I can apparently look at the Rectangle property of the brush, but it is readonly so it can't be changed. Is there a better way to draw a gradient line that doesn't depend on knowing where the start and end points are? In theory, it'd make sense to have a gradient from "start of line" to "end of line" which doesn't need to know where those are when the brush is created.
(Note, this is not a duplicate of c# GDI+, Creating a LinearGradientBrush in a loop (memory leaks) because that question has predictable start and end points and colors, and mine need to change every time.)
Edit: Here's the loop-handling code, copied from somewhere, I forget, but it might be relevant:
void HandleApplicationIdle(object sender, EventArgs e)
{
while (IsApplicationIdle())
{
Update();
}
}
[StructLayout(LayoutKind.Sequential)]
private struct NativeMessage
{
public IntPtr Handle;
public uint Message;
public IntPtr WParameter;
public IntPtr LParameter;
public uint Time;
public Point Location;
}
[DllImport("user32.dll")]
private static extern int PeekMessage(out NativeMessage message, IntPtr window, uint filterMin, uint filterMax, uint remove);
bool IsApplicationIdle()
{
return PeekMessage(out _, IntPtr.Zero, 0, 0, 0) == 0;
}
The code above is called from the Update() function here.
I'm able to draw vertical line anywhere on the screen with following code:
[DllImport("User32.dll")]
public static extern IntPtr GetDC(IntPtr hwnd);
[DllImport("User32.dll")]
private void Form2_Load(object sender, EventArgs e)
{
// DRAWING
IntPtr desktopPtr = GetDC(IntPtr.Zero);
Graphics f = Graphics.FromHdc(desktopPtr);
Pen pen = new Pen(Color.FromArgb(255, 255, 111, 0));
f.DrawLine(pen, this.Location.X, this.Location.Y, this.Location.X, 100);
f.Dispose();
ReleaseDC(IntPtr.Zero, desktopPtr);
}
Above sample code draw the vertical line on coordinates where form is open but however this line disappear when i move anything over it (cursor, form, etc).
I want to keep this line until application is closed.
Next what i Want to achieve is make this line movable with mouse move events. Is it possible?
My goal is very simple. Imagine opening MSPaint, clicking the line tool, holding mouse down, and dragging it around. It anchors the starting coordinates where you clicked mouse down and constantly draws and redraws a line to your current position.
Except me trying to do this in C# isn't working as well as I would hope.
[DllImport("user32.dll")]
static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("User32.dll")]
static extern int ReleaseDC(IntPtr hwnd, IntPtr dc);
protected override void OnPaint(PaintEventArgs e)
{
endingPoint = GetMouseCoords();
DrawLine(startingPoint, endingPoint);
}
private void DrawLine(Point startingCoords, Point endingCoords)
{
IntPtr desktop = GetDC(IntPtr.Zero);
Pen pen = new Pen(Brushes.Red, 3);
using (Graphics g = Graphics.FromHdc(desktop))
{
g.DrawLine(pen, startingCoords.X, startingCoords.Y, endingCoords.X, endingCoords.Y);
g.Dispose();
}
ReleaseDC(IntPtr.Zero, desktop);
}
Using it this way, I only get the line drawn once. However, if I move the DrawLine() to a more static event like MouseUp, it will draw it, then disappear after about a quarter of a second.
What would be the best way to accomplish my goal here?
I would think that whatever event is being used to make the line disappear is what I would want to attach the drawing of the line to in the first place.
You need to have two drawing calls:
One for the non-persistent line that follows the cursor in the MouseMove using someControls.CreateGraphics
the other for the persisting line, triggered in the MouseUp, where
you store the coordinates and
call Invalidate on your canvas control and
draw in the Paint event of your canvas using its e.Graphics object.
Here is a minimal example code:
List<Point> allPoints = new List<Point>();
Point mDown = Point.Empty;
private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
mDown = e.Location;
}
private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
allPoints.Add(e.Location);
pictureBox1.Invalidate();
}
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button.HasFlag(MouseButtons.Left))
{
pictureBox1.Refresh();
using (Graphics G = pictureBox1.CreateGraphics())
G.DrawLine(Pens.Red, mDown, e.Location);
}
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (allPoints.Count > 1) e.Graphics.DrawLines(Pens.Black, allPoints.ToArray());
}
Note that this uses a PictureBox as the canvas control. It is the control meant for this kind of interaction. Your code seems to draw onto the desktop, which doesn't belong to you. Drawing onto it in a persistent manner is not anything like what you would do with the/any Paint application.
Also note that my example stores a list of points and draws them as one non-closed Polyline. To draw them closed exchange DrawLines for DrawPolygon! To draw several such polylines or polygons you need to..
..decide on the user interface for it, maybe add segment points only while the control-key is pressed and otherwise finish the current polyline
store the points in a List<List, Point>>
Also note that this is one of the rare examples where using control.CreateGraphics is called for, as you actually do want a non-persistent drawing while the user moves the mouse.
In most other cases the Winforms graphics basic rule #1 applies:
Never use control.CreateGraphics! Never try to cache a Graphics object! Either draw into a Bitmap bmp using a Graphics g = Graphics.FromImage(bmp) or in the Paint event of a control, using the e.Graphics parameter..
So I created a simple form to test out using an SVG image to draw a custom shaped window. Inspiration found here
It seems to work fine, but no matter what I do my window size is too small to put any controls on.
Reasons for doing this: It's cool? Windows needs better themeing support. I was bored!
I am using Svg from nuget.com from within Visual Studio
Code:
using Svg;
public const int WM_NCLBUTTONDOWN = 0xA1; public const int HT_CAPTION = 0x2;
internal class NativeMethods
{
// Allows forms with Toolbox property set to false to be moved
[System.Runtime.InteropServices.DllImportAttribute("user32.dll", CharSet = CharSet.Unicode)]
internal static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
[System.Runtime.InteropServices.DllImportAttribute("user32.dll", CharSet = CharSet.Unicode)]
internal static extern bool ReleaseCapture();
}
public Form1()
{
InitializeComponent();
}
protected override void OnPaint(PaintEventArgs e)
{
System.Drawing.Drawing2D.GraphicsPath frmshp = new System.Drawing.Drawing2D.GraphicsPath();
//frmshp.AddEllipse(0, 0, this.Width, this.Height);
//SvgDocument.Open(#"TestWindowsshape.svg");
SvgDocument newshp = SvgDocument.Open(#"TestWindowsshape.svg");
frmshp = (newshp.Path);
this.Region = new Region(frmshp);
}
private void Form1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
// Make settings window movable without a titlebar
if (e.Button == MouseButtons.Left)
{
NativeMethods.ReleaseCapture();
NativeMethods.SendMessage(Handle, WM_NCLBUTTONDOWN (IntPtr)HT_CAPTION, (IntPtr)0);
}
}
I have tried to increase the svg size, played with the code some, but nothing I do will make the drawn window bigger. I know I can do this with a BMP, and the TransparancyKey option, but I would like to not do it that way, since the BMP & transparency method has the drawback of not being able to use one color in the bitmap itself. Any advice would be appreciated
Edit:
Matrix m = new Matrix();
m.Scale(100, 100, MatrixOrder.Append);
m.Translate(100, 100, MatrixOrder.Append);
newshp.Path.Transform(m);
Has been tried, with no effect. I would assume that this should have worked does that mean the problem is within my SVG?
The problem seems to be in the SVG file, i adjusted the size of a Rectangle in Notepad++ and got a bigger windows, however more complex shapes will be a hassle. It seems Inkscape cannot create SVGs of a reliable size... I have the "Document properties" set to my screen resolution, but the vectors all turn out too small. Perhaps Illustrator can do this properly.
I am trying to create a program which gets the handle of the window under your cursor, show's some data about it and draws a filled rectangle (with very low alpha) on top of the whole window. I am using C# and winforms.
I have succeeded in doing so, but the problem is my draw method is in a BackgroundWorker's loop and it keeps making more and more rectangles (-> rectangle with higher alpha) on the window or when moving mouse to another window the old one still exists.
I haven't managed to find a method to clear the drawn rectangle as it just "is" on the screen and isn't bound to the graphics object or anything.
I have tried using certain native methods such as
[DllImport("User32.dll")]
public static extern Int64 SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
public static extern bool InvalidateRect(IntPtr hWnd, IntPtr lpRect, bool bErase);
[DllImport("user32.dll")]
public static extern bool UpdateWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, RedrawWindowFlags flags);
but none of the above has worked correctly. Some of them do work but as the messages get in the queue the redrawing doesn't occur immediately or is very slow and glitched (flickering etc).
So, the question is, how would I "remove" the rectangle I have drawn using Graphics.FromHwnd(handleOfWindowUnderCursor)? I actually think it doesn't matter that it is drawn on other window as I have had the very same problem earlier when trying to get rid of the drawings on my own form too (never got that fixed either!).
Alternatively, any suggestions on how I could accomplish drawing and removing the rectangle on the window under cursor without using the methods I am now?
I noticed that drawing using
Graphics g = Graphics.FromHwnd(form.Handle);
draws on the form background, under its controls. Is it whatyou want to acomplish?
// draw the rectangle
Brush b = new SolidBrush(Color.FromArgb(20, 0, 0, 255));
g.FillRectangle(b, new Rectangle(5, 5, 200, 200));
// clear the rectangle
g.Clear(this.BackColor);
If I draw on the screen directly, with this:
Graphics g = Graphics.FromHwnd(IntPtr.Zero);
the rectangle disapears immediately after Windows refreshes the screen.
There is a third option, which is not realy strightforward.
Instead of drawing a rectangle, create a form with lowered opacity, TopMost property set to true and without borders. Then make it transparent to events:
protected override void WndProc(ref Message m)
{
const int WM_NCHITTEST = 0x0084;
const int HTTRANSPARENT = (-1);
if (m.Msg == WM_NCHITTEST)
{
m.Result = (IntPtr)HTTRANSPARENT;
}
else
{
base.WndProc(ref m);
}
}
The only things you have to take care after that is this form's Visible, Location and Size properties.
bool change = false;
private void timer1_Tick(object sender, EventArgs e)
{
try
{
if (change)
{
InvalidateRect(IntPtr.Zero, IntPtr.Zero, true);
change = false;
}
else
{
PaintRectangleToScreen();
change = true;
}
}
catch (System.Exception caught)
{
MessageBox.Show(caught.Message);
}
}