Looking for the best approach to change the background color (and other properties) of the focused data entry control in a Delphi (VCL) application?
By (Windows) design, the control on a data entry form which has the input focus is not drawn (/highlighted) differently from other controls (i.e. those without the input focus). In case of the TEdit (or TMemo and alike) control, only the blinking insertion point is displayed.
To provide visually more attractive user-friendly interfaces for your Delphi applications, you could decide to change the background color (and maybe some other properties) of the currently selected control – the one that has the input focus. Of course, this would include restoring to the original background color when the focus shifts to another control.
OnEnter + OnExit := NoNo
All Delphi controls which can receive the input focus (those derived from the base TWinControl class) expose the OnEnter and OnExit events. The OnEnter is fired when a control receives the input focus, OnExit is fired when the input focus shifts away from the current control to another.
Naturally, the first idea you got is to use the OnEnter and OnExit events of a control to change the Color property to “focused color” in the OnEnter event and restore the “original color” in the OnExit event.
Since Delphi controls can share event handlers , you could decide to write two common procedures to handle OnEnter and OnExit events for all the data entry controls on a form.
Let’s say you have a TEdit (“Edit1”), a TMemo (“Memo1”) and a TComboBox (“ComboBox1”) on a form. If you set all those controls to share a common OnEnter and OnExit event handling procedures, your code would need to look something like:
//No, not the suggested approach... const focusedColor = clSkyBlue; var oldColor : TColor; procedure TMainForm.FormCreate(Sender: TObject) ; begin Button1.OnEnter := ControlOnEnter; ComboBox1.OnEnter := ControlOnEnter; Memo1.OnEnter := ControlOnEnter; Button1.OnExit := ControlOnExit; ComboBox1.OnExit := ControlOnExit; Memo1.OnExit := ControlOnExit; end; procedure TMainForm.ControlOnEnter(Sender: TObject) ; begin if Sender is TEdit then begin oldColor := TEdit(Sender).Color; TEdit(Sender).Color := focusedColor; end; if Sender is TComboBox then begin oldColor := TComboBox(Sender).Color; TComboBox(Sender).Color := focusedColor; end; //... other cases end; procedure TMainForm.ControlOnExit(Sender: TObject) ; begin if Sender is TEdit then begin TEdit(Sender).Color := oldColor; end; //... other cases end;
Why is this approach far from ideal:
- You lose the ability to handle the OnEnter and OnExit event on a control level – as Delphi does not support multicast events.
- You cannot simply use “TWinControl(Sender).Color” as the Color property is not exposed by TWinControl – the class all input controls inherit from. The Color property comes from the TControl class as protected, controls make it public on their own.
- As you drop more controls on the form, you have to make sure the correct class is already handled. For example, if you drop a TRadioButton, you need to add the code to check if the Sender is TRadioButton.
- What if you want to provide such visuall feedback for all your data entry forms? No go!
So, what’s the solution? How to handle those OnEnter and OnExit on a higher level?
RTTI and TScreen to the rescue!
A much better approach is to handle the “got focus” and “lost focus” situations on a higher level.
The TScreen class exposes the OnActiveControlChange event immediately after input focus changes to a new windowed control. This event is fired by the global Screen object that all Delphi applications can exploit, by default. There’s no need to handle each and every control’s OnEnter and OnExit event. OnActiveControlChange is fired when the input focus shifts within the active form, and across forms to a new form when it becomes the active form. Great!
To get around the limitation of having to check the type of the control to type cast to the correct class in order to be able to access the Color property, you can reach for Delphi’s RTTI.
By using Delphi’s run-time type information you can check if a control exposes a particular property as public, get or set the new value – all this without worrying about the class of the control.
For start, you need to define a value for the background color of the currently active (with the input focus) control. You also need to have two variables to hold the reference to the previously active control and its background color. Add the following declarations to the interface section of your application’s Main form:
activeControl : TWinControl; prevInactiveColor : TColor;
Since there is no TScreen component you can drop on a form, you need to attach the event handler for the OnActiveControlChange event manually.
The Main form’s OnCreate event handler is the best place. You also need to make sure that you clean up the screen object when the form is freed by detaching the event handler from the event in the main form’s OnDestroy event handler:
procedure TMainForm.FormCreate(Sender: TObject) ; begin Screen.OnActiveControlChange := ScreenActiveControlChange; end; procedure TMainForm.FormDestroy(Sender: TObject) ; begin Screen.OnActiveControlChange := nil; end;
Here’s the implementation of the “ScreenActiveControlChange” procedure.
procedure TMainForm.ScreenOnActiveControlChange(Sender: TObject); var noEnter, noExit : boolean; prevActiveControl : TWinControl; procedure ColorEnter(Sender : TObject); begin if Assigned(Sender) AND IsPublishedProp(Sender,'Color') then begin prevInactiveColor := GetOrdProp(Sender, 'Color'); SetOrdProp(Sender, 'Color', clSkyBlue); //change clSkyBlue to something else or read from some application configuration :) end; end; (*ColorEnter*) procedure ColorExit(Sender : TObject); begin if Assigned(Sender) AND IsPublishedProp(Sender,'Color') then SetOrdProp(Sender, 'Color', prevInactiveColor); end; (*ColorExit*) begin if Screen.ActiveControl = nil then begin activeControl := nil; Exit; end; noExit := false; noEnter := NOT Screen.ActiveControl.Enabled; noEnter := noEnter OR (Screen.ActiveControl is TForm); //disabling active control focuses the form noEnter := noEnter OR (Screen.ActiveControl is TCheckBox); // skip checkboxes prevActiveControl := activeControl; if prevActiveControl <> nil then begin noExit := prevActiveControl is TForm; noExit := noExit OR (prevActiveControl is TCheckBox); end; activeControl := Screen.ActiveControl; if NOT noExit then ColorExit(prevActiveControl); if NOT noEnter then ColorEnter(activeControl); end;
When the OnActiveControlChange is fired by the Screen object, the ScreenActiveControlChange is executed. Here’s what we want to happen:
- The “previousActiveControl” variable holds the reference to the control that had the input focus before the currently active control got the focus.
- The ActiveControl property (TWinControl type) of the Screen object Indicates which control currently has input focus on the screen (any of the forms in your project). Therefore, we first need to make sure the ActiveControl is assigned.
- Some controls, like TCheckBox, do not look nice if we change their background color. The “noEnter” and “noExit” variables are used to ensure that we really want to change the background color on the active control (and restore on the last focused). The TCheckBox derives from a more generic TButtonControl – which encapsulates behavior common to button controls, check boxes, and radio buttons. In short, we do not want to change the back color of such controls.
- Finally, a custom “ColorExit” procedure is called passing the reference to the control that had the input focus prior to OnActiveControlCnahge being raised. Next, “EnterColor” is called to change the background color of the currently focused control.
Delphi’s RTTI methods are hidden inside the “TypInfo” unit – make sure you add “TypInfo” to the uses clause of the unit where RTTI methods are used.
As for the ColorExit and ColorEnter procedure, first you make sure that the “Color” property is exposed by “Sender” using the “IsPublishedProp” procedure. Next, you store the original background color value using the “GetOrdProp” procedure. Finally, the “SetOrdProp” changes the background color to the color we specified to be used for focused
Here’s the sample application for download and testing.
Do note that this approach works with multiple forms in your application – having the above code only present in the main form, since Screen.OnActiveControlChange covers all controls on all forms.
Runtime Themes Enabled
When you build your application by including support for runtime themes (Project – Options – Application – “Enable Runtime Themes”) some controls, like TCheckBox (and TLabel) get their coloring from the user’s active theme – and changing the Color property (even of the Font.Color) makes no difference. However, for the rest, like TEdit, TMemo and alike the above solution works like a charm and I have to say users of my Delphi applications are supper happy with it 🙂
If you are using VCL styles to style your controls and still want custom coloring, check out: Changing the color of Edit Controls with VCL Styles Enabled.
This article is my old one. There are references to it on the internet with a few modifications, like this one: “Drawing a Focus Rectangle Around the Active Control in Delphi Applications“. If you have a suggestion to coloring focused controls, do share.
After a few changes (units, madexcept, etc), it worked like a charm in D7.
Just wondering if the two assignments for noEnter (lines 29 and 30) were meant to be one?
It did? Even with the glaring bugs in it? 🙂
noEnter := NOT Screen.ActiveControl.Enabled;
noEnter := Screen.ActiveControl is TCheckBox; //skip check boxes (makes sense when no styles applied)
What happens with the first assignment to noEnter?
if prevActiveControl nil then
noExit := NOT prevActiveControl.Enabled;
noExit := prevActiveControl is TCheckBox; //skip check boxes (makes sense when no styles applied)
and with the first assignment to noExit?
@ Ian Krigsman, HeartWare,
Thanks for noticing, fixed. Ah, this is what happens when you copy code from “real” app to test one and you concentrate more on the text of the post and less on testing the test app 😉
What a dreadful idea. To think that every UI designer since the year dot has avoided this approach, and then ignore that fact and go ahead with it?
Joe, ideas are to be implemented or not, completely your decision.