What to do/code in our custom control?
In our digital clock control we want to address the following problems, consider them our project requirements.
- We want to show the current time, down to seconds precision
- We want to show background image to achieve closer look and feel to real digital clock.
- We want to be able to adjust the location of the text
Before proceed with doing actual coding, we should consider which class is the most appropriate for our control. in VCL, in developing custom control we usually have two options for the base class. They're:
The main differences between the two are that TWinControl descendants have the ability to contain another control and also able to receive focus. Since none of our requirements require any of those, we can safely choose TGraphicControl for our base class.
Base class: TGraphicControl.
Analyzing the requirements, we can summary the task as:
Draw the background image, if available, to the screen, and then draw the current time on top of it.
To properly draw something with TGraphicControl, we want to do it in TGraphicControl's Paint virtual method. Paint will be called any time the control's parent (i.e. the container) needs to update its display. Therefore we will override this Paint method, and do our drawing there.
Override Paint method and do the drawing there.
How to draw?
Now that we know when to draw, time to find out how to do the drawing. Fortunately TGraphicControl already provided nearly anything we need to draw anything on top of it. It has a property named Canvas with type of TCanvas (click to see official documentation). I think you will find the names are very intuitive, since we are indeed doing all the drawing using it.
We can draw text onto a Canvas by using TCanvas.TextOut method, like this:
Canvas.TextOut(X, Y, 'Hello world');
X and Y defined the coordinate where the text should started. X defined how many pixels from the left boundary of the control, and Y defined how many pixels from the top boundary.
To draw a graphic/image onto a Canvas, we can use TCanvas.Draw method, like this:
Canvas.Draw(X, Y, MyImage);
MyImage is an instance of TGraphic class containing the image to be drawn onto the canvas.
Drawing is done by using TGraphicControl.Canvas.
What to Draw?
We have two entities to draw,
- String containing the current time information
- Background image
1. String of the current time
We get this string from Now function from SysUtils unit. Of course we need to process the information returned by Now to get it into string format. We can use either TimeToStr from SysUtils if we want to use default time format of the computer, or FormatDateTime, also from SysUtils unit, if we want to get special format for the time.
Use Now combined with TimeToStr routine to get string of current time default formatted, or Now combined with FormatDateTime to get string of current time with specific format.
2. Background image
The main problem here is how to specify image that we want to use as the background image. We want the background image could be specified at design time and still available at runtime without complex code. Here we will use the help of an instance of TPicture. Using TPicture we can store any kind of image (that was supported) into our control, instead of just one kind of image if we use a specific descendant of TGraphic. For example, if we decide to use TBitmap instead of TPicture, then we can only properly store and load images of TBitmap type. We can't store JPGs.
Our TPicture must be instantiated in the constructor, to make sure it's available to receive stream of image data stored in the dfm.
When drawing, we don't directly use the TPicture. But we will use the graphic stored in the TPicture's Graphic. So when we are drawing the background image we will go something like:
Canvas.Draw(X, Y, FPicture.Graphic);
Use TPicture to manage background image
Background image actually is in TPicture.Graphic.
Time to Code!
Let's put together our findings. Note that in this tutorial we will name the custom control: TDigitalClock.
- Base class: TGraphicControl. This should turn into code like:
unit MyClock; uses Controls ; type TDigitalClock=class(TGraphicControl) end;
- Override Paint method and do the drawing there. According to this, we should update the class into:
type TDigitalClock=class(TGraphicControl) protected procedure Paint; override; end; implementation procedure TDigitalClock.Paint; begin end;
- Drawing is done by using TGraphicControl.Canvas. Okay, we'll incorporate this with the next findings.
- Use Now combined with TimeToStr routine to get string of current time default formatted, or Now combined with FormatDateTime to get string of current time with specific format.
Let's assume that we want to support specific time format. So we have to add a field to store the format, we name the field FFormat, and then publish it as a property with name Format. If this property is empty, we will be using the computer's default format for time. So let's update our class declaration into:
TDigitalClock=class(TGraphicControl) private procedure SetFormat(const Value: string); protected procedure Paint; override; published property Format: string read FFormat write SetFormat; property Font; end; end;
We have to use a setter method for Format property since we want to clock to update its display immediately when the format is changed. The code for SetFormat is:
procedure TDigitalClock.SetFormat(const Value: string); begin if FFormat <> Value then begin FFormat := Value; Invalidate; // ask the control to update its display asap end; end;Note the Invalidate method.
And now we can update our overrided Paint implementation into something like this:
var vText: string; vLeft: Integer; vTop : Integer; begin // get into "transparent" background mode Canvas.Brush.Style := bsClear; if FFormat='' then // if no format specified, use default format vText := TimeToStr(Now) else // use the specified time format vText := FormatDateTime(FFormat, Now); // clear the drawing area Canvas.FillRect(BoundsRect); Canvas.Font := Self.Font; vLeft := GetLeftPos(vText); vTop := GetTopPos(vText); Canvas.TextOut(2, 2, vText);
- Use TPicture to manage background image. Let's create a field named FBackground of type TPicture, and create it in our digital clock's constructor. We publish this field as property with setter method, since we'll use its Assign method to change the content instead of directly change the object reference.
Our class declaration to be something like this:
TDigitalClock=class(TGraphicControl) private FFormat: string; FBackground: TPicture; procedure SetFormat(const Value: string); procedure SetBackground(const Value: TPicture); protected procedure Paint; override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; published property Format: string read FFormat write SetFormat; property Background: TPicture read FBackground write SetBackground; property Font; end;
implementations of the contructor, destructor, and SetBackground:
constructor TDigitalClock.Create(AOwner: TComponent); begin inherited; Width := 200; Height := 80; FBackground := TPicture.Create; end; destructor TDigitalClock.Destroy; begin FBackground.Free; inherited; end; procedure TDigitalClock.SetBackground(const Value: TPicture); begin FBackground.Assign(Value); DoAutoSize; Invalidate; end;
Note the call to Invalidate in SetBackground. The purpose is to tell the control to update its display asap.
- Background image actually is in TPicture.Graphic. With this finding, we can finally draw the background image. So let's update our Paint method into:
procedure TDigitalClock.Paint; var vText: string; vLeft: Integer; vTop : Integer; begin // get into "transparent" background mode Canvas.Brush.Style := bsClear; if FFormat='' then // if no format specified, use default format vText := TimeToStr(Now) else // use the specified time format vText := FormatDateTime(FFormat, Now); if FBackground.Graphic<>nil then // draw the background, if available Canvas.Draw(0, 0, FBackground.Graphic) else // if no background graphic available, clear the drawing area Canvas.FillRect(BoundsRect); // Make sure canvas uses the same font with the control Canvas.Font := Self.Font; vLeft := GetLeftPos(vText); vTop := GetTopPos(vText); // draw the time text Canvas.TextOut(vLeft, vTop, vText); end;
Okay, this is it!! Well, not!! If you run the control up to this version, you will get the clock to update the displayed time only when it was uncovered from another window. Because only at this time normally Paint will be called. So in order to synchronize the display with the actual current time, we have to force our control to update its display at specified interval.
To force the update, we can call Invalidate like we already use in several locations. But to be sure to call it at specified interval, we need the help of TTimer. Enabled TTimer allows execution of codes after specified time elapsed. The execution would be repeatable as long as it stays Enabled.
So let's add a TTimer to our digital clock. The class declaration would become:
type TDigitalClock=class(TGraphicControl) private FFormat: string; FTimer : TTimer; FBackground: TPicture; procedure SetFormat(const Value: string); procedure SetBackground(const Value: TPicture); procedure HandleTimer(ASender: TObject); protected procedure Paint; override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; published property Format: string read FFormat write SetFormat; property Background: TPicture read FBackground write SetBackground; property Font; end;
HandleTimer method is the handler of our TTimer FTimer's OnTimer event. OnTimer event will be fired after an interval time specified in Interval property of the TTimer elapsed since enabling the TTimer or since the time OnTimer fired. In our constructor we have to create the TTimer and then set proper values so it works properly.
Our constructor would become:
constructor TDigitalClock.Create(AOwner: TComponent); begin inherited; Width := 200; Height := 80; FTimer := TTimer.Create(Self); FTimer.Enabled := False; FTimer.Interval := 500; FTimer.OnTimer := HandleTimer; FTimer.Enabled := True; FBackground := TPicture.Create; end;
There you are! We now actually have a working digital clock controls. We just need another routine to reqister the control to make it available in component pallette. For this, just add this line somewhere in the interface section of the unit:
and this in the implementation:
procedure Register; begin RegisterComponents('Samples', [TDigitalClock]); end;
After you put the unit into a package and register the package, you will get a new control in your component pallette, under tab Samples. You can drop this control from component pallette into your form to get a digital clock. See to it that you can change the background easily at design time, and the background available both in design time and runtime.
The complete source code of the digital clock is Demo.zip 14.57KB 452 downloads, along with demo project. There are some improvements you will find in the attached code compared to the codes in this tutorial. So, enjoy!
Edited by LuthfiHakim, 06 January 2012 - 10:27 PM.