tListBox using AutoComplete navigation from other ListBox - delphi

Is there a way to use native tListBox AutoComplete navigation system but based on the items of other ListBox? So when ListBox1 focused when i type some chars items should be selected according to data from ListBox2. Both of them have same amount of items.

Is there a way to use native tListBox AutoComplete navigation system but based on the items of other ListBox?
Yes, but only if the TListBox.Style property is set to either lbVirtual or lbVirtualOwnerDraw. In that case, you must use the TListBox.Count property and TListBox.OnData event to provide strings to the ListBox. The auto-complete functionality will then fire the TListBox.OnDataFind event asking you to locate the typed characters in whatever source you get your strings from. In that event handler, you can search the other TListBox as needed. Just know that the Integer returned by the OnDataFind event handler must be an index relative to the TListBox that the user is typing into, not to the TListBox that you are searching. When the OnDataFind event handler exits, whichever index you return will be selected, unless you return -1 to indicate the characters were not found.

Well i tried to make it native but as far as i get it - it's impossible to combine different item-height functionality (Style=lbOwnerDrawVariable) with DataFind events handling. Ofcourse one can edit TCustomListBox.KeyPress procedure in "VCL\StdCtrls", but i dont like to dig into vcl sources. So i decided to implement AutoComplete feature by myself from a scratch.
First of all i decided to index item-strings for faster search, but there are no need to make full graph (octotree... whatever...) cuz search in my case (and i have about 1k items) anyways performed nearly instantly - so cuz my list is lexicographically sorted i made indexing only for first letters...
//...
const
acceptCharz=[' '..'z'];
//...
type
twoWords=packed record a,b:word; end;
//...
var
alphs:array[' '..'z']of twoWords;// indexes of first letters
fs:tStringList;// list for searching in
fnd:string;// charbuffer for search string
tk:cardinal;// tickCounter for timing
//...
procedure Tform1.button1Click(Sender: TObject);// procedure where list filled with items
var
k,l:integer;
h,q:char;
begin
//... here list-content formed ...
if(fs<>nil)then fs.Free;
fs:=tStringList.Create;
// fltr is tListBox which data should be source of AutoCompletion (ListBox2)
fs.AddStrings(fltr.Items);
for h:=low(alphs)to high(alphs)do alphs[h].a:=$FFFF;// resetting index
h:=#0;
l:=fs.Count-1;
if(l<0)then exit;
for k:=0 to l do begin
s:=AnsiLowerCase(fs.Strings[k]);// for case-insensetivity
fs.Strings[k]:=s;
if(length(s)<0)then continue;
q:=s[1];
if(h<>q)and(q in acceptCharz)then begin
if(k>0)then alphs[h].b:=k-1;
h:=q;
alphs[h].a:=k;// this will work only with sorted data!
end;
end;
if(h<>#0)then alphs[h].b:=l;
end;
//...
// fl is tListBox with custom drawing and OwnerDrawVariable style (ListBox1)
// also fl has same amount of items as fltr
procedure Tform1.flKeyPress(Sender: TObject; var Key: Char);
var
n,i,k,e,l,u,m,a:integer;
s:string;
h:char;
function CharLowerr(h:char):char;// make char LowerCase
begin
Result:=char(LoWord(CharLower(Pointer(h))));
end;
begin
if(getTickCount-tk>=800)then fnd:='';// AutoComplete timeout
tk:=getTickCount;
h:=CharLowerr(key);// for case-insensetivity
if(h in ['a'..'z','0'..'9',' ','-','.'])then fnd:=fnd+h;// u can add all needed chars
if(fnd='')then exit;// if no string to find
h:=fnd[1];// obtain first letter of search-string
a:=alphs[h].a;// get index of first item starting with letter
l:=alphs[h].b;// get index of last item starting with letter
if(a=$FFFF)then exit;// if no such items
e:=length(fnd);// get length of search-string
u:=1;// current length of overlap
m:=1;// max overlap
i:=a;// index to select
if(e>1)then for k:=a to l do begin
s:=fs.Strings[k];
if(length(s)<e)then continue;
for n:=2 to e do begin// compare strings char-by-char
if(s[n]<>fnd[n])then begin// compare failed
u:=n-1;
break;
end else u:=n;
if(u>m)then begin// current overlap is max
m:=u;
i:=k;
end;
end;
if(u=e)or(u<m)then break;// if end of search reached
end;
// select needed index:
fl.ClearSelection;
SendMessage(fl.Handle, LB_SELITEMRANGE, 1, MakeLParam(i, i));
fl.ItemIndex:=i;
inherited;
end;
//...
Yeah this code is kinda ugly, but it works fine, as i said - nearly instantly, i just can see how selection jumps thru items while i type, and i type pretty fast...
So it was code which i wrote yesterday and all this could end here, but today i realized that it was absolutely dumb decision at whole: in my case, as i mentioned above, i have listbox with OwnerDrawVariable style, so i have custom MeasureItem and DrawItem procedures and the best decision in this kind of situation will be to turn AutoComplete property true and to fill ListBox1 with items from ListBox2. And strings needed to display could be shown anyways in DrawItem procedure. Also possible to remove ListBox2 and keep strings for displaying inside tStringList variable. So morale is - dont rush into writing boilerplates and try to think more before acting =))
P.S. but one can use this code for some custom AutoComplete handling...

Related

How to get the values of TListView selected item in Firemonkey Delphi Rio

I have a TListView populated with data from TFDQuery using Livebindings.
I would like to get the values of the selected item like the item.text, itemheader.text, etc. I already figured out the selected item through listview1.itemindex but to get the values is a struggle to me. I am new with TListView and livebindings. I've spent over a day already looking for answers in the internet but looks too complicated for a very simple task. I know there is a straight method for this.
Anyone care to share some clues (codes) on how to get the values of listview selected item?
MORE DETAILS:
I am using the Livebindings dynamic appearance. I created items for my query fields and map them accordingly to my TListView.
It so happen that I did not use the default item.text but instead map fields to my created items like item.text1, item.text2, item.item3.
Hence, this is the reason why I am not getting the caption from the formula given by MartynA below.
Perhaps I am missing your point (in which case I'll delete this) but the following FMXcode works fine for me:
procedure TForm1.Button2Click(Sender: TObject);
var
Index : Integer;
begin
Index := ListView1.ItemIndex;
if Index >= 0 then
Caption := ListView1.Items[Index].Text;
// OR ShowMessage(ListView1.Items[Index].Text);
// OR Label1.Text := ListView1.Items[Index].Text;
end;

Tlistview - There is any component like Tlistview but with DB access?

I've been trying to make a creative thing to avoid the dbgrids, and i've found the Tlistview (using the one from alphaskins, tslistview), and seems to be a nice way!
The problem is, I don't want to code the event onclick on every tlistview to position a record/dataset according to the item I selected on the tlistview .. and I'm doing it with the tlistview item's caption.. and there could be records with the same names
Here is one of the codes I want to avoid:
with q_find_process do
begin
close;
sql.Clear;
sql.Add('Select * from t_process where process_name like '+quotedstr(streeview1.Selected.Text)+');
open;
end;
And no, I don't want to put the ID of the Record on the item caption..!
Any ideas?
Does anyone know other way of showing a lot of records without being only text text and more text? I don't know all components on the tool palette, maybe someone could suggest me other one..
I have sometimes used listviews which have been loaded from database tables - only for small amounts of data. I don't understand what you mean by I don't want to code the event onclick on every tlistview to position a record/dataset according to the item I selected on the tlistview, so I'm going to show you how I solved this problem.
Basically, I create a sub-item which holds the primary key of each record. All the user interface code uses two list views, and at the end, the database is updated. There is no interaction with the database between loading and storing (which might be where I avoid your 'onclick' problem). The widths of each fields are set in the Object Inspector; the final subitem's width is 0 (ie not displayed).
Loading the list view:
srclist.items.clear;
with qSrcList do
begin
close;
params[0].asdate:= dt; // use date of deposit
open;
while not eof do
begin
ListItem:= srclist.Items.Add;
ListItem.Caption:= fieldbyname ('kabnum').asstring;
ListItem.SubItems.Add (fieldbyname ('price').asstring);
ListItem.SubItems.Add (fieldbyname ('duedate').asstring);
ListItem.SubItems.Add (fieldbyname ('docket').asstring);
ListItem.SubItems.Add (fieldbyname ('id').asstring);
next
end;
close
end;
Saving data:
with dstlist do
for index:= 1 to items.count do
with qInsert do
begin
dstlist.itemindex:= index - 1;
lvitem:= dstlist.selected;
parambyname ('p1').asinteger:= deposit;
parambyname ('p2').asinteger:= strtoint (lvitem.SubItems[3]);
parambyname ('p3').asfloat:= strtofloat (lvitem.SubItems[0]);
execsql;
end;
I hope that this helps you. The context of this code (not that it matters too much) is in a financial application where the user wishes to populate a bank deposit form with cheques. SrcList holds the cheques which have yet to be deposited (there will only be a few per given date) and DstList holds the cheques which have already been connected to a given deposit form.

How to stop user highlighting and deleting text in an edit control?

I have an Edit Control on a DevExpress Ribbon of type TcxBarEditItem which I am recording the keypresses of to update a "floating" listbox of possible functionality to fire.
For some reason, the TcxBarEditItem and it's parent classes' event handler's do not work at all like Delphi's vanilla equivalents, meaning I have to record these keypresses.
My question however, is how to record/or prohibit, the user doing things like pasting in loads of text, or highlighting and deleting loads of text?
The way in which these controls seem to work means using String(TcxBarEditItem(control).EditValue) (which is how I would access the control as it is a member of a Command class - TS8RibbonCommand) isn't actually indicative of the text in the edit control until the user clicks out of it. I've tried doing loads of things like programmatically setting focus elsewhere and refocusing but nothing else seems to work bar recording the keypresses.
In the code snippet mirroredJumpStart is my copy of what the user is typing. The RefreshJumpStart function takes a string value and iterates over all the different string values in a list and populates a Listbox using AnsiContainsString.
procedure TS8RibbonJumpStartEdit.KeyPress(Sender: TObject; var Key: Char);
begin
if (Key in ['a'..'z']) or (Key in ['A'..'Z']) or (Key in ['0'..'9']) or (Key = ' ') then
manager.mirroredJumpStart := manager.mirroredJumpStart + Key
else if (Key = Chr(VK_BACK)) and (Length(manager.mirroredJumpStart) <> 0) then
Delete(manager.mirroredJumpStart, Length(manager.mirroredJumpStart), 1);
manager.RefreshJumpStart(manager.mirroredJumpStart);
end;
Any help would be great!
Assuming this is a standard TEdit control...
You can limit the amount of text by using MaxLength property
You can catch individual keystrokes by observing Key parameter in the event OnKeyDown
Just a couple little tips, not sure if it will help, because you don't say you're using the TEdit

Need a ComboBox with filtering

I need some type of ComboBox which can load it's items from DB. While I type some text in to it, it should filter it's list, leaving only those items, that have my text somewhere (at the beginning, middle...). Not all my DataSet's have filtering capabilities, so it is not possible to use them. Is there any ready to use components with such abilities? I have tried to search in JVCL, but without luck.
You could try customizing the autocomplete functionality of a regular ComboBox. Loading its items from a DB is easy:
ComboBox1.Items.Clear;
while not Table1.Eof do begin
ComboBox1.Items.AddObject( Table1.FieldByName('Company').AsString,
TObject(Table1.FieldByName('CustNo').AsInteger) );
Table1.Next;
end;
As far as the auto-complete for middle-of-word matching, you might try adapting this code. The functionality that matches at the beginning of the text in the Items is enabled by setting AutoComplete to true, and needs to be turned off before you try writing your own OnChange event handler that does auto-complete. I suggest that you could more safely do the match and selection on the enter key, because attempting to do it on the fly makes things quite hairy, as the code below will show you:
Here's my basic version: Use a regular combobox with onKeyDown, and onChange events, and AutoComplete set to false, use above code to populate it, and these two events
procedure TForm2.ComboBox1Change(Sender: TObject);
var
SearchStr,FullStr: string;
i,retVal,FoundIndex: integer;
ctrl:TComboBox;
begin
if fLastKey=VK_BACK then
exit;
// copy search pattern
ctrl := (Sender as TCombobox);
SearchStr := UpperCase(ctrl.Text);
FoundIndex := -1;
if SearchStr<>'' then
for i := 0 to ctrl.Items.Count-1 do begin
if Pos(SearchStr, UpperCase(ctrl.Items[i]))>0 then
begin
FoundIndex := i;
fsearchkeys := ctrl.Text;
break;
end;
end;
if (FoundIndex>=0) then
begin
retVal := ctrl.Perform(CB_SELECTSTRING, 0, LongInt(PChar(ctrl.Items[FoundIndex]))) ;
if retVal > CB_Err then
begin
ctrl.ItemIndex := retVal;
ctrl.SelStart := Pos(SearchStr,UpperCase(ctrl.Text))+Length(SearchStr)-1;
ctrl.SelLength := (Length(ctrl.Text) - Length(SearchStr));
end; // retVal > CB_Err
end; // lastKey <> VK_BACK
end;
procedure TForm2.ComboBox1KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
fLastKey := Key;
end;
Suppose the contents of the list are "David Smith", and "Mark Smithers". You type S and it matches the first letter of the last name, in David Smith. Now it shows David Smith with the "David S" part not selected, and the "mith" part selected (so that the next characters you type will replace the auto completed portion, a standard auto-complete technique). Note that the above code has had to prefix the S you typed with the "David " part you didn't type. If you are a lot more clever than me, you can find a way to remember that the user typed "s" and then, maybe an "m", followed by some more letters, and eventually having typed "Smithe", match Smithers, instead of always David smith. Also note that you can only set the SelStart and SelLength to select a continuous length of a string.
The code I have provided will only work when the list of items never contains any repeated substrings. There are good reasons why the Windows Common Control combobox "autocomplete" functionality only works with prefix matching, and not mid-string matching.
Since anything that would implement mid-string matching should probably draw the part you typed in not-selected, and since that not-selected part would be in mid-string, you would probably need to write your own control from scratch and not rely on the TComboBox base code, and its underlying MS Common Controls combobox functionality.
DevExpress' "TcxExtLookupCombobox" has this capability - and more. Might be overkill though.

Am I restricted to the cursor numbers defined in Controls.pas under Delphi 7?

I am using Delphi 7 under Windows 7 to download files.
I want to change the cursor during the download.
I set the Screen.Cursor := crHourGlass; , but , after looking at the constant cursor numbers in Controls.pas, I was wondering whether there are other numbers I can use to change the cursor into ( I don't want to add a cursor to my resource file, I just want to use standard numbers which I can use without having to add resources ).
does other numbers produce meaning
full cursors
No. Other numbers besides built-in cursors constants will produce a default cursor which is identical to TCursor(crDefault) (in other terms - HCURSOR(Screen.Cursors[crDefault])). These built-in cursors resides in the application resources and preloaded on VCL startup. To add custom cursor you HAVE to add CURSOR resource and then load it and add it to VCL.
procedure TForm1.FormCreate(Sender: TObject); platform;
const
crCustom = 42;
var
Cursor: HCURSOR;
begin
Cursor := LoadCursor(HInstance, 'CUSTOM');
Win32Check(Cursor <> 0); // required error check
Screen.Cursors[crCustom] := Cursor;
{ Done, newly added crCustom is ready to use }
Self.Cursor := crCustom; // for example - lets show custom cursor
{ also, TScreen object will manage resource handle }
{ and perform cleanup for you, so DestroyCursor call is unnecessary }
end;
More complicated example with indirect cursor construction NB: example have numerous flaws: 1) DestroyIcon call is erroneous 2) they'd noticed it if there was error checking after all API calls
The "standard numbers" (crHourglass, crDefault, etc) are the predefined cursors that are provided by Delphi's VCL. You can define your own and load them into the application from a resource or from file via the Windows API, but there are no magic unpublished TCursor definitions (or stray numbers) that will mean anything. Trying to set Screen.Cursors[] to an unknown number without first loading a cursor will cause an array out of bounds error at minimum, and an access violation at the worst result in the default cursor being displayed (see TScreen.GetCursors in Forms.pas).
Quick explanation: TCursorRec is defined in the VCL source as a record containing a pointer to the next record, an index, and a cursor handle (HCURSOR). It's basically a singly-linked list, and when you ask for a cursor by accessing the Cursors list, the VCL looks through the list starting at the first item and sequentially stepping through until it either finds an index that matches the one you requested (at which point it sets the cursor to that item's HCURSOR value), or determines that the index you requested isn't assigned, in which case it returns the default cursor.
crHourGlass is of type TCursor, which is an integer alias (more or less). It is an Index that can be used to set a cursor from stock.
You can add cursors using
Screen.Cursors[Number] := ... needs to be a HCURSOR.
So if you have a handle to a new cursor, you can use that in Delphi.
Note that the crXXX constants an the TCursor type are defined in Controls and the Screen class is defined in Forms. So you can see the code for yourself.

Resources