delphi 下拉弹出式窗体

{
 下拉弹出式窗体
 2022-04-07 by tag 649998142@qq.com
 这里写的比较简单只支持一个方向。靠左下拉的弹出式窗体
 窗口设置BorderStyle := bsNone; 但是要给窗口一个 WS_BORDER 支持改变大小在 WMNCHIST 消息里处理
 思路:
 FormDeactivate 失去焦点事件 关闭自己 (本程序转移焦点时,比如点到其他控件时)
 WMACTIVATEAPP 本窗口失去焦点消息 关闭自己 (切换到其他程序时)
 计算popup 坐标: 参考dev 的popupcontrol控件
 popupcontrol 可以修改大小是自己再做的一个逻辑判断是MouseMove MouseDown MouseUp 处理的

}
unit UPopupWindow;

interface

uses
  Windows, Classes, SysUtils, Messages, Controls, Forms, MultiMon;

const
 WM_InternelClose = WM_USER + 1;

type
  //弹出控件的信息 close的时候要还原回去
  TPopupControlData = record
    Align: TAlign;
    Bounds: TRect;
    Parent: TWinControl;
    Visible: Boolean;
    BorderStyle: TFormBorderStyle;
    ActiveControl: TWinControl;
  end;

  TPopupWindow = class(TForm)
    procedure FormDeactivate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormShow(Sender:TObject);
    procedure FormCreate(Sender:TObject);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
  private
     { Private declarations }
    FPrevActiveWindow: HWND;
    FPopupPoint:TPoint;
    FPopupControlData:TPopupControlData;
    FOnInitPopup, FOnPopup, FOnCloseUp : TNotifyEvent;
    FOnCloseQuery:TCloseQueryEvent;
    FPopupControl: TControl;
    FPopupControlParent :TWinControl;
    procedure WMNCHIST(var Msg: TMessage);message WM_NCHITTEST;
    procedure WMActivate(var Message: TWMActivate); message WM_ACTIVATE;
    procedure WMACTIVATEAPP(var Msg: TWMActivateApp);message WM_ACTIVATEAPP;
    procedure WMInternelClose(var Msg: TMessage);message WM_InternelClose;
    procedure CalculateSize(AFocusedControl: TWinControl);
    procedure Closeup;
    procedure SavePopupControlData;
    procedure RestorePopupControlData;
  public
     { Public declarations }
     constructor Create; reintroduce; overload;
     procedure CreateParams(var Params :TCreateParams);override;
     property OnInitPopup: TNotifyEvent read FOnInitPopup write FOnInitPopup;
     property OnPopup: TNotifyEvent read FOnPopup write FOnPopup;
     property CloseQuery: TCloseQueryEvent read FOnCloseQuery write FOnCloseQuery;
     property OnCloseUp: TNotifyEvent read FOnCloseUp write FOnCloseUp;
     property PopupControl: TControl read FPopupControl write FPopupControl;
     procedure InitPopup;
     procedure Popup(AFocusedControl: TWinControl); overload;
     procedure Popup(AFocusedControl: TWinControl; APopupControl:TControl); overload;
     procedure Popup(P:TPoint; APopupControl:TControl); overload;
  end;

implementation


{ TPopupWindow }
constructor TPopupWindow.Create;
begin
 CreateNew(nil);
 Visible := False;
 DefaultMonitor := dmDesktop;
 OnCreate := FormCreate;
 OnDeactivate := FormDeactivate;
 OnShow := FormShow;
 OnCloseQuery := FormCloseQuery;
 OnClose := FormClose;
 FPopupPoint := Point(0,0);
 DoubleBuffered := True;
end;

procedure TPopupWindow.CreateParams(var Params: TCreateParams);
begin
 //https://blog.csdn.net/suiyunonghen/article/details/2325416
  BorderStyle := bsNone;
  inherited;
//  Params.Style := WS_POPUP or WS_CLIPSIBLINGS or WS_CLIPCHILDREN or WS_POPUPWINDOW;
  Params.Style := Params.Style or WS_BORDER;  //有个边框这样鼠标移动到边框就不会被控件给遮住了,不然放的控件如果是aclient 消息会穿透不了
 //下面这些虽然也可以,但是标题栏还是会有点空隙
// Params.Style := Params.Style or WS_THICKFRAME;
// Params.Style := Params.Style or WS_SIZEBOX;
end;

//弹出前先保存控件信息
procedure TPopupWindow.SavePopupControlData;
var
  APopupControl: TControl;
begin
  if FPopupControl <>  nil then
  with FPopupControl do
  begin
    FPopupControlData.Align := Align;
    if FPopupControl is TCustomForm then
    begin
      FPopupControlData.BorderStyle := TCustomForm(FPopupControl).BorderStyle;
      TCustomForm(FPopupControl).BorderStyle := bsNone;
    end;
    FPopupControlData.Bounds := BoundsRect;
    FPopupControlData.Parent := Parent;
    FPopupControlData.Visible := Visible;
    FPopupControlData.ActiveControl := nil;
    FPopupControl.Visible := True;
  end;
end;

//关闭时还原回去
procedure TPopupWindow.RestorePopupControlData;
begin
 if FPopupControl <> nil then
   with FPopupControl do
   begin
     while (FPopupControlData.ActiveControl <> nil) and (FPopupControlData.ActiveControl <> Self) do
     begin
       FPopupControlData.ActiveControl.Perform(CM_EXIT, 0, 0);
       FPopupControlData.ActiveControl := FPopupControlData.ActiveControl.Parent;
     end;
     Visible := False;
     Parent := FPopupControlData.Parent;
     Align := FPopupControlData.Align;
     //这里暂时不恢复, 下拉有可能改变了大小。下次下拉就还是一样就好了
//     BoundsRect := FPopupControlData.Bounds;
     Visible := FPopupControlData.Visible;
   end;
end;

//支持改变窗口大小
procedure TPopupWindow.WMNCHIST(var Msg: TMessage);
var
 MouseX,MouseY: integer;
begin
 MouseX := LOWORD(Msg.LParam);
 MouseY := HIWORD(Msg.LParam);
 if(MouseX >= Left + Width - 2) and (MouseY >= Top + Height - 2) then
   Msg.Result := HTBOTTOMRIGHT
 else if (MouseX <= Left + 2) and (MouseY <= Top + 3) then
  Msg.Result := HTTOPLEFT
 else if (MouseX <= Left + 2) and (MouseY<= Top + Height - 2) then
  Msg.Result := HTBOTTOMLEFT
 else if MouseX >= Left + Width -2 then
  Msg.Result := HTRIGHT
 else if MouseY >= Top + Height - 2 then
  Msg.Result := HTBOTTOM
 else if Mousex <= Left + 2 then
  Msg.Result := HTLEFT
 else if MouseY <= Top + 2 then
  Msg.Result := HTTOP
 else
 Inherited;
end;

procedure TPopupWindow.FormClose(Sender: TObject;
  var Action: TCloseAction);
begin
// ShowWindow(Handle, SW_HIDE);
 if Assigned(OnCloseUp) then
  OnCloseUp(Self);
 RestorePopupControlData;
// Action := caFree;
end;

procedure TPopupWindow.FormDeactivate(Sender: TObject);
begin
 Closeup;
end;

procedure TPopupWindow.FormShow(Sender: TObject);
begin
 if Assigned(OnPopup) then
  OnPopup(Self);
end;

procedure TPopupWindow.FormCreate(Sender: TObject);
begin
 FOnInitPopup :=nil;
 FOnPopup := nil;
 FOnCloseUp := nil;
 FOnCloseQuery := nil;
 FPopupControl := nil;
end;

procedure TPopupWindow.FormCloseQuery(Sender: TObject;
  var CanClose: Boolean);
begin
 if Assigned(CloseQuery) then
  CloseQuery(Sender,CanClose);
end;

procedure TPopupWindow.InitPopup;
begin
 if Assigned(OnInitPopup) then
  OnInitPopup(Self);
end;

//计算窗口大小位置坐标
procedure TPopupWindow.CalculateSize(AFocusedControl: TWinControl);
 function GetMonitorWorkArea(const AMonitor: Integer): TRect;
 var
   Info: TMonitorInfo;
 begin
   if AMonitor = 0 then
     Result := Screen.WorkAreaRect
   else
   begin
     Info.cbSize := SizeOf(Info);
     GetMonitorInfo(AMonitor, @Info);
     Result := Info.rcWork;
   end;
 end;

 //获取桌面区域
 function GetDesktopWorkArea(const P: TPoint): TRect;
 begin
  //返回离该点最近的显示监视器的句柄。
   Result := GetMonitorWorkArea(MonitorFromPoint(P, MONITOR_DEFAULTTONEAREST));
 end;

 //获取要弹出的坐标
 function GetPopupPoint:TPoint;
 var
  OwnerBounds, AOwnerScreenBounds, ADesktopWorkArea:TRect;
  AParent: TWinControl;
  function MoreSpaceOnTop: Boolean;
  begin
   Result := AOwnerScreenBounds.Top - ADesktopWorkArea.Top > ADesktopWorkArea.Bottom - AOwnerScreenBounds.Bottom;
  end;
 begin
   //直接给出坐标
   if AFocusedControl = nil then
   begin
    Result := FPopupPoint;
    OwnerBounds := Rect(0,0,PopupControl.Width,2);
    OffsetRect(OwnerBounds, FPopupPoint.X, FPopupPoint.Y);
   end
   else
   begin
    Result := AFocusedControl.ClientToScreen(Point(0,AFocusedControl.Height));
    OwnerBounds := AFocusedControl.ClientRect;
    OffsetRect(OwnerBounds, AFocusedControl.Left, AFocusedControl.Top);
   end;
   
   AOwnerScreenBounds := OwnerBounds;
   //获取相对于屏幕的坐标
   if Assigned(AFocusedControl) and Assigned(AFocusedControl.Parent) then
    AParent := AFocusedControl.Parent
   else
    AParent := Application.MainForm;

  //转换出相对于屏幕的坐标
  with AOwnerScreenBounds do
  begin
   TopLeft := AParent.ClientToScreen(TopLeft);
   BottomRight := AParent.ClientToScreen(BottomRight);
  end;
  Result.X := AOwnerScreenBounds.Left;
  Result.Y := AOwnerScreenBounds.Bottom;

  //判断是否超出屏幕的区域 这里弹出只支持垂直的方向。
  ADesktopWorkArea := GetDesktopWorkArea(Result);

  //Y坐标判断 
  if (Result.Y + PopupControl.Height > ADesktopWorkArea.Bottom) and MoreSpaceOnTop then
    Result.Y := AOwnerScreenBounds.Top - PopupControl.Height;

  //判断X坐标
  if Result.X + Width > ADesktopWorkArea.Right then
    Result.X := ADesktopWorkArea.Right - Width;
    
  if Result.X < ADesktopWorkArea.Left then
     Result.X := ADesktopWorkArea.Left;
 end;
var
 p:TPoint;
begin
 if FPopupControl = nil then
 begin
  self.Width := 100;
  self.Height := 150;
 end
 else
 begin
  FPopupControlParent := FPopupControl.Parent;
  FPopupControl.Parent := Self;
//  Width := FPopupControl.Width;
//  Height := FPopupControl.Height;
  FPopupControl.Align := alClient;
  P := GetPopupPoint;
//  Left := p.X;
//  Top  := p.Y;
  SetBounds(p.X,p.Y, FPopupControl.Width, FPopupControl.Height);
 end;
end;

procedure TPopupWindow.Popup(AFocusedControl: TWinControl);
// function IsMouseDownMessage(AMsg: WPARAM): Boolean;
// begin
//   case AMsg of
//     WM_NCLBUTTONDOWN, WM_NCLBUTTONDBLCLK, WM_LBUTTONDOWN, WM_LBUTTONDBLCLK,
//     WM_NCRBUTTONDOWN, WM_NCRBUTTONDBLCLK, WM_RBUTTONDOWN, WM_RBUTTONDBLCLK,
//     WM_NCMBUTTONDOWN, WM_NCMBUTTONDBLCLK, WM_MBUTTONDOWN, WM_MBUTTONDBLCLK:
//       Result := True
//   else
//     Result := False;
//   end;
// end;
var
 Msg: TMsg;
begin
 SavePopupControlData;
 InitPopup;
 CalculateSize(AFocusedControl);
 Show;
 SetWindowPos(Handle, 0, 0, 0, 0, 0,
        SWP_NOACTIVATE or SWP_NOMOVE or SWP_NOSIZE or SWP_SHOWWINDOW);
// repeat
//  case Integer(GetMessage(Msg, 0, 0, 0)) of
//    -1: Break;
//    0:begin
//        PostQuitMessage(Msg.wParam);
//        Break;
//      end;
//  end;
//  TranslateMessage(Msg);
//  DispatchMessage(Msg);
//  if IsMouseDownMessage(Msg.message) then
//  begin
//   ShowWindow(Handle, SW_HIDE);
//   Hide;
//  end;
// until not Visible;
end;

procedure TPopupWindow.Popup(AFocusedControl: TWinControl;
  APopupControl: TControl);
begin
 PopupControl := APopupControl;
 FPopupPoint := Point(0,0);
 Popup(AFocusedControl);
end;

procedure TPopupWindow.Popup(P:TPoint; APopupControl:TControl);
begin
 PopupControl := APopupControl;
 FPopupPoint := P;
 Popup(nil);
end;

procedure TPopupWindow.Closeup;
begin
 Close;
end;

procedure TPopupWindow.WMActivate(var Message: TWMActivate); 
begin
 inherited;
 //失去激活
 if Message.Active <> WA_INACTIVE then
 begin
  //activeform 保持得到焦点 标题栏不会变成失去焦点的状态    向一个窗体发送 WM_NCACTIVATE 消息,可以是标题栏显示为激活或者非激活状态.
  FPrevActiveWindow := Message.ActiveWindow;
  SendMessage(FPrevActiveWindow, WM_NCACTIVATE, WPARAM(True), 0);
 end;
end;


procedure TPopupWindow.WMACTIVATEAPP(var Msg: TWMActivateApp);
begin
  inherited;
  //失去焦点
 if not Msg.Active then
 begin
  SendMessage(FPrevActiveWindow, WM_NCACTIVATE, WPARAM(False), 0);
  Closeup;
 end;
end;

procedure TPopupWindow.WMInternelClose(var Msg: TMessage);
begin
 Close;
end;

end.

 调用:

//弹出下拉form基于按钮

with TPopupWindow.Create do
  Popup(btn2, mmo1);

 //坐标是相对于form的
 with TPopupWindow.Create do
  Popup(Point(btn3.Left, btn3.Top+btn3.Height), mmo1);

这里创建后窗体是没有释放的。所以是要拿个全部变量存一下。最后释放掉
也可以修改源码,多一个参数出来,控制close 事件是否cafree

 另一个下拉窗口实现思路:

https://www.cnblogs.com/chilanger/p/14200347.html

posted @ 2022-04-07 16:05  Tag  阅读(362)  评论(0编辑  收藏  举报