Distorted image when scrolling in DoubleBufferPanel

M

moss

Hi,
i have a UserControl called DoubleBufferPanel which is a typical Panel
but is set to be DoubleBuffered by the SetStyle() method. I draw my
images on that Panel and no clipping occurs, but when I set it to
AutoScroll and begin scrolling, the image becomes distorted like this:
http://www.flickr.com/photos/37459057@N04/3882558185/

When I resize the form which contains the panel and when I size the
image to fit the panel's dimensions everything is ok, but when I
scroll in any direction this kind of distortion keeps appearing. I
used this code to set the AutoScroll and draw the image:

this.imagePanel.AutoScroll = true;
this.imagePanel.AutoScrollMinSize = MyBitmap.Size;
g.DrawImage(MyBitmap, new RectangleF
(this.imagePanel.AutoScrollPosition.X, this.AutoScrollPosition.Y,
MyBitmap.Width, MyBitmap.Height));

What is the cause of this problem and how can it be solved?
 
P

Peter Duniho

Hi,
i have a UserControl called DoubleBufferPanel which is a typical Panel
but is set to be DoubleBuffered by the SetStyle() method. I draw my
images on that Panel and no clipping occurs, but when I set it to
AutoScroll and begin scrolling, the image becomes distorted like this:
http://www.flickr.com/photos/37459057@N04/3882558185/

The "distortion" is simply bits of your image being left in an undrawn
area of the control.

By the way, I could see inherited Panel (to get the scrollbars
automatically), but I don't see the point in inheriting UserControl, nor
is it clear from your question whether you are actually inheriting Panel
or UserControl.
When I resize the form which contains the panel and when I size the
image to fit the panel's dimensions everything is ok, but when I
scroll in any direction this kind of distortion keeps appearing. I
used this code to set the AutoScroll and draw the image:

this.imagePanel.AutoScroll = true;
this.imagePanel.AutoScrollMinSize = MyBitmap.Size;
g.DrawImage(MyBitmap, new RectangleF
(this.imagePanel.AutoScrollPosition.X, this.AutoScrollPosition.Y,
MyBitmap.Width, MyBitmap.Height));

What is the cause of this problem and how can it be solved?

Impossible to say for sure without a concise-but-complete code example
that reliably demonstrates the problem. There are too many possible
reasons for this kind of redraw problem to occur. But, I suspect based on
the code and picture that you've shown that you aren't limiting scrolling
to having the bottom edge of the bitmap appear no higher than the bottom
edge of the control, and thus when you redraw, nothing that would erase or
otherwise redraw the area not part of the bitmap happens, so you just get
the left-over bits from the stuff that was drawn before.

If so, the solution would be to either make sure something is drawn there
(e.g. erase to the background color) or limit scrolling so that the image
never is scrolled far enough for there to be parts of the control not
drawn when you draw the image.

If none of that seems to apply, you should post a concise-but-complete
code example that reliably demonsrates the problem. That way, it would be
possible for someone to know what the code is actually doing, and thus
have some idea as to how to fix it.

Pete
 
M

moss

By the way, I could see inherited Panel (to get the scrollbars  
automatically), but I don't see the point in inheriting UserControl, nor  
is it clear from your question whether you are actually inheriting Panel  
or UserControl.

I created a user control DoubleBufferPanel that inherits from
UserControl, not Panel,
because I wanted the control to be DoubleBuffered (to get rid of image
clipping),
AFAIK the only way that could be done is by inheriting from
UserControl.
I tried "manual" double buffering (from bobpowell.net) but that didn't
work,
so I created the control, set it's DoubleBuffered property to true,
added a Panel
to the user control, both control and Panel are set to AutoScroll =
false; by default
(I enabled that property in the code you saw before), Panel Anchor and
Dock
are set to None. When I put the control on the form I set it's Dock
property to Fill,
all other properties are defaults.

This is how the class looks like:

public partial class DoubleBufferPanel : UserControl
{
public DoubleBufferPanel()
{
this.SetStyle(
ControlStyles.ResizeRedraw |
ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer, true);
}
}

Any ideas?
 
P

Peter Duniho

I created a user control DoubleBufferPanel that inherits from
UserControl, not Panel,
because I wanted the control to be DoubleBuffered (to get rid of image
clipping),
AFAIK the only way that could be done is by inheriting from
UserControl.

The DoubleBuffered property comes from the Control class. It's present in
both Panel and UserControl. There's no specific need for you to inherit
UserControl just to get the property.
I tried "manual" double buffering (from bobpowell.net) but that didn't
work,

It should have worked fine, but you also should just be able to set the
DoubleBuffered property. Without a code example for either attempt you
made, it's not possible to say how the code might have been wrong.
[...]
This is how the class looks like:

public partial class DoubleBufferPanel : UserControl
{
public DoubleBufferPanel()
{
this.SetStyle(
ControlStyles.ResizeRedraw |
ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer, true);
}
}

Any ideas?

Well, that's not a concise-but-complete code example, so no...nothing too
specific.

I will point out that it should be sufficient to just set DoubleBuffered
to true. I have done that in the past and it's worked fine, without
messing with the control styles. I will also point out that at least part
of your problem is probably related to setting AllPaintingInWmPaint, if
not entirely. This tells .NET that there's no need to erase the control
background before a repaint, because the Paint event will handle all the
drawing. So if you fail to draw to the entire control in the handling of
the Paint event, you get left-over "cruft" like what you're seeing.

My previous suggestions, based on what conjecture I'm forced to make given
the lack of a proper code example, still hold: either fix the scrolling so
that you never have an area in the control that isn't displaying a portion
of the bitmap, or make sure you draw _everywhere_ in the control, even the
area outside of where the bitmap is being displayed.

And again, if those suggestions don't seem helpful to you, you need to
post a proper code example so that it's possible to see exactly what
you're doing, what effect that has, and how to fix it.

Pete
 
M

moss

I sent you an e-mail with full code so you can check what's wrong.
Thanks a lot for your help ;)
 
J

Jeff Johnson

This is the third group I have seen this question multiposted to.

[Canned response]

You have posted this question individually to multiple groups. This is
called Multiposting and it's BAD. Replies made in one group will not be
visible in the other groups, which may cause multiple people to respond to
your question with the same answer because they didn't know someone else had
already done it. This is a waste of time.

If you MUST post your message to multiple groups, post a single message and
select all the groups (or type their names manually, separated by commas) in
which you want it to be seen. This is called Crossposting and when used
properly it is GOOD.

(This isn't just my opinion. Look here:
http://www.oakroadsystems.com/genl/unice.htm#xpost)
 
M

moss

This is the third group I have seen this question multiposted to.

[Canned response]

You have posted this question individually to multiple groups. This is
called Multiposting and it's BAD. Replies made in one group will not be
visible in the other groups, which may cause multiple people to respond to
your question with the same answer because they didn't know someone else had
already done it. This is a waste of time.

If you MUST post your message to multiple groups, post a single message and
select all the groups (or type their names manually, separated by commas) in
which you want it to be seen. This is called Crossposting and when used
properly it is GOOD.

(This isn't just my opinion. Look here:http://www.oakroadsystems.com/genl/unice.htm#xpost)

Yeah, I'm sorry about this, just recently I've started using google
groups for posting on usenet and forgot I can also crosspost here
(totally stupid I know). Until now I have been using 40tude Dialog and
I always crossposted in these situations. I promise you this won't
happen again.
 
P

Peter Duniho

I sent you an e-mail with full code so you can check what's wrong.
Thanks a lot for your help ;)

Please don't do that. My participation in this newsgroup is predicated on
the idea that when I ask or answer a question, I do so publicly so that
the answer to the question can benefit more than just the person who asked
it (whether that's me or someone else).

Taking your question "offline" so to speak, by sending it directly to me
is already inappropriate, and failing to convert your code into a proper
concise-but-complete code example (which would be perfectly appropriate to
post in the newsgroup) demonstrates an unwillingness to put the correct
level of effort into your question yourself.

If I have time, I might put together a concise-but-complete code example
that demonstrates the basic techniques you should be using to solve your
problem. But you should probably go ahead and refine your question so
that you can post a proper code example, in case I don't find the time for
the example, and in any case you should not expect that you can somehow
improve the chances of getting a solution for your problem by directing
follow-ups for your question to a particular person rather than posting it
here.

Pete
 
M

moss

Taking your question "offline" so to speak, by sending it directly to me  
is already inappropriate, and failing to convert your code into a proper  
concise-but-complete code example (which would be perfectly appropriate to  
post in the newsgroup) demonstrates an unwillingness to put the correct  
level of effort into your question yourself.

Peter, I'm not unwilling to "put the correct level of effort" into my
question, I simply don't know what should I put as a concise-but-
complete code example in this case! :) As you have said, anything
could be wrong, so I'll put the imagePanel Paint event handler and the
function GenerateImageDimensions() which is used for resizing the
image when in SizeToFit mode...

imagePanel Paint event handler:

if (MyBitmap != null)
{
try
{
Graphics g = e.Graphics;
g.InterpolationMode =
InterpolationMode.HighQualityBicubic;

// if the image fits inside the panel
if (MyBitmap.Width <= imagePanel.Width &&
MyBitmap.Height <= imagePanel.Height)
{
this.imagePanel.AutoScroll = false;

if (CenterImagesInWindow)
{
MyPoint.X = (imagePanel.Width -
MyBitmap.Width) / 2;
MyPoint.Y = (imagePanel.Height -
MyBitmap.Height) / 2;
}
else
{
MyPoint.X = 0;
MyPoint.Y = 0;
}

g.DrawImage(MyBitmap, new RectangleF
(MyPoint.X, MyPoint.Y, MyBitmap.Width, MyBitmap.Height));

this.Text = Path.GetFileName(GetFilePath()) +
" - ImageViewer.NET";
Zoom = 1.00F; // only used for displaying the
current zoom factor in the statusBar, isn't used in calculating the
image size
}
else // if the image doesn't fit the panel
{
if (FitImagesToWindow)
{
this.imagePanel.AutoScroll = false;

// calculate new image size
Size imgSize = GenerateImageDimensions
(MyBitmap.Width, MyBitmap.Height, this.imagePanel.Width,
this.imagePanel.Height);

// create a new Bitmap with proper
dimensions
Bitmap ResizedBitmap = new Bitmap
(MyBitmap, imgSize.Width, imgSize.Height);

// I have problems with the next few lines of code, something is wrong
with the display of images, some fit inside the panel, and some don't,
some parts of the image breach the panel borders when resizing the
form...
int posX = 0;
int posY = 0;
if (CenterImagesInWindow)
{
if(imagePanel.Width >
imagePanel.Height)
posX = (imagePanel.Width -
ResizedBitmap.Width) / 2;
else
posY = (imagePanel.Height -
ResizedBitmap.Height) / 2;
}

g.DrawImageUnscaled(ResizedBitmap, posX,
posY);

this.Text = Path.GetFileName(GetFilePath
()) + " - ImageViewer.NET (Zoom: " + ResizedBitmap.Width.ToString() +
" x " + ResizedBitmap.Height.ToString() + ")";
Zoom = (float)Math.Min((float)
ResizedBitmap.Width / MyBitmap.Width, (float)ResizedBitmap.Height /
MyBitmap.Height);
}
else // used when not in SizeToFit mode, this
is the code shown earlier
{
this.imagePanel.AutoScroll = true;
this.imagePanel.AutoScrollMinSize =
MyBitmap.Size;

g.DrawImage(MyBitmap, new RectangleF
(this.imagePanel.AutoScrollPosition.X, this.AutoScrollPosition.Y,
MyBitmap.Width, MyBitmap.Height));

this.Text = Path.GetFileName(GetFilePath
()) + " - ImageViewer.NET";
Zoom = 1.00F;
}
}
// some less important stuff, doesn't concern drawing on the panel
ShowStatusBarInfo();

MenuItemReopen.Enabled = false;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}

// function that returns resized image dimensions
private Size GenerateImageDimensions(int currentWidth, int
currentHeight, int panelWidth, int panelHeight)
{
double scale = 0;

if (panelHeight > panelWidth)
{
scale = (double)panelWidth / (double)currentWidth;
}
else
{
scale = (double)panelHeight / (double)currentHeight;
}
return new Size((int)(currentWidth * scale), (int)
(currentHeight * scale));
}

I hope this will be of some help.
 
P

Peter Duniho

Peter, I'm not unwilling to "put the correct level of effort" into my
question, I simply don't know what should I put as a concise-but-
complete code example in this case!

The word "complete" should be just as informative to you as "concise".
Both matter.

Here are some links you may find helpful:
http://www.yoda.arachsys.com/csharp/complete.html
http://www.yoda.arachsys.com/csharp/incomplete.html
http://sscce.org/ (some Java-centric stuff, but mostly applicable to any
programming questions)

That said, I went ahead and wrote a short sample that demonstrates the
specific ideas I've described. As an added bonus, as an actual
concise-but-complete code example, it should help you understand what is
meant by that phrase. :) You'll note in that example that the only
sub-classes I had to create are of System.Windows.Forms.Form and
System.Windows.Forms.Control. No need to sub-class Panel (which is used
as a scrollable container in my example) nor UserControl (which is
completely unneeded).

Pete


using System;
using System.Windows.Forms;
using System.Drawing;

namespace TestDoubleBufferedScrolling_OneFile
{
class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}

public class Form1 : Form
{
public Form1()
{
InitializeComponent();

tboxWidth.Text = customControl11.Width.ToString();
tboxHeight.Text = customControl11.Height.ToString();
}

private void checkBox1_CheckedChanged(object sender, EventArgs e)
{
CheckBox check = (CheckBox)sender;

customControl11.DoubleBuffered = check.Checked;
customControl11.Invalidate();
}

private void tboxWidth_TextChanged(object sender, EventArgs e)
{
int dxWidth;

if (int.TryParse(tboxWidth.Text, out dxWidth))
{
customControl11.Width = dxWidth;
}
}

private void tboxHeight_TextChanged(object sender, EventArgs e)
{
int dxHeight;

if (int.TryParse(tboxHeight.Text, out dxHeight))
{
customControl11.Height = dxHeight;
}
}

/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;

/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be
disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}

#region Windows Form Designer generated code

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.panel1 = new System.Windows.Forms.Panel();
this.checkBox1 = new System.Windows.Forms.CheckBox();
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.tboxWidth = new System.Windows.Forms.TextBox();
this.tboxHeight = new System.Windows.Forms.TextBox();
this.customControl11 = new
TestDoubleBufferedScrolling_OneFile.CustomControl1();
this.panel1.SuspendLayout();
this.SuspendLayout();
//
// panel1
//
this.panel1.Anchor =
((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top
| System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.panel1.AutoScroll = true;
this.panel1.Controls.Add(this.customControl11);
this.panel1.Location = new System.Drawing.Point(13, 37);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(528, 335);
this.panel1.TabIndex = 0;
//
// checkBox1
//
this.checkBox1.AutoSize = true;
this.checkBox1.Location = new System.Drawing.Point(13, 13);
this.checkBox1.Name = "checkBox1";
this.checkBox1.Size = new System.Drawing.Size(100, 17);
this.checkBox1.TabIndex = 1;
this.checkBox1.Text = "DoubleBuffered";
this.checkBox1.UseVisualStyleBackColor = true;
this.checkBox1.CheckedChanged += new
System.EventHandler(this.checkBox1_CheckedChanged);
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(127, 14);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(38, 13);
this.label1.TabIndex = 2;
this.label1.Text = "Width:";
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(274, 14);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(41, 13);
this.label2.TabIndex = 3;
this.label2.Text = "Height:";
//
// tboxWidth
//
this.tboxWidth.Location = new System.Drawing.Point(168, 11);
this.tboxWidth.Name = "tboxWidth";
this.tboxWidth.Size = new System.Drawing.Size(100, 20);
this.tboxWidth.TabIndex = 4;
this.tboxWidth.TextChanged += new
System.EventHandler(this.tboxWidth_TextChanged);
//
// tboxHeight
//
this.tboxHeight.Location = new System.Drawing.Point(321, 11);
this.tboxHeight.Name = "tboxHeight";
this.tboxHeight.Size = new System.Drawing.Size(100, 20);
this.tboxHeight.TabIndex = 5;
this.tboxHeight.TextChanged += new
System.EventHandler(this.tboxHeight_TextChanged);
//
// customControl11
//
this.customControl11.DoubleBuffered = false;
this.customControl11.Location = new System.Drawing.Point(4, 3);
this.customControl11.Name = "customControl11";
this.customControl11.Size = new System.Drawing.Size(521, 329);
this.customControl11.TabIndex = 0;
this.customControl11.Text = "customControl11";
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(553, 384);
this.Controls.Add(this.tboxHeight);
this.Controls.Add(this.tboxWidth);
this.Controls.Add(this.label2);
this.Controls.Add(this.label1);
this.Controls.Add(this.checkBox1);
this.Controls.Add(this.panel1);
this.Name = "Form1";
this.Text = "Form1";
this.panel1.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();

}

#endregion

private System.Windows.Forms.Panel panel1;
private CustomControl1 customControl11;
private System.Windows.Forms.CheckBox checkBox1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.TextBox tboxWidth;
private System.Windows.Forms.TextBox tboxHeight;
}

public class CustomControl1 : Control
{
public CustomControl1()
{
InitializeComponent();
}

public new bool DoubleBuffered
{
get { return base.DoubleBuffered; }
set { base.DoubleBuffered = value; }
}

protected override void OnPaint(PaintEventArgs pe)
{
base.OnPaint(pe);

string strText = "Test Pattern";
SizeF sizeText = pe.Graphics.MeasureString(strText, Font);

using (Brush brush = new SolidBrush(ForeColor))
{
PointF ptDraw = new PointF();

while (ptDraw.Y < Height)
{
while (ptDraw.X < Width)
{
pe.Graphics.DrawString(strText, Font, brush,
ptDraw);
ptDraw.X += sizeText.Width;
}
ptDraw.X = 0;
ptDraw.Y += sizeText.Height;
}
}
}

/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;

/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be
disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}

#region Component Designer generated code

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
}

#endregion
}
}
 
M

moss

Thanks for this example. I've managed to fix the problem by removing
the panel from the control so it's basically a blank UserControl, now
I'm only using DoubleBuffer = true; and SetStyle(ResizeRedraw, true);
in the constructor of the control, as well as DrawImageUnscaled() for
drawing the image instead of the DrawImage():

Point Display = imageDisplay.AutoScrollPosition;
g.DrawImageUnscaled(MyBitmap, Display.X, Display.Y);

I still get some leftover bits when scrolling but the image clears up
in a second, this is probably because .NET and GDI+ are very slow :)
 
Top