使用 VCL 样式时,标题栏中的程序图标看起来很奇怪

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

在 Windows 7 Pro 64 位系统上使用 Delphi XE7。 如果我选择 'Charcoal Dark Slate' VCL 样式,32x32 程序图标中的 16x16 像素标题栏图标 down-sized 看起来不像预期的那样。

它应该看起来像下面的小图标。如果我以 16x16 像素格式加载程序图标,它在标题栏中看起来不错,但在任务栏中看起来很丑,因为放大了 16 到 32 像素。

这是 VCL 样式的已知问题 http://qc.embarcadero.com/wc/qcmain.aspx?d=106224

另请参阅 Embarcadero 较新的 QC 站点中的此问题:https://quality.embarcadero.com/browse/RSP-11572 --- 自最初报告以来已经 3 年了,但仍未修复。如果有足够多的人投票支持这个问题,也许它会得到一些关注。

作为解决方法,您可以将适当的 16x16 图标加载到表单的 Icon 属性 中。

为了使其正常工作,您还必须在 .dpr 文件中设置 Application.MainFormOnTaskBar := false;

但是这会产生一些其他不良影响,因为它会禁用 Windows Vista 或 Windows 7 Aero 效果,包括实时任务栏缩略图、动态 Windows、Windows 翻转, 和 Windows 翻转 3D。参见:MainFormOnTaskBar

在任何情况下都不要更改您的应用程序图标大小,因为这是最糟糕的解决方案。

我终于找到了这个问题的根源,并弄清楚了为什么这在没有 VCL 样式的情况下有效,而在 VCL 样式中不起作用。

前言: 多年来,VCL 一直不支持具有多种图标大小的图标图形的概念:TIcon 始终被假定为一个图形 -不是一组不同尺寸和分辨率的图形。这仍然是正确的,一个设计问题可能不容易在 VCL 中更正。

VCL 将通过 WM_SETICON 消息设置窗体图标。 VCL 总是将 wParam 设置为 ICON_BIG:对 VCL 源代码的检查表明它在设置图标时从不使用 ICON_SMALL。此外,WNDCLASSEX结构的hIconhIconSm成员变量在创建windowclass时总是NULL。因此,很明显 VCL 甚至从未尝试设置一个小图标。通常,如果一个应用程序从来没有设置过小图标,Windows 会将大图标调整为小图标,这非常难看。但是,该规则有一个重要的例外。

请注意,Windows 资源文件的 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 指出只会加载 32x32 大图标。然而,这实际上并非严格正确。似乎Windows本身在HICON和原始资源之间维护了一个link,所以如果需要其他尺寸的图标,Windows可以根据需要加载它们。

这个事实不是 well-documented,但 MSDN 中有一个地方说明了这个事实:WNDCLASSEX structurehIconSm 变量:

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.

因此,即使 VCL 没有通过 public TForm.Icon class 正确支持小图标(例如,通过在设计时从 属性 编辑器分配它时间),仍然可以使用以下两种方法之一使事情正常工作:

  • 保留 TForm.Icon 属性 未设置(无图标)。在这种情况下,表单将从 TApplication.Icon 获取图标。它的默认值来自应用程序的 MAINICON 资源。来自 TApplication.Create:
    FIcon := TIcon.Create;
    FIcon.Handle := LoadIcon(MainInstance, 'MAINICON');
  • 如果不想使用应用默认图标,可以在运行时加载不同的图标资源;在 C++ 中:
    myForm->Icon->LoadFromResourceName(FindHInstance(...), L"OtherResource");

因此,VCL 提供了对小图标的基本支持,因为它支持从资源加载图标,Windows支持从资源加载的大图标加载小图标。

问题: VCL 样式显然不依赖 Windows-default 渲染行为 non-client 区域,例如标题栏。相反,它自己处理所有渲染。渲染中的一项任务是 VCL 样式必须确定要渲染的图标。事实证明,尽管主要的 VCL 表单 classes 不支持小图标 - VCL 样式挂钩支持!好吧,有点。这发生在 TFormStyleHook.GetIcon:

TmpHandle := THandle(SendMessage(Handle, WM_GETICON, ICON_SMALL, 0));
if TmpHandle = 0 then
  TmpHandle := THandle(SendMessage(Handle, WM_GETICON, ICON_BIG, 0));

问题是挂钩调用 WM_GETICON 时使用 ICON_SMALL 而不是 ICON_SMALL2。来自 MSDN:

  • ICON_SMALL: 检索 window. 的小图标
    在上面的代码中,这将return NULL因为VCL没有设置小图标。
  • ICON_SMALL2:获取应用程序提供的小图标。 如果应用程序没有提供,系统会使用 system-generated 图标作为 window。(强调我的)
    在上面的代码中,这会return一个真正的HICON:问题是系统是如何生成图标的?我的实验表明,不管怎样,你都会得到一个小图标:
    • 如果将大图标 link 编辑到具有适当图标大小的基础 Windows 资源,则该图标 return 编辑。
    • 否则,系统将调整另一个图标的大小以适应所需的小图标尺寸,并且return那个。

修复: VCL 在调用 WM_GETICON 时需要使用 ICON_SMALL2 而不是 ICON_SMALL。 (请注意 TFormStyleHook.TMainMenuBarStyleHook.GetIcon 中有类似的代码也需要修复。)由于这是一个非常简单的修复,我希望 Embarcadero 尽快应用它。

解决方法:在修复 VCL 之前,您必须制作自己的派生表单挂钩。不幸的是,TFormStyleHook.GetIcon 是私人的,很难到达。所以我们尝试了一种不同的技术:当 wParamICON_SMALL 时,改变 WM_GETICON 的消息处理行为,这样它就会像 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,

谢谢!虽然发布的代码中有一个小故障,但对我来说效果很好:

Message.Result = SendMessage(this->Control->Handle, WM_GETICON,
    ICON_SMALL2, Message.LParam);
PreventRecursion = false;
this->Handled = true;

删除PreventRecursion = false;

这是您的代码的 Delphi 端口:

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.

那么在你的 DPR 项目中:

TStyleManager.Engine.RegisterStyleHook(TForm, TFixedFormStyleHook);
TStyleManager.Engine.RegisterStyleHook(TCustomForm, TFixedFormStyleHook);