Solved

ListBox Like Delphi's Code Complete

Posted on 2002-07-09
11
1,984 Views
Last Modified: 2011-09-20
I'm trying to make a ListBox decendent that works like Delphi's Code Complete/Insight TPopupListBox. I am not concerned with _when_ the ListBox should popup, but _how_ to make the ListBox popup and work properly.

I know that for this to work properly the ListBox should have the windows desktop set as its parent (the same way that Delphi's does), and an "invisible" window/WindowProc assigned with AllocateHWND; also it should SetFocus to the associated Memo immediately after appearing. But, I am stuck after that point.

The real problem seems to be how to hide/show the ListBox. The SetWindowPos API does not seem to be very effective -- when the ListBox does get shown, if at all, it contains items but none of the items' text/images are visible.

Can anyone show me the right way to make this work?


* Also, I have tried using a Form (with fsStayOnTop set), but this creates additional focus problems (e.g. hiding the Form/ListBox when the mouse is clicked elsewhere), so I would prefer to avoid this method.



Thanks,

camou
0
Comment
Question by:camou
11 Comments
 
LVL 27

Expert Comment

by:kretzschmar
ID: 7140721
listening . . . -> some ideas, but no time :-(
0
 
LVL 7

Expert Comment

by:Cynna
ID: 7140989
camou,

What is Delphi's Code Complete/Insight TPopupListBox url?
Since I don't know about this component, I'm not sure exactly what you need.
But, from the rest of your description, I think this code might help you out:


TPopupListbox = class(TCustomListbox)
protected
   procedure CreateParams(var Params: TCreateParams); override;
   procedure CreateWnd; override;
end;

procedure TPopupListBox.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  with Params do begin
    Style := Style or WS_BORDER;
    ExStyle := WS_EX_TOOLWINDOW or WS_EX_TOPMOST;
    WindowClass.Style := CS_SAVEBITS;
  end;
end;

procedure TPopupListbox.CreateWnd;
begin
  inherited CreateWnd;
  Windows.SetParent(Handle, 0);
  CallWindowProc(DefWndProc, Handle, wm_SetFocus, 0, 0);
end;



Demo:
--------

procedure TForm1.Button1Click(Sender: TObject);
begin
  p:=TPopupListbox.Create(Self);
  with p do begin
        SetBounds(10,10, 50,100);
        Parent:=nil;
        ParentWindow:=GetDesktopWindow;
        Items.Add('First');
        Items.Add('Second');
   end;
end;
0
 

Author Comment

by:camou
ID: 7141097
Cynna,

There is no download URL for Delphi's TPopupListBox. If you open the Delphi IDE and make the Code Complete ListBox appear, you will see with a utility like Spy++ that the Code Complete ListBox's class name is "TPopupListBox". (Note: this is also the class name of the drop down ListBox that is used with Delphi's object inspector).

The code you supplied does place the ListBox "on the desktop", but it does not solve my problem of how to manage hiding/showing the list.

Bascially what I am looking for is a owner-draw ListBox that will function (popup) in the same manner as the Code Complete ListBox from the Delphi IDE.


Regards,

camou

0
 
LVL 7

Expert Comment

by:Cynna
ID: 7141332
camou,

Oh, I see now - you ment Code completion feature in Delphi...

> ... but it does not solve my problem of how to manage hiding/showing the list
I don't see what is the problem - it's a matter of simple Visible:=TRUE/FALSE

statement.

Anyway, you told me what you want, so I put together a little demo that uses this
component like Delphis Code completion engine.

Put Richedit1 on the form, and copy/paste following code:




// (..your code...)

// in the interface section:

TPopupListbox = class(TCustomListbox)
protected
   procedure CreateParams(var Params: TCreateParams); override;
   procedure CreateWnd; override;
end;


// (..your code...)

// global vars:
var  p: TPopupListBox;



// (..your code...)

// in the implementation section:

procedure TPopupListBox.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  with Params do begin
    Style := Style or WS_BORDER;
    ExStyle := WS_EX_TOOLWINDOW or WS_EX_TOPMOST;
    WindowClass.Style := CS_SAVEBITS;
  end;
end;

procedure TPopupListbox.CreateWnd;
begin
  inherited CreateWnd;
  Windows.SetParent(Handle, 0);
  CallWindowProc(DefWndProc, Handle, wm_SetFocus, 0, 0);
end;



// Events - FormCreate, RichEdit1KeyDown, RichEdit1KeyPress:

procedure TForm1.FormCreate(Sender: TObject);
var i: Integer;
begin
  p:=TPopupListbox.Create(Self);
  with p do begin
        Visible:=FALSE;
        SetBounds(400,600, 100,100);
        ParentWindow:=GetDesktopWindow;
        for i:=0 to 10 do Items.Add('Item #'+IntToStr(i));
   end;

end;


procedure TForm1.RichEdit1KeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState); // this event handles the popup manipulation
var pt, spt: TPoint;
begin
  with p do begin
       if Visible then begin
          Case Key of
              VK_DOWN: if ItemIndex=(Items.Count-1) then ItemIndex:=0
                                                    else ItemIndex:=ItemIndex+1;
              VK_UP  : if ItemIndex=0 then ItemIndex:=Items.Count-1
                                      else ItemIndex:=ItemIndex-1;
              VK_ESCAPE: p.Visible:=FALSE;
              VK_RETURN: begin
                           p.Visible:=FALSE;
                           Richedit1.SelLength:=0;
                           Richedit1.SelText:=Items.Strings[ItemIndex];
                           // Supress Enter:
                           RichEdit1.Tag:=1;
                         end;
          end;
          if Key in [VK_DOWN, VK_UP, VK_ESCAPE] then Key:=0;
       end;
  end;
  if (Key=VK_SPACE) and (ssCtrl in Shift) then begin
      // Get caret screen coordinates:
      with Richedit1 do begin
           Perform(Messages.EM_POSFROMCHAR, WPARAM(@pt), SelStart );
           spt:=ClientToScreen(pt);
           spt.x:=spt.x-p.Width div 2;
           spt.y:=spt.y+Abs(Font.Height)+3;
      end;
      // Popup and maybe select out listbox:
      with p do begin
           Left:=spt.x; Top:=spt.y; Visible:=TRUE;
          if ItemIndex<0 then ItemIndex:=0
      end;


  end;
end;

procedure TForm1.RichEdit1KeyPress(Sender: TObject; var Key: Char);
begin
  // Eat enter if necessary:
  if (RichEdit1.Tag=1) and (Key=Chr(13)) then Key:=Char(0);
  RichEdit1.Tag:=0;
end;




Notes:
----------

 - this is a simple imitation of Delphis Code completion engine look & feel
 - to start it, press Ctrl+Space
 - Enter will insert item, Escape will close popup

0
 

Author Comment

by:camou
ID: 7141579
Cynna,

The code works fine when the ListBox style is lbStandard, but when the ListBox is style is lbOwnerDrawFixed or lbOwnerDrawVariable, none of the Items get drawn.

I ammended the following to your code above to illustrate:

//
// Add an ImageList to the Form and 1 16x16 Bitmap to the ImageList
//
procedure TForm1.FormCreate(Sender: TObject);
var i: Integer;
begin
 p:=TPopupListbox.Create(Self);
 with p do begin
    Visible:=FALSE;
    SetBounds(400,600, 100,100);
    ParentWindow:=GetDesktopWindow;
    Style := lbOwnerDrawFixed; // <-- added
    ItemHeight := 18;          // <-- added
    OnDrawItem := LBDrawItem;  // <-- added
    for i:=0 to 10 do Items.Add('Item #'+IntToStr(i));
  end;
end;

procedure TForm1.LBDrawItem(Control: TWinControl; Index: Integer;
  Rect: TRect; State: TOwnerDrawState);
var R: TRect;
begin
  R := Rect;
  with (Control as TPopupListBox) do begin
    ImageList1.Draw(Canvas, R.Left + 1, (R.Top + R.Bottom - 16) div 2, 0, True);
    Inc(Rect.Left, 20);
    DrawText(Canvas.Handle, PChar(Items[Index]), -1, Rect, DT_LEFT or DT_SINGLELINE or DT_VCENTER);
  end;
end;


Also, the problem of how to properly hide the ListBox still remains. For example, if the user clicks somewhere else on the form, on another control, or on the form's non-client area --- what to do?


Regards,

camou
0
Highfive + Dolby Voice = No More Audio Complaints!

Poor audio quality is one of the top reasons people don’t use video conferencing. Get the crispest, clearest audio powered by Dolby Voice in every meeting. Highfive and Dolby Voice deliver the best video conferencing and audio experience for every meeting and every room.

 
LVL 3

Expert Comment

by:smurff
ID: 7142135
listening
0
 
LVL 12

Expert Comment

by:Lee_Nover
ID: 7142154
maybe there's some hooking involved ?
dunno really, interested though
0
 
LVL 7

Expert Comment

by:Cynna
ID: 7142838
camou,

> …. but when the ListBox is style is lbOwnerDrawFixed or lbOwnerDrawVariable, none of the Items get drawn

You are absolutely right, sorry I didn’t test my code thoroughly enough.
The problem is that out control doesn’t get WM_DRAWITEM message due to the fact
that its parent is desktop. So, solution is simply setting its parent to any VCL control.
The most logical choice would be Form1, but this could create problems if forms AutoScroll
property is set. You could dynamically switch it on/off but this would make the code clumsy.

So, I decided creating dummy control that is used as a parent. Not too elegant, but works…
OK, on to the code, these are the changes:

1. Add another global var:
 var
   dummy: TWinControl;

2. Create it in FormCreate, and set is as parent to PopupListBox:1:

  procedure TForm1.FormCreate(Sender: TObject);
    // …..
    dummy:=TWinControl.Create(Self);
    dummy.Parent:=Form1;
    // …..
        //ParentWindow:=GetDesktopWindow; // wrong - not getting WM_DRAWITEM              
        Parent:=dummy;
    // …..



> For example, if the user clicks somewhere else on the form, on another control, or on the form's non-client area --- what to do?

Well, it’s pretty straightforward: you just have to "catch" this. There are number of options,
but the easiest one, IMHO, is simply intercepting mouse clicks on application level and then
hiding our control if it’s not click target. The code that does this is below:
 
// In the interface section (Form1 declaration):
//….
  private
    procedure AppMessage(var Msg: TMsg; var Handled: Boolean);
// ….

// In the implementation:
procedure TForm1.AppMessage(var Msg: TMsg; var Handled: Boolean);
begin
  with Msg, p do // Hide p if necessary
    if Visible and (Hwnd<>Handle) then
       if (message=WM_LBUTTONDOWN) or (message=WM_RBUTTONDOWN) or
          (message=WM_NCLBUTTONDOWN) or (message=WM_NCRBUTTONDOWN) or
          (message=CM_DEACTIVATE) then Visible:=FALSE;
end;
0
 

Author Comment

by:camou
ID: 7144377
Cynna,

Using the "Dummy" control seems to work (have no idea why, though). But I was wondering.. since my main form (the one used with the PopupListBox) will always have AutoScroll as false, would it be ok to set PopListBox's parent to Application.MainForm or Form1..?

Unfortunately, the AppMessage procedure did not work, the ListBox still remained onscreen even when I switched to another application. BTW, I'm using Delphi4 if that matters.

Would setting/unsetting the mouse capture to the PopupListBox be a better way to hide it when the user clicks somewhere else (don't know how to implement this though)?


Regards,

camou
0
 
LVL 7

Accepted Solution

by:
Cynna earned 500 total points
ID: 7144786
camou,

> Using the "Dummy" control seems to work (have no idea why, though).

If desktop is used as a parent for TPopupListBox, it won't receive WM_DRAWITEM
message, therefore not firing OnDrawItem event. Only VCL parent will dispatch message
to our component. Any windowed control could be used for this; I used TwinControl, but
any of its ancestors will do fine too.


>... always have AutoScroll as false, would it be ok to set PopListBox's parent to Application.MainForm or Form1?

Sure it would. I didn't use it only because Form1 will automatically show scroll bars if
TPopupListBox exceeds its height. But if you switch AutoScroll off, then this is not a
problem, and it would certainly be more elegant solution then a "dummy parent" :).


> Unfortunately, the AppMessage procedure did not work, the ListBox still remained onscreen even when I switched to another application.

Did you assigned  AppMessage to Application.OnMessage event in TForm1.FormCreate?
I forgot to explicate this, sorry:

procedure TForm1.FormCreate(Sender: TObject);
begin
  Application.OnMessage := AppMessage; // <--- now all app messages are send  to AppMessage
  // …. rest of the code



> Would setting/unsetting the mouse capture to the PopupListBox be a better way to hide it when the user clicks somewhere else (don't know how to implement this though)?

No it wouldn't - we need a mechanism to know when mouse is clicked anywhere outside
our PopupListBox. Once we know this, all that remains is hiding it  (if it's visible, of course).
Alternative to using application-level message handler is using mouse hook, but this certainly
seems like an over-kill for our problem.
Also, there is always a moronic solution of TTimer and periodical mouse button-down check.

0
 

Author Comment

by:camou
ID: 7147255
Cynna,

Thanks for all the help! I had forgot to set Application.OnMessage := AppMessage; in my main form's OnCreate event. Everything works fine now.  :)

Also, using the application message handler you suggested was much better for me than the alternative I was looking into - using a mouse hook.

FWIW though, I would be keen to see how Borland implemented their "PopupListBox" - whether or not they are using a mouse hook or not.


Thanks again,

camou
0

Featured Post

Enabling OSINT in Activity Based Intelligence

Activity based intelligence (ABI) requires access to all available sources of data. Recorded Future allows analysts to observe structured data on the open, deep, and dark web.

Join & Write a Comment

Suggested Solutions

Title # Comments Views Activity
SIM number for APK using Delphi 10 2 363
delphi custom sort exception 6 111
Run video youtube webbrowse 10 45
Create a path if not exists 7 47
Hello everybody This Article will show you how to validate number with TEdit control, What's the TEdit control? TEdit is a standard Windows edit control on a form, it allows to user to write, read and copy/paste single line of text. Usua…
In my programming career I have only very rarely run into situations where operator overloading would be of any use in my work.  Normally those situations involved math with either overly large numbers (hundreds of thousands of digits or accuracy re…
This video discusses moving either the default database or any database to a new volume.
This video explains how to create simple products associated to Magento configurable product and offers fast way of their generation with Store Manager for Magento tool.

743 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

15 Experts available now in Live!

Get 1:1 Help Now