Printing drawn objects in C# - c#

I have an application that draws object on a Panel using
private void Canvas_Paint(object sender, PaintEventArgs e)
The function draws a user defined amount of rectangles that represent molds of urethane. There are also strings that label the "mold" on the Panel. The Panel is scrollable and, like I said, has a variable (and potentially large) amount of rectangles within it.
I have been asked to provide a printing function for these rectangles. I am completely new to C#, in fact I have never used it before this program. Is there a way to take what I have drawn on the Panel and print them? I have seen some methods of drawing to a PrintDocument, but it looks like I would have to redraw them inside the PrintPage function. I would like to just print what I have already drawn.

The image on the panel should be accessible through the .Image property.
Using a new graphics object, draw that image to the PrintDocument.
Alternatively, you could hold your own reference to the image in the form as you draw it.
However, due to the difference between printers and screens, you're probably going to want to re-render the whole thing anyway using different anti-aliasing settings, etc.

Related

Controlling how, when, and if child controls are drawn (.NET)

I am writing an application in .NET that has a plugin interface. That plugin interface provides several ways to draw information (controls) onto the surface of the application window. While there are several reasons why I am doing this, the main reason is to provide custom colorization to text, either through the use of a graphic or directly manipulating the color of the text based on the background color. I do this through the use of a "text mask" which is a black and white bitmap that works as an "alpha" map to let the Paint method know where to apply the texture/color changes.
The plugin developer has the option of using regular text (such as with a label), mask text (which is drawn to the mask rather than as a regular control), OR letting the user decide. To go along with this, I have provided a modified label class that can either be drawn "normally' (when the text mask is not set for the control), or to the text mask when the User OR Developer decides (depending on what the plugin developer wishes to offer to the user). Here is the class's code so that you understand how this is being done:
public class MaskingLabel : Label
{
private static readonly SolidBrush maskBrush = new SolidBrush(Color.White);
public Bitmap Mask { get; set; }
public MaskingLabel() : base() { }
protected override void OnPaint(PaintEventArgs e)
{
if (Mask == null)
base.OnPaint(e);
else
{
Graphics g = Graphics.FromImage(Mask);
g.DrawString(Text, Font, maskBrush, Location);
}
}
}
The problem I am running into is that this approach requires that I handle controls in a very specific order so that the form is drawn correctly. I need to find the most efficient approach to get the tasks listed below done in the order given. I have thought of three possibilities discussed further down. For reference, this is the order in which tasks must be done:
All "MaskingLabel" controls that have the bitmap object set to the mask must be drawn first so that the mask is created before the next step.
The mask is applied to the background picture.
The resulting Bitmap is drawn in a way similar to the way a background would be drawn (except that it is modified first).
The rest of the controls are drawn as normal.
Is there a way for me to insure this happens without separating the controls manually? My first guess is no. As such, I have a few guesses below about how I should go about this. I was hoping someone with more in depth knowledge of GDI+ could offer some insight.
One idea that has occurred to me is to draw the masked controls during the OnPaintBackground method. However, I don't want to waste time by painting the controls twice. This means I would need to filter out which controls are drawn during the main Paint method which effectively leads us to option 2 (FAIK):
I can manually filter out the controls which draw to the mask so that they don't get added to the control. My question here though is would they get drawn at all? Can I manually force them to invoke the OnPaint method?
If doing that wouldn't work, then perhaps I can create a separate derived panel control to serve as a "backdrop" child control that acts as the background picture which can be forced to be drawn first?
EDIT (With Part of the answer):
I realized after posting this that I already have part of the solution built into my project. Still, I think it is a legitimate question to ask, so if anyone can add insight beyond what I have done in my description below, it is welcome.
Specifically, my project has only two controls that are added to the "root" form: a bar that goes to the top (docked at the top when it is shown), and a transparent panel that occupies the rest of the space (with a dock style set to fill). So my solution would be to add the mask controls to the main form and add all the rest to the panel. This only leaves one remaining issue to be resolved: How do I make sure that the panel and the bar are drawn last? (As part of step 4 in the first list?)

Assign event to custom drawn shape

How do I draw a line and assign events to it? I would like to draw a custom shape, but it should act like a normal Control, and have properties and events. E.g.:
button1_Click(object sender, EventArgs e)
{
DrawLine(width, height, location, location, color, panelToDrawShapeOn, nameThisShape);
}
nameThisShape_Click(object sender, EventArgs e)
{
MessageBox.Show("Click event raised.");
}
private void DrawLine(int width, int height, int location, int location, Color color, Panel panel, string controlName)
{
// Code to draw shape and setup events for it.
}
To confirm, I do know how to draw shapes using GDI+ but the problem is, they are static, and I can't "interact" with them, and no amount of searching has led me to the right place to find out how to interact with the shapes that I draw.
You're not going to be able to treat custom-drawn shapes as you would controls. As you've found, GDI+ is an immediate mode graphics system (as opposed a retained mode system). This means that if you want a persistent scene graph full of shapes to be rendered, you need to create and manage that yourself. Then, you would hook the events of interest on the control that's the drawing target and handle them by doing hit tests on your list of renderable objects (e.g., to find what shape, if any, the mouse is over).
Writing that code can be a lot of work, but you can find libraries to help you. For instance, in one of my work projects, we used a computational geometry library called JTS for the geometry representation and hit test code. If you want to avoid third-party libraries, you may get part of the way there with the Region class, which will at least give you hit tests.

Redraw panel contents after ClientSizeChanged

So my application runs in fixed size window and in full screen. The problem I'm facing is how to properly scale the current contents of the panel (which depend on the application use) when the window is resized. This is my current code:
private void Form1_ClientSizeChanged(object sender, EventArgs e)
{
System.Drawing.Drawing2D.Matrix transformMatrix = new System.Drawing.Drawing2D.Matrix();
float px = panel2.Width;
float py = panel2.Height;
panel2.Width = this.Width / 2;
panel2.Height = panel2.Width;
panel2.Location = new Point(this.Width - panel2.Width - 30, 30);
transformMatrix.Scale(panel2.Width / px, panel2.Height / py);
panel2.Region.Transform(transformMatrix);
//Rest of the code
}
But the drawn content doesn't scale, and if I use Invalidate() or Refresh() the drawn content gets cleared (the panel is redrawn empty). What am I missing?
.NET doesn't remember what's drawn on the panel, as simple as that. As soon as anything invalidates the windows bitmap buffer (causing a WM_PAINT), it's going to be repainted again. So, you have to draw what you want to draw using the Paint event (or overriding OnPaint).
However, there is another way that might be easier to implement - don't paint into a Panel. Instead, paint into a PictureBox (or rather, a Bitmap assigned to the Image property of the PictureBox). The Bitmap will be reused when invalidating (and redrawing) the picture box, so nothing will be lost. By using PictureBox.ScaleMode, you can define how you want the picture box to scale the bitmap, and it will do so as well as it can.
In any case, transforming the Region property doesn't do anything useful - you're simply changing the region, not doing anything to the drawing itself. To use 2D transformation matrices, you want to apply them on a Graphics object during the drawing (in Paint handler or OnPaint override) - drawing anything on the Graphics object will then transform everything you're trying to draw, which in your case means scaling the painting.
So you have to decide: do you want to just scale a stored bitmap with the painted image, or do you want to redraw it all from scratch (which also means you can pick any level of detail you can provide)?
I think that you're mistaking what the Region property is meant for. According to the MSDN docs (empasis mine, replace 'window' with 'control' when reading):
The window region is a collection of pixels within the window where the operating system permits drawing. The operating system does not display any portion of a window that lies outside of the window region. The coordinates of a control's region are relative to the upper-left corner of the control, not the client area of the control.
All that you're doing is changing the region that the OS will allow painting, which explains why you're not seeing anything. I think that you should be resizing the control when the form is resized, either through Anchor, or through my preference of Dock with several controls, or a panel like TableLayoutPanel where it will handle scaling and relative sizing for you.
Thank you for your answers, but I wrote my own function and logic that serves the purpose for this application. Basically the function checks for the state of the application variables, and calls the appropriate function that originally drew the content, and since those functions use the panel width and height as arguments they properly scale the drawn content and retain the drawing composition.
P.S. I'll accept Luaan's answers since it offers a valid alternative and is complete.

How to get the exact regions that need to be draw in OnPaint() event?

In a WinForm application, when subscribing to OnPaint() event, PaintEventArgs provide a ClipRectangle property that define the region to be drawn.
When the form is resized vertically or horizontally, it gives the minimum rectangle to be draw.
But when window is resized in both directions, there several regions that need to be draw (one on right, one at bottom) and OnPaint event merge them. It results in a rectangle having same size as Form (thus everything is redraw). What i'd like to have is individual regions separately (the two rectangles on the picture)
I know that GDI+ automatically clips what doesn't need to be draw (things are outside the two rectangles, not just ClipRectangle) but i'd like to minimize GDI+ calls at maximum (i already have performance issues when drawing in OnPaint event because of many GDI+ calls, this is not premature optimisation)
Painting in Windows is initiated by the WM_PAINT message handler. It must call BeginPaint() to get info about what needs to be painted. Which returns a struct of type PAINTSTRUCT, it looks like this:
typedef struct tagPAINTSTRUCT {
HDC hdc;
BOOL fErase;
RECT rcPaint; // <=== here
BOOL fRestore;
BOOL fIncUpdate;
BYTE rgbReserved[32];
} PAINTSTRUCT, *PPAINTSTRUCT;
The rcPaint member is the one that you get from Graphics.ClipRectangle. The Graphics.Clip and Graphics.ClipBounds properties are not relevant, they only work if you intentionally clip yourself by assigning the Clip property.
Clearly Windows itself does not let you find out what you are asking for. rcPaint is a RECT, a simple rectangle. Windows only keeps track of a dirty rectangle, not a region. New rectangles added by InvalidateRect() are merged with the existing one and you'll indeed easily end up with the entire client area.
The only reasonable way to tackle this problem is to pay attention to the ResizeBegin and ResizeEnd events. When you get ResizeBegin then you know that the user is dragging a window edge or corner. Knowledge that you can use to optimize the painting, skipping expensive bits that make the modal resizing loop work poorly.

Drawing an image onto a Panel control gives artefacts when resizing

Currently I'm trying to do what I thought would be a simple task:
Draw an image onto the full area of a Panel control in Windows Forms. (Please ignore for the moment that I could use the BackgroundImage property)
The image to draw looks like this:
I.e. a yellow box with an 1 pixel blue frame around.
To draw, I'm using the Paint event of the Panel control:
private void panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.DrawImage(Resources.MyImage, panel1.ClientRectangle);
}
This looks fine when initially displaying the form:
When resizing the form (and the docked panel, too), it either cuts the edges when being made smaller...
...or it draws artefacts, when being made larger:
I'm pretty sure that there is going on something rather simple and straight-forward but I really cannot understand the reason.
Since I'm ignoring the ClipRectangle and always draw everything, I thought the image would be scaled all the time.
My questions are:
What is the reason for the artefacts? (I love to understand this!)
What do I have to do in order to get rid of the artefacts? (beside calling Invalidate on each resize)
Update, SOLUTION:
Thanks to Ryan's answer, I was able to find an acceptable solution. Basically I derived a class from Panel, did an override of OnPaintBackground and did not call the base method. Last, I added the following code to the constructor of my derived panel:
base.DoubleBuffered = true;
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.ResizeRedraw, true);
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
UpdateStyles();
The reason for the artefacts is that the entire surface isn't redrawn when the form is resized; only the necessary parts are. The generally best solution is what you don't want to do, calling Invalidate on each resize. However, if this is in fact your situation, just use a PictureBox instead. If it's not, you might consider overriding OnPaint in your form instead, and using this.SetStyle(ControlStyles.ResizeRedraw, true) to do this automatically.

Categories