Sunday, May 23, 2004

Hack #1: Write access to a read-only property

The other day I was faced with the task of making our main application behave better systems with different screen resolutions (or rather pixel density, as in pixels per inch). This is the classic Large Font/Small Font problem, and getting forms and dialogs to scale properly to show readable fonts and text on all displays. There are several things to keep in mind, some of them are covered here - there are more complications due to MDI windows and Form inheritance.

To make my testing easier (and possibly to let the end-user override the default scaling behavior) I decided to let the current screen density (as determined by Screen.PixelsPerInch) be controlled from a setting in the Registry. The built-in Delphi form scaling works reasonably well, and relies on the fact that the form's design-time PixelsPerInch value is different form the run-time Screen.PixelsPerInch value. Now, PixelsPerInch is a read-only, public property of the singleton TScreen class. It is initialized in the TScreen constructor to the number of vertical pixels per inch as returned by the current graphics driver:

  DC := GetDC(0); 
FPixelsPerInch := GetDeviceCaps(DC, LOGPIXELSY);

For my testing purposes, I wanted to set the value of the PixelsPerInch property without going to the hassle of actually changing my system setup, but to do that I would somehow have to modify the value of the read-only property. Impossible, right? Well, in software, nothing is really impossible. Software is soft, so we can change it :-). Changing the declaration of TScreen to make the property writeable would work, but as Allen has pointed out, making changes in the interface section of RTL and VCL units can have cascading effects, that are often undesirable. Besides, that would not really qualify as a bona-fide hack - it would have been too easy. Nah, lets do something a little more fun ;P. PixelsPerInch is only a public property, so there is no RTTI for it. Lets declare a descendent class, that promotes the property to published:

TScreenEx = class(TScreen)
property PixelsPerInch;

Now, since TScreen indirectly inherits from TPersistent, and TPersistent was compiled in the $M+ mode, published properties in our TScreenEx class will have RTTI generated for them. But PixelsPerInch is still a read-only property - and there is no way our TScreenEx can make it writeable, because the backing field FPixelsPerInch is private, not protected, and so is off-limits for us.

The cunning thing about the RTTI generated for the TScreenEx.PixelsPerInch property, is that it includes enough information about where to find the backing field in the object instance. Open TypInfo.pas and locate the TPropInfo record that describes the RTTI for each property. Included is the GetProc pointer. For backing fields, this contains the offset off the field in the object instance (sans some flag bits). After decoding this offset and adding it to the base address of the object instance, we now can get a pointer to the backing field and thus modify it - voila write-access! Here is the short version:

procedure SetPixelsPerInch(Value: integer); 
PInteger(Integer(Screen) + (Integer(GetPropInfo(TScreenEx, 'PixelsPerInch').GetProc) and $00FFFFFF))^ := Value;

Decoding that is left as an exercisecise for the reader.


Anonymous said...

Cool thing with the RTTI, Hallvard! now has a link to your blog.

Anonymous said...

No RSS Feed?

Hallvard Vassbotn said...
This comment has been removed by a blog administrator.
Anonymous said...

"Nasty" hack - confirms the risks of having RTTI information. Nothing is "sacred" anymore. Lars F.

Anonymous said...

Added a Atom link now - use any RSS converter if you like.

Also added a few links to other bloggers and a link to an article about form resizing etc.

Anonymous said...

That's cool, esp. for a Norwegian descendant in Vikingland. I'm going to re-WRITE Delphi :).

Anonymous said...

RTTI can be very useful..

Before Troy Wolbrink's excellent Unicode components, I made a simple routine that you call in a Form's OnCreate. It recursively looks at all components on the form that publish a "Font" property and uses RTTI to set the Charset and Name properties of the Font so that it would be able to display a CJK language fitting to the system's locale.

It is rumored that WinXP ignores Font.Charset so my hack became obsolete :)

Alister Christie said...

I'll Certainly take a strong look at this. I have previously been using
ChangeScale(FormZoomLevel, 100);
in the constructor of certain forms, but this is really cool - and evil.

Fernando Madruga said...

Just a minor correction:

1) "pixel density, as in pixels per inch"
2) "This is the classic Large Font/Small Font problem"

Those are not necessarily true, that is, you can have Large/Small fonts *and/or* you can have "normal" (96 DPI) or larger (120 DPI) or even custom (any other DPI) but one does not necessarily imply the other nor do they result in the same type of problems.

For instance, I run my Dell Latitude D810's 15.4" 1680x1050 display with "normal fonts" but at 120 DPI, which is not the 96 DPI default value but is a lot closer to this display's true DPI. I've run into some problems with some poorly written/tested software which even works just fine if I simply set Large Fonts *and* 96 DPI, but shows things out of place or too big if I simply set normal fonts using 120 DPI...

The two (small/large fonts and normal/different DPI) result in different problems as many components behave correctly with one and not the other...

Still, that's a nice hack, even if it's used to break true OOP... :)

Anonymous said...

How about:
PInteger(@(Screen.PixelsPerInch))^ := Value;

Hallvard Vassbotn said...

> PInteger(@(Screen.PixelsPerInch))^ := Value

Try it.

You cannot take the address of a property.

And if you cast the value returned by PixelsPerInch, cast it to a pointer and dereference it, you'll quicky get an AV...

Anonymous said...

>>> You cannot take the address of a property.
Yes, you can not take address for properties like this:
property Height: Integer read GetHeight; // here: GetHeight is a method

@Screen.Height gives you "Variable required" error.

But PixelsPerInch is declared as:
property PixelsPerInch: Integer read FPixelsPerInch; // FPixelsPerInch is a field

@Screen.PixelsPerInch actually gives you @Screen.FPixelsPerInch, i.e. a pointer to private field!

So, cast it to PInteger, dereference it, and you will gain full access to private property.

I had checked it in D7, D2007.

Copyright © 2004-2007 by Hallvard Vassbotn