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

后端 未结 3 1505
渐次进展
渐次进展 2021-01-12 10:21

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

相关标签:
3条回答
  • 2021-01-12 10:27

    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));
    
    0 讨论(0)
  • 2021-01-12 10:42

    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.

    0 讨论(0)
  • 2021-01-12 10:45

    @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);
    
    0 讨论(0)
提交回复
热议问题