How to correctly have modeless form appear in taskbar
Asked Answered
P

2

12

I am trying to achieve the age-old Delphi dream of having a modeless form appear in the taskbar.

What is the correct way to have a modeless form appear in the taskbar?


Research Effort

These are my attempts to solve the problem. There are a lot of things needed to make it behave correctly - simply having a button appear on the taskbar is not a solution. Having a Windows application behave correctly as a Windows application should is my goal.

For those who know me, and how deep my "shows research effort" goes, hang on because it will be wild ride down a rabbit hole.

The question is in the title, as well above the horizontal line above. Everything below only serves to show why some on the oft-repeated suggestions are incorrect.

Windows only creates as taskbar button for unowned windows

Initially i have my "Main Form", from that i show this other modeless form:

procedure TfrmMain.Button2Click(Sender: TObject);
begin
    if frmModeless = nil then
        Application.CreateForm(TfrmModeless, frmModeless);

    frmModeless.Show;
end;

This correctly shows the new form, but no new button appears on the taskbar:

enter image description here

The reason no taskbar button is created is because that is by design. Windows will only show a taskbar button for a window that "unowned". This modeless Delphi form is most definitely owned. In my case it is owned by the Application.Handle:

enter image description here

My project's name is ModelessFormFail.dpr, which is the origin of the Windows class name Modelessformfail associated with the owner.

Fortunately there is a way to force Windows to create a taskbar button for a window, even though the window is owned:

Just use WS_EX_APPWINDOW

The MSDN documentation of WS_EX_APPWINDOW says it:

WS_EX_APPWINDOW 0x00040000L Forces a top-level window onto the taskbar when the window is visible.

It also a well-known Delphi trick to override CreateParams and manually add the WS_EX_APPWINDOW style:

procedure TfrmModeless.CreateParams(var Params: TCreateParams);
begin
    inherited;

    Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW; //force owned window to appear in taskbar
end;

When we run this, the newly created modeless form does indeed get its own taskbar button:

enter image description here

And we're done? No, because it doesn't behave correctly.

If the user clicks on the frmMain taskbar button, that window is not brought forward. Instead the other form (frmModeless) is brought forward:

enter image description here

This makes sense once you understand the Windows concept of ownership. Windows will, by design, bring any child owned forms forward. It was the entire purpose of ownership - to keep owned forms on top of their owners.

Make the form actually unowned

The solution, as some of you know is not to fight against the taskbar heuristics and windows. If i want the form to be unowned, make it unowned.

This is (fairly) simple. In CreateParam force the owner windows to be null:

procedure TfrmModeless.CreateParams(var Params: TCreateParams);
begin
    inherited;

    //Doesn't work, because the form is still owned
//  Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW; //force owned windows to appear in taskbar

    //Make the form actually unonwed; it's what we want
    Params.WndParent := 0; //unowned. Unowned windows naturally appear on the taskbar.
          //There may be a way to simulate this with PopupParent and PopupMode.
end;

As an aside, i wanted to investigate is there was a way to use the PopupMode and PopupParent properties to make a window unowned. I swear i read a comment (from you David) somewhere on SO saying that if you passed Self as the PopupParent, e.g.:

procedure TfrmMain.Button1Click(Sender: TObject);
begin
    if frmModeless = nil then
    begin
        Application.CreateForm(TfrmModeless, frmModeless);
        frmModeless.PopupParent := frmModeless; //The super-secret way to say "unowned"? I swear David Heffernan mentioned it somewhere on SO, but be damned if i can find it now.
        frmModeless.PopupMode := pmExplicit; //happens automatically when you set a PopupParent, but you get the idea
    end;

    frmModeless.Show;
end;

it was supposed to be the super-secret way to indicate to Delphi that you want to form to have "no owner". But i cannot find the comment anywhere on now. Unfortunately, no combination of PopupParent and PopupMode cause a form to actually be un-owned:

  • PopupMode: pmNone
    • Owner hwnd: Application.Handle/Application.MainForm.Handle
  • PopupMode: pmAuto
    • Owner hwnd: Screen.ActiveForm.Handle
  • PopupMode: pmExplicit
    • PopupParent: nil
      • Owner hwnd: Application.MainForm.Handle
    • PopupParent: AForm
      • Owner hwnd: AForm.Handle
    • PopupParent: Self
      • Owner hwnd: Application.MainForm.Handle

Nothing i could do could cause the form to actually have no owner (each time checking with Spy++).

Setting the WndParent manually during CreateParams:

  • does make the form unowned
  • it does have a taskbar button
  • and both taskbar buttons dobehave correctly:

enter image description here

And we're done, right? I thought so. I changed everything to use this new technique.

Except there are problems with my fix that seem to cause other problems - Delphi didn't like me changing to ownership of a form.

Hint Windows

One of the controls on my modeless window has a tooltop:

enter image description here

The problem is that when this tooltip window appears, it causes the other form (frmMain, the modal one) to come forward. It doesn't gain activation focus; but it does now obscure the form i was look at:

enter image description here

The reason is probably logical. The Delphi HintWindow is probably owned either by Application.Handle or Application.MainForm.Handle, rather than being owned by the form that it should be owned by:

enter image description here

I would have considered this a bug on Delphi's part; using the wrong owner.

Diversion to see the actual app layout

Now it's important that i take a moment to show that my application isn't a main form and a modeless form:

enter image description here

It's actually:

  • a login screen (a sacrificial main form that gets hidden)
  • a main screen
  • a modal control panel
  • that shows the modeless form

enter image description here

Even with the reality of the application layout, everything except for hint window ownership works. There are two taskbar buttons, and clicking them brings the proper form forward:

enter image description here

But we still have the problem of the HintWindow ownership bringing the wrong form forward:

enter image description here

ShowMainFormOnTaskbar

It was when i was attempting to create a minimal application to reproduce the problem when i realize i couldn't. There was something different:

  • between my Delphi 5 application ported to XE6
  • a new application created in XE6

After comparing everything, i finally traced it down to the fact that new applications in XE6 add the MainFormOnTaskbar := True by default in any new project (presumably to not break existing applications):

program ModelessFormFail;
//...
begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TfrmSacrificialMain, frmSacrificialMain);
  //Application.CreateForm(TfrmMain, frmMain);
  Application.Run;
end.

When i added this option, then the appearance of the tooltip didn't bring the wrong form forward!:

enter image description here

Success! Except, people who know what's coming know what's coming. My "sacrificial" main login form shows the "real" main form, hiding itself:

procedure TfrmSacrificialMain.Button1Click(Sender: TObject);
var
    frmMain: TfrmMain;
begin
    frmMain := TfrmMain.Create(Application);
    Self.Hide;
    try
        frmMain.ShowModal;
    finally
        Self.Show;
    end;
end;

When that happens, and i "login", my taskbar icon disappers entirely:

enter image description here

This happens because:

  • the un-owned sacrificial main form is not invisible: so the button goes with it
  • the real main form is owned so it does not get a toolbar button

Use WS_APP_APPWINDOW

Now we have the opportunity to use WS_EX_APPWINDOW. I want to force my main form, which is owned, to appear on the taskbar. So i override CreateParams and force it to appear on the taskbar:

procedure TfrmMain.CreateParams(var Params: TCreateParams);
begin
    inherited;

    Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW; //force owned window to appear in taskbar
end;

and we give it a whirl:

enter image description here

Looking pretty good!

  • two taskbar buttons
  • the tooltip doesn't pop the wrong owner form forward

except, when i click on the first toolbar button, the wrong form comes up. It shows the modal frmMain, rather than the currently modal frmControlPanel:

enter image description here

Presumably because the newly created frmControlPanel was PopupParented to Application.MainForm rather than Screen.ActiveForm. Check in Spy++:

enter image description here

Yes, the parent is MainForm.Handle. This turns out to be because of another bug in the VCL. If the form's PopupMode is:

  • pmAuto
  • pmNone (if it's a modal form)

the VCL attempts to use Application.ActiveFormHandle as the hWndParent. Unfortunately it then checks if the modal form's parent is enabled:

if (WndParent <> 0) and (
      IsIconic(WndParent) or 
      not IsWindowVisible(WndParent) or
      not IsWindowEnabled(WndParent)) then

Of course the modal form's parent is not enabled. If it was, it would not be a modal form. So the VCL falls back to using:

WndParent := Application.MainFormHandle;

Manual parenting

This means i probably have to be sure to manually(?) set the popup parenting?

procedure TfrmMain.Button2Click(Sender: TObject);
var
    frmControlPanel: TfrmControlPanel;
begin
    frmControlPanel := TfrmControlPanel.Create(Application);
    try
        frmControlPanel.PopupParent := Self;
        frmControlPanel.PopupMode := pmExplicit; //Automatically set to pmExplicit when you set PopupParent. But you get the idea.
        frmControlPanel.ShowModal;
    finally
        frmControlPanel.Free;
    end;
end;

Except that didn't work either. Clicking the first taskbar button causes the wrong form to activate:

enter image description here

At this point i'm thoroughly confused. The parent of my modal form should be frmMain, and it is!:

enter image description here

So what now?

I have a sense of what might be going on.

That taskbar button is a representation of frmMain. Windows is bringing that for forward.

Except it behaved correctly when MainFormOnTaskbar was set to false.

There must be some magic in Delphi VCL that caused correctness before, but gets disabled with MainFormOnTaskbar := True, but what is it?

I am not the first person to want a Delphi application to behave nicely with the Windows 95 toolbar. And i've asked this question in the past, but those answers were always geared towards Delphi 5 and it's old central routing window.

I've been told that everything was fixed around Delphi 2007 timeframe.

So what is the correct solution?

Bonus Reading

Pugging answered 12/6, 2015 at 18:4 Comment(14)
@KenWhite images are not the problem (I think). Main issue with this question is that, unless I spend next hour reading it, I don't think I will be able to tell what is the real question and what is desired behavior.Fara
@DalijaPrasnikar: Yeah. Someone missed the part in the help center that said "Don't write a book. Your question should be brief, and not require dozens of illustrations." Users of mobile devices (particularly those using a web browser and not the SO app) are going to be really irritated here. I'm tempted to send Ian a bill for data charges.Moro
@DalijaPrasnikar I edit the question to add a horizontal line to indicate where you can stop reading. Everything after it is research effort.Pugging
@IanBoyd: The long and short of it is that VCL was never designed for this, so you have to do a LOT of hacking to make it work the way you are asking (if you are even successful). BorCoDero made some pretty bad implementation decisions from day 1 when ShowMainFormOnTaskbar was first added, and they kept compounding the problem by tying more and more features to it instead of keeping them separated, all in the name of backwards compatibility with minimal changes to user code, resulting in a messy implementation that does all of the wrong things nowadays. Good luck trying to untangle it!Melancholy
@RemyLebeau I remember seeing a QC article that, i thought, asked for ShowMainFormOnToolbar to be removed, as it does not do what everyone thinks it does. Was that yours? BorImCoDero....you mean that started to stick!? :) And, finally, i thought, i really really thought, that Delphi XE6 would finally be Windows 95 compatible :( I kinda sort hinted to customers that they might be able to have modeless forms now that 'we moved the tool we use to a newer version". :(Pugging
The title of the question isn't clear.Wrench
@IanBoyd: IMHO, they should never have introduced ShowMainFormOnTaskbar in the first place. They should have instead introduced separate TApplication.ShowOnTaskbar and TForm.ShowOnTaskbar properties instead, and maybe even a TForm.ApplicationWindowIsOwner property. How many times over the years have people asked to hide the TApplication window from the taskbar, or add a TForm window to the taskbar? Too many to count. Those should have been added as separate features. Maybe they can still do that, and deprecate ShowMainFormOnTaskbar to set the appropriate properties.Melancholy
@Wrench Yeah, you're right, that was pretty bad grammer. I started writing the question yesterday afternoon, and i never re-read the title.Pugging
It depends on whether you want the modeless form to be owned or not. That determines whether you set WndParent, or not. If you do set WndParent then you need to make it an app window. And then you need to deal with the consequences. You'll have to fix the bug in the hint system.Kendrick
Congratulations on the longest question on Stack Overflow :-)Scatology
Might have better luck just writing your own implementation of forms rather than tweaking VCL.Scatology
@JerryDodge You should see this question of mine from a few years ago; although that was really to prove to people that i'm not crazy. There was also this question from a couple of weeks ago that i got no end of grief over!Pugging
Here's what I would try: First, don't make your "sacrificial" main form be your main form. Make it be a short-lived form that exists only as long as it takes to log in. Once you're logged in, then create the main form. Then you can let the main form be on the taskbar just as Delphi wants it to be, without changing its creation parameters. I also wouldn't make frmMain be modal. Alternatively, if the login form is to continue being the sacrificial main form, then I still wouldn't make frmMain be modal. Instead, I'd make it be a top-level unowned window just like frmModeless.Fillagree
You see, I never got as far as the part about the hidden main form. Don't do that. In fact, call Application.CreateForm exactly once. For the real main form. For the modeless form, decide whether you want it to be owned or not. Should all work out swimmingly.Kendrick
K
7

It seems to me that the fundamental problem is that your main form is, in the eyes of the VCL, not your main form. Once you fix that, all the problems go away.

You should:

  1. Call Application.CreateForm exactly once, for the real main form. That is a good rule to follow. Consider the job of Application.CreateForm to be to create the main form of your application.
  2. Create the login form and set its WndParent to 0. That makes sure it appears on the taskbar. Then show it modally.
  3. Create the main form in the usual way by calling Application.CreateForm.
  4. Set MainFormOnTaskbar to be True.
  5. Set WndParent to 0 for the modeless form.

And that's it. Here's a complete example:

Project1.dpr

program Project1;

uses
  Vcl.Forms,
  uMain in 'uMain.pas' {MainForm},
  uLogin in 'uLogin.pas' {LoginForm},
  uModeless in 'uModeless.pas' {ModelessForm};

{$R *.res}

begin
  Application.Initialize;
  Application.ShowHint := True;
  Application.MainFormOnTaskbar := True;
  with TLoginForm.Create(Application) do begin
    ShowModal;
    Free;
  end;
  Application.CreateForm(TMainForm, MainForm);
  Application.Run;
end.

uLogin.pas

unit uLogin;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs;

type
  TLoginForm = class(TForm)
  protected
    procedure CreateParams(var Params: TCreateParams); override;
  end;

implementation

{$R *.dfm}

procedure TLoginForm.CreateParams(var Params: TCreateParams);
begin
  inherited;
  Params.WndParent := 0;
end;

end.

uLogin.dfm

object LoginForm: TLoginForm
  Left = 0
  Top = 0
  Caption = 'LoginForm'
  ClientHeight = 300
  ClientWidth = 635
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'MS Sans Serif'
  Font.Style = []
  OldCreateOrder = False
  PixelsPerInch = 96
  TextHeight = 13
end

uMain.pas

unit uMain;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, uModeless;

type
  TMainForm = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.Button1Click(Sender: TObject);
begin
  with TModelessForm.Create(Self) do begin
    Show;
  end;
end;

end.

uMain.dfm

object MainForm: TMainForm
  Left = 0
  Top = 0
  Caption = 'MainForm'
  ClientHeight = 300
  ClientWidth = 635
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'MS Sans Serif'
  Font.Style = []
  OldCreateOrder = False
  PixelsPerInch = 96
  TextHeight = 13
  object Button1: TButton
    Left = 288
    Top = 160
    Width = 75
    Height = 23
    Caption = 'Button1'
    TabOrder = 0
    OnClick = Button1Click
  end
end

uModeless.pas

unit uModeless;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TModelessForm = class(TForm)
    Label1: TLabel;
  protected
    procedure CreateParams(var Params: TCreateParams); override;
  end;

implementation

{$R *.dfm}

procedure TModelessForm.CreateParams(var Params: TCreateParams);
begin
  inherited;
  Params.WndParent := 0;
end;

end.

uModeless.dfm

object ModelessForm: TModelessForm
  Left = 0
  Top = 0
  Caption = 'ModelessForm'
  ClientHeight = 300
  ClientWidth = 635
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'MS Sans Serif'
  Font.Style = []
  OldCreateOrder = False
  ShowHint = True
  PixelsPerInch = 96
  TextHeight = 13
  object Label1: TLabel
    Left = 312
    Top = 160
    Width = 98
    Height = 13
    Hint = 'This is a hint'
    Caption = 'I'#39'm a label with a hint'
  end
end

If you'd rather the modeless form was owned by the main form, you can achieve that by replacing TModelessForm.CreateParams with:

procedure TModelessForm.CreateParams(var Params: TCreateParams);
begin
  inherited;
  Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW;
end;
Kendrick answered 13/6, 2015 at 7:6 Comment(6)
An issue with that change is that closing the "main" form no longer returns you to the "login" form. In the revised version the main form closes and the application terminates. I did follow your suggestion of adding (another) fix to Vcl.Controls.pas to set the proper parent for the THintWindow, and it seemed to work. In general my reason for asking was that i had blindly assumed that Delphi had fixed it. For years people were telling me "stop running an outdated version of Delphi". I figured it was all solved, and i was just missing something like .ShowModless or something.Pugging
You can sort that out by changing the close action on the main form and showing a new login form, or re-showing the original one. Start from the premise that you make the main form be the VCL main form, and fit around that. Then all will be well.Kendrick
Ought, but i don't wannnaaaa do that. :) Then i remember the words from Raymond Chen: Nobody said programming was going to be easy.Pugging
Well, you should want to do that. It makes everything easier.Kendrick
Can you think of a way to re-show the login form as the MainForm has closed? In your example you destroy the login form before getting to the MainForm and Application.Run. I would need to re-show the login form that was.Pugging
The problem with creating a new one is that it is not the old one. I found the best hack ever. Rather than showing my "main" form using .ShowModal, i show it using Application.Run. That way all the code after the call the .ShowModal can continue to run (inside all it's existing try..finally), and best of all the user doesn't lose the existing logon form they had.Pugging
M
0

Great working thanks so much I use this

  Application.Initialize;
 // Application.MainFormOnTaskbar := True;///*
  Application.CreateForm(TAT_musteriler, AT_dATA);
  Application.CreateForm(TForm2, Form2);
  Application.CreateForm(TForm1, Form1);
  Application.CreateForm(TForm3, Form3);
  Application.CreateForm(TForm4, Form4);

.... which form is showing (active) that show on windows task bar if * line active only 1 form showing on taskbar when i hide main form and show other form i cant see at windows task bar

Milk answered 8/11, 2022 at 8:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.