Ken Getz
I’m guessing that there’s someone out there (there has to be at least one) who cares how the home theater turned out. (For some of the details, check out the instance of this column two issues back.) After much consternation, grumpiness, and finger-pointing (we can’t be the only ones to find a home remodel stressful, right?) we ended up with a 15x20 finished room with hardwood floors, built-in 7.1 surround sound, screen in the front that’s triggered off of the projector’s starting up (it magically flops down and rises up when we turn the projector on and off), and an Epson projector that handles the geometry of the room perfectly. For the video geeks out there, the projector is only 720p/1080i-I couldn’t stomach the 100% wallet-drain bump for 1080p-but even so, it looks glorious. I’m awaiting the standardization of HDMI 1.3 before jumping into the hi-def DVD world, so for now, we’re using an old receiver and DVD player. We’ll survive. When I mentioned this project to people, almost every one of them asked if we were getting “the vibrating chairs.” Funny, I had never even heard of the concept in home theater, and no, we went with standard recliners. Even I have my limits.
You learn funny things during projects like this. We learned that no matter how much insulation you put in the ceiling of a second floor room, if it’s got a ton of roof exposure, it’s going to be hot. It’s been over 100 degrees for several days now, and that room gets to be 85 or 90 degrees each afternoon. (We’re cheap. We don’t run the A/C unless we absolutely have to.) We had the A/C guys out to set up a separate zone for the room upstairs, so we can just run the A/C up there, and not in the rest of the house. Funny how these things work. They were happy to set this all up, but neglected to tell us that it’s impossible to run the unit just for that room-it freezes up the condenser. So, there’s always leakage into the rest of the house, where it’s plenty cool already. The only way to get the theater cool enough to be in, in the late afternoon/early evening, is to waste a lot of energy. The alternative, of course, is to put in a separate unit just for the new room. Are we looking for ways to throw more money at it? I don’t think so.
Speaking of wasting energy, I think back to a demo I wrote a few years back-an analog clock “widget” that I keep running on my desktop. During speaking engagements, it keeps me honest about the current time. At the point when I wrote it, back in 2001, I was just learning the .NET Framework, and to get the hands located on the clock face, I wrote a lot of code using trigonometry. I was so proud of myself for working that all out! (At the time I am writing this, you can download the existing sample from Microsoft’s site: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dv_vstechart/html/gdiclock.asp. I can’t promise it will be there at the time you read this, of course.) When you resize the clock face, all the parts of the clock scale to match the size of the container, and that took lots of manual calculation, as well.
Why was this wasting energy? It seems that because Windows no longer includes a floating clock application, it’s a nice thing to have. And so it is. But trigonometry? Does anyone really need or want to dig into that? As far as I can tell, the answer is “no.” It’s easy to scale drawings, and rotate them, using methods provided directly by the .NET Framework. And although I can’t reproduce the whole process of creating the “new” clock here (I really need to update that demo on MSDN, that’s for sure), I can at least walk you through getting started in the thought process. If you follow along, you’ll create at least the seconds hand for a clock. I’ll leave the rest as an “exercise for the reader.”
If you’re going to deal with GDI, and with drawing things on forms, you’ll most likely want to override the form’s OnPaint method, and add your own code to draw on the form. To get started, create a new Windows application, and in the default form’s class, add the following Imports/using statement:
[Visual Basic]
Imports System.Drawing.Drawing2D
[C#]
using System.Drawing.Drawing2D;
To perform the painting, add this procedure to your form’s class (the code doesn’t actually do anything yet):
[Visual Basic]
Private Sub HandleOnPaint(ByVal e As
PaintEventArgs)
Dim g As Graphics = e.Graphics
g.SmoothingMode =
SmoothingMode.AntiAlias
End Sub
[C#]
private void HandleOnPaint(PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode =
SmoothingMode.AntiAlias;
}
To cause the HandleOnPaint procedure to be called, override your form’s OnPaint method, adding this procedure to the class (note that this procedure calls the base class’ OnPaint method before handling the painting itself-you must always include this code, unless you really know what you’re doing):
[Visual Basic]
Protected Overrides Sub OnPaint( _
ByVal e As
System.Windows.Forms.PaintEventArgs)
MyBase.OnPaint(e)
HandleOnPaint(e)
End Sub
[C#]
protected override void
OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
HandleOnPaint(e);
}
Add the following declarations, outside of a procedure, to track the current angle for the seconds hand and the original radius of the form (that is, the minimum of the width and the height of the form):
[Visual Basic]
Private angle As Integer = 0
Private radius As Integer = 0
[C#]
private int angle = 0;
private int radius = 0;
Although in a real application you might use a better timer, for now, add a Timer control to the form. Set the Timer control’s Interval property to 1000 and its Enabled property to True. In the timer’s Tick event handler, add the following code, which calculates the angle for the seconds hand and invalidates the form (which causes the .NET Runtime to invoke the form’s OnPaint override):
[Visual Basic]
Private Sub Timer1_Tick( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Timer1.Tick
angle = DateTime.Now.Second * 6 + 180
Me.Invalidate()
End Sub
[C#]
private void timer1_Tick(
object sender, EventArgs e)
{
angle = DateTime.Now.Second * 6 + 180;
this.Invalidate();
}
Now, it’s time to actually draw the seconds hand. In the HandleOnPaint procedure, add the following line of code, which draws the hand for you:
[Visual Basic]
g.DrawLine(New Pen(Color.Black, 4),
0, 0, 0, 80)
[C#]
g.DrawLine(new Pen(Color.Black, 4),
0, 0, 0, 80);
Of course, nothing causes the hand to relocate itself to the center of the form, or to rotate (much less scale to match the size of the form). These three issues (translation, rotation, and scaling) are all handled for you by the TranslateTransform, RotateTransform, and ScaleTransform methods of the Graphics class. Although these methods support far more functionality than you’ll see here, it’s easy to get started demonstrating by setting up the clock face.
To help calculate the scaling factor, add the following procedure to your form’s class. This code calculates the minimum of the width and height (the radius of the clock):
[Visual Basic]
Private Function GetRadius(ByVal rct As
Rectangle) As Integer
Return CInt(Math.Min(rct.Width,
rct.Height) / 2)
End Function
[C#]
private int GetRadius(Rectangle rct)
{
return Convert.ToInt32(
Math.Min(rct.Width, rct.Height) /
2);
}
Add the following code to your form’s Load event handler. These two lines of code ensure that the form invalidates its contents when you resize it, and calculates the initial radius of the form:
[Visual Basic]
Me.SetStyle(ControlStyles.ResizeRedraw,
True)
radius = GetRadius(Me.ClientRectangle)
[C#]
this.SetStyle(ControlStyles.
ResizeRedraw, true);
radius =
GetRadius(this.ClientRectangle);
In the HandleOnPaint method, above the line of code that calls the DrawLine method, add the following two lines. These lines calculate the current scaling factor, and scale the form appropriately:
[Visual Basic]
Dim scale As Double =
GetRadius(Me.ClientRectangle) / radius
g.ScaleTransform(scale, scale)
[C#]
float scale =
GetRadius(this.ClientRectangle) /
(float) radius;
g.ScaleTransform(scale, scale);
Run the application, and resize the form-the seconds hand you’re drawing should resize to match the scale of the form.
Back in Design view, add the following code in the HandleOnPaint procedure, above the call to the DrawLine method. This code translates the form’s coordinate system so that the point 0, 0 is at the center of the form, and draws a circle around the “clock face.” Run the application again to verify the translation:
[Visual Basic]
g.TranslateTransform(radius, radius)
g.DrawEllipse(Pens.Black, _
-radius, -radius, radius * 2, radius *
2)
[C#]
g.TranslateTransform(radius, radius);
g.DrawEllipse(Pens.Black,
-radius, -radius, radius * 2, radius *
2);
Finish the code by adding the line that handles the rotation. You might also want to change the length of the hand so that its length is relative to the original radius of the form:
[Visual Basic]
g.RotateTransform(angle)
g.DrawLine(New Pen(Color.Black, 4), _
0, 0, 0, CInt(radius * 0.75))
[C#]
g.RotateTransform(angle);
g.DrawLine(new Pen(Color.Black, 4),
0, 0, 0, (int)(radius * 0.75));
Run the application again, and note that as you resize the form, the clock face stays correctly sized, and the hand rotates as necessary. Pretty neat, right? Did you really ever want to revisit high-school trig again? (I used to teach high-school trig, so it’s still pretty darned close to the surface for me, but most people I know either submerged the entire thing, or spent that period of their lives day-dreaming about anything else they could possibly find to cease the unending pain.)
Certainly, taking the time to find the right solution to a problem like this one saves a lot of energy. You’ll laugh when you see the code hoops I went through in the original version-sin and cos and maybe even a tan and arctan thrown in for fun, clearly unnecessarily. I wish I could find as simple a solution to the wasted electricity, cooling unpopulated rooms in the house. (And to head off the rebuttals: Yes, I know there are plenty of clock widgets available on the Web. That’s not really the point here-the clock is just an example that shows off using the various methods of the Graphics class. Run with it.)