I have 2 problems while trying to print from a WinForms application. The first is a very very bad quality no matter what I try. The second is that I have a big page margin from the top left corner and the winform is cutting. Any ideas? This is my code:
Bitmap MemoryImage;
public void GetPrintArea(Panel pnl)
{
MemoryImage = new Bitmap(pnl.Width, pnl.Height);
Rectangle rect = new Rectangle(0, 0, pnl.Width, pnl.Height);
pnl.DrawToBitmap(MemoryImage, new Rectangle(0, 0, pnl.Width, pnl.Height));
}
protected override void OnPaint(PaintEventArgs e)
{
if (MemoryImage != null)
{
e.Graphics.DrawImage(MemoryImage, 0, 0);
base.OnPaint(e);
}
}
void printdoc1_PrintPage(object sender, PrintPageEventArgs e)
{
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBilinear;
e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
Rectangle pagearea = e.PageBounds;
e.Graphics.DrawImage(MemoryImage, (pagearea.Width / 2) - (this.panel1.Width / 2), this.panel1.Location.Y);
}
public void Print(Panel pnl)
{
panel1 = pnl;
GetPrintArea(pnl);
printPreviewDialog1.Document = printdoc1;
printPreviewDialog1.ShowDialog();
}
private void button2_Click(object sender, EventArgs e)
{
Print(this.panel1);
}
This comes up over and over again. There just is no magic solution, although eventually the problem is likely to disappear. The emergence of "retina" displays is pivotal.
The core issue is that monitors have a resolution that's drastically worse than printers. A typical printer has a resolution of 600 dots per inch. Which makes it capable of printing 6600 x 5100 individual pixels on a piece of paper. Much, much more than what a monitor can display, a full HD monitor tops out at 1920 x 1080 pixels. Roughly a factor of 5 worse, give or take.
This works out poorly when you print what shows up on a monitor on a piece of paper and try to keep it the same size. Inevitably, because of the lack of pixels on the monitor, each one pixel from the monitor is printed as a 5x5 blob on paper. If you try to keep the pixel mapping one-to-one, you will get a razor-sharp copy on paper. But it has turned into a postage-stamp.
Inevitably, the printout looks very grainy due those pixel blobs. What looks especially poor is text. Operating systems use lots of tricks to make text look good on monitors with poor resolution. Anti-aliasing is standard, tricks like ClearType were designed to take advantage of monitor physics that can help increase the perceived resolution. This no longer works when the text is printed, those anti-aliasing pixels turn into blobs and become very visible, completely ruining the effect. Especially bad for ClearType text on a color printer, the red and blue color fringes now can be clearly seen.
The only decent approach is to render to a printer using the actual resolution and not the monitor resolution. Like using the PrintDocument class in .NET. Using a report generator can help avoid having to write the code for it.
You should draw yourself on the Graphics object you get when the PrintDocument prints. That gives you all the control you need. Still everything Hans Passant said applies here as well...
Keep in mind that this is the simplest implementation that merely demo's what can be achieved, I'm not claiming that this is the easiest/best/most productive way... my code doesn't take multiple pages, controls contained in contaimers or controls not of the type Label and PictureBox.
I used the Draw... methods from System.Drawing.Graphics
sligthly adapted from the code above to get this working:
public void GetPrintArea(Panel pnl, Graphics gr)
{
// scale to fit on width of page...
if (pnl.Width > 0)
{
gr.PageScale = gr.VisibleClipBounds.Width/pnl.Width;
}
// this should recurse...
// just for demo so kept it simple
foreach (var ctl in pnl.Controls)
{
// for every control type
// come up with a way to Draw its
// contents
if (ctl is Label)
{
var lbl = (Label)ctl;
gr.DrawString(
lbl.Text,
lbl.Font,
new SolidBrush(lbl.ForeColor),
lbl.Location.X, // simple based on the position in the panel
lbl.Location.Y);
}
if (ctl is PictureBox)
{
var pic = (PictureBox)ctl;
gr.DrawImageUnscaledAndClipped(
pic.Image,
new Rectangle(
pic.Location.X,
pic.Location.Y,
pic.Width,
pic.Height));
}
}
}
void printdoc1_PrintPage(object sender, PrintPageEventArgs e)
{
e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.HighQuality;
e.Graphics.InterpolationMode =Drawing2D.InterpolationMode.HighQualityBilinear;
e.Graphics.PixelOffsetMode = Drawing2D.PixelOffsetMode.HighQuality;
GetPrintArea(panel1, e.Graphics);
}
You can actually print sharper controls by applying scaling to them on a "vector" level instead of bitmap level.
This snapshot shows the result of the following technique (please don't mind my Win2000-ish UI :-) ):
What we do is to iterate through the control's ControlCollection in a very similar fashion as rene shows in his answer.
But - in addition we apply scale to location, size and font to the control itself before we draw its bitmap to a preset size bitmap which in this case is 5 times bigger (4 times would represent about 300 DPI which is the effective print resolution on most printers).
The reason for this is to keep the thin line on the control sharp on print, or we could just scale the bitmap itself which wouldn't give us any benefit resolution-wise. By scaling font we reduce the anti-alias effect and can provide a better print quality.
To do so you can first in the button's click event setup the following:
//this will produce 5x "sharper" print
MemoryImage = new Bitmap((Panel1.Width * 5), (Panel1.Height * 5));
Using Graphics g = Graphics.FromImage(MemoryImage) {
ScaleControls(Panel1, g, 5);
};
PrintPreviewDialog1.Document = printdoc1;
PrintPreviewDialog1.ShowDialog();
Now in the ScaleControls function, which is recursive, we scale location, size and font in order to make each control itself in higher resolution before we draw them to the bitmap:
private void ScaleControls(Control c, ref Graphics g, double s)
{
//To detach controls for panels, groupboxes etc.
List<Control> hold = null;
foreach (Control ctrl in c.Controls) {
if (ctrl is GroupBox || ctrl is Panel) {
//backup reference to controls
hold = new List<Control>();
foreach (Control gctrl in ctrl.Controls) {
hold.Add(gctrl);
}
ctrl.Controls.Clear();
}
//backup old location, size and font (see explanation)
Point oldLoc = ctrl.Location;
Size oldSize = ctrl.Size;
Font oldFont = ctrl.Font;
//calc scaled location, size and font
ctrl.Location = new Point(ctrl.Location.X * s, ctrl.Location.Y * s);
ctrl.Size = new Size(ctrl.Size.Width * s, ctrl.Height * s);
ctrl.Font = new Font(ctrl.Font.FontFamily, ctrl.Font.Size * 5,
ctrl.Font.Style, ctrl.Font.Unit);
//draw this scaled control to hi-res bitmap
using (Bitmap bmp = new Bitmap(ctrl.Size.Width, ctrl.Size.Height)) {
ctrl.DrawToBitmap(bmp, ctrl.ClientRectangle);
g.DrawImage(bmp, ctrl.Location);
}
//restore control's geo
ctrl.Location = oldLoc;
ctrl.Size = oldSize;
ctrl.Font = oldFont;
//recursive for panel, groupbox and other controls
if (ctrl is GroupBox || ctrl is Panel) {
foreach (Control gctrl in hold) {
ctrl.Controls.Add(gctrl);
}
ScaleControls(ctrl, g, s);
}
}
}
and finally in the event handler for printing:
double scale = MemoryImage.Width / e.PageBounds.Width;
e.Graphics.DrawImage(MemoryImage, 0, 0,
Convert.ToInt32(MemoryImage.Width / scale),
Convert.ToInt32(MemoryImage.Height / scale));
Now, in this example we scale the controls in-place. This is not ideal of course as they will appear to live their own life while we do a print preview.
Ideally we would clone each control as we iterated and discard it after we drew to bitmap. This would also eliminate the need to backup geometries. But for the example of principle I left it as-is. I'll leave it to you to clone etc.
The reason for detaching controls is that if we don't (as the code is now - this can surely be changed by providing another method of iterating, ie. pre-scale cloned controls) the non-scaled in the f.ex. GroupBox control will be printed first, then the scaled ones will show on top of those when they are iterated. This because we DrawToBitmap the GroupBox before scaling its controls. This is something I'll leave for you to handle.
The bitmap we're working on will not necessary fit the resolution of the print that the user end up with from setting up the print dialog, but we get a higher resolution to work with which in turn yield a better result than the poor screen bitmap resolution we have initially.
You would of course need to add support for special cases for other controls than Panel and GroupBox that can hold other controls, image controls and so forth.
I spent days looking for a way to print a panel and its contents in high quality. This didnt work, and I tried other peoples codes and they were all either wrong or just bad quality, until I found this:
http://rkinfopedia.blogspot.com/2008/07/printing-contents-of-panel-control.html
just put the eventhandler inside the print button click event handler, and include the print method in it too, like this:
private void button3_Click(object sender, EventArgs e)
{
printdoc1.PrintPage += new PrintPageEventHandler(printdoc1_PrintPage);
Print(panel1);
}
and place an if statement inside the override OnPaint method, like this:
protected override void OnPaint(PaintEventArgs e)
{
if (MemoryImage != null)
{
e.Graphics.DrawImage(MemoryImage, 0, 0);
base.OnPaint(e);
}
}
rest is fine as is, and youll finally get almost perfect print quality
Just wanted to share this gem, you are welcome internet stranger!
Thank you Mr. Rakesh!
Related
I am trying to make a retro game engine in C#. I want to use a resolution of 320x200, but the screen does not natively support that, so I'm trying to decide what is the most efficient method of emulating that. Do I create a Bitmap object and then use SetPixel and create methods for drawing basic shapes? Then scale the image to the size of the screen. Should I draw little Rectangle objects instead to mimic the pixels? What do you think would be the most efficient? Also any other ideas?
You could just use the DrawImage of the Graphics object to paint your 320x200 bitmap on a rectangle of any size.
By setting the interpolation mode on the graphics object first, you can control the way the image is painted when resized. Different interpolation modes should give different visual results and chances are you will be satisfied with one of built-in modes so that you don't have to provide any custom implementation of the streching algorithm.
On the other hand, have you considered OpenGL/DirectX rather than GDI+?
If you must use GDI+ then let it handle the scaling using this method can leverage hardware acceleration if available as opposed to drawing into bitmaps. But agreed with other poster there are better frameworks for this have a look at XNA.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
var gameTick = new Timer {Interval = 10};
gameTick.Tick += (s, e) => BeginInvoke((Action)(Invalidate));
gameTick.Start();
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
//calculate the scale ratio to fit a 320x200 box in the form
var width = g.VisibleClipBounds.Width;
var height = g.VisibleClipBounds.Height;
var widthRatio = width / 320f;
var heightRatio = height / 200f;
var scaleRatio = Math.Min(widthRatio, heightRatio);
g.ScaleTransform(scaleRatio, scaleRatio);
//draw a 320x200 rectangle (because of scale transform this always fills form)
g.FillRectangle(Brushes.Gray, 0, 0, 320, 200);
}
}
How can I repaint a panel in smooth?
I am using a timer that is invalidating the panel(panel1.Invalidate();) every 300ms, and then with the panel1_Paint event I am adding images to that panel the issue is that it looks like is jumping and I need to be moving one image on it as fast as I can.
This is a link of the screen-cast issue: http://screencast.com/t/HdtIV99YN
private void panel1_Paint(object sender, PaintEventArgs e)
{
PaintMapObstacles(e);
PaintMapStartAndGoal(e);
if (trayectoryIndex < 1000)
{
MapPoint point = GetTrayectoryPoint(0, trayectoryIndex);
e.Graphics.DrawImage(new Bitmap("robot.jpg"), point.X*squareSize, point.Y*squareSize, 60, 60);
trayectoryIndex++;
}
}
private void PaintMapStartAndGoal(PaintEventArgs e)
{
MapPoint start = new MapPoint { X = 0, Y = 0 };
MapPoint goal = new MapPoint { X = 7, Y = 8 };
// e.Graphics.DrawImage(new Bitmap("start.jpg"), start.X * squareSize, start.Y * squareSize, 60, 60);
e.Graphics.DrawImage(new Bitmap("goal.jpg"), goal.X * squareSize, goal.Y * squareSize, 60, 60);
isFirstRun = true;
}
private void PaintMapObstacles(PaintEventArgs e)
{
foreach (MapPoint mpoint in obstacles.Obstacles)
{
e.Graphics.DrawImage(new Bitmap("obstacle.png"), mpoint.X * squareSize, mpoint.Y * squareSize, 60, 60);
}
}
private void PanelTimer_Tick(object sender, EventArgs e)
{
panel1.Invalidate();
}
It is called "flicker", an artifact that's always around when you repaint a window from scratch. It is especially noticeable in your program because your painting code is so inefficient. You see the window's background getting drawn, erasing the old painting. Then slowly getting the bitmaps drawn back onto the background. The erasure step is visible to the eye and looks like flicker.
The general cure for flicker is double-buffering, composing the window content into a bitmap first, then quickly blitting the bitmap to the screen. It is a built-in feature for Winforms, the DoubleBuffered property turns it on. Double-buffering is not enabled by default for the Panel class, it was designed to be a container control that doesn't do painting on its own beyond drawing the background. A PictureBox will work just as well in your case, it has double-buffering enabled by default. Or you can turn on double-buffering for the Panel class, shown here.
You do want to eventually address the problems with your painting code, beyond it being very slow, it can crash your program with an OutOfMemoryException. A problem caused by the way you use the Bitmap class, it should be disposed after you used it. Always use the using statement for System.Drawing objects.
You'll make it a lot faster by creating the bitmaps just once, the form constructor is the best place. You make it really fast by prescaling the bitmaps to fit the grid and paying attention to the pixel format. PixelFormat.Format32bppPArgb is directly compatible with the frame buffer format of almost any modern video adapter, the bitmap can be directly copied into the frame buffer without conversion. Ten times faster than all the other formats. Conversion code is here.
In my program, I'm coding a basic image editor. Part of this allows the user to draw a rectangular region and I pop up a display that shows that region zoomed by 3x or so (which they can adjust further with the mouse wheel). If they right click and drag this image, it will move the zoom region around on the original image, basically acting as a magnifying glass.
The problem is, I'm seeing some serious performance issues even on relatively small bitmaps. If the bitmap showing the zoomed region is around 400x400 it's still updating as fast as mouse can move and is perfectly smooth, but if I mouse wheel the zoom up to around 450x450, it immediately starts chunking, only down to around 2 updates per second, if that. I don't understand why such a small increase incurs such an enormous performance problem... it's like I've hit some internal memory limit or something. It doesn't seem to matter the size of the source bitmap that is being zoomed, just the size of the zoomed bitmap.
The problem is that I'm using Graphics.DrawImage and a PictureBox. Reading around this site, I see that the performance for both of these is typically not very good, but I don't know enough about the inner workings of GDI to improve my speed. I was hoping some of you might know where my bottlenecks are, as I'm likely just using these tools in poor ways or don't know of a better tool to use in its place.
Here are some snippets of my mouse events and related functions.
private void pictureBox_MouseDown(object sender, MouseEventArgs e)
{
else if (e.Button == System.Windows.Forms.MouseButtons.Right)
{
// slide the zoomed part to look at a different area of the original image
if (zoomFactor > 1)
{
isMovingZoom = true;
// try saving the graphics object?? are these settings helping at all??
zoomingGraphics = Graphics.FromImage(displayImage);
zoomingGraphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighSpeed;
zoomingGraphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Low;
zoomingGraphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighSpeed;
zoomingGraphics.PixelOffsetMode = PixelOffsetMode.HighSpeed;
}
}
}
private void pictureBox_MouseMove(object sender, MouseEventArgs e)
{
if (isMovingZoom)
{
// some computation on where they moved mouse ommitted here
zoomRegion.X = originalZoomRegion.X + delta.X;
zoomRegion.Y = originalZoomRegion.Y + delta.Y;
zoomRegionEnlarged = scaleToOriginal(zoomRegion);
// overwrite the existing displayImage to prevent more Bitmaps being allocated
createZoomedImage(image.Bitmap, zoomRegionEnlarged, zoomFactor, displayImage, zoomingGraphics);
}
}
private void createZoomedImage(Bitmap source, Rectangle srcRegion, float zoom, Bitmap output, Graphics outputGraphics)
{
Rectangle destRect = new Rectangle(0, 0, (int)(srcRegion.Width * zoom), (int)(srcRegion.Height * zoom));
outputGraphics.DrawImage(source, destRect, srcRegion, GraphicsUnit.Pixel);
if (displayImage != originalDisplayImage && displayImage != output)
displayImage.Dispose();
setImageInBox(output);
}
// sets the picture box image, as well as resizes the window to fit
void setImageInBox(Bitmap bmp)
{
pictureBox.Image = bmp;
displayImage = bmp;
this.Width = pictureBox.Width + okButton.Width + SystemInformation.FrameBorderSize.Width * 2 + 25;
this.Height = Math.Max(450, pictureBox.Height) + SystemInformation.CaptionHeight + SystemInformation.FrameBorderSize.Height * 2 + 20;
}
private void pictureBox_MouseUp(object sender, MouseEventArgs e)
{
else if (e.Button == System.Windows.Forms.MouseButtons.Right)
{
if (isMovingZoom)
{
isMovingZoom = false;
zoomingGraphics.Dispose();
}
}
}
As you can see, I'm not declaring a new Bitmap every time I want to draw something, I'm reusing an old Bitmap (and the Bitmap's graphics object, though I don't know if there is much cost with calling Graphics.FromImage repeatedly). I tried adding Stopwatches around to benchmark my code, but I think DrawImage passes functionality to another thread so the function claims to be done relatively quickly. I'm trying to Dispose all my Bitmap and Graphics objects when I'm not using them, and avoid repeated calls to allocate/deallocate resources during the MouseMove event. I'm using a PictureBox but I don't think that's the problem here.
Any help to speed up this code or teach me what's happening in DrawImage is appreciated! I've trimmed some excess code to make it more presentable, but if I've accidentally trimmed something important, or don't show how I'm using something which may be causing problems, please let me know and I'll revise the post.
The way I handle issues like that is when receiving the Paint event, I draw the whole image to a memory bitmap, and then BLT it to the window.
That way, all visual flash is eliminated, and it looks fast, even if it actually is not.
To be more clear, I don't do any painting from within the mouse event handlers.
I just set up what's needed for the main Paint handler, and then do Invalidate.
So the painting happens after the mouse event completes.
ADDED: To answer Tom's question in a comment, here's how I do it. Remember, I don't claim it's fast, only that it looks fast, because the _e.Graphics.DrawImage(bmToDrawOn, new Point(0,0)); appears instantaneous. It just bips from one image to the next.
The user doesn't see the window being cleared and then repainted, thing by thing.
It gives the same effect as double-buffering.
Graphics grToDrawOn = null;
Bitmap bmToDrawOn = null;
private void DgmWin_Paint(object sender, PaintEventArgs _e){
int w = ClientRectangle.Width;
int h = ClientRectangle.Height;
Graphics gr = _e.Graphics;
// if the bitmap needs to be made, do so
if (bmToDrawOn == null) bmToDrawOn = new Bitmap(w, h, gr);
// if the bitmap needs to be changed in size, do so
if (bmToDrawOn.Width != w || bmToDrawOn.Height != h){
bmToDrawOn = new Bitmap(w, h, gr);
}
// hook the bitmap into the graphics object
grToDrawOn = Graphics.FromImage(bmToDrawOn);
// clear the graphics object before drawing
grToDrawOn.Clear(Color.White);
// paint everything
DoPainting();
// copy the bitmap onto the real screen
_e.Graphics.DrawImage(bmToDrawOn, new Point(0,0));
}
private void DoPainting(){
grToDrawOn.blahblah....
}
I'm attempting to use a custom built .PNG file of letters from the alphabet to give my game some nice looking textual graphics. However, since the core functionality of my WinForms game works using minimal graphics (I have a timer set to fire a little animation routine that fades out the word "Guessed" when a user enters a word that they've already tried - this is done using DrawString()). Right now I'm trying to get DrawImage(Image, dstRect, srcRect, Unit) to work, and I have it set to run on form load.
private void DrawLetters()
{
// Create image.
Graphics letters = this.CreateGraphics();
Image newImage = Image.FromFile(strPath + "Letter_Map.png");
// Create rectangle for displaying image.
Rectangle destRect = new Rectangle(25, 25, 80, 80);
// Create rectangle for source image.
Rectangle srcRect = new Rectangle(0, 0, 833, 833);
GraphicsUnit units = GraphicsUnit.Pixel;
// Draw image to screen.
letters.DrawImage(newImage, destRect, srcRect, units);
}
This is practically verbatim from the MSDN site. My form populates with an array of Labels right now to show the user the grid of letters needed for the game. Is it them being drawn that overwrites the custom rectangle? I want to replace them all with images eventually. I understand srcRect to be (x, y, width, height) so I gave it the full size of the 'spritesheet'. For dstRect I only want it to place a block 80x80px on the form starting around 25x, 25y.
For the heck of it I created a blank form and called my DrawLetters() function during that form load event (I copied the function into that Form's code). I saw nothing, though, so I am starting to get a bit confused. I may need some education about just HOW the drawing works in conjunction with forms and controls being drawn on screen.
EDIT This https://stackoverflow.com/questions/837423/render-a-section-of-an-image-to-a-bitmap-c-sharp-winforms was what initially got me going, but this user has a working knowledge of XNA and seems to be trying to combine that with WinForms. XNA would be overkill, I believe, for the simple text game I am trying to 'pretty up'.
You need to override the forms OnPaintMethod to get access to the forms Graphics object which you can then use to display the image on the form.
If you want to display a portion of the image, you will need to use a different overload of DrawImage, as follows:
public partial class DrawImageDemo : Form
{
public DrawImageDemo()
{
InitializeComponent();
}
private Image _sprites;
public Image Sprites
{
get
{
if (_sprites == null)
{
_sprites = Image.FromFile("test.jpg");
}
return _sprites;
}
}
protected override void OnPaint(PaintEventArgs e)
{
// The forms graphics object
Graphics g = e.Graphics;
// Portion of original image to display
Rectangle region = new Rectangle(0, 0, 80, 80);
g.DrawImage(Sprites, 25, 25, region, GraphicsUnit.Pixel);
base.OnPaint(e);
}
}
I think I'm missing something trivial here. I derived simple control directly from Control. I'm overriding OnPaint and painting the rectangle (e.Graphics.DrawRectangle)and a text inside it (e.Graphics.DrawString). I did not override any other members.
It paints itself well when the control is resized to the smaller size, but when it gets resized to the larger size, new area is not repainted properly. As soon as I resize it to the smaller size again, even if by one pixel, everything repaints correctly.
OnPaint gets called properly (with appropriate PaintEventArgs.ClipRectangle set correctly to new area), but the new area is not painted (artifacts appear) anyway.
What am I missing?
EDIT:
Code:
protected override void OnPaint(PaintEventArgs e)
{
// Adjust control's height based on current width, to fit current text:
base.Height = _GetFittingHeight(e.Graphics, base.Width);
// Draw frame (if available):
if (FrameThickness != 0)
{
e.Graphics.DrawRectangle(new Pen(FrameColor, FrameThickness),
FrameThickness / 2, FrameThickness / 2, base.Width - FrameThickness, base.Height - FrameThickness);
}
// Draw string:
e.Graphics.DrawString(base.Text, base.Font, new SolidBrush(base.ForeColor), new RectangleF(0, 0, base.Width, base.Height));
}
private int _GetFittingHeight(Graphics graphics, int width)
{
return (int)Math.Ceiling(graphics.MeasureString(base.Text, base.Font, width).Height);
}
Try adding this in your constructor:
public MyControl() {
this.ResizeRedraw = true;
this.DoubleBuffered = true;
}
and in your paint event, clear the previous drawing:
protected override void OnPaint(PaintEventArgs e) {
e.Graphics.Clear(SystemColors.Control);
// yada-yada-yada
}
While ResizeRedraw will work, it forces the entire control to repaint for every resize event, rather than only painting the area that was revealed by the resize. This may or may not be desirable.
The problem the OP was having is caused by the fact that the old rectangle does not get invalidated; only the revealed area gets repainted, and old graphics stay where they were. To correct this, detect whether the size of your rectangle has increased vertically or horizontally, and invalidate the appropriate edge of the rectangle.
How you would specifically go about this would depend on your implementation. You would need to have something that erases the old rectangle edge and you would have to call Invalidate passing an area containing the old rectangle edge. It may be somewhat complicated to get it to work properly, depending what you're doing, and using ResizeRedraw after all may be much simpler if the performance difference is negligible.
Just for example, here is something you can do for this problem when drawing a border.
// member variable; should set to initial size in constructor
// (side note: should try to remember to give your controls a default non-zero size)
Size mLastSize;
int borderSize = 1; // some border size
...
// then put something like this in the resize event of your control
var diff = Size - mLastSize;
var wider = diff.Width > 0;
var taller = diff.Height > 0;
if (wider)
Invalidate(new Rectangle(
mLastSize.Width - borderSize, // x; some distance into the old area (here border)
0, // y; whole height since wider
borderSize, // width; size of the area (here border)
Height // height; all of it since wider
));
if (taller)
Invalidate(new Rectangle(
0, // x; whole width since taller
mLastSize.Height - borderSize, // y; some distance into the old area
Width, // width; all of it since taller
borderSize // height; size of the area (here border)
));
mLastSize = Size;