September 2006 Archives

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) :

Auto Scroll Test with a ScrollableControl

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)

I attended an MSDN Event yesterday, over at the Regal Hacienda theaters in Dublin. I always enjoy these events because they are free and there is always useful information presented. One of the speakers, Anand Iyer, is very enjoyable to watch. He's very good at presenting technical information and sprinkling little humorous comments throughout. A year ago my friend Dave got a kick out of Anand explaining ClickOnce: "You click once to download the application from a web site...and then you click once again to install it on your PC. It should have been called Click Twice!" Yesterday, Anand was slapping himself for promoting Google Maps when talking about AJAX and Atlas (Microsoft's AJAX sdk for ASP.NET): "I should be promoting Live Local Maps instead!"

CodeCamp at FootHill College.  Click Here for Details and RegistrationAnand mentioned the Silicon Valley Code Camp 2006 that will be held on October 7th and 8th (the weekend) at Foothill College. This is also a free event, and the sessions will cover all kinds of topics, from .NET related technologies to Amazon Web Services. There's also a free Vista Install Fair, where you can upgrade your current Windows XP SP 2 computer to Windows Vista Ultimate. There are some restrictions for memory, graphics cards, etc., and you should backup your data. But it sounds like a good deal, getting some help with an upgrade, and you will receive a free copy of Windows Vista Ultimate after it is finally released. You can just bring your computer without the monitor, keyboard, or mouse.

External Link:
Silicon Valley Code Camp 2006

In my previous entry, "Using ToolStripControlHost with a Trackbar", I noted that the problem with this method resulted in the ToolStripTrackBar looking unlike anything else on the ToolStrip when it was in ManagerRenderMode. This has driven me nuts during the preceding week. Here are some of the steps that I took to solve this problem, all of which failed:

Tried to set Backcolor on ToolStripTrackBar: no effect.
Override OnPaint method with ToolStripTrackBar: no effect.
Derive MyTrackBar from TrackBar, override OnPaintBackground, put MyTrackBar in ToolStripControlHost: color, but no trackbar!

The latter step failed because TrackBar is one of those .NET controls that wants to perform all the drawing--if you override OnPaint, it won't be called unless you call SetStyle, as Tom Holt described in this CodeProject article:

// this call says "I'll draw it myself"
this.SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.ResizeRedraw |
ControlStyles.UserPaint, true);

From what I read on various message boards, no one had my specific problem (needing a TrackBar on a ToolStrip), but plenty of people had a situation where they wanted the TrackBar to be transparent on a Form that contained a bitmap image or specific visual style. I could see that type of TrackBar being useful and potentially the key to solving this problem, and I found a great article from Bob Powell showing how to create a transparent C# control. After finding this MSDN article with sample code for the TrackBarRenderer class, I decided to create my own control: CrystalTrackBar.

CrystalTrackBar
CrystalTrackBar has two different modes you can set. One is "TransparencyMode", which allows the background of the Form to show through, as it does with the second control in this screenshot. I then proceeded to take the CrystalTrackBar, and wrap it in a ToolStripControlHost to create CrystalToolStripTrackBar. The transparency mode allows the finely rendered background of the ToolStrip to show through, creating a seemless effect. As a bonus, I created a second mode that works when TransparencyMode is set to false--a gradient effect is created, as in the first control within the form. You can set Color1, Color2, and the angle of the gradient.

I'll be sharing the code soon. CrystalTrackBar isn't yet as feature rich as the TrackBar and I want to include some additional modes. I've got a small painting bug when I move the thumb around, but I may release it in the hopes that someone will suggest a better way of doing things.

Windows NT Splash Screen Joke
I worked for Microsoft during 1990 to 1993 on the Visual Studio team. During the end of that period, Windows NT was being developed and about to be released. I didn't work on the main operating system--my job was to help get MFC ported on the new platform. We would receive various builds and they would run slowly on the computers in our office. Someone in the company (not me) got frustrated with the performance issues and created this splash screen as a joke. I always found it to be funny and have saved it on my hard drive for over ten years. I wonder if people on the Windows Vista team have done any pranks like this?

I'm currently developing an application that displays images (bmp, jpg, etc) in a Windows Forms application. The application can scale the image within a panel, making it fit, or allowing an interactive scale. To enhance this functionality, I wanted to include a slider Trackbar control to my toolstrip. The end result needed to look like this:

trackbar on toolstrip

Fortunately, this is easily accomplished with .NET 2.0, by using the ToolStripControlHost class to wrap controls for the ToolStrip. I was even able to find code that specifically added the Trackbar on WindowsForms.net. All I had to do was place this code inside a file called ToolStripTrackBar.cs (see the link to this below), add a few using clauses to bring in the required assemblies, and it compiled just fine. Now what? I spent a good period of time just trying to find TrackBar in the Visual Studio Toolbox. At first I thought it might show up where the user controls are located, under the first control group with the name of your program. Nothing new there. Nada for the "All Windows Forms" control group. I couldn't find it anywhere. Compiled again, exited and restarted Visual Studio. Nada. I decided to give up and decided to add some buttons to my toolstrip. Voila! On the bottom of the menu that allows you to add toolstrip items, the Trackbar item had wonderfully appeared!

trackbar on toolstrip menu

It's all very easy and automatic, however none of the references to this specific example told me where to find the Trackbar. The benefits of having the Trackbar in Visual Studio designer are great, as you can visually position and resize the control. However, the Properties settings for the trackbar object, embedded in the ToolStripControlHost, were not persistent. I had to write a method to intialize the trackbar object:

private void InitTrackBar()
{
    zoomToolStripTrackBar.TrackBar.TickStyle = 
        TickStyle.None;
    zoomToolStripTrackBar.TrackBar.BackColor = 
        this.toolStrip1.BackColor;
    zoomToolStripTrackBar.TrackBar.Scroll += 
        new EventHandler(this.scrollZoom_Scroll);
    zoomToolStripTrackBar.TrackBar.Value = 5;
}

That was problem #1. Problem #2 was that while the ToolStrip was set to ManagerRenderMode, this property does not automatically extend to the ToolStripControlHost. If you look at my first picture, you'll notice that the TrackBar is grey while the rest of the ToolStrip is blue. If I figure out the reason for this, I'll record it here.

Link:
ToolStripTrackBar.cs