Hack#16: Published field RTTI replacement trick
We're back to fixing the interesting (learning-wise) problem of the flickering TProgressBar on Windows Vista. We have already looked at two relatively dirty and intrusive hacks that either overwrites the TClass reference inside each progress bar instance or overwrites the dynamic method table pointer of the original TProgressBar VMT. There are other related code page overwrite hacks that we may look at in the future (overwriting a virtual method slot, for instance), but this time we will look at a much simpler and (IMO) more elegant solution; tricking the compiler to replace the RTTI it generates for the component field on the for with a fixed version of TProgressBar.
As you know (if you have read the published fields and details articles), the IDE inserts component fields into the unnamed published section of your form class declaration as you drop components and controls on to the form at design time. Since the fields are published, the compiler generates RTTI for them - including the name, type and offset of the field.
For instance, creating a new form and dropping a couple of labels, a button and a progress bar on it, the IDE has generated the following code.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes,
Graphics, Controls, Forms,
Dialogs, ComCtrls, StdCtrls;
type
TForm1 = class(TForm)
Label1: TLabel;
Label2: TLabel;
Button1: TButton;
ProgressBar1: TProgressBar;
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
end.
The published field RTTI includes the type of each field - encoded as a TClass reference. But how does the compiler know what TClass to use? By using the normal Pascal scoping rules, of course. TLabel and TButton both refers to the classes defined in the StdCtrls unit. For TProgressBar the compiler first checks the StdCtrls unit, but failing to find any TProgressBar class there, it looks in ComCtrls unit and finds it there.
Here lies the clue to our trick - we can simply add another unit to the interface uses clause, one that contains a fixed version of TProgressBar. We just have to make sure this unit is listed after the unit that contains the original TProgressBar (ComCtrls in this case). Let's get to it.
First we write (yet another version of) the fixed TProgressClass and put it inside an aptly named unit.
unit HVProgressBarVistaFix;
interface
uses Messages, ComCtrls;
type
TProgressBar = class(ComCtrls.TProgressBar)
private
procedure WMEraseBkgnd(var Message: TWmEraseBkgnd);
message WM_ERASEBKGND;
end;
implementation
procedure TProgressBar.WMEraseBkgnd(var Message: TWmEraseBkgnd);
begin
DefaultHandler(Message);
end;
end.
Notice two things; the class must have the same name as the original class 'TProgressBar' and to avoid a compiler error message like
[Error] HVProgressBarVistaFix.pas(8): Type 'TProgressBar' is not yet completely defined
The class we inherit from has to be explicitly qualified with the unit it comes from 'ComCtrls.TProgressBar'.
With this simple unit in hand, we can fix the progress bar flickering simply by referencing the HVProgressBarVistaFix unit from all forms that uses progress bars (search your code for TProgressBar). Make sure it is listed last in the uses clause of the interface section.
uses
Windows, Messages, SysUtils, Variants, Classes,
Graphics, Controls, Forms,
Dialogs, ComCtrls, StdCtrls, HVProgressBarVistaFix;
This ensures that the compiler inserts the TClass reference of our fixed version of the TProgressBar class into the published field RTTI tables of the form. When the streaming system reads the .dfm at runtime it uses these published field tables (read the details here) to find the TClass reference (or TComponentClass reference, to be exact) and uses it (and the virtual constructor of TComponent) to create the component. Because we have replaced the ComCtrls.TProgressBar class reference with our fixed version in HVProgressBarVistaFix.TProgressBar, it is our class that will be created.
With this simple kind of hack you can introduce any number of fixes, overriding virtual, dynamic or message methods, for instance. Look ma - no dirty hands! ;).
4 comments:
Sounds like you are depending on the compiler to produce the RTTI in a particular order. I know from experience that the compiler can get a bit fuzzy on desired order (I've seen it totally ignore required order and initize units in the wrong order even).
Would it not also be desirable to add the 2 units to the project file (.dpr) uses in the desired order as well just be sure?
ie:
Uses
baseunit,
replacementunit,
form1 ...
After all, the compiler takes much stronger order clues from the .dpr to support things like replacement memory managers.
Note that the trick does not depend on unit *initialization* order - after all there is no initialization code in the unit.
Instead we rely on the compiler's scope-based identifier lookup rules. The per-unit lookup is performed in the reversed uses-clause order.
Adding the unit to the .dpr's uses clause has no effect (unless TProgressBar is used in the project code).
FWIW, I've checked in the fix to the VCL so we'll have it going forward and this will be one less issue to worry about. Not that this post is really focused on TProgressBar but still. :)
Post a Comment