How can i create a "drop-down" window using Delphi?
Everything beyond this point is research effort; and is in no way related to the answer.
Research Effort
Making a proper drop-down requires a lot of pieces to carefully work together. I assume people don't like the difficult question, and would rather i asked seven separate questions; each one addressing one tiny piece of the problem. Everything that follows is my research effort into solving the deceptively simple problem.
Note the defining characteristics of a drop-down window:
- 1. The drop-down extends outside it's "owner" window
- 2. The "owner" window keeps focus; the drop-down never steals focus
- 3. The drop-down window has a drop-shadow
This is the Delphi variation of the same question i asked about in WinForms:
The answer in WinForms was to use the ToolStripDropDown class
. It is a helper class that turns any form into a drop-down.
Lets do it in Delphi
I will start by creating a gaudy dropdown form, that serves as the example:
Next i will drop a button, that will be the thing i click to make the drop-down appear:
And finally i will wire-up some initial code to show the form where it needs to be in the OnClick:
procedure TForm3.Button1MouseDown(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
frmPopup: TfrmPopup;
pt: TPoint;
begin
frmPopup := TfrmPopup.Create(Self);
//Show the form just under, and right aligned, to this button
pt := Self.ClientToScreen(Button1.BoundsRect.BottomRight);
Dec(pt.X, frmPopup.ClientWidth);
frmPopup.Show(Self, Self.Handle, pt);
end;
Edit: Changed it to MouseDown rather than Click. Click is incorrect, as the drop-down is shown without the need to click. One of the unresolved issues is how to hide a drop-down if the user mouse-downs the button again. But we'll leave that for the person who answers the question to solve. Everything in this question is research effort - not a solution.
And we're off:
Now how to do it the right way?
First thing we notice right away is the lack of a drop-shadow. That's because we need to apply the CS_DROPSHADOW
window style:
procedure TfrmPopup.CreateParams(var Params: TCreateParams);
const
CS_DROPSHADOW = $00020000;
begin
inherited CreateParams({var}Params);
Params.WindowClass.Style := Params.WindowClass.Style or CS_DROPSHADOW;
end;
That fixes that:
Focus Stealing
The next issue is that calling .Show
on the popup causes it to steal focus (the title bar of the application indicates that it has lost focus). Sertac comes up with the solution to this.
- when the popup receives it's
WM_Activate
message indicating that it is receiving focus (i.e.Lo(wParam) <> WA_INACTIVE
): - send the parent form a
WM_NCActivate
(True, -1) to indicate that it should draw itself like it still has focus
We handle the WM_Activate
:
protected
procedure WMActivate(var Msg: TWMActivate); message WM_ACTIVATE;
and the implementation:
procedure TfrmPopup.WMActivate(var Msg: TWMActivate);
begin
//if we are being activated, then give pretend activation state back to our owner
if (Msg.Active <> WA_INACTIVE) then
SendMessage(Self.PopupParent.Handle, WM_NCACTIVATE, WPARAM(True), -1);
inherited;
end;
So the owner window looks like it still has focus (who knows if that is the correct way to do it - it only looks like it still has focus):
Rolling up
Fortunately, Sertac already solves the problem of how to dismiss the window whenever the user clicks away:
- when the popup receives it's
WM_Activate
message indicating that it is losing focus (i.e.Lo(wParam) = WA_INACTIVE
): - send the owner control a notification that we are rolling up
- Free the popup form
We add that to our existing WM_Activate
handler:
procedure TfrmPopup.WMActivate(var Msg: TWMActivate);
begin
//if we are being activated, then give pretend activation state back to our owner
if (Msg.Active <> WA_INACTIVE) then
SendMessage(Self.PopupParent.Handle, WM_NCACTIVATE, WPARAM(True), -1);
inherited;
//If we're being deactivated, then we need to rollup
if Msg.Active = WA_INACTIVE then
begin
//TODO: Tell our owner that we've rolled up
//Note: The parent should not be using rollup as the time to read the state of all controls in the popup.
// Every time something in the popup changes, the drop-down should give that inforamtion to the owner
Self.Release; //use Release to let WMActivate complete
end;
end;
Sliding the dropdown
Dropdown controls use AnimateWindow
to slide the drop-down down. From Microsoft's own combo.c
:
if (!(TEST_EffectPUSIF(PUSIF_COMBOBOXANIMATION))
|| (GetAppCompatFlags2(VER40) & GACF2_ANIMATIONOFF)) {
NtUserShowWindow(hwndList, SW_SHOWNA);
}
else
{
AnimateWindow(hwndList, CMS_QANIMATION, (fAnimPos ? AW_VER_POSITIVE :
AW_VER_NEGATIVE) | AW_SLIDE);
}
After checking if animations should be used, they use AnimateWindow
to show the window. We can use SystemParametersInfo
with SPI_GetComboBoxAnimation:
Determines whether the slide-open effect for combo boxes is enabled. The pvParam parameter must point to a BOOL variable that receives TRUE for enabled, or FALSE for disabled.
Inside our newly consecrated TfrmPopup.Show
method, we can check if client area animations are enabled, and call either AnimateWindow
or Show
depending on the user's preference:
procedure TfrmPopup.Show(Owner: TForm; NotificationParentWindow: HWND;
PopupPosition: TPoint);
var
pt: TPoint;
comboBoxAnimation: BOOL;
begin
FNotificationParentWnd := NotificationParentWindow;
//We want the dropdown form "owned" by (i.e. not "parented" to) the OwnerWindow
Self.Parent := nil; //the default anyway; but just to reinforce the idea
Self.PopupParent := Owner; //Owner means the Win32 concept of owner (i.e. always on top of, cf Parent, which means clipped child of)
Self.PopupMode := pmExplicit; //explicitely owned by the owner
//Show the form just under, and right aligned, to this button
Self.BorderStyle := bsNone;
Self.Position := poDesigned;
Self.Left := PopupPosition.X;
Self.Top := PopupPosition.Y;
if not Winapi.Windows.SystemParametersInfo(SPI_GETCOMBOBOXANIMATION, 0, @comboBoxAnimation, 0) then
comboBoxAnimation := False;
if comboBoxAnimation then
begin
//200ms is the shell animation duration
AnimateWindow(Self.Handle, 200, AW_VER_POSITIVE or AW_SLIDE or AW_ACTIVATE);
end
else
inherited Show;
end;
Edit: Turns out there is SPI_GETCOMBOBOXANIMATION
which should probably use over SPI_GETCLIENTAREAANIMATION
. Which points to the depths of difficulty hidden behind the subtle "How to simulate a drop-down". Simulating a drop-down requires a lot of stuff.
The problem is that Delphi forms pretty much fall over dead if you try to use ShowWindow
or AnimateWindow
behind their back:
How to solve that?
It's also odd that Microsoft itself uses either:
ShowWindow(..., SW_SHOWNOACTIVATE)
, orAnimateWindow(...)
*(withoutAW_ACTIVATE
)
to show the drop-down listbox without activation. And yet spying on a ComboBox with Spy++ i can see WM_NCACTIVATE
flying around.
In the past people have simulated a slide window using repeated calls to change the Height
of the drop-down form from a timer. Not only is this bad; but it also changes the size of the form. Rather than sliding down, the form grows down; you can see all the controls change their layout as the drop-down appears. No, having the drop-down form remain it's real size, but slide down is what is wanted here.
I know AnimateWindow
and Delphi have never gotten along. And the question has been asked, a lot, long before Stackoverflow arrived. I even asked about it in 2005 on the newsgroups. But that can't stop me from asking again.
I tried to force my form to redraw after it animates:
AnimateWindow(Self.Handle, 200, AW_VER_POSITIVE or AW_SLIDE or AW_ACTIVATE);
Self.Repaint;
Self.Update;
Self.Invalidate;
But it doesn't work; it just sits there mocking me:
Now showing again when i want to close-up
If a combobox is dropped down, and the user tries to MouseDown on the button, the real Windows ComboBox control does not simply show the control again, but instead hides it:
The drop-down also knows that it is currently "dropped-down", which is useful so that it can draw itself as if it is in "dropped down" mode. What we need is a way to know that the drop-down is dropped down, and a way to know that the drop-down is no longer dropped down. Some kind of boolean variable:
private
FDroppedDown: Boolean;
And it seems to me that we need to tell the host that we're closing up (i.e. losing activation). The host then needs to be responsible for destroying the popup. (the host cannot be responsible for destroying the popup; it leads to an unresolvable race condition). So i create a message used to notify the owner that we're closing up:
const
WM_PopupFormCloseUp = WM_APP+89;
Note: I don't know how people avoid message constant conflicts (especially since CM_BASE
starts at $B000 and CN_BASE
starts at $BC00).
Building on Sertac's activation/deactivation routine:
procedure TfrmPopup.WMActivate(var Msg: TWMActivate);
begin
//if we are being activated, then give pretend activation state back to our owner
if (Msg.Active <> WA_INACTIVE) then
SendMessage(Self.PopupParent.Handle, WM_NCACTIVATE, WPARAM(True), -1);
inherited;
//If we're being deactivated, then we need to rollup
if Msg.Active = WA_INACTIVE then
begin
//DONE: Tell our owner that we've rolled up
//Note: We must post the message. If it is Sent, the owner
//will get the CloseUp notification before the MouseDown that
//started all this. When the MouseDown comes, they will think
//they were not dropped down, and drop down a new one.
PostMessage(FNotificationParentWnd, WM_PopupFormCloseUp, 0, 0);
Self.Release; //use release to give WM_Activate a chance to return
end;
end;
And then we have to change our MouseDown code to understand that the drop-down is still there:
procedure TForm3.Edit1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
frmPopup: TfrmPopup;
pt: TPoint;
begin
//If we (were) dropped down, then don't drop-down again.
//If they click us, pretend they are trying to close the drop-down rather than open a second copy
if FDroppedDown then
begin
//And since we're receiving mouse input, we by defintion must have focus.
//and since the drop-down self-destructs when it loses activation,
//it can no longer be dropped down (since it no longer exists)
Exit;
end;
frmPopup := TfrmPopup.Create(Self);
//Show the form just under, and right aligned, to this button
pt := Self.ClientToScreen(Edit1.BoundsRect.BottomRight);
Dec(pt.X, frmPopup.ClientWidth);
frmPopup.Show(Self, Self.Handle, pt);
FDroppedDown := True;
end;
And i think that's it
Aside from the AnimateWindow
conundrum, i may have been able use my research effort to solve all the problems i can think of in order to:
Simulate a drop-down form in Delphi
Of course, this could all be for naught. It might turn out there's a VCL function:
TComboBoxHelper = class;
public
class procedure ShowDropDownForm(...);
end;
In which case that would be the correct answer.
WM_SETFOCUS
) window is defined as the window that receives keyboard input. Either you're wrong about your observations, or you use focus to mean a different thing. – Abiosis