windows - How to simulate drop-down form in Delphi? -


how can create "drop-down" window using delphi?

everything beyond point research effort; , in no way related answer.

research effort

making proper drop-down requires lot of pieces work together. assume people don't difficult question, , rather asked 7 separate questions; each 1 addressing 1 tiny piece of problem. follows research effort solving deceptively simple problem.


note defining characteristics of drop-down window:

enter image description here

  • 1. drop-down extends outside it's "owner" window
  • 2. "owner" window keeps focus; drop-down never steals focus
  • 3. drop-down window has drop-shadow

this delphi variation of same question asked in winforms:

the answer in winforms use toolstripdropdown class. helper class turns form drop-down.

lets in delphi

i start creating gaudy dropdown form, serves example:

enter image description here

next drop button, thing click make drop-down appear:

enter image description here

and wire-up initial code show form needs in onclick:

procedure tform3.button1mousedown(sender: tobject;        button: tmousebutton; shift: tshiftstate; x, y: integer); var     frmpopup: tfrmpopup;     pt: tpoint; begin     frmpopup := tfrmpopup.create(self);      //show form under, , right aligned, button     pt := self.clienttoscreen(button1.boundsrect.bottomright);     dec(pt.x, frmpopup.clientwidth);      frmpopup.show(self, self.handle, pt); end; 

edit: changed mousedown rather click. click incorrect, drop-down shown without need click. 1 of unresolved issues how hide drop-down if user mouse-downs button again. we'll leave person answers question solve. in question research effort - not solution.

and we're off:

enter image description here

now how right way?

first thing notice right away lack of drop-shadow. that's because need apply 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:

enter image description here

focus stealing

the next issue calling .show on popup causes steal focus (the title bar of application indicates has lost focus). sertac comes solution this.

  • when popup receives it's wm_activate message indicating receiving focus (i.e. lo(wparam) <> wa_inactive):
  • send parent form wm_ncactivate(true, -1) indicate should draw still has focus

we handle wm_activate:

protected    procedure wmactivate(var msg: twmactivate); message wm_activate; 

and implementation:

procedure tfrmpopup.wmactivate(var msg: twmactivate); begin     //if being activated, give pretend activation state our owner     if (msg.active <> wa_inactive)         sendmessage(self.popupparent.handle, wm_ncactivate, wparam(true), -1);      inherited; end; 

so owner window looks still has focus (who knows if correct way - looks still has focus):

enter image description here

rolling up

fortunately, sertac solves problem of how dismiss window whenever user clicks away:

  • when popup receives it's wm_activate message indicating losing focus (i.e. lo(wparam) = wa_inactive):
  • send owner control notification rolling up
  • free popup form

we add our existing wm_activate handler:

procedure tfrmpopup.wmactivate(var msg: twmactivate); begin     //if being activated, give pretend activation state our owner     if (msg.active <> wa_inactive)         sendmessage(self.popupparent.handle, wm_ncactivate, wparam(true), -1);      inherited;      //if we're being deactivated, need rollup     if msg.active = wa_inactive     begin         //todo: tell our owner we've rolled          //note: parent should not using rollup time read state of controls in popup.         //      every time in popup changes, drop-down should give inforamtion owner         self.release; //use release let wmactivate complete     end; end; 

sliding dropdown

dropdown controls use animatewindow slide drop-down down. 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 used, use animatewindow show window. can use systemparametersinfo spi_getcomboboxanimation:

determines whether slide-open effect combo boxes enabled. pvparam parameter must point bool variable receives true enabled, or false disabled.

inside our newly consecrated tfrmpopup.show method, can check if client area animations enabled, , call either animatewindow or show depending on user's preference:

procedure tfrmpopup.show(owner: tform; notificationparentwindow: hwnd;       popupposition: tpoint); var     pt: tpoint;     comboboxanimation: bool; begin     fnotificationparentwnd := notificationparentwindow;      //we want dropdown form "owned" (i.e. not "parented" to) ownerwindow     self.parent := nil; //the default anyway; reinforce idea     self.popupparent := owner; //owner means win32 concept of owner (i.e. on top of, cf parent, means clipped child of)     self.popupmode := pmexplicit; //explicitely owned owner      //show form under, , right aligned, 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)         comboboxanimation := false;      if comboboxanimation     begin         //200ms 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 spi_getcomboboxanimation should use on spi_getclientareaanimation. points depths of difficulty hidden behind subtle "how simulate drop-down". simulating drop-down requires lot of stuff.

the problem delphi forms pretty fall on dead if try use showwindow or animatewindow behind back:

enter image description here

how solve that?

it's odd microsoft uses either:

  • showwindow(..., sw_shownoactivate), or
  • animatewindow(...) *(without aw_activate)

to show drop-down listbox without activation. , yet spying on combobox spy++ can see wm_ncactivate flying around.

in past people have simulated slide window using repeated calls change height of drop-down form timer. not bad; changes size of form. rather sliding down, form grows down; can see controls change layout drop-down appears. no, having drop-down form remain it's real size, slide down wanted here.

i know animatewindow , delphi have never gotten along. , question has been asked, lot, long before stackoverflow arrived. asked in 2005 on newsgroups. can't stop me asking again.

i tried force form redraw after animates:

animatewindow(self.handle, 200, aw_ver_positive or aw_slide or aw_activate); self.repaint; self.update; self.invalidate; 

but doesn't work; sits there mocking me:

enter image description here

now showing again when want close-up

if combobox dropped down, , user tries mousedown on button, real windows combobox control not show control again, instead hides it:

enter image description here

the drop-down knows "dropped-down", useful can draw if in "dropped down" mode. need way know drop-down dropped down, , way know drop-down no longer dropped down. kind of boolean variable:

private    fdroppeddown: boolean; 

and seems me need tell host we're closing (i.e. losing activation). the host needs responsible destroying popup. (the host cannot responsible destroying popup; leads unresolvable race condition). create message used notify owner we're closing up:

const    wm_popupformcloseup = wm_app+89; 

note: don't know how people avoid message constant conflicts (especially since cm_base starts @ $b000 , cn_base starts @ $bc00).

building on sertac's activation/deactivation routine:

procedure tfrmpopup.wmactivate(var msg: twmactivate); begin     //if being activated, give pretend activation state our owner     if (msg.active <> wa_inactive)         sendmessage(self.popupparent.handle, wm_ncactivate, wparam(true), -1);      inherited;      //if we're being deactivated, need rollup     if msg.active = wa_inactive     begin         //done: tell our owner we've rolled         //note: must post message. if sent, owner         //will closeup notification before mousedown         //started this. when mousedown comes, think         //they not dropped down, , drop down new one.         postmessage(fnotificationparentwnd, wm_popupformcloseup, 0, 0);          self.release; //use release give wm_activate chance return     end; end; 

and have change our mousedown code understand drop-down still there:

procedure tform3.edit1mousedown(sender: tobject; button: tmousebutton; shift: tshiftstate; x, y: integer); var     frmpopup: tfrmpopup;     pt: tpoint; begin     //if (were) dropped down, don't drop-down again.     //if click us, pretend trying close drop-down rather open second copy     if fdroppeddown     begin         //and since we're receiving mouse input, defintion must have focus.         //and since drop-down self-destructs when loses activation,          //it can no longer dropped down (since no longer exists)         exit;     end;      frmpopup := tfrmpopup.create(self);      //show form under, , right aligned, button     pt := self.clienttoscreen(edit1.boundsrect.bottomright);     dec(pt.x, frmpopup.clientwidth);      frmpopup.show(self, self.handle, pt);     fdroppeddown := true; end; 

and think that's it

aside animatewindow conundrum, may have been able use research effort solve problems can think of in order to:

simulate drop-down form in delphi

of course, naught. might turn out there's vcl function:

tcomboboxhelper = class; public    class procedure showdropdownform(...); end; 

in case that correct answer.

at bottom of procedure tform3.button1click(sender: tobject); call frmpopup.show; change showwindow(frmpopup.handle, sw_shownoactivate); , after need call frmpopup.visible := true; else components on form won't show

so new procedure looks this:

uses   frmpopupu;  procedure tform3.button1click(sender: tobject); var   frmpopup: tfrmpopup;   pt: tpoint; begin   frmpopup := tfrmpopup.create(self);   frmpopup.borderstyle := bsnone;    //we want dropdown form "owned", not "parented"   frmpopup.parent := nil; //the default anyway; reinforce idea   frmpopup.popupparent := self;    //show form under, , right aligned, 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 components on form won't show   frmpopup.visible := true; end; 

but won't prevent popup stealing focus. inorder preventing that, need override wm_mouseactivate event in popup form

type   tfrmpopup = class(tform) ...     procedure wmmouseactivate(var message: twmmouseactivate); message wm_mouseactivate; ...   end; 

and implementation

procedure tfrmpopup.wmmouseactivate(var message: twmmouseactivate); begin   message.result := ma_noactivate; end; 

i've decided play arround popup window: first thing added close button. simple tbutton in onclick event calls close:

procedure tfrmpopup.button1click(sender: tobject); begin   close; end; 

but hide form, in order freeing added onformclose event:

procedure tfrmpopup.formclose(sender: tobject; var action: tcloseaction); begin   action := cafree; end; 

then thought funny add resize function

i did overriding wm_nchittest message :

procedure tfrmpopup.wmnchittest(var message: twmnchittest); const   edgedetect = 7; //adjust suit var   deltarect: trect; //not used rect, convenient structure begin   inherited;    message, deltarect   begin     left := xpos - boundsrect.left;     right := boundsrect.right - xpos;     top := ypos - boundsrect.top;     bottom := boundsrect.bottom - ypos;      if (top < edgedetect) , (left < edgedetect)       result := httopleft     else if (top < edgedetect) , (right < edgedetect)       result := httopright     else if (bottom < edgedetect) , (left < edgedetect)       result := htbottomleft     else if (bottom < edgedetect) , (right < edgedetect)       result := htbottomright     else if (top < edgedetect)       result := httop     else if (left < edgedetect)       result := htleft     else if (bottom < edgedetect)       result := htbottom     else if (right < edgedetect)       result := htright;   end; end; 

so i've ended :

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 suit var   deltarect: trect; //not used rect, convenient structure begin   inherited;    message, deltarect   begin     left := xpos - boundsrect.left;     right := boundsrect.right - xpos;     top := ypos - boundsrect.top;     bottom := boundsrect.bottom - ypos;      if (top < edgedetect) , (left < edgedetect)       result := httopleft     else if (top < edgedetect) , (right < edgedetect)       result := httopright     else if (bottom < edgedetect) , (left < edgedetect)       result := htbottomleft     else if (bottom < edgedetect) , (right < edgedetect)       result := htbottomright     else if (top < edgedetect)       result := httop     else if (left < edgedetect)       result := htleft     else if (bottom < edgedetect)       result := htbottom     else if (right < edgedetect)       result := htright;   end; end;  end. 

hope can use it.

full , functional code

the following unit tested in delphi 5 (emulated support popupparent). beyond that, drop-down needs. sertac solved animatewindow problem.

unit dropdownform;  {     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                 fpopup := tfrmoverduereportspopup.create(self);             if fpopup.droppeddown //don't drop-down again if we're showing                 exit;              pt := self.clienttoscreen(smartspeedbutton1.boundsrect.bottomright);             dec(pt.x, fpopup.width);              fpopup.showdropdown(self, pt);         end;      make form descend 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, creator can measure 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 no longer supported)     //but use of cs_dropshadow , spi_getdropshadow supported on xp (5.1) or newer     if (win32majorversion > 5) or ((win32majorversion = 5) , (win32minorversion >= 1))     begin         //use of drop-shadow controlled system preference         if not windows.systemparametersinfo(spi_getdropshadow, 0, @dropshadow, 0)             dropshadow := false;          if dropshadow             params.windowclass.style := params.windowclass.style or cs_dropshadow;     end;  {$ifndef supportspopupparent} //delphi 5 support "popupparent" style form ownership     if fpopupparent <> nil         params.wndparent := fpopupparent.handle; {$endif} end;  procedure tdropdownform.docloseup; begin     if assigned(foncloseup)         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 dropdown form "owned" (i.e. not "parented" to) ownerform     self.parent := nil; //the default anyway; reinforce idea     self.popupparent := ownerform; //owner means win32 concept of owner (i.e. on top of, cf parent, means clipped child of) {$ifdef supportspopupparent}     self.popupmode := pmexplicit; //explicitely owned owner {$endif}      //show form under, , right aligned, button //  self.borderstyle := bsnone; moved during formcreate; can creator can know our width measurements     self.position := podesigned;     self.left := popupposition.x;     self.top := popupposition.y;      //use of drop-down animation controlled preference     if not windows.systemparametersinfo(spi_getcomboboxanimation, 0, @comboboxanimation, 0)         comboboxanimation := false;      if comboboxanimation     begin         //delphi doesn't react having form show behind (e.g. showwindow, animatewindow).         //force delphi create wincontrols exist when form shown.         := 0 controlcount - 1         begin             if controls[i] twincontrol , controls[i].visible ,                     not twincontrol(controls[i]).handleallocated             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 being activated, give pretend activation state our owner     if (msg.active <> wa_inactive)         sendmessage(self.popupparent.handle, wm_ncactivate, wparam(true), -1);      inherited;      //if we're being deactivated, need rollup     if msg.active = wa_inactive     begin         {             post message (not send message) oursleves we're closing up.             gives chance mouse/keyboard event triggered closeup             believe drop-down still dropped down.             intentional, person dropping down knows not drop down again.             want clicking button while dropped hide it.             in order hide it, must still dropped down.         }         postmessage(self.handle, wm_popupformcloseup, wparam(self), lparam(0));     end; end;  procedure tdropdownform.wmnchittest(var message: twmnchittest); var     deltarect: trect; //not used rect, convenient structure     cx, cy: integer; begin     inherited;      if not self.resizable         exit;      //the sizable border preference     cx := getsystemmetrics(sm_cxsizeframe);     cy := getsystemmetrics(sm_cysizeframe);      message, deltarect     begin         left := xpos - boundsrect.left;         right := boundsrect.right - xpos;         top := ypos - boundsrect.top;         bottom := boundsrect.bottom - ypos;          if (top < cy) , (left < cx)             result := httopleft         else if (top < cy) , (right < cx)             result := httopright         else if (bottom < cy) , (left < cx)             result := htbottomleft         else if (bottom < cy) , (right < cx)             result := htbottomright         else if (top < cy)             result := httop         else if (left < cx)             result := htleft         else if (bottom < cy)             result := htbottom         else if (right < cx)             result := htright;     end; end;  procedure tdropdownform.wmpopupformcloseup(var msg: tmessage); begin     //this message gets posted us.     //now it's time closeup.     self.hide;      docloseup; //raise oncloseup event *after* we're hidden end;  end. 

Popular posts from this blog