First first attempt involved using the PixelsPerInch property of a form. The
help says that PixelsPerInch is used by the form to do scaling when on a
different DPI system (and ScaleBy is set to true). i thought i could do
something like:
Button1.Left := MulDiv(THE_BUTTON_WIDHT, Screen.PixelsPerInch,
Self.PixelsPerInch);
i assumed that the forms PixelsPerInch (e.g. Self.PixelsPerInch) was a
property that was streamed with the form, and loaded at runtime. So if the
form was originally designed on a 96dpi system to have the button at 167px,
and was now running on a 108dpi system, the math would be
Button1.Left := MulDiv(167, 108, 96); //188px
But it turns out that when you read a form's PixelsPerInch property it is
not saved at 96dpi, but is actually 108dpi. So the math turns out to be:
Button1.Left := MulDiv(167, 108, 108); //167px
But that's not possible. Without the reference of the form's designed dpi,
how is Delphi's streaming system able to know what it needs to scale the
form by? Whatever mechanism that it is using, i can then use that same thing
to calculate my own sizings. Turns out that the documentation is lying, the
PixelsPerInch property is not used to perform any scaling or position of any
elements on the form.
Instead, the Delphi form streamer writes out a hidden value (TextHeight) to
the DFM. This is height in pixels of the the number '0' in the form's font.
This is the benchmark that the entire form is scaled by, not PixelsPerInch.
Problem is that TextHeight value is not a property of the form, it's stored
in the DFM by the streaming system, and read back just so it can be used to
scale everything on the form. This means that i have no access to the value.
i cannot access the PixelsPerInch of a form as it was designed, nor do i
have access to the font's height (in pixels) when it was designed. What then
do i use as my base scaling value? i could hard-code 96dpi, but then all
developers (even me) must be running at 96dpi.
i know nobody else honors dpi settings in Delphi, but how *would* you do it
if you had to? Everything would have been perfect if the designed
PixelsPerInch was available.
A future post will be asking why "DpiAware" Delphi application running on
Vista do not scale properly. i know nobody's done it because nobody has
asked about it.
NOTE: Never use SetProcessDPIAware, instead manifest your application to be
high dpi aware using:
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"
xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" >
<asmv3:application>
<asmv3:windowsSettings
xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>
See "Why SetProcessDPIAware shouldn't exist" at
http://jamesfinnigan.spaces.live.com/blog/cns!9062539B2F0077D6!395.entry
for more information.
Button1.Left := MulDiv(DESIGNED_LEFT, PixelsPerInch, 96);
that's because Delphi's form scaling routine when serializing it based on
font size (in pixels). There is a huge rounding error introduced by sizing
based on font sizes.
For example:
96dpi: 8pt = 11 px (10.666px)
108dpi: 8pt = 12 px (12.000px)
So if you had an item that had to be positioned at 250px (based on 96dpi
form) then the two conversion mechanisms are:
Method 1: Scale using DPI differences:
250px * (108dpi / 96dpi) = 281px (281.25px)
Method 2: Scale using font size differences (dfm streaming mechanism)
250px * (12px / 11px) = 273px (272.7272 px)
The difference between these two methods introduces a large error because of
the rounding of font pixel sizes.
So if i want my Delphi applications to be DPI compatible, i either need
access to the form's secret TextHeight value, or i need to go into every
form, turn off ScaleBy, and manually call ScaleBy(PixelsPerInch, 96) on
every form create.
It's no wonder nobody handles dpi settings corectly, and why in Vista MS
gave up and just stretches everyone's forms for them.
For example, i had a form that had a ClientWidth of 400px (at 96dpi). When
Vista is running at 108dpi, the form is scaled (by Vista) to 450px.
400px * (108dpi / 96dpi) = 450px
On the other hand if i manifest the application to be dpi aware (which tells
Vista not to scale the form for me, my application will do it itself),
Delphi uses it's own scaling algorithm (based on highly error prone font
sizes) and gives a form client width of 431px (which doesn't actually add up
if you do the math):
GetTextHeight('0') = 13px (at 96dpi)
GetTextHeight('0') = 15px (at 108dpi)
400px * (15px / 13px) = 462px (461.53px)
As opposed to an actual ClientWidth of 431px.
Point being Delphi's scaling algorithm is so off the charts wrong, how does
anyone else do it?
Turns out that you *cannot* perform scaling based on pixels per inch
Before:
PixelsPerInch = 96
After
PixelsPerInch = 108
Then when the 108dpi developer goes to run the application everything is too
small because the DFM streaming system thinks that the form was designed at
108dpi.
So then the 108dpi developer has to reset the PixelsPerInch property of the
form back to 96dpi. This means that he has to design a form at 96dpi when
his machine is running 108dpi.
Before Scaling
Panel1.Height = 21
Panel2.Height = 21
After Scaling
Panel1.Height = 23
Panel2.Height = 24
This is because rather than scaling the height of a control, they scale the
bottom of a control:
H := MulDiv(FTop + FHeight, M, D) - Y
Which in the one case is
H := MulDiv(220 + 21, 108, 96) - 248
= 271.125 - 248
= 271 - 248
= 23
and in the other
H := MulDiv(250 + 21, 108, 96) - 281
= 304.875 - 281
= 305 - 281
= 24
Which on my particular form, the difference is glaring - which is how i
noticed it.
BTW, there's a bug in the D5 scaling algorithm for height. Fortunatly it's
never come up because nobody has ever dared to touch the scaling Flags
(which default to all set):
if (sfWidth in Flags) and not (csFixedWidth in ControlStyle) then
if sfLeft in Flags then
W := MulDiv(FLeft + FWidth, M, D) - X
else
W := MulDiv(FWidth, M, D)
else
W := FWidth;
if (sfHeight in Flags) and not (csFixedHeight in ControlStyle) then
if sfHeight in Flags then <------- here, should be sfTop
H := MulDiv(FTop + FHeight, M, D) - Y
else
H := MulDiv(FTop, M, D ) <-------and here, should be FHeight
else
H := FHeight;
No wonder MS had to forget trying to get applications to behave nicely under
high dpi by just lying to the application and doing the scaling itself. It's
not even the developer's fault, it's the development environment that the
developer was using.
i'm sure this is due to the fact that the 8pt font is actually 11px. But an
11px font at 108dpi is actually 7pt:
11px / 108dpi * 72pt/in = 7.33pt ==> 7pt
What makes no sense is that the only thing being adjusted on the form is the
font point sizes. No control is resized or repositioned.
Which makes no sense.
> What makes no sense is that the only thing being adjusted on the form
> is the font point sizes. No control is resized or repositioned.
The problem is that the DFM file stores a fonts size on pixels, not in
points. The points size is calculated from the pixel size and the
PixelsPerInch value.
--
Peter Below (TeamB)
Don't be a vampire (http://slash7.com/pages/vampires),
use the newsgroup archives :
http://www.tamaracka.com/search.htm
http://groups.google.com
You'd think that an 8pt font should be 8pt no matter what. And on a higher
dpi system it happens to take up more pixels.
CurrentHeightOfZeroCharacter / OriginallyDesignedHeightOfZeroCharacter
rather than
CurrentDpiSetting / DesignedDpiSetting
This leads to a scaling fraction that you cannot recover, cannot reproduce
for your own calculations, and is also wrong. So the idea is to just live
with it as many forms as possible since it is pretty good. Then only on
forms that contain hard-coded pixel values must you disable the built-in
scaling and do it yourself.
If you *do* have to do any work in pixels, then you have to modify all pixel
values at runtime to be:
pixels := MulDiv(pixels, Screen.Resolution, 96);
NOTE: substitute the dpi you were working at when you arrived at the
hard-coded "pixels" value.
You must then also disable the form's Scaled property, and during OnCreate
of your form you must manually call:
Self.ScaleBy(Screen.Resolution, 96);
NOTE: again substitute the dpi you were working at when you designed the
form.
Have you tried anything like ElasticForm?
Roy Lambert
On wait, ElasticForm. Singular
http://home.flash.net/~qsystems/ElasticFormIndex.htm
No, and i wouldn't be able to explain why i want to spend $50 for a feature
that nobody except i will use.
With AutoScroll enabled, the form's size is saved using it's Width and
Height. With AutoScroll disabled, the form's size is saved using it's
ClientWidth and ClientHeight.
There is a non-dpi reason to always have AutoScroll off. In the transition
from Windows9x/2000 to XP/Vista, the Windows caption bar took up more space.
If the caption takes up more space, but the form is saved using a fixed
overall height, there is less space available for content. This pushes items
off the bottom of some forms (making a scrollbar appear.) And because a
scrollbar is on the right, there is now less horizontal space available for
content, and a horizontal scrollbar appears.
You can always pick out a Delphi application that was designed before XP
because things don't fit anymore.
i've never heard of it. And looks like nobody else has, with only 3 google
search results.
How about you've already spent about $100 posting here <g>
Roy Lambert
Touche!
The software also seems to solve problems that i don't want solving -
stretching everything on a form as i resize it.
>The software also seems to solve problems that i don't want solving -
>stretching everything on a form as i resize it.
Off the top of my head I can't see a way round that. You could try emailing Claudia (if she's back from working in Europe she should be pretty responsive). If I get a gap where I don't mind screwing my display I'll have a play around with ElasticForm and see if I can do anything. One thought does occur you can all components to an exclude list so only your own get resized.
Roy Lambert
Ian,
I have read a good bit of such information dealing with different DPI displays, particularly posts in these groups from Peter Below. Based on what I read, when I have a problem reported to me with my code, I am able to easily add a procedure method called FormFixUp to my code that makes all corrections on an ad hoc basis at runtime. The amazing thing to me, is that once fixed they stay fixed.
However, I have not had good luck solving the setting of the initial position of programs in multi-monitor systems; but this is another story.
Rgds, JohnH
it needs to be disabled. It is disabled by setting the Scaled property of
the form to False.
This then allows (and requires) that during the form's constructor you must
scale the form yourself.
procedure TForm1.FormCreate(Sender: TObject);
begin
ScaleBy(PixelsPerInch, 96);
end;
Note the form is assumed to be designed at 96dpi. This is true in a
development house where more than one developer is working on some software.
With the form no longer using some scaling when opened in the IDE, we need
to pick a dpi that the form was designed in. Since most people will be
using 96dpi, we'll use that. (If i were to design the form for my computer's
108dpi, then the unScaled form on another developer's computer would look
too big.)
The downside of this is that the Scaled property defaults to True. This
means that if you open the form in your high-dpi IDE, the form will be
scaled to 108dpi before you get a chance to turn off the Scaled property!
In order to get around this, you need to manually edit the (text) DFM file,
adding the Scaled property:
object Form1: TForm1
...
PixelsPerInch = 96
Scaled = False <-- add
TextHeight = 13
...
end
This will let you open the form in the IDE without having Delphi apply it's
incorrect scaling.
This is so much of a nightmare, and i won't be doing it much longer.
Why in the name of god Borland didn't you either
a) scale by dpi
b) expose the designed DPI/TextHeight
c) both a & b
and save anyone a hell of a lot of work?
i'd rather not, but i'm going to have to settle for having 2nd-class
applications.
I was busy with similar problem recently. I needed ability to resize
forms for customer, who had changed on Windows Display properties under
Appearance to use large fonts (Terminal service, so dpi changing was not
suitable). And our application had scaling disabled anyway. Things were
easier, because we use our own base form class for all forms. But in
most forms we were using specified font sizes - not windows default. In
doing scaling we discovered, that at least under Delphi 5 it didn't
handle at all controls anchored to right/bottom. So I wrote custom
TControl, TWinControl, TCustomForm scaling functions using Borlands
initial code as base.
And I hardcoded initial DPI as 96 and scaling is enabled manually for
specific size (since systems actual DPI is same, it's only way anyway).
I seems to be mostly working now - so it's possible to make it work.
--
Virgo Pärna
virgo...@mail.ee
Of course, if i wanted to really do it right, i've have a ScaleByX and
ScaleByY, since fonts can have different aspect ratios. We can all agree
that you cannot use Scaled, since that math is not reproducable. And you
can't use ScaleBy since it gives incorrect results.
You might be interested in a function that recursivly goes through all
controls on a form and "Standardizes" it's font. Any font that is
MS Sans Serif (the Delphi default)
Tahoma (the Windows 2000/XP default, and the Vista default if you don't
support Vista)
MS Shell Dlg (the Win9x internationalization font)
MS Shell Dlg 2 (the 2000/XP/Vista internationalization font)
and 8pt, will get converted to the current user's preference font face and
point. Which is by default:
2000/XP: Tahoma 8pt
Vista: Segoe UI 9pt
Oh, and it also forces ClearType on.
procedure StandardizeFont(AControl: TControl; ForceClearType: Boolean=False;
FontName: string=''; FontSize: Integer=0);
const
CLEARTYPE_QUALITY = 5;
var
i: Integer;
RunComponent: TComponent;
AControlFont: TFont;
// ncm: TNonClientMetrics;
CanChangeName: Boolean;
CanChangeSize: Boolean;
lf: TLogFont;
begin
if not Assigned(AControl) then
Exit;
{$IFDEF ForceClearType}
ForceClearType := True;
{$ELSE}
if g_ForceClearType then
ForceClearType := True;
{$ENDIF}
{
Font size of 0 means get system default.
Font size of -1 means don't change it
}
if FontSize = 0 then
begin
{
The non-client metrics are things are not your application
CaptionFont,
SmallCaptionFont,
MenuFont,
StatusFont,
MessageBox font
ZeroMemory(@ncm, SizeOf(ncm));
ncm.cbSize := SizeOf(ncm);
if SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, @ncm, 0) then
begin
FontName := PChar(Addr(ncm.lfMessageFont.lfFaceName[0]));
FontSize := ncm.lfMessageFont.lfHeight;
}
ZeroMemory(@lf, SizeOf(lf));
if SystemParametersInfo(SPI_GETICONTITLELOGFONT, SizeOf(lf), @lf, 0) then
begin
FontName := PChar(Addr(lf.lfFaceName[0]));
FontSize := lf.lfHeight;
// FontSize := -1; //Our forms are not designed for font sizes other than
8. Let Delphi's dpi scaling handle that
// if ForceClearType then
// ncm.lfMessageFont.lfQuality := CLEARTYPE_QUALITY;
end
else
begin
FontName := 'MS Shell Dlg 2';
FontSize := -1; //we can't get it, stop trying
end;
end;
for i := 0 to AControl.ComponentCount-1 do
begin
RunComponent := AControl.Components[i];
if RunComponent is TControl then
StandardizeFont(TControl(RunComponent), ForceClearType, FontName,
FontSize);
end;
if AControl is TForm then
AControlFont := TForm(AControl).Font
else if AControl is TLabel then
AControlFont := TLabel(AControl).Font
else if AControl is TEdit then
AControlFont := TEdit(AControl).Font
else if AControl is TMemo then
AControlFont := TMemo(AControl).Font
else if AControl is TButton then
AControlFont := TButton(AControl).Font
else if AControl is TCheckBox then
AControlFont := TCheckBox(AControl).Font
else if AControl is TRadioButton then
AControlFont := TRadioButton(AControl).Font
else if AControl is TRadioButton then
AControlFont := TRadioButton(AControl).Font
else if AControl is TListBox then
AControlFont := TListBox(AControl).Font
else if AControl is TComboBox then
AControlFont := TComboBox(AControl).Font
else if AControl is TGroupBox then
AControlFont := TGroupBox(AControl).Font
else if AControl is TRadioGroup then
AControlFont := TRadioGroup(AControl).Font
else if AControl is TPanel then
AControlFont := TPanel(AControl).Font
{ Additional }
else if AControl is TBitBtn then
AControlFont := TBitBtn(AControl).Font
else if AControl is TSpeedButton then
AControlFont := TSpeedButton(AControl).Font
//maskedit
//stringgrid
//Checklistbox
else if AControl is TStaticText then
AControlFont := TStaticText(AControl).Font
{ Win32 }
else if AControl is TTabControl then
AControlFont := TTabControl(AControl).Font
else if AControl is TPageControl then
AControlFont := TPageControl(AControl).Font
else if AControl is TDateTimePicker then
AControlFont := TDateTimePicker(AControl).Font
//MonthCalendar
else if AControl is TTreeView then
AControlFont := TTreeView(AControl).Font
else if AControl is TListView then
AControlFont := TListView(AControl).Font
else if AControl is TStatusBar then
begin
TStatusBar(AControl).UseSystemFont := True;
// AControlFont := TStatusBar(AControl).Font
AControlFont := nil; //StatusBar has their own system font
end
else if AControl is TToolbar then
AControlFont := TToolbar(AControl).Font
else if AControl is TCoolbar then
AControlFont := TCoolbar(AControl).Font
{ System }
else if AControl is TPaintBox then
AControlFont := TPaintBox(AControl).Font
{ TEditListView }
{$IFNDEF NoEditListView}
else if AControl is TCustomListView then
AControlFont := TEditListView(AControl).Font
{$ENDIF}
{ Cannot fontify}
else if AControl is TToolButton then
AControlFont := nil
else if AControl is TBevel then
AControlFont := nil
{ else if AControl is TImage then
Exit
else if AControl is TProgressBar then
Exit
else if AControl is TPaintBox then
Exit
else if AControl is TShape then
Exit
}
else
begin
AControlFont := nil;
// ShowMessage('Unknown font class: '+AControl.Name+'
('+AControl.Classname+')');
end;
//Standardize the font if it's currently "MS Sans Serif" (the Delphi
default)
//or "Tahoma" (when "MS Shell Dlg 2" should have been used)
if not Assigned(AControlFont) then
Exit;
CanChangeName :=
(AControlFont.Name = 'MS Sans Serif') or
(AControlFont.Name = 'Tahoma') or
(AControlFont.Name = 'MS Shell Dlg 2') or
(AControlFont.Name = 'MS Shell Dlg');
CanChangeSize :=
(FontSize <> 0) and
(FontSize <> -1) and
(AControlFont.Size = 8) and
(AControlFont.Height <> FontSize);
if CanChangeName or CanChangeSize or ForceClearType then
begin
if GetObject(AControlFont.Handle, SizeOf(TLogFont), @lf) <> 0 then
begin
//Change the font attributes and put it back
if CanChangeName then
StrPLCopy(Addr(lf.lfFaceName[0]), FontName, LF_FACESIZE);
if CanChangeSize then
lf.lfHeight := FontSize;
if ForceClearType then
lf.lfQuality := CLEARTYPE_QUALITY;
AControlFont.Handle := CreateFontIndirect(lf);
end
else
begin
if CanChangeName then
AControlFont.Name := FontName;
if CanChangeSize then
begin
if FontSize > 0 then
AControlFont.Size := FontSize
else if FontSize < 0 then
AControlFont.Height := FontSize;
end;
end;
end;
end;
"Virgo Pärna" <virgo...@mail.ee> wrote in message
news:4798...@newsgroups.borland.com...
Our application had scaling disabled anyway. I wrote custom
While it's interesting, it does not really suite when UI uses many
labels/buttons/edit boxes (data entry forms), because there you need to
change positions and sizes of those controls (larger font would not fit
into existing control). And scaling function actually specifies larger
font to use, as a part of scaling. We have specified the fonts to be
used on those controls in design time and specifying larger font does
not change proportions. But if I'd change font face, then it could
happen, that controls proportions don't suit for large fonts. I'm not
even sure, that there is perfect solutions for scaling problems because
in perfect case I would be using fonts from users preferences.
Luckily we all use small fonts in our development computers, so we
don't have additional problems caused by that factor.
--
Virgo Pärna
virgo...@mail.ee
Windows Vista User Experience Guidelines
Layout Metrics
http://msdn2.microsoft.com/en-us/library/bb847924.aspx
also
Layout
http://msdn2.microsoft.com/en-us/library/aa511279.aspx
Since scaling should be done vertically based on font height, and
horizontally based on average character width, scaling of controls in the
horizontal and vertical directions is different.
For example, the 2000/XP default font Tahoma 8pt, at 96dpi is:
height: 13 pixels
average character width: 6 pixels
The Vista default font Segoe UI 9pt, at 96dpi is:
height: 15 pixels
average character width: 7 pixels
Getting these numbers for the users's current font choice is not easy, since
it requires a form canvas that you can actually measure a length of text,
and figure out the average width that way. You can get the average font face
and size by using:
procedure GetUserFontPreference(out FaceName: string; out PixelHeight:
Integer);
var
lf: LOGFONT;
begin
ZeroMemory(@lf, SizeOf(lf));
if SystemParametersInfo(SPI_GETICONTITLELOGFONT, SizeOf(lf), @lf, 0)
then
begin
FaceName := PChar(Addr(lf.lfFaceName[0]));
PixelHeight := lf.lfHeight;
end
else
begin
{
If we can't get it, then assume the same non-user preferences
that
everyone else does.
}
FaceName := 'MS Shell Dlg 2';
PixelHeight := 8;
end;
end;
So now you can change the font your form uses. The value here is that the
font height returned (in pixels) honors the users's DPI settings.
But you still don't know the metrics of that font. The Microsoft KB article:
How to calculate dialog box units based on the current font in Visual C++
http://support.microsoft.com/kb/145994
talks about having to do it. Basically you measure the length of the string
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" and divide by 52 to
get the average character width. The height
function GetDialogBaseUnitsReal(const AForm: TForm): TSize;
var
dc: HDC;
tm: TTextMetric;
BaseUnitX: LongInt;
BaseUnitY: LongInt;
Size: TSize;
begin
dc := AForm.Canvas.Handle;
if dc = 0 then
RaiseLastWin32Error;
Win32Check(GetTextMetrics(dc, tm));
BaseUnitY := tm.tmHeight;
Win32Check(GetTextExtentPoint32(
dc,
PChar('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'),
52,
Size));
BaseUnitX := (Size.cx div 26 + 1) div 2;
Result.cx := BaseUnitX;
Result.cy := BaseUnitY;
end;
This function returns the average size of the font (in pixels):
Segoe UI, 9pt, 96dpi: (15, 7)
Tahoma, 8pt, 96dpi: (13, 6)
Once you measure the average width and height of the users's current font
choice, and set all controls on the form to that font, you can then scale
all those controls' sizes and positions. Since you know that you always
design your form with Tahoma 8pt 96dpi, you can use your ScaleFormBy
routine:
procedure ScaleFormBy(AForm: TForm; Myy, Dy: Integer; Mx, Dx: Integer);
ScaleFormBy(Self, FontSize.cy, 13, FontSize.cx, 6);
It's important though that the ScaleFormBy routine not try to scale fonts.
That needs to be done separatly by a routine that cycles through all
controls on a form, and anything that is 8pt Tahoma/MS Shell Dlg/MS Sans
Serif convert to the user preference font. i have a routine that does that
called StandardizeFont
StandardizeFont(Self);
FontSize := GetDialogBaseUnitsReal(Self);
ScaleFormBy(Self, FontSize.cy, 13, FontSize.cx, 6);
i suppose all three of these could be put into a new super fixall function.
i just now need to write the ScaleFormBy() function.