【发布时间】: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 来显示窗口。我们可以将SystemParametersInfo 与SPI_GetComboBoxAnimation一起使用:
确定是否启用组合框的滑动打开效果。 pvParam 参数必须指向一个 BOOL 变量,该变量接收 TRUE 表示启用,或 FALSE 表示禁用。 p>
在我们新奉献的TfrmPopup.Show 方法中,我们可以检查是否启用了客户区动画,并根据用户的偏好调用AnimateWindow 或Show:
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。这指出了隐藏在微妙“如何模拟下拉菜单”背后的困难深度。模拟下拉菜单需要很多东西。
问题是,如果你试图在他们背后使用ShowWindow 或AnimateWindow,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