【问题标题】:How to simulate drop-down form in Delphi?如何在Delphi中模拟下拉表单?
【发布时间】:2015-06-15 11:41:09
【问题描述】:

如何使用 Delphi 创建一个“下拉”窗口

除此之外的一切都是研究工作;并且与答案无关。

研究工作

制作一个合适的下拉菜单需要很多部分仔细协作。我假设人们不喜欢这个困难的问题,我宁愿我问七个单独的问题;每个人都解决了一小部分问题。接下来的一切都是我研究解决这个看似简单的问题。


注意下拉窗口的定义特征:

  • 1. 下拉菜单超出了它的“所有者”窗口
  • 2. “所有者” 窗口保持焦点;下拉菜单永远不会窃取焦点
  • 3.下拉窗口有阴影

这是我在 WinForms 中提出的同一问题的 Delphi 变体:

WinForms 中的答案是使用ToolStripDropDown class。它是一个帮助类,可以将任何形式变成下拉列表。

让我们在 Delphi 中完成

我将从创建一个华丽的下拉表单开始,作为示例:

接下来我将放下一个按钮,我点击该按钮以使下拉菜单出现:

最后我将连接一些初始代码以在 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;

编辑:将其更改为 MouseDown 而不是 Click。点击不正确,因为下拉显示不需要点击。未解决的问题之一是如果用户再次按下鼠标按钮,如何隐藏下拉菜单。但是我们将把它留给回答问题的人来解决。这个问题中的所有内容都是研究工作 - 不是解决方案。

我们走了:

现在如何以正确的方式做到这一点?

我们立即注意到的第一件事是缺少阴影。那是因为我们需要应用CS_DROPSHADOW 窗口样式:

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;

解决了这个问题:

焦点窃取

下一个问题是在弹出窗口上调用.Show 会导致它窃取焦点(应用程序的标题栏表明它已经失去焦点)。 Sertac 提出了解决方案。

  • 当弹出窗口收到WM_Activate 消息时,表明它正在接收焦点(即Lo(wParam) <> WA_INACTIVE):
  • 向父窗体发送WM_NCActivate(True, -1) 以表明它应该像仍然有焦点一样绘制自己

我们处理WM_Activate:

protected
   procedure WMActivate(var Msg: TWMActivate); message WM_ACTIVATE;

和实施:

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;

所以所有者窗口看起来仍然有焦点(谁知道这是否是正确的方法 - 它只是 看起来 像它仍然有焦点):

卷起来

幸运的是,Sertac 已经解决了用户点击离开时如何关闭窗口的问题:

  • 当弹出窗口收到 WM_Activate 消息时,表明它正在失去焦点(即 Lo(wParam) = WA_INACTIVE):
  • 向所有者控件发送我们正在汇总的通知
  • 释放弹出表单

我们将它添加到我们现有的 WM_Activate 处理程序中:

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;

滑动下拉菜单

下拉控件使用AnimateWindow 将下拉列表向下滑动。来自微软自己的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);
}

检查是否应该使用动画后,他们使用AnimateWindow 来显示窗口。我们可以将SystemParametersInfoSPI_GetComboBoxAnimation一起使用:

确定是否启用组合框的滑动打开效果。 pvParam 参数必须指向一个 BOOL 变量,该变量接收 TRUE 表示启用,或 FALSE 表示禁用。 p>

在我们新奉献的TfrmPopup.Show 方法中,我们可以检查是否启用了客户区动画,并根据用户的偏好调用AnimateWindowShow

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;

编辑:原来有SPI_GETCOMBOBOXANIMATION 应该使用超过SPI_GETCLIENTAREAANIMATION。这指出了隐藏在微妙“如何模拟下拉菜单”背后的困难深度。模拟下拉菜单需要很多东西。

问题是,如果你试图在他们背后使用ShowWindowAnimateWindow,Delphi 表单几乎会崩溃:

如何解决?

微软自己也使用这两种方法也很奇怪:

  • ShowWindow(..., SW_SHOWNOACTIVATE),或
  • AnimateWindow(...) *(没有AW_ACTIVATE)

显示下拉列表框没有激活。然而,使用 Spy++ 监视 ComboBox,我可以看到 WM_NCACTIVATE 飞来飞去。

过去,人们通过重复调用来模拟幻灯片窗口,以从计时器更改下拉表单的Height。这不仅不好;但它也会改变表单的大小。形状不是向下滑动,而是向下生长;当下拉菜单出现时,您可以看到所有控件都更改了它们的布局。不,让下拉表单保持它的实际大小,但这里需要的是向下滑动。

我知道AnimateWindow 和 Delphi 从来没有相处过。早在 Stackoverflow 到来之前,这个问题就已经被问了很多。我什至在 2005 年的新闻组上问过这个问题。但这并不能阻止我再次询问。

我试图强制我的表单在动画后重绘:

AnimateWindow(Self.Handle, 200, AW_VER_POSITIVE or AW_SLIDE or AW_ACTIVATE);
Self.Repaint;
Self.Update;
Self.Invalidate;

但它不起作用;它只是坐在那里嘲笑我:

现在我想特写时再次显示

如果下拉组合框,并且用户尝试在按钮上MouseDown,则真正的 Windows ComboBox 控件不会简单地再次显示该控件,而是将其隐藏:

下拉也知道它当前是“dropped-down”,这很有用,这样它就可以像在“dropped down”中一样绘制自己了 模式。我们需要的是一种知道下拉的方式是下拉的,一种方式是知道下拉不再下拉的。某种布尔变量:

private
   FDroppedDown: Boolean;

在我看来,我们需要告诉主机我们正在关闭(即失去激活)。 然后主机需要负责销毁弹出窗口。 (主机不能负责销毁弹出窗口;这会导致无法解决的竞争条件)。所以我创建了一条消息,用于通知所有者我们正在关闭:

const
   WM_PopupFormCloseUp = WM_APP+89;

注意:我不知道人们如何避免消息持续冲突(尤其是因为CM_BASE 的起始价为 $B000,CN_BASE 的起始价为 $BC00)。

以 Sertac 的激活/停用例程为基础:

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;

然后我们必须更改我们的 MouseDown 代码以了解下拉菜单仍然存在:

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;

我认为就是这样

除了AnimateWindow 难题之外,我可能已经能够利用我的研究努力解决我能想到的所有问题,以便:

在Delphi中模拟下拉表单

当然,这一切都可能是徒劳的。可能会发现有一个 VCL 函数:

TComboBoxHelper = class;
public
   class procedure ShowDropDownForm(...);
end;

在这种情况下,那个将是正确的答案。

【问题讨论】:

  • 一次只有一个窗口有焦点。那是最终的。使用键盘找出是哪一个。
  • 我的意思是,在最后一个示例中按“向下”,“q-z”将被突出显示,而不是“数据绑定” - 列表视图没有焦点。
  • 一个focused('keyboard focus'的缩写,就像在WM_SETFOCUS的文档中使用的那样)窗口被定义为接收键盘输入的窗口。要么你的观察有误,要么你用 focus 来表达不同的意思。
  • 不知道为什么投反对票,您为此付出了很多努力并且有一个明确的目标。
  • @David - 这是一个非常糟糕的问题,它只要求不存在的 VCL 一体化下拉解决方案,还有很多不相关的旁白。如果问题没有这样问,那么提问者已经接受或至少评论了为问题中所述的失败部分提供原因和解决方案的答案。至少这将是我投反对票的原因,目前是第三票。

标签: windows shell delphi drop-down-menu delphi-xe6


【解决方案1】:

procedure TForm3.Button1Click(Sender: TObject); 的底部,您调用frmPopup.Show; 将其更改为ShowWindow(frmPopup.Handle, SW_SHOWNOACTIVATE);,之后您需要调用frmPopup.Visible := True;,否则表单上的组件将不会显示

所以新程序如下所示:

uses
  frmPopupU;

procedure TForm3.Button1Click(Sender: TObject);
var
  frmPopup: TfrmPopup;
  pt: TPoint;
begin
  frmPopup := TfrmPopup.Create(Self);
  frmPopup.BorderStyle := bsNone;

  //We want the dropdown form "owned", but not "parented" to us
  frmPopup.Parent := nil; //the default anyway; but just to reinforce the idea
  frmPopup.PopupParent := Self;

  //Show the form just under, and right aligned, to this button
  frmPopup.Position := poDesigned;
  pt := Self.ClientToScreen(Button1.BoundsRect.BottomRight);
  Dec(pt.X, frmPopup.ClientWidth);
  frmPopup.Left := pt.X;
  frmPopup.Top := pt.Y;

  //  frmPopup.Show;
  ShowWindow(frmPopup.Handle, SW_SHOWNOACTIVATE);
  //Else the components on the form won't show
  frmPopup.Visible := True;
end;

但这不会阻止您的弹出窗口窃取焦点。为了防止这种情况发生,您需要在弹出表单中覆盖 WM_MOUSEACTIVATE 事件

type
  TfrmPopup = class(TForm)
...
    procedure WMMouseActivate(var Message: TWMMouseActivate); message WM_MOUSEACTIVATE;
...
  end;

以及实现

procedure TfrmPopup.WMMouseActivate(var Message: TWMMouseActivate);
begin
  Message.Result := MA_NOACTIVATE;
end;

我决定用你的弹出窗口来玩arround:我添加的第一件事是一个关闭按钮。只是一个简单的 TButton,它在其 onCLick 事件中调用 Close:

procedure TfrmPopup.Button1Click(Sender: TObject);
begin
  Close;
end;

但这只会隐藏表单,为了释放它,我添加了一个OnFormClose 事件:

procedure TfrmPopup.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Action := caFree;
end;

最后我觉得加个resize函数会很有趣

我通过覆盖 WM_NCHITTEST 消息来做到这一点:

procedure TfrmPopup.WMNCHitTest(var Message: TWMNCHitTest);
const
  EDGEDETECT = 7; //adjust to suit yourself
var
  deltaRect: TRect; //not really used as a rect, just a convenient structure
begin
  inherited;

  with Message, deltaRect do
  begin
    Left := XPos - BoundsRect.Left;
    Right := BoundsRect.Right - XPos;
    Top := YPos - BoundsRect.Top;
    Bottom := BoundsRect.Bottom - YPos;

    if (Top < EDGEDETECT) and (Left < EDGEDETECT) then
      Result := HTTOPLEFT
    else if (Top < EDGEDETECT) and (Right < EDGEDETECT) then
      Result := HTTOPRIGHT
    else if (Bottom < EDGEDETECT) and (Left < EDGEDETECT) then
      Result := HTBOTTOMLEFT
    else if (Bottom < EDGEDETECT) and (Right < EDGEDETECT) then
      Result := HTBOTTOMRIGHT
    else if (Top < EDGEDETECT) then
      Result := HTTOP
    else if (Left < EDGEDETECT) then
      Result := HTLEFT
    else if (Bottom < EDGEDETECT) then
      Result := HTBOTTOM
    else if (Right < EDGEDETECT) then
      Result := HTRIGHT;
  end;
end;

所以我终于得到了这个:

unit frmPopupU;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TfrmPopup = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormCreate(Sender: TObject);
  private
    procedure WMMouseActivate(var Message: TWMMouseActivate); message WM_MOUSEACTIVATE;
    procedure WMNCHitTest(var Message: TWMNCHitTest); message WM_NCHITTEST;
  public
    procedure CreateParams(var Params: TCreateParams); override;
  end;

implementation

{$R *.dfm}

{ TfrmPopup }

procedure TfrmPopup.Button1Click(Sender: TObject);
begin
  Close;
end;

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;

procedure TfrmPopup.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Action := caFree;
end;

procedure TfrmPopup.FormCreate(Sender: TObject);
begin
  DoubleBuffered := true;
  BorderStyle := bsNone;
end;

procedure TfrmPopup.WMMouseActivate(var Message: TWMMouseActivate);
begin
  Message.Result := MA_NOACTIVATE;
end;

procedure TfrmPopup.WMNCHitTest(var Message: TWMNCHitTest);
const
  EDGEDETECT = 7; //adjust to suit yourself
var
  deltaRect: TRect; //not really used as a rect, just a convenient structure
begin
  inherited;

  with Message, deltaRect do
  begin
    Left := XPos - BoundsRect.Left;
    Right := BoundsRect.Right - XPos;
    Top := YPos - BoundsRect.Top;
    Bottom := BoundsRect.Bottom - YPos;

    if (Top < EDGEDETECT) and (Left < EDGEDETECT) then
      Result := HTTOPLEFT
    else if (Top < EDGEDETECT) and (Right < EDGEDETECT) then
      Result := HTTOPRIGHT
    else if (Bottom < EDGEDETECT) and (Left < EDGEDETECT) then
      Result := HTBOTTOMLEFT
    else if (Bottom < EDGEDETECT) and (Right < EDGEDETECT) then
      Result := HTBOTTOMRIGHT
    else if (Top < EDGEDETECT) then
      Result := HTTOP
    else if (Left < EDGEDETECT) then
      Result := HTLEFT
    else if (Bottom < EDGEDETECT) then
      Result := HTBOTTOM
    else if (Right < EDGEDETECT) then
      Result := HTRIGHT;
  end;
end;

end.

希望你可以使用它。

完整的功能代码

以下单元仅在 Delphi 5 中进行了测试(模拟支持 PopupParent)。但除此之外,它还可以完成下拉菜单所需的一切。 Sertac 解决了AnimateWindow 问题。

unit DropDownForm;

{
    A drop-down style form.

    Sample Usage
    =================

        procedure TForm1.SpeedButton1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
        var
            pt: TPoint;
        begin
            if FPopup = nil then
                FPopup := TfrmOverdueReportsPopup.Create(Self);
            if FPopup.DroppedDown then //don't drop-down again if we're already showing it
                Exit;

            pt := Self.ClientToScreen(SmartSpeedButton1.BoundsRect.BottomRight);
            Dec(pt.X, FPopup.Width);

            FPopup.ShowDropdown(Self, pt);
        end;

    Simply make a form descend from TDropDownForm.

        Change:
            type
                TfrmOverdueReportsPopup = class(TForm)

        to:
            uses
                DropDownForm;

            type
                TfrmOverdueReportsPopup = class(TDropDownForm)
}

interface

uses
    Forms, Messages, Classes, Controls, Windows;

const
    WM_PopupFormCloseUp = WM_USER+89;

type
    TDropDownForm = class(TForm)
    private
        FOnCloseUp: TNotifyEvent;
        FPopupParent: TCustomForm;
        FResizable: Boolean;
        function GetDroppedDown: Boolean;
{$IFNDEF SupportsPopupParent}
        procedure SetPopupParent(const Value: TCustomForm);
{$ENDIF}
    protected
        procedure CreateParams(var Params: TCreateParams); override;
        procedure WMActivate(var Msg: TWMActivate); message WM_ACTIVATE;
        procedure WMNCHitTest(var Message: TWMNCHitTest); message WM_NCHITTEST;

        procedure DoCloseup; virtual;

        procedure WMPopupFormCloseUp(var Msg: TMessage); message WM_PopupFormCloseUp;

{$IFNDEF SupportsPopupParent}
        property PopupParent: TCustomForm read FPopupParent write SetPopupParent;
{$ENDIF}
  public
        constructor Create(AOwner: TComponent); override;

        procedure ShowDropdown(OwnerForm: TCustomForm; PopupPosition: TPoint);
        property DroppedDown: Boolean read GetDroppedDown;
        property Resizable: Boolean read FResizable write FResizable;

        property OnCloseUp: TNotifyEvent read FOnCloseUp write FOnCloseUp;
  end;

implementation

uses
    SysUtils;

{ TDropDownForm }

constructor TDropDownForm.Create(AOwner: TComponent);
begin
    inherited;

    Self.BorderStyle := bsNone; //get rid of our border right away, so the creator can measure us accurately
    FResizable := True;
end;

procedure TDropDownForm.CreateParams(var Params: TCreateParams);
const
    SPI_GETDROPSHADOW = $1024;
    CS_DROPSHADOW = $00020000;
var
    dropShadow: BOOL;
begin
    inherited CreateParams({var}Params);

    //It's no longer documented (because Windows 2000 is no longer supported)
    //but use of CS_DROPSHADOW and SPI_GETDROPSHADOW are only supported on XP (5.1) or newer
    if (Win32MajorVersion > 5) or ((Win32MajorVersion = 5) and (Win32MinorVersion >= 1)) then
    begin
        //Use of a drop-shadow is controlled by a system preference
        if not Windows.SystemParametersInfo(SPI_GETDROPSHADOW, 0, @dropShadow, 0) then
            dropShadow := False;

        if dropShadow then
            Params.WindowClass.Style := Params.WindowClass.Style or CS_DROPSHADOW;
    end;

{$IFNDEF SupportsPopupParent} //Delphi 5 support for "PopupParent" style form ownership
    if FPopupParent <> nil then
        Params.WndParent := FPopupParent.Handle;
{$ENDIF}
end;

procedure TDropDownForm.DoCloseup;
begin
    if Assigned(FOnCloseUp) then
        FOnCloseUp(Self);
end;

function TDropDownForm.GetDroppedDown: Boolean;
begin
    Result := (Self.Visible);
end;

{$IFNDEF SupportsPopupParent}
procedure TDropDownForm.SetPopupParent(const Value: TCustomForm);
begin
    FPopupParent := Value;
end;
{$ENDIF}

procedure TDropDownForm.ShowDropdown(OwnerForm: TCustomForm; PopupPosition: TPoint);
var
    comboBoxAnimation: BOOL;
    i: Integer;

const
    AnimationDuration = 200; //200 ms
begin
    //We want the dropdown form "owned" by (i.e. not "parented" to) the OwnerForm
    Self.Parent := nil; //the default anyway; but just to reinforce the idea
    Self.PopupParent := OwnerForm; //Owner means the Win32 concept of owner (i.e. always on top of, cf Parent, which means clipped child of)
{$IFDEF SupportsPopupParent}
    Self.PopupMode := pmExplicit; //explicitely owned by the owner
{$ENDIF}

    //Show the form just under, and right aligned, to this button
//  Self.BorderStyle := bsNone; moved to during FormCreate; so can creator can know our width for measurements
    Self.Position := poDesigned;
    Self.Left := PopupPosition.X;
    Self.Top := PopupPosition.Y;

    //Use of drop-down animation is controlled by preference
    if not Windows.SystemParametersInfo(SPI_GETCOMBOBOXANIMATION, 0, @comboBoxAnimation, 0) then
        comboBoxAnimation := False;

    if comboBoxAnimation then
    begin
        //Delphi doesn't react well to having a form show behind its back (e.g. ShowWindow, AnimateWindow).
        //Force Delphi to create all the WinControls so that they will exist when the form is shown.
        for i := 0 to ControlCount - 1 do
        begin
            if Controls[i] is TWinControl and Controls[i].Visible and
                    not TWinControl(Controls[i]).HandleAllocated then
            begin
                TWinControl(Controls[i]).HandleNeeded;
                SetWindowPos(TWinControl(Controls[i]).Handle, 0, 0, 0, 0, 0,
                        SWP_NOSIZE or SWP_NOMOVE or SWP_NOZORDER or SWP_NOACTIVATE or SWP_SHOWWINDOW);
            end;
        end;
        AnimateWindow(Self.Handle, AnimationDuration, AW_VER_POSITIVE or AW_SLIDE or AW_ACTIVATE);
        Visible := True; // synch VCL
    end
    else
        inherited Show;
end;

procedure TDropDownForm.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
        {
            Post a message (not Send a message) to oursleves that we're closing up.
            This gives a chance for the mouse/keyboard event that triggered the closeup
            to believe the drop-down is still dropped down.
            This is intentional, so that the person dropping it down knows not to drop it down again.
            They want clicking the button while is was dropped to hide it.
            But in order to hide it, it must still be dropped down.
        }
        PostMessage(Self.Handle, WM_PopupFormCloseUp, WPARAM(Self), LPARAM(0));
    end;
end;

procedure TDropDownForm.WMNCHitTest(var Message: TWMNCHitTest);
var
    deltaRect: TRect; //not really used as a rect, just a convenient structure
    cx, cy: Integer;
begin
    inherited;

    if not Self.Resizable then
        Exit;

    //The sizable border is a preference
    cx := GetSystemMetrics(SM_CXSIZEFRAME);
    cy := GetSystemMetrics(SM_CYSIZEFRAME);

    with Message, deltaRect do
    begin
        Left := XPos - BoundsRect.Left;
        Right := BoundsRect.Right - XPos;
        Top := YPos - BoundsRect.Top;
        Bottom := BoundsRect.Bottom - YPos;

        if (Top < cy) and (Left < cx) then
            Result := HTTOPLEFT
        else if (Top < cy) and (Right < cx) then
            Result := HTTOPRIGHT
        else if (Bottom < cy) and (Left < cx) then
            Result := HTBOTTOMLEFT
        else if (Bottom < cy) and (Right < cx) then
            Result := HTBOTTOMRIGHT
        else if (Top < cy) then
            Result := HTTOP
        else if (Left < cx) then
            Result := HTLEFT
        else if (Bottom < cy) then
            Result := HTBOTTOM
        else if (Right < cx) then
            Result := HTRIGHT;
    end;
end;

procedure TDropDownForm.WMPopupFormCloseUp(var Msg: TMessage);
begin
    //This message gets posted to us.
    //Now it's time to actually closeup.
    Self.Hide;

    DoCloseup; //raise the OnCloseup event *after* we're actually hidden
end;

end.

【讨论】:

  • 显示处于非活动状态的表单不会阻止将来变得活跃。当用户单击弹出表单中的某个位置时,此解决方案是否可以防止父表单处于非活动状态?
  • @IanBoyd 也许如果您对我的代码发表评论,我会在您的问题上投入更多精力。到目前为止,我只是在浪费时间
  • 好吧,我真的很欣赏你的工作;并且你花时间去做。我试图帮助您指出 Sertac 解决输/赢/输问题的方法(这看起来是 Combobox 在内部也是如何做到的)。虽然我没有立即调整下拉菜单的需要;它肯定会对以后遇到这个问题的其他人有用。
  • 我最终使用了您的 WM_NCHitTest 代码来调整大小。一旦我有了下拉菜单,我就意识到我希望能够调整表单的大小。唯一的例外是我使用了调整边框大小(GetSystemMetrics(SM_CXSIZEFRAME)GetSystemMetrics(SM_CYSIZEFRAME))的用户首选项,而不是硬编码7 像素。干得好。
  • 小建议:在Create()中,在设置BorderStyle之前保存ClientWidth+ClientHeight,然后恢复,否则表单ClientArea会比设计时大。
【解决方案2】:

如何使用 Delphi 创建“下拉”窗口?

您将总结的所有点点滴滴放在一起,没有一个 VCL 类/函数会生成下拉表单。

不过,您的研究中有几点需要提及。


首先,您将激活与焦点混淆了。当另一个窗口在它前面弹出时,焦点不会保留在调用表单中,激活是 - 或者看起来是这样。焦点是键盘输入的位置,显然是在弹出/放下的窗口或其中的控件上。


AnimateWindow 未显示控件的问题是,VCL 不会创建 TWinControls 的底层本机 (OS) 控件,直到有必要(非 wincontrols 不是问题)。就 VCL 而言,在它们可见之前通常不需要创建它们,即当你将表单的 Visible 设置为 true(或调用 Show)时,你不能从那时起没有动画,当然除非你在动画之后设置visible

这也是您尝试刷新表单时缺少的要求:

AnimateWindow(Self.Handle, 200, AW_VER_POSITIVE or AW_SLIDE or AW_ACTIVATE);
Self.Repaint;
Self.Update;
Self.Invalidate;

请注意,在上述问题的引用中,没有一个调用失败。但是没有什么可以画的,表单甚至还不是visible

任何强制创建控件并使其可见的方法都会使您的动画栩栩如生。

...
if comboBoxAnimation then
begin
  for i := 0 to ControlCount - 1 do
    if Controls[i] is TWinControl and Controls[i].Visible and
        not TWinControl(Controls[i]).HandleAllocated then begin
      TWinControl(Controls[i]).HandleNeeded;
      SetWindowPos(TWinControl(Controls[i]).Handle, 0, 0, 0, 0, 0,
          SWP_NOSIZE or SWP_NOMOVE or SWP_NOZORDER or SWP_NOACTIVATE or
          SWP_SHOWWINDOW);
    end;
  AnimateWindow(Handle, 200, AW_VER_POSITIVE or AW_SLIDE or AW_ACTIVATE);
  Visible := True; // synch VCL
end
else
  ...

这只是一个示例,在屏幕外显示表单或任何其他创造性方法都可以同样有效。在这里,in this answer,我通过在将visible 设置为 true 之前将动画表单的高度设置为“0”来实现相同的效果(不过我更喜欢这个答案中的方法..)。


关于当表单已经被下拉时不再删除,您不必为此向调用表单发布消息。事实上不要那样做,它需要调用表单的不必要的合作。只有一个实例可以被下拉,所以你可以使用全局:

  TfrmPopup = class(TForm)
    ...
    procedure FormDestroy(Sender: TObject);
  private
    FNotificationParentWnd: HWND;
    class var
      FDroppedDown: Boolean;
  protected
    ...


procedure TfrmPopup.Show(Owner: TForm; NotificationParentWindow: HWND;
  ...

  if not FDroppedDown then begin
      if comboBoxAnimation then begin

        // animate as above

        Visible := True; // synch with VCL
        FDroppedDown := True;
      end
      else
        inherited Show;
    end;
end;

procedure TfrmPopup.FormDestroy(Sender: TObject);
begin
  FDroppedDown := False;
end;

【讨论】:

    猜你喜欢
    • 2012-01-29
    • 1970-01-01
    • 2018-03-24
    • 2011-02-28
    • 1970-01-01
    • 2020-03-19
    • 1970-01-01
    • 1970-01-01
    • 2019-10-25
    相关资源
    最近更新 更多