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
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
listening . . . -> some ideas, but no time :-(
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(Se lf);
with p do begin
SetBounds(10,10, 50,100);
Parent:=nil;
ParentWindow:=GetDesktopWi ndow;
Items.Add('First');
Items.Add('Second');
end;
end;
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
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,
end;
Demo:
--------
procedure TForm1.Button1Click(Sender
begin
p:=TPopupListbox.Create(Se
with p do begin
SetBounds(10,10, 50,100);
Parent:=nil;
ParentWindow:=GetDesktopWi
Items.Add('First');
Items.Add('Second');
end;
end;
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
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(Se lf);
with p do begin
Visible:=FALSE;
SetBounds(400,600, 100,100);
ParentWindow:=GetDesktopWi ndow;
for i:=0 to 10 do Items.Add('Item #'+IntToStr(i));
end;
end;
procedure TForm1.RichEdit1KeyDown(Se nder: 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.S trings[Ite mIndex];
// 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_POSFRO MCHAR, WPARAM(@pt), SelStart );
spt:=ClientToScreen(pt);
spt.x:=spt.x-p.Width div 2;
spt.y:=spt.y+Abs(Font.Heig ht)+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(S ender: 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
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
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,
end;
// Events - FormCreate, RichEdit1KeyDown, RichEdit1KeyPress:
procedure TForm1.FormCreate(Sender: TObject);
var i: Integer;
begin
p:=TPopupListbox.Create(Se
with p do begin
Visible:=FALSE;
SetBounds(400,600, 100,100);
ParentWindow:=GetDesktopWi
for i:=0 to 10 do Items.Add('Item #'+IntToStr(i));
end;
end;
procedure TForm1.RichEdit1KeyDown(Se
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.S
// 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_POSFRO
spt:=ClientToScreen(pt);
spt.x:=spt.x-p.Width div 2;
spt.y:=spt.y+Abs(Font.Heig
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(S
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
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(Se lf);
with p do begin
Visible:=FALSE;
SetBounds(400,600, 100,100);
ParentWindow:=GetDesktopWi ndow;
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
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(Se
with p do begin
Visible:=FALSE;
SetBounds(400,600, 100,100);
ParentWindow:=GetDesktopWi
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:
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
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:=GetDesktop Window; // 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;
> …. 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(
dummy.Parent:=Form1;
// …..
//ParentWindow:=GetDesktop
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)
(message=CM_DEACTIVATE) then Visible:=FALSE;
end;
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
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
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
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
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