Program icon looks curious in the title bar when using a VCL style

孤街醉人 提交于 2019-12-01 04:08:42
Dalija Prasnikar

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.

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:

  • Leave the 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');
    
  • If you don't want to use the application default icon, you can load a different icon resource at runtime; in C++:

    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.
    In the above code, this will return 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)
    In the above code, this would return a real 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:
    • If the large icon is linked to an underlying Windows resource that has an appropriate icon size, then that icon is returned.
    • Otherwise, the system will resize another icon to fit the required small icon dimensions, and return that.

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));

@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);
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!