I've never been much of a graphics oriented programmer. Somehow I've always gotten by, using List and Tree controls, or various graphical toolkits to do my dirty work. Fortunately, GDI+ offers a lot of ways to draw your own content within a class derived from ScrollableControl if you need to do so. I wrote a control (which you will see here later in October) for viewing images in a horizontal slider, and everything looked great until I moved the scroll bar to the right. Instead of seeing the next set of images, I got a disjointed or improperly painted set of images.
After a day of trying to debug my code, I decided to go back to basics. I really didn't understand how to scroll a control and handle even the most basic painting events. I created a demo project called TestControlPaint (VS 2005, 38 K) :
It has a class called TestControl (derived from ScrollableControl) which is designed to do one thing: draw a grid of 20 squares either horizontally or vertically.
/// <summary> /// Tests scrolling a control horizontally and vertically /// </summary> public partial class TestControl : ScrollableControl
Within the class, there is a field called HorizontalMode, which can be set from the designer the comments tell you what it does:
/// <summary> /// True means that the control displays horizontally; false means it displays /// a vertical grid. /// </summary> public bool HorizontalMode { get { return _horizontalMode; } set { if (_horizontalMode != value) { _horizontalMode = value; Invalidate(); } } }
In the picture below, I've put a horizontal Split Container into my Form, with one horizontal instance of TestControl in the upper panel, and a vertical instance of TestControl in the bottom panel. Horizontally we have squares 1-20 and vertically we have square 1-20 in 4 rows of 5 cells. Scroll bars magically appear and appear to work well! How did this happen? Some of the key steps were the following:
1. Make sure that the AutoScroll property is set to true on the control. It could set in the designer or in another method, but if it's not set somewhere, none of this will work:
AutoScroll = true;
2. Set AutoScrollMinSize to the virtual size of the content within your control:
// Set the auto scroll size to the total width and // height of the content drawn within the control. AutoScrollMinSize = new Size(_cumulativeWidth, _cumulativeHeight);
3. In your OnPaint method override, call TranslateTransform with the current AutoScrollPosition:
//handle possibility that the viewport is //scrolled,adjust my origin coordintates //to compensate Point pt = AutoScrollPosition; gfx.TranslateTransform(pt.X, pt.Y);
The first two bullet items I had figured out from reading books and articles, but the third one eluded me. TranslateTranform, as soon as I put it in my code, made the scrolling work instantly. This makes sense when you see what I was drawing within OnPaint:
// Create the grid... for (int index = 1; index < (_numSquares+1); index++) { //... // Calculate the coordinates for a square rectangle Rectangle theRect = new Rectangle(_squareWidth * (startX++) + _margin, startY, _squareWidth, _squareHeight); //... // Draw the rectangle with a black brush Pen borderPen = new Pen(theBrush); gfx.DrawRectangle(borderPen, theRect);
A series of squares are drawn: from 0 - 100, 105 - 205, 210 - 310, etc. When a paint message is received, the control drew the first few visible squares correctly. What happens when you need to draw square 4 at starting location (315, 5) and the client area is only 250 wide? Without TranslateTransform, the Graphics object (gfx in this case) cannot draw anything beyond the client rectangle of the control. BTW, I forgot where I saw TranslateTransform, but it was Bob Powell's comment on a message board that gave me the clue to look into this.
Speaking of Mr. Powell, he helped solve another problem. When you have a virtual drawing surface, how can you accurately determine the mouse coordinates? OnMouseClick only returns the mouse coordinates within the client rectangle, not from your virtual rect setup with AutoScrollMinSize. To get the virtual mouse location, take a look at Powell's Back-track the mouse example. He takes the AutoScrollPosition and transforms it into the virtual mouse location. I've included this method in TestControl.cs:
/// <summary> /// Gets the mouse location according to the virtual /// position in the control. /// </summary> /// <returns>The virtual mouse location</returns> protected Point GetVirtualMouseLocation(MouseEventArgs e)
TestControl receives a mouse click event in the OnMouseClick override. It calls GetVirtualMouseLocation for the virtual location, and passes this Point object to a method called getSquareID:
// get the virtual mouse location using the transforms Point mouseLocation = GetVirtualMouseLocation(e); // Translate the virtual mouse location into a square ID int squareID = getSquareID(mouseLocation); StringBuilder strBuild = new StringBuilder(); strBuild.AppendFormat("You clicked on Square ID {0}", squareID); // if we found a valid square, show the id in // the message box if (squareID > 0) MessageBox.Show(strBuild.ToString());
The test Form will display the ID of the square that you click on within either the horizontal or vertical forms. This simple technique worked equally well within my larger, more complex control.
Download:TestControlPaint VS 2005 Project (38K)




Thank you for this article and example program. I had another way of doing this which did not work so well. Basically, instead of writing on a virtual space, I drew on a panel that was "contained" by a scrollable panel. This worked up to the point of the maximum width (32,767 pixels)of the panel on which I was drawing.
Your technique will work much better for me.
Woo-hoo!
This control is just what I was about to write (well, I still have to draw my images instead of rects, but that's the easy part).
Thank you Richard!
Hope you don't mind me using your code as a base for my class.
Finally, did the trick for me