Link to home
Start Free TrialLog in
Avatar of camou
camou

asked on

ListBox Like Delphi's Code Complete

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
Avatar of kretzschmar
kretzschmar
Flag of Germany image

listening . . . -> some ideas, but no time :-(
Avatar of Cynna
Cynna

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;
Avatar of camou

ASKER

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

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

Avatar of camou

ASKER

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
listening
maybe there's some hooking involved ?
dunno really, interested though
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;
Avatar of camou

ASKER

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
ASKER CERTIFIED SOLUTION
Avatar of Cynna
Cynna

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of camou

ASKER

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