Using Delphi XE7 on a Windows 7 Pro 64-bit system. If I choose \'Charcoal Dark Slate\' VCL style, the 16x16 pixel titel bar icon down-sized from the 32x32 program icon looks
I finally got to the bottom of this issue, and figured out why this worked without VCL styles, and doesn't work with VCL styles.
Preface: For years, VCL has not supported the concept of an icon graphic with multiple icon sizes: a TIcon
was always assumed to be one single graphic - not a set of graphics of varying dimensions and resolutions. This is still true, and a design issue probably not easy to correct in the VCL.
The VCL will set the form icon by way of the WM_SETICON message. VCL always sets wParam
to ICON_BIG
: an examination of VCL sources shows it never uses ICON_SMALL
when setting the icon. Additionally, the hIcon
and hIconSm
member variables of WNDCLASSEX
structure are always NULL
when creating the window class. Therefore, it's clear that VCL never even attempts to set a small icon. Normally, if an app never sets a small icon, Windows will resize the large icon to be the small size, which is quite ugly. However, there is an important exception to that rule.
Note that a Windows resource file's ICON resource will actually store what is known as an icon group, which is a set of the individual icon images from the original .ico
file. The LoadIcon API states that only the large 32x32 icon will be loaded. However, this is not actually strictly true. It seems that Windows itself maintains a link between an HICON
and the original resource, so that if icons of other sizes are required, Windows can go load them as needed.
This fact is not well-documented, but there is one place in MSDN that states this fact: WNDCLASSEX structure, hIconSm
variable:
A handle to a small icon that is associated with the window class. If this member is NULL, the system searches the icon resource specified by the hIcon member for an icon of the appropriate size to use as the small icon.
Therefore, even though VCL did not support small icons properly by way of the public TForm.Icon
class (e.g. by assigning it from the property editor at design time), it was still possible to get things working right using one of these two methods:
TForm.Icon
property unset (no icon). In that case, the form will get the icon from TApplication.Icon
. The default value of this comes from the application's MAINICON
resource. From TApplication.Create
: FIcon := TIcon.Create;
FIcon.Handle := LoadIcon(MainInstance, 'MAINICON');
myForm->Icon->LoadFromResourceName(FindHInstance(...), L"OtherResource");
Therefore, VCL provides basic support for small icons, because it supports loading icons from resources, and Windows supports loading small icons from large icons that were loaded from a resource.
The problem: VCL styles obviously does not rely on Windows-default rendering behavior for non-client areas, such as the title bar. Instead, it handles all the rendering itself. One task in rendering is that VCL styles must determine what icon to render. As it turns out, even though the main VCL form classes don't support small icons - the VCL styles hook does! Well, sort of. This happens in TFormStyleHook.GetIcon
:
TmpHandle := THandle(SendMessage(Handle, WM_GETICON, ICON_SMALL, 0));
if TmpHandle = 0 then
TmpHandle := THandle(SendMessage(Handle, WM_GETICON, ICON_BIG, 0));
The problem is that the hook calls WM_GETICON with ICON_SMALL
and not ICON_SMALL2
. From MSDN:
ICON_SMALL
: Retrieve the small icon for the window.
NULL
because VCL is not setting a small icon.ICON_SMALL2
: Retrieves the small icon provided by the application. If the application does not provide one, the system uses the system-generated icon for that window. (emphasis mine)
HICON
: the question is how does the system generate the icon? My experiments show that one way or another, you'll get a small icon:
The fix: VCL needs to use ICON_SMALL2
instead of ICON_SMALL
whenever calling WM_GETICON
. (Note there is similar code in TFormStyleHook.TMainMenuBarStyleHook.GetIcon
that will also require fixing.) Since this is such a trivially easy fix, I hope Embarcadero applies it soon.
The workaround: Until VCL is fixed, you have to make your own derived form hook. Unfortunately, TFormStyleHook.GetIcon
is private, and really hard to get to. So we try a different technique: alter message handling behavior for WM_GETICON
when wParam
is ICON_SMALL
so that it will instead be like ICON_SMALL2
.
class TFixedFormStyleHook : public TFormStyleHook
{
public:
bool PreventRecursion;
__fastcall virtual TFixedFormStyleHook(TWinControl* AControl)
: TFormStyleHook(AControl), PreventRecursion(false) {}
virtual void __fastcall WndProc(TMessage &Message)
{
if (Message.Msg == WM_GETICON && Message.WParam == ICON_SMALL &&
!PreventRecursion && this->Control &&
this->Control->HandleAllocated())
{
// Just in case some future Windows version decides to call us again
// with ICON_SMALL as response to being called with ICON_SMALL2.
PreventRecursion = true;
Message.Result = SendMessage(this->Control->Handle, WM_GETICON,
ICON_SMALL2, Message.LParam);
PreventRecursion = false;
this->Handled = true;
} else {
this->TFormStyleHook::WndProc(Message);
}
}
};
// In your WinMain function, you have to register your hook for every data
// type that VCL originally registered TFormStyleHook for:
TCustomStyleEngine::RegisterStyleHook(__classid(TForm),
__classid(TFixedFormStyleHook));
TCustomStyleEngine::RegisterStyleHook(__classid(TCustomForm),
__classid(TFixedFormStyleHook));
This is known issue with VCL Styles http://qc.embarcadero.com/wc/qcmain.aspx?d=106224
Also see this issue in Embarcadero's newer QC site: https://quality.embarcadero.com/browse/RSP-11572 --- it's been 3 years since initially reported, and still not fixed. If enough people vote for that issue, maybe it will get some attention.
As workaround you can load proper 16x16 icon into form's Icon
property.
In order for that to work you have to also set Application.MainFormOnTaskBar := false;
in your .dpr
file
However that has some other undesirable effects because it will disable Windows Vista or Windows 7 Aero effects, including live taskbar thumbnails, Dynamic Windows, Windows Flip, and Windows Flip 3D. See: MainFormOnTaskBar
In any case do not change your application icon size because it is the worst solution.
@James Johnston,
Thank you! It works fine for me although there was a small glitch in the posted code:
Message.Result = SendMessage(this->Control->Handle, WM_GETICON,
ICON_SMALL2, Message.LParam);
PreventRecursion = false;
this->Handled = true;
Remove PreventRecursion = false;
Here is the Delphi port of your code:
unit FixIconHook;
interface
uses
WinAPI.Windows,
WinAPI.Messages,
Vcl.Graphics,
Vcl.Controls,
Vcl.Forms,
Vcl.Themes;
type
TFixedFormStyleHook = class(TFormStyleHook)
private
PreventRecursion: Boolean;
strict protected
procedure WndProc(var Message: TMessage); override;
public
constructor Create(AControl: TWinControl); override;
end;
implementation
constructor TFixedFormStyleHook.Create(AControl: TWinControl);
begin
inherited Create(AControl);
PreventRecursion := False;
end;
procedure TFixedFormStyleHook.WndProc(var Message: TMessage);
begin
if (Message.Msg = WM_GETICON) and (Message.WParam = ICON_SMALL) and
(not PreventRecursion) and (Assigned(Control) and
Control.HandleAllocated) then
begin
// Just in case some future Windows version decides to call us again
// with ICON_SMALL as response to being called with ICON_SMALL2.
PreventRecursion := true;
Message.Result := SendMessage(Control.Handle,
WM_GETICON, ICON_SMALL2, Message.LParam);
Handled := true;
exit;
end;
inherited WndProc(Message);
end;
end.
Then in your DPR project:
TStyleManager.Engine.RegisterStyleHook(TForm, TFixedFormStyleHook);
TStyleManager.Engine.RegisterStyleHook(TCustomForm, TFixedFormStyleHook);