Making an owner draw control aware of the VCL styles
Owner draw controls are powerful. They allow you to completely tune your GUI for the needs of your users and potentially enable your application to display data in a more familiar way. In the end, owner draw controls improve the user experience with your application. However, owner draw controls do not always fit well with the VCL custom styles. Why? Because if you try to draw something by yourself, you could be tempted to use a "fixed" color, such clRed
or clYellow
, or you could be tempted to use the operating system color, such as clBtnFace
or clWindow
. Doing so, your owner draw controls will be not style aware and will be drawn in the same way regardless of the current VCL style. In this recipe, you'll learn how to make custom graphics remaining being in topic with the selected VCL style.
Getting ready
Let's say you are in charge of developing a controller panel for a hotel's light system. You have a list of lamps to power on, and you, using some hardware, have to power on some lamps by clicking on a button. Customers tell you that buttons should show some additional information about the lamp, for example:
- Served zone (corridor, hall, room number, and so on)
- State (on/off using some fancy graphics)
- The time the lamp was powered on
- The time when electrical problems have been detected, showing a red icon to indicate that the lamp is off even when current supplies the line, so the circuit is interrupted somewhere
- Other custom information not currently known, such as small graphs showing lamp state history during the last 24 hours
The question is how to implement this kind of UI. One of the possible ways is to use TDrawGrid and draw all the needed details in each cell, using the cell also as a button. Using TDrawGrid, you have a grid of buttons for free. You have also the greatest flexibility about the information displayed because you are using the TCanvas
method to custom draw each cell. This is quite a popular solution for this kind of non-standard UI. However, when you deploy this application, the customers ask about the possibility of changing the style of the application to fit the needs of the current user. So, you think about VCL styles, and you are right. However, the graphics drawn into the cells don't follow the currently selected VCL style, and your beautiful application becomes a bad mix of colors. In other words, when users change the selected VCL style, all the controls reflect the new style, but the owner drawn grid, which is unaware to the selected style, doesn't look as nice as the rest of the UI. How to solve this problem? How to draw custom graphics by adhering to the selected VCL style? In this recipe, you'll learn how to do it using the lamp control grid example.
How it works…
At design time, the form looks like the one shown in the following screenshot:
When the form is created, the list of available styles is loaded in the Radio group using code similar to the following one:
RadioGroup1.Items.Clear; RadioGroup1.Columns := Length(TStyleManager.StyleNames); for LStyleName in TStyleManager.StyleNames do RadioGroup1.Items.Add(LStyleName); RadioGroup1.ItemIndex := 0; TStyleManager.SetStyle('Windows');
Then, a list of the TLampInfo
object is created and initialized using the information contained in the Zones
array. After that, the draw grid is initialized according to the LAMPS_FOR_EACH_ROW
constant. Here's the relevant code:
FLamps := TObjectList<TLampInfo>.Create(True); for I := 1 to LAMPS_FOR_EACH_ROW * 4 do begin FLamps.Add(TLampInfo.Create(Zones[I])); end; DrawGrid1.DefaultColWidth := 128; DrawGrid1.DefaultRowHeight := 64; DrawGrid1.ColCount := LAMPS_FOR_EACH_ROW; DrawGrid1.RowCount := FLamps.Count div LAMPS_FOR_EACH_ROW;
The FormCreate
event handler initializes the styles list and the list of the lamps (the model) of the form. Now, we'll see how the other event handlers will use them.
The TDrawGrid OnSelectCell
event, as the name suggests, is used to address the current "lamp" from the FLamps
and to toggle its state. That's it. If the lamp is on, then the lamp will be powered down, else the lamp will be powered on. After that, the code forces the grid to redraw using the Invalidate
method:
procedure TMainForm.DrawGrid1SelectCell(Sender: TObject; ACol, ARow: Integer; var CanSelect: Boolean); begin FLamps[ACol + ARow * LAMPS_FOR_EACH_ROW].ToggleState; DrawGrid1.Invalidate; end;
Now, really interesting things happened in the DrawThemed
method called inside the TDrawGrid OnDrawCell
event. This method receives information about the coordinates of the cell to draw, and then it draws a button on the canvas using the information contained in the correspond TLampInfo
instance. The code is quite long, but an interesting concept is that no specific colors are used. When it is necessary to draw something, the code asks StyleService
to get the correct color according to the current style. This approach is also used for font color and for system colors. Here's a handy table that summarizes these concepts:
Method name |
Description |
---|---|
|
Returns the color defined in the style for the element specified by |
|
Returns the font color for the element specified by |
|
Returns the system color defined in the current style |
So, when we have to highlight the (pseudo) button, if there are electrical problems on the power line, we will use the following code:
if LLamp.ThereAreElectricalProblems then LCanvas.Brush.Color := StyleServices.GetStyleColor(scButtonHot) else LCanvas.Brush.Color := StyleServices.GetStyleColor(scWindow); LCanvas.FillRect(LRect);
When we've got to draw normal text, we will use the following code:
LCanvas.Font.Color := StyleServices.GetStyleFontColor(sfButtonTextNormal); LCanvas.TextRect(LRect, LValue, [TTextFormats.tfCenter, TTextFormats.tfVerticalCenter]);
It is clear that the paradigm is:
- Get the current color for the selected element of the UI according to the style
- Draw the graphics using that color
Clicking on the Simulate Problems button, it is possible to see how the graphics is drawn in the case of problems on the power line. The images are drawn directly from the image list using the following code:
procedure TMainForm.DrawImageOnCanvas(ACanvas: TCanvas; var ARect: TRect; ImageIndex: Integer); begin ImageList1.Draw(ACanvas, ARect.Left + 4, ARect.Top + ((ARect.Bottom - ARect.Top) div 2) - 16, ImageIndex); end;
Using this approach, the application created in this recipe, which has a lot of custom graphics, behaves very well even on VCL styles. Here are some screenshots:
As you see, the application correctly draws the owner draw parts of the UI using the right colors from the selected style.
There's more…
The VCL style infrastructure is very powerful. In the case of TWinControl descendants, you can even define specific hooks for you components using TStyleHook
. TStyleHook
is a class that handles messages for controls acting as a wrapper for the hooked control. If you have a custom control that you want to be style enabled, inherit from TStyleHook
and provide custom processing for that control. As examples, see TEditStyleHook
and TComboBoxStyleHook
. You need to register the style hook class with the style engine using the RegisterStyleHook
method as shown in the following code:
TCustomStyleEngine.RegisterStyleHook(TCustomEdit, TEditStyleHook);
Moreover, the StyleServices
function returns an instance of TCustomStyleServices
, which provides a lot of customization methods related to the VCL styles. Check out the related documentation at http://docwiki.embarcadero.com/Libraries/en/Vcl.Themes.TCustomStyleServices_Methods to see all the possibilities