3
votes

How to accomplish the following goal in Delphi 10.2 Tokyo: I need Delphi to automatically set not just the big icon, but both the big and small icons for each window. I need to have an opportunity, for some of the forms, and for the TApplication, to change icons at run-time. I want this to be accomplished without modifying VCL.Forms.pas (the small icon is one that displayed in a window title bar, to the left from window caption).

There is a function in TCustomForm:

function GetIconHandle: HICON;

Unfortunately, Delphi only sets big icon handle, for example, here is a quote from VCL.Forms.pas:

  SendMessage(Handle, WM_SETICON, ICON_BIG, GetIconHandle);

As you see, the above code only sets the big icon handle, but I also need to set the small icon handle, since the .ICO files that I use contain different images for big and small icons.

Let me brefly summarize the differences between the big and the small icons, since even the Microsoft documentation almost tells nothing about it. Here are the main differences:

  • Small icon image is displayed on the window title bar.

  • Large icon image is displayed in the Windows taskbar (usually located at the bottom part of the screen), if the taskbar is thick; the large icon image is also displayed when you press Alt+Tab.

See https://blog.barthe.ph/2009/07/17/wmseticon/ for more information on big and small icons.

Delphi, by setting only the large window handle, effectively phases out the alternative image for a smaller icon shown on window titles. If only big icon is given but not the small one, Windows resamples the image from the bigger icon to the smaller one, quality worsens, and the main idea of a smaller, simpler image is lost.

See the example image courtesy sanyok. The left column, labeled v7.4.16 are screenshots from a program compiled with a code that sets both ICON_BIG and ICON_SMALL. The right column, labeled v7.4.16.22 is a screenshot from the same program that doesn't explicitly sets both small and big icons, but just assigns TIcon to a form, an then Delphi using its standard code just assigns just the big icon, so the image from the windows title bar is resized by Windows from the large icon. You may see how poor the quality becomes as a result of standard Delphi behaviour.

Big vs Small Icons

In the past, I was changing the GetIconHandle in the interface section of the VCL.Forms.pas from static to virtual, changing it from function to procedure and adding two parameters:

procedure GetIconHandle(var Big, Small: HICON); virtual;

So the subsequent code in the VCL.Forms.pas was like the following:

var
  Big, Small: HICON;
begin    
  [...]
  GetIconHandle(Big, Small);
  SendMessage(Handle, WM_SETICON, ICON_BIG, LParam(Big));
  SendMessage(Handle, WM_SETICON, ICON_SMALL, LParam(Small));
  [...]

Is it possible to easily accomplish this without modifying the VCL.Forms.pas?

I did solve the problem in Delphi 2007 by modifying VCL units, but I can no longer modify VCL units in Delphi 10.20 Tokyo for the following reasons:

  1. VCL units compile, but then, when I compile my application, I get "Internal Error: AV0047C6C7-R000004CC-0", regardless of a target targets (Win32/Win64; Debug/Release), see https://quality.embarcadero.com/browse/RSP-18455 - the first part of the error number (address) is different, but the second - R000004CC-0 - is always the same.

  2. I have to manually add (TObject) to each of the classes that do not inherit from any class; otherwise I gen an error that Create or Destroy is not found in base class. In previous versions of Delphi, writing simply class without any ancestor implicitly inherited it from TObject, but when I compile code from command-line by dcc32 with dcc32 -Q -M -$D- -$M+ command line options, this error occurs that Create or Destroy is not found in base class.

Here is how I did loaded the icons in the past:

procedure LoadIconPair(var Big, Small: hIcon; AName: PChar);
begin
  if Win32MajorVersion < 4 then
  begin
    Big := LoadIcon(hInstance, AName);
    Small := 0;
  end
  else
  begin
    Big := LoadImage(hInstance, AName, IMAGE_ICON, 32, 32, LR_DEFAULTCOLOR);
    Small := LoadImage(hInstance, AName, IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR);
  end;
end;

This code can be further improved: the hard-coded sizes of 32x32 and 16x16 can be changed, as suggested at https://blog.barthe.ph/2009/07/17/wmseticon/ , to GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON) for big icons and GetSystemMetrics(SM_CXSMICON) and GetSystemMetrics(SM_CYSMICON) for small icons.

So, each form essentially called LoadIconPair and then returned the handles via the overwritten procedure GetIconHandle(var Big, Small: HICON); override;.

So the questions are the following:

  1. Is it possible to let Delphi set both small and big icons without much hassle and without modifying VCL.Forms.pas? (this is the main question) -- I need to have an opportunity, for some of the forms, and for the TApplication, to change icons at run-time.;

  2. If not, how to add a modified source VCL unit to your application under Delphi 10.2 Tokyo, where the interface section of a unit is modified? Are there any instructions, or an official guide? If anybody managed to do that, how did you accomplish that? Did you compile them from the GUI IDE? Or using command line dcc32/dcc64? Or using msbuild? Or otherwise? Do you also have to manually add (TObject) to classes that do not inherit from any class to avoid the Create or Destroy is not found in base class error?

Update #1: Setting the icons again after VCL.Forms.pas has set it is not a complete solution: we have to also take care about the Application icon, not only the forms icons; besides that, VCL.Forms.pas sets the icons anyway, but only the ICON_BIG, we have to set the icons again, this time setting both small and big. Do you have an idea on how can we patch the VCL.Forms.pas to add setting the ICON_SMALL whenever it sets the big icon, so we only patch the implementation section, and will call some messages, even WM_USER+N to request the icon handles from the form, and our descendant of TForm will implement this message handler?

Update #2: TApplication and TForm have similar interfaces with regard to icons, but TApplication is a descendant of TComponent which do not have a window handle, and, respectively, don't have message handlers. What we can do with a TForm, we cannot do with TApplication.

Update #3: I have implemented a solution which is a mixture of what kobik has suggested in his post and Sertac Akyuz has suggested in his later post. Thank you also to the other people who have contributed in the comments. I've compiled the program and gave it to the beta testers and they have confirmed that the problem has been fixed, the icon look good now, also the animation of the icon in the TApplication via changing icons by timer is also working properly. Thank you all!

3
How is your GetIconHandle method looks like? What kind of icons you use? I mean is it consists of multiple sizes? (Can you upload a sample?) Also do you override this method for each form and return the appropriate icons? To be honest I never even thought about this issue... Interesting. in any case patching the interface section is a bad idea IMO.kobik
1. No. I consider runtime patching not hassle free. 2. You can't. Modifiying interface sections of source units is not supported.Sertac Akyuz
With so long a background story it is not surprising that you've got two answers which neither answers none of your two question. Additionally I wonder if you're interested with an actual answer regarding your comments to those answers.Sertac Akyuz
Update #2: TApplication and TForm have similar interfaces with regard to icons, but TApplication is a descendant of TComponent which do not have a window handle... this is simply wrong. Application.Handle represent its internal Window handle. You can SendMessage(Application.Handle, WM_SETICON... as I mentioned in my answer. maybe that was something you missed all along,kobik
@MaximMasiutin, so you cannot unify the work with TForm and TApplicatoin via a common ancestor - This is true. I never told otherwise. but I explained how to handle the Application icon.kobik

3 Answers

7
votes

Patching the interface section of Delphi's core VCL/RTL sources in not allowed (in theory). The fact that you managed to do so previously returned as a boomerang now. You can in most case do what you need without patching the sources, e.g. by using inheritance, class helpers, patching code in runtime, detours, and in other cases (which IMO is the last resort) patch the implementation section and use a local copy for you project which is allowed - see also How to recompile modifications to VCL source file and How to change VCL code?

I suggest to create an ancestor base class for all your forms (I think any large project should do so) in the application and override CreateWnd:

procedure TBaseForm.CreateWnd;
var
  Big, Small: HICON;
begin
  inherited;
  if BorderStyle <> bsDialog then
  begin
    GetIconHandles(Big, Small);
    if Big <> 0 then
      SendMessage(Handle, WM_SETICON, ICON_BIG, LParam(Big));
    if Small <> 0 then
      SendMessage(Handle, WM_SETICON, ICON_SMALL, LParam(Small));
  end;
end;

Introduce two virtual methods:

procedure TBaseForm.GetIconResName(var Name: string);
begin
  Name := 'MAINICON';
end;

procedure TBaseForm.GetIconHandles(var Big, Small: HICON);
var
  ResName: string;
begin
  Big := 0;
  Small := 0;
  GetIconResName(ResName);
  if ResName = '' then Exit;

  Big := LoadImage(HInstance, PChar(ResName), IMAGE_ICON,
    GetSystemMetrics(SM_CXICON),
    GetSystemMetrics(SM_CYICON),
    0);
  Small := LoadImage(HInstance, PChar(ResName), IMAGE_ICON,
    GetSystemMetrics(SM_CXSMICON),
    GetSystemMetrics(SM_CYSMICON),
    0);
end;

All you need to do in your child classes is to override the GetIconResName.

i.e:

TMyChildForm = class(TBaseForm)
protected 
  procedure GetIconResName(var Name: string); override;
end;

procedure TMyChildForm.GetIconResName(var Name: string);
begin
  Name := 'SPIDERMAN';
end;

This is not a complete solution...

I tried to give you some leads to show patching the VCL source is not needed.

In any case, I don't have any issues if I use the Icon property (both Application and Form) and provide icon with at least 2 sizes (16x16 and 32x32) 32 bit depth (use other formats if needed), Windows will display the correct icon. i.e. the system displays the large icon in the ALT+TAB dialog box, and the small icon in the window caption. even though only ICON_BIG is sent to the Form/Application window handle. (Delphi7/Win7). (This is why I asked for MCVE. including information about your Icons format. and not just fragments of code like you did ...)


Since I am confused about your precise requirements, and you still refuse to provide MCVE, I'll try to provide another approach:

You say you need to handle the Application Icon also. the Application icon is set early when the Application is created - not simple to handle in your forms because they are not yet created. but whenever the Application.Icon is changed the application notifies the forms with CM_ICONCHANGED (see: procedure TApplication.IconChanged(Sender: TObject);). so you can re-set the the Application icon in that message handler via SendMessage(Application.Handle, WM_SETICON... (this won't trigger CM_ICONCHANGED) or setting the Application.Icon directly (which will also trigger CM_ICONCHANGED). if needed set the big and small icons via WM_SETICON message. You will also need to set the class icon:

SetClassLong(Application.Handle, GCL_HICON, FIcon);

So whenever the application icon is changed, covered by CM_ICONCHANGED in your forms.

TBaseForm = class(TForm)
private
  procedure CMIconChanged(var Message: TMessage); message CM_ICONCHANGED;
...
procedure TBaseForm.CMIconChanged(var Message: TMessage);
...

If you need to set that Icon early in your application (which I don't think is required) do the above in the Main form creation only.

To catch/handle the Forms icons use the WM_SETICON messages handler in your forms:

TBaseForm = class(TForm)
private
  procedure WMSetIcon(var Message: TWMSetIcon); message WM_SETICON;
...

procedure TBaseForm.WMSetIcon(var Message: TWMSetIcon);
begin
  if (Message.Icon <> 0) and (BorderStyle <> bsDialog) then
  begin    
    // this big icon is being set by the framework
    if Message.BigIcon then
    begin      
      // FBigIcon := LoadImage/LoadIcon...
      // if needed set Message.Icon to return a different big icon
      // Message.Icon := FBigIcon;
      // in practice create a virtual method to handle this section so your child forms can override it if needed
      inherited;

      FSmallIcon := LoadImage/LoadIcon...    
      // set small icon - this will also re-trigger WMSetIcon
      Perform(WM_SETICON, ICON_SMALL, FSmallIcon);
    end else
      inherited;
  end
  else
    inherited;
end;

This should get you covered in all situations.

4
votes

I just created a new VCL forms application, set the Application icon in the project options and the (only, main) form's Icon to the same file, which I had modified to make the 16x16 (32 bit depth) look differently, and added the following code:

{ TForm27 }

procedure TForm27.SetIcons;
var
  MainIcon, SmallIcon: HICON;
begin
  MainIcon := Application.Icon.Handle;
  SmallIcon := Icon.Handle;
  Perform(WM_SETICON, ICON_BIG, MainIcon);
  Perform(WM_SETICON, ICON_SMALL, SmallIcon);
end;

procedure TForm27.FormCreate(Sender: TObject);
begin
  SetIcons;
end;

This produced the following display:

Modified form icon

Note that both use the same .ico file, an ugly modification of ib.ico that comes with 10.2 Tokyo. I just painted a few black lines and ovals on the small icon only.

I don't know the code of your GetIconHandle modification, but you could do the same here. Also note that it doesn't work if I use (the Form's) Icon.Handle for both. If I do that, the ugly 16x16 icon gets displays in the application bar too.

But I did not have to modify any VCL code. You can do this in any VCL application.

2
votes

There are two clear questions at the very end of the post.

  1. Is it possible to let Delphi set both small and big icons without much hassle and without modifying VCL.Forms.pas? (this is the main question);

In the context of the question, this really means to ask if forcing the VCL framework itself, without modifying it, to load different icon sizes is possible. This has a very short answer: No.

If taken literally however, it is possible to use Delphi to do this. Code snippets in your question is proof of that, which doesn't seem to have much hassle at all.

  1. If not, how to add a modified source VCL unit to your application under Delphi 10.2 Tokyo, where the interface section of a unit is modified?

Very short answer here too: not possible, you cannot modify interface sections of Delphi source files.



The question in the title is no more explanatory than the ones included in the post.

There is an update part in the second question of the post:

Setting the icons again after VCL.Forms.pas has set it is not a complete solution ..

This really doesn't help. Having read the update several times and thinking over it; this update has no relation to what have been asked as questions, or even to the background information given in the post. And I have absolutely no idea what it is talking about.


Digging more, I encounter a comment of yours to Rudy's answer:

... Delphi itself sometimes sets ICON_BIG, so in some complex scenarios you will have to catch these cases and call the WM_SETICON with ICON_BIG again. ...

I'll assume the problem is this. Despite your setting of suitable icons, in some unforeseen cases, the VCL overwrites your icons. If this is not what you want to be solved, please modify your question to ask it.

I have not seen anything like this, in any case we don't need to modify "forms.pas" to disregard the framework's icon setting. A simple WM_SETICON handler on the form is enough, just don't let the message to be processed.

So how are we gonna set the icon? There is an answer to this on WM_GETICON's documentation:

.. A window that has no icon explicitly set (with WM_SETICON) uses the icon for the registered window class, ..

So, below is the solution contained in a form class of the assumed problem:

type
  TForm1 = class(TForm)
  protected
    procedure WMSetIcon(var Message: TWMSetIcon); message WM_SETICON;
    procedure CreateParams(var Params: TCreateParams); override;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.CreateParams(var Params: TCreateParams);
begin
  inherited;
  Params.WindowClass.hIcon := LoadIcon(HInstance, 'ICON_1');
end;

procedure TForm1.WMSetIcon(var Message: TWMSetIcon);
begin
end;

Below is a screen shot of the form running on W7, small icon (enter image description here) in the caption, big icon (enter image description here) on the top-left of the alt+tab dialog:

enter image description here

Note that XE2 has a problem with its resource manager, I identified the icon as 'CANCEL' in the "resources and images" dialog, but the icon in the executable ended up with "ICON_1" as identifier, so I had to use that in the code.