Introduction
The code demonstrates the simplicity of using GDI+ for simple drawing tasks. I have used it to make a simple desktop clock. In this little clock, users have the ability to choose the clock image that they want (which is just an image file); I have also added this little feature that each clock image can be associated with an optional .ini file that has the settings of that clock image. These settings include: the color and size of each clock hand, the centre of the clock (in case that the image is not symmetric), showing date, etc.
I have actually put some clock images and their .ini files in the project folder to demonstrate the features of this program.
The Code Structure
There is the AceClockForm
class which is the only form used in the program, all the calculations and drawings using GDI+ are done in this class. The INISettingsReaderWriter
class is used to store and retrieve settings for the program, and to retrieve the settings of clocks. Finally, the Settings
and ClockSettings
classes store the settings of the program and clocks.
Drawing the Clock
The Math!
To draw the clock hands, which are simply lines in my application, we need to know their coordinates! For each line, one of the two points of the line is simply the centre of the clock; for the other point, the angle of that line (or clock hand) is calculated first:
double alpha = now.Hour * (360/12) + now.Minute / 2;
double beta = now.Minute * (360/60) + now.Second / 10;
double gama = (now.Second+(drawSHContinuously? now.Millisecond/1000f : 0f)) * 360/60;
Here is the explanation:
The hour hand: the circle of a clock face is divided by 12 hours, so each hour is 1/12 of the circle, and since a circle is 360 degrees, each hour is 360/12 degrees, so the angle of the hour hand is calculated this way, but as you all have noticed, no analog clock's hour hand stands still exactly on the current hour number until an hour is passed. Instead, it moves slowly towards the next number. In order to calculate this angle, the minute part of the time is taken into account. The calculation is almost the same except that since each 60 minutes equal an hour, a division by 60 is added.
The minute hand: It is like the hour hand, the difference is that each minute is 1/60 of the circle, so it is 360/60 degrees, and again, to make it look smoother, the seconds are taken into account, and since each 60 seconds is 1 minute, a division by 60 is added.
The second hand: again, each second is 1/60 of the circle, etc... but there is this drawSHContinuously
thing. In my application, there is an option that allows the second hand to be drawn continually. If that option is activated, then the milliseconds become significant because the second hand is redrawn many times between each 2 seconds.
So far the angles of each hand are calculated, but these angles are in degrees, they are clockwise (!) (as opposed to trigonometry), and they are relative to the vertical line, so they are converted to CCW, radiant, relative to the horizontal line angles:
alpha = -alpha * Math.PI / 180d + Math.PI / 2d;
beta = -beta * Math.PI / 180d + Math.PI / 2d;
gama = -gama * Math.PI / 180d + Math.PI / 2d;
Then these angles are used to calculate the coordinates of each hand's end points. This is done in the getTaleCoordinates()
method, which gets the coordinates of one end point of a line, its length and angle, and calculates its other end point.
Using GDI+ to Draw the Clock
I have used BufferedGraphics
in the code to eliminate possible blinks of the clock image. This BufferedGraphics
is allocated in the SetImage()
method in the code. The BufferedGraphics
class has a Graphics
property which returns the Graphics
object on which the clock is drawn.
The actual drawing is done in the drawClock()
method. The following line of code draws the clock image:
bgg.DrawImage(this.image, 0, 0, this.Width, this.Height);
and these lines draw the clock hands:
bgg.DrawLine(hourPen,
getTaleCoordinates(centerPoint.X, centerPoint.Y,
clockSettings.HourTale, alpha+Math.PI),
getTaleCoordinates(centerPoint.X, centerPoint.Y,
clockSettings.HourLength, alpha));
bgg.DrawLine(minutePen,
getTaleCoordinates(centerPoint.X, centerPoint.Y,
clockSettings.MinuteTale, beta + Math.PI),
getTaleCoordinates(centerPoint.X, centerPoint.Y,
clockSettings.MinuteLength, beta));
bgg.DrawLine(secondPen, centerPoint,
getTaleCoordinates(centerPoint.X, centerPoint.Y,
clockSettings.SecondTale, gama + Math.PI));
bgg.DrawLine(secondPen, centerPoint,
getTaleCoordinates(centerPoint.X, centerPoint.Y,
clockSettings.SecondLength, gama));
The first parameter to the DrawLine
method is a Pen
, and the second and the third are the ending points of the line to be drawn.
The date is also drawn. The DrawString
method is used to draw strings in GDI+. The use of this method is simple, it takes the string to be drawn, the font by which the string is to be drawn, a brush which can be used to define a texture for the text, and the coordinates of the upper left corner of the string. Here is the code:
bgg.DrawString(now.ToShortDateString(), dateFont, dateBrush,
clockSettings.DateCenterX - strsize.Width / 2,
clockSettings.DateCenterY - strsize.Height / 2);
Settings and ClockSettings Classes
The Settings
class stores the settings of the program, and the ClcokSettings
class stores the settings of a clock. Loading and saving the data of these classes is explained below.
Loading and Saving Clocks and Programs Settings
The INISettingsReaderWriter
class is used to save and load settings for both the clocks and the application itself. As the name suggests, these settings are stored in the form of INI files, in which each piece of data is stored in a separate line in this form:
Key=Value ;this is a comment!
This class uses Regular Expressions to parse data stored in INI files, and uses Reflection to fill objects with these data, or write them to a file. Here is the Regular Expression pattern that is used:
static Regex iniFilePattern =
new Regex(@"^\s*(?<key>\w+)\s*=[\s-[\n]]*(?<value>" +
@"[\w//:/. -]*)/s*(;.*)?\\:\.]*)\s*(;.*)?",
RegexOptions.Multiline | RegexOptions.Compiled);
The methods dictionaryToObject
and objcetToDictionary
are provided to store the data in a System.Collections.Generic.Dictionary<string, string>
object in the properties of the given object, and vice versa. As I have mentioned above, this is done through Reflection:
Type t=obj.GetType();
foreach (var pi in t.GetProperties())
{
...
}
History