Delphi: Shift-Up and Shift-Down in the Listview - delphi

Is there a feature in the Listview control to shift items up and down?

Not having worked with TListView very much (I mostly use database grids), I took your question as a chance to learn something. The following code is the result, it is more visually oriented that David's answer. It has some limitations: it will only move the first selected item, and while it moves the item, the display for vsIcon and vsSmallIcon is strange after the move.
procedure TForm1.btnDownClick(Sender: TObject);
var
Index: integer;
temp : TListItem;
begin
// use a button that cannot get focus, such as TSpeedButton
if ListView1.Focused then
if ListView1.SelCount>0 then
begin
Index := ListView1.Selected.Index;
if Index<ListView1.Items.Count then
begin
temp := ListView1.Items.Insert(Index+2);
temp.Assign(ListView1.Items.Item[Index]);
ListView1.Items.Delete(Index);
// fix display so moved item is selected/focused
ListView1.Selected := temp;
ListView1.ItemFocused := temp;
end;
end;
end;
procedure TForm1.btnUpClick(Sender: TObject);
var
Index: integer;
temp : TListItem;
begin
// use a button that cannot get focus, such as TSpeedButton
if ListView1.Focused then
if ListView1.SelCount>0 then
begin
Index := ListView1.Selected.Index;
if Index>0 then
begin
temp := ListView1.Items.Insert(Index-1);
temp.Assign(ListView1.Items.Item[Index+1]);
ListView1.Items.Delete(Index+1);
// fix display so moved item is selected/focused
ListView1.Selected := temp;
ListView1.ItemFocused := temp;
end;
end;
end;

You have two options:
Delete them and then re-insert them at the new location.
Use a virtual list view and move them in your data structure.
My routine for doing the first of these options is like this:
procedure TBatchTaskList.MoveTasks(const Source: array of TListItem; Target: TListItem);
var
i, InsertIndex: Integer;
begin
Assert(IsMainThread);
BeginUpdate;
Try
//work out where to move them
if Assigned(Target) then begin
InsertIndex := FListItems.IndexOf(Target);
end else begin
InsertIndex := FListItems.Count;
end;
//create new items for each moved task
for i := 0 to high(Source) do begin
SetListItemValues(
FListItems.Insert(InsertIndex+i),
TBatchTask(Source[i].Data)
);
Source[i].Data := nil;//handover ownership to the new item
end;
//set selection and focus item to give feedback about the move
for i := 0 to high(Source) do begin
FListItems[InsertIndex+i].Selected := Source[i].Selected;
end;
FBatchList.ItemFocused := FListItems[InsertIndex];
//delete the duplicate source tasks
for i := 0 to high(Source) do begin
Source[i].Delete;
end;
Finally
EndUpdate;
End;
end;
The method SetListItemValues is used to populate the columns of the list view.
This is a perfect example of why virtual controls are so great.

Related

TMainMenu build menu item dynamic

On my system , the main menu is fully dynamic, is builded from data in the database.
I have a specific situation where some menu items need to be assembled before being displayed.
Let's assume that my menu has the following main items:
Files - Customer - Reports - About
When I click in Reports menu item must assemble the items before they are displayed.
I did an analysis of the code in TMainMenu, TMenu and TMenuItem class. Unfortunately I have not found a simple solution to the problem.
Exist a way to create these items before being displayed?
There is a trick you can use. You need to add one dummy TMenuItem under Reports and set its Visible property to False. Then add OnClick event to Reports item and do your populating logic there. Before you add new items you have to delete existing ones, but you should leave your dummy item intact.
Something like:
procedure TForm1.ReportItemClick(Sender: TObject);
var
Item: TMenuItem;
I: Integer;
begin
// Delete all items but first - dummy - one
for I := ReportItem.Count - 1 downto 1 do
ReportItem.Items[I].Free;
Item := TMenuItem.Create(ReportItem);
Item.Caption := 'abc';
// Item.OnClick := ...
// or
// Item.Action := ...
ReportItem.Add(Item);
Item := TMenuItem.Create(ReportItem);
Item.Caption := '123';
// Item.OnClick := ...
// or
// Item.Action := ...
ReportItem.Add(Item);
end;
Lets Suppose that you have an DS (TDataSet) how load all your data reports. And in the moment that you drop Reports Menu you can call UpdateMenuReport.
type
TMenuArray = array of TMenuItem;
var MyMenu: TMenuArray;
procedure TMain.MyClickPopupMenu(Sender: TObject);
begin
case TMenuItem(TMenuItem(Sender).Parent).Tag of
// do Something
end;
end;
procedure TMain.UpdateMenuReport;
procedure InitMyMenu(var AMenu: TMenuArray);
var i: Integer;
begin
if Length(AMenu)>0 then for i:= 0 to Length(AMenu)-1 do AMenu[i].Free;
SetLength(AMenu, 0);
end;
var j, i: integer;
begin
InitMyMenu(MyMenu);
Reports.Clear;
if DS.Active and (DS.RecordCount > 0) then
begin
SetLength(MyMenu, DS.RecordCount);
for i:= 0 to DS.RecordCount-1 do
begin
MyMenu[i] := TMenuItem.Create(Self);
MyMenu[i].Name := 'Reports_'+ IntToStr(i);
MyMenu[i].Caption := DS.FieldByname('NOM_REPORT').AsString;
MyMenu[i].Tag := DS.FieldByname('ID').AsInteger;
MyMenu[i].OnClick := MyClickPopupMenu;
end;
end;
end;

Updating field in cxGrid acting strange

I have a function to update a cxGrid made with help from answers to Loop through records on a cxgrid and update a field/column
But it is sometimes acting a bit strange. If I open the form with the cxGrid and click the columnheader without doing anything else, the records are updateted OK. But if the 'selectorbar' is moved away from the top, the record marked is not updated.
I am sure it is a property that needs to be changed, but which one.
The variable fSelected is set to False at FormShow and is ther so that the user can unselect records as well.
procedure TfrmContactsSelect.colContactSelectedHeaderClick(Sender: TObject);
var
i: Integer;
Index: Integer;
BookMark : TBookMark;
Contact: variant;
begin
if fMulti = True then
begin
Screen.Cursor := crHourGlass;
fSelected := not fSelected;
BookMark := qryContacts.GetBookmark;
qryContacts.DisableControls;
try
for i := 0 to grdContactsView1.DataController.FilteredRecordCount - 1 do
begin
Index := grdContactsView1.DataController.FilteredRecordIndex[i];
Contact := grdContactsView1.DataController.Values[Index, 4];
if grdContactsView1.DataController.LocateByKey(Contact) then
begin
qryContacts.Edit;
qryContacts.FieldByName('fldcontact_selected').AsBoolean := fSelected;
qryContacts.Post;
end;
end;
finally
qryContacts.EnableControls;
qryContacts.GotoBookmark(BookMark);
qryContacts.FreeBookmark(BookMark);
end;
Screen.Cursor := crDefault;
end;
end;
Delphi XE7, DevExpress 14.2.2, UniDAC 5.5.12 for DB access
Comment:
I have ended up with the following solution based on the answer and input from MartynA
procedure TfrmContactsSelect.colContactSelectedHeaderClick(Sender: TObject);
var
i: Integer;
Index: Integer;
MarkedRecord: variant;
CurrentRecord: variant;
begin
if fMulti = True then
begin
Screen.Cursor := crHourGlass;
fSelected := not fSelected;
Index := grdContactsView1.DataController.FocusedRecordIndex;
MarkedRecord := grdContactsView1.DataController.Values[Index, colContactGuid.ID];
try
for i := 0 to grdContactsView1.DataController.FilteredRecordCount - 1 do
begin
Index := grdContactsView1.DataController.FilteredRecordIndex[i];
CurrentRecord := grdContactsView1.DataController.Values[Index, colContactGuid.ID];
if grdContactsView1.DataController.LocateByKey(CurrentRecord) then
begin
grdContactsView1.DataController.Edit;
grdContactsView1.DataController.SetEditValue(colContactSelected.ID, fSelected, evsText);
grdContactsView1.DataController.Post;
end;
end;
finally
grdContactsView1.DataController.LocateByKey(MarkedRecord);
end;
Screen.Cursor := crDefault;
end;
end;
I can reproduce your problem using the sample project I posted in my answer to your other q.
Try this: Add a TMemo to your form, and inside the 'if grdContactsView1.DataController.LocateByKey(Contact) then' block, write the value of a row-unique datafield and the Selected datafield value to the memo.
Then, what I get when some row other than the top row is selected is that one row is listed twice in the memo, with Selected both false and true, and one of the rows in the filter isn't listed at all, which I think accounts for the behaviour you're seeing. If I then comment out the .Edit .. .Post lines, it correctly lists all the rows in the filter.
So evidently doing the Selected field changes inside a block which iterated the FilteredRecordIndex property of the DBTableView is what's causing the problem.
Personally, I find that it goes a bit against the grain to modify dataset rows in code via a DB-aware control (because you usually end up fighting the DB-awareness of the control), but in this case, it's straightforward to do the processing via the DBTableView of the cxGrid.
procedure TForm1.ProcessFilteredRecords;
var
PrevV,
V : Variant;
i,
Index: Integer;
S : String;
begin
// First, pick up a reference to the current record
// so that we can return to it afterwards
Index := cxGrid1DBTableView1.DataController.FocusedRecordIndex;
PrevV := cxGrid1DBTableView1.DataController.Values[Index, 0];
try
for i := 0 to cxGrid1DBTableView1.DataController.FilteredRecordCount - 1 do begin
Index := cxGrid1DBTableView1.DataController.FilteredRecordIndex[i];
V := cxGrid1DBTableView1.DataController.Values[Index, 0];
if cxGrid1DBTableView1.DataController.LocateByKey(V) then begin
cxGrid1DBTableView1.DataController.Edit;
// 2 is the index of my Selected column in the grid
if cxGrid1DBTableView1.DataController.SetEditValue(2, True, evsText) then
Caption := 'OK'
else
Caption := 'Failed';
cxGrid1DBTableView1.DataController.Post;
end;
end;
finally
if cxGrid1DBTableView1.DataController.LocateByKey(PrevV) then
Caption := 'OK'
else
Caption := 'Failed';
end;
end;
Another way to avoid the problem is to change the Selected states in two steps:
Iterate the FilteredRecordIndex to build a list of rows to change - in your case this would be a list of guids
Then, iterate the list of rows and update their Selected states.
Code:
procedure TForm1.ProcessFilteredRecords;
var
V : Variant;
i,
Index: Integer;
BM : TBookMark;
S : String;
TL : TStringList;
begin
Memo1.Lines.Clear;
TL := TStringList.Create;
try
for i := 0 to cxGrid1DBTableView1.DataController.FilteredRecordCount - 1 do begin
Index := cxGrid1DBTableView1.DataController.FilteredRecordIndex[i];
V := cxGrid1DBTableView1.DataController.Values[Index, 0];
if cxGrid1DBTableView1.DataController.LocateByKey(V) then begin
if CDS1.FieldByName('Selected').AsBoolean then
S := 'True'
else
S := 'False';
S := CDS1.FieldByName('Name').AsString + ' ' + S;
Memo1.Lines.Add(S);
TL.Add(CDS1.FieldByName('Guid').AsString);
end;
end;
try
BM := CDS1.GetBookMark;
CDS1.DisableControls;
for i := 0 to TL.Count - 1 do begin
if CDS1.Locate('guid', TL[i], []) then begin
CDS1.Edit;
CDS1.FieldByName('Selected').AsBoolean := True;
CDS1.Post;
end
end;
finally
CDS1.EnableControls;
CDS1.GotoBookmark(BM);
CDS1.FreeBookmark(BM);
end;
finally
TL.Free;
end;
end;
Like you, I was expecting that changing a property or two of the cxGrid might avoid the problem without any code, but I haven't been able to find anything which does.

How to do incremental search in delphi FM2?

Hi i am having a problem with incremental search in delphi.
I Have looked at this http://delphi.about.com/od/vclusing/a/lb_incremental.htm
But this doesn't work in firemonkey so i came up with this :
for I := 0 to lstbxMapList.Items.Count-1 do
begin
if lstbxMapList.Items[i] = edtSearch.Text then
begin
lstbxMapList.ItemByIndex(i).Visible := True;
end;
if lstbxMapList.Items[I] <> edtSearch.Text then
begin
lstbxMapList.ItemByIndex(i).Visible := False;
end;
end;
When i use this the listbox is just blank.
You're hiding every item that doesn't exactly match edtSearch.Text. Try this instead (tested in XE3):
// Add StrUtils to your uses clause for `StartsText`
uses
StrUtils;
procedure TForm1.edtSearchChange(Sender: TObject);
var
i: Integer;
NewIndex: Integer;
begin
NewIndex := -1;
for i := 0 to lstBxMapList.Items.Count - 1 do
if StartsText(Edit1.Text, lstBxMapList.Items[i]) then
begin
NewIndex := i;
Break;
end;
// Set to matching index if found, or -1 if not
lstBxMapList.ItemIndex := NewIndex;
end;
Following from Kens answer, if you want to hide items as per your question, just set the Visible property but note that since the expression of an if statement returns a boolean and Visible is a boolean property it's possible to greatly simplify things. Note also that I've also used ContainsText which will match the string anywhere in the item text:
procedure TForm1.edtSearchChange(Sender: TObject);
var
Item: TListBoxItem;
begin
for Item in lstbxMapList.ListItems do
Item.Visible := ContainsText(Item.Text.ToLower, Edit1.Text.ToLower);
end;

How to use Listview correctly in delphi?

My code is the below, it's working correctly but, but after compiling program i see all the fullname and country listed vertically something like :
_________________________________
Fullname1
Country1
Fullname2
Country2
Fullname3
Country3
etc...
SQLQuery1.SQL.Text := 'SELECT * FROM users where user_age="'+age+'"';
SQLQuery1.Open;
rec := SQLQuery1.RecordCount;
SQLQuery1.First; // move to the first record
ListView1.Visible := false;
if rec>0 then
begin
while(not SQLQuery1.EOF)do begin
ListView1.Visible := true;
// do something with the current item
ListView1.AddItem('Full name: '+SQLQuery1['fullname'], Self);
ListView1.AddItem('Country: '+SQLQuery1['cntry'], Self);
// move to the next record
SQLQuery1.Next;
end;
But i want something Like :
First: add the column headers:
var
Col: TListColumn;
begin
Col := ListView1.Columns.Add;
Col.Caption := 'Name';
Col.Alignment := taLeftJustify;
Col.Width := 140;
Col := ListView1.Columns.Add;
Col.Caption := 'Country';
Col.Alignment := taLeftJustify;
Col.Width := 140;
end;
then add the records as follows:
var
Itm: TListItem;
begin
// start of your query loop
Itm := ListView1.Items.Add;
Itm.Caption := SQLQuery1['fullname'];
Itm.SubItems.Add(SQLQuery1['cntry']);
// end of your query loop
end;
Update:
Of course, in order to get the list as in your screenshot, you need to set the ListView's ViewStyle property to vsReport
Your code should look like that:
var
ListItem: TListItem;
...
ListView.Items.BeginUpdate;
try
while(not SQLQuery1.EOF)do begin
ListItem:= ListView.Items.Add;
ListItem.Caption:= 'Full name: '+SQLQuery1['fullname'];
with ListItem.SubItems do begin
Add('Country: '+SQLQuery1['cntry']);
// if you need more columns, add here
end;
SQLQuery1.Next;
end;
finally
ListView.Items.EndUpdate;
end;
You should also set ListView.Style to vsReport to show listview as grid.
I'm not sure how to get the listview to multiline, but I do know you're not using the Query correctly.
As it stands your code has an SQL-injection hole and the implicit reference to 'fieldbyname' inside the loop makes it slow.
var
FullName: TField;
Country: TField;
ListItem: TListItem;
begin
//Use Params or suffer SQL-injections
SQLQuery1.SQL.Text := 'SELECT * FROM users where user_age= :age';
SQLQuery1.ParamByName('age').AsInteger:= age;
SQLQuery1.Open;
if SQLQuery1.RecordCount = 0 then Exit;
//Never use `FieldByName` inside a loop, it's slow.
FullName:= SQLQuery1.FieldByName('fullname');
Country:= SQLQuery1.FieldByName('cntry');
ListView1.Style:= vsReport;
SQLQuery1.First; // move to the first record
SQLQuery1.DisableControls; //Disable UI updating until where done.
try
ListView1.Items.BeginUpdate;
//ListView1.Visible := false;
while (not SQLQuery1.EOF) do begin
//Code borrowed from #Serg
ListItem:= ListView.Items.Add;
ListItem.Caption:= 'Full name: '+Fullname.AsString;
ListItem.SubItems.Add('Country: '+Country.AsString);
SQLQuery1.Next;
end; {while}
finally
SQLQuery1.EnableControls;
ListView1.Items.EndUpdate;
end;
end;
The Delphi documentation contains this example that does exactly what you want.
procedure TForm1.FormCreate(Sender: TObject);
const
Names: array[0..5, 0..1] of string = (
('Rubble', 'Barney'),
('Michael', 'Johnson'),
('Bunny', 'Bugs'),
('Silver', 'HiHo'),
('Simpson', 'Bart'),
('Squirrel', 'Rocky')
);
var
I: Integer;
NewColumn: TListColumn;
ListItem: TListItem;
ListView: TListView;
begin
ListView := TListView.Create(Self);
with ListView do
begin
Parent := Self;
Align := alClient;
ViewStyle := vsReport;
NewColumn := Columns.Add;
NewColumn.Caption := 'Last';
NewColumn := Columns.Add;
NewColumn.Caption := 'First';
for I := Low(Names) to High(Names) do
begin
ListItem := Items.Add;
ListItem.Caption := Names[I][0];
ListItem.SubItems.Add(Names[I][2]);
end;
end;
end;
For all that the Delphi documentation is much maligned, it often has very useful examples like this. The gateway page to the examples is here and the examples are even available on sourceforge so you can check them out using your favourite svn client.
Procedure TForm1.GetUsers;
var
ListItem: TListItem;
begin
try
ListView1.Items.BeginUpdate;
try
ListView1.Clear;
MySQLQuery.SQL.Clear;
MySQLQuery.SQL.Add('select * from users;');
MySQLQuery.Open;
while (not MySQLQuery.EOF) do
begin
ListItem := ListView1.Items.Add;
ListItem.Caption:= VarToSTr(MySQLQuery['username']);
with ListItem.SubItems do
begin
Add(VarToSTr(MySQLQuery['password']));
Add(VarToSTr(MySQLQuery['maxscore']));
end;
MySQLQuery.Next;
end;
MySQLQuery.Close;
finally
ListView1.Items.EndUpdate;
end;
except
on E: Exception do
MessageDlg(PWideChar(E.Message), TMsgDlgType.mtError, [TMsgDlgBtn.mbOK], 0);
end;
end;

to retrieve stringlist items in memo from table POS_CatBreakDownValue into tchecklistbox

i have a problem on how to retrieve the items in memo from table into tchecklistbox...before this i do the procedure to insert the items that have been checked into memo in tbl POS_CatBreakDownValue..but now i want to know how to retrieve items from table and automatic check in tchecklistbox...thanks..here my procedure to insert the checklist into memo in table..i hope anyone can help me..thanks
procedure TfrmSysConfig.saveCatBreakDownValue;
var
lstCat:TStringList;
i :integer;
begin
lstcat := TStringList.Create;
try
for i:=0 to clCat.Items.Count-1 do begin
if clCat.Checked[i] then begin
lstcat.Add(clcat.Items.Strings[i]);
end;
end;
tblMainPOS_CatBreakDownValue.Value := lstCat.Text;
finally
lstcat.Free;
end;
end;
procedure TfrmSysConfig.saveCatBreakDownValue;
var
lstCat: TStringList;
i:integer;
begin
lstcat := TStringList.Create;
try
for i:=0 to clCat.Items.Count-1 do begin
if clCat.Checked[i] then begin
lstcat.Add(clcat.Items.Strings[i]);
end;
end;
tblMainPOS_CatBreakDownValue.Value := lstCat.Text;
finally
lstcat.Free;
end;
end;
Reading your code I'm guessing you've got a MemoField in a database that you are reading and writing the checked values from/to. You also have a predefined list of checkable items.
So you'll need to create a new string list and read the field back into it (Revesing the writing code). for each item in the list get the Index and check it.
Something like..
procedure TfrmSysConfig.saveCatBreakDownValue;
var
lstCat: TStringList;
i, Index:integer;
begin
lstcat := TStringList.Create;
try
lstcat.Text = tblMainPOS_CatBreakDownValue.Value;
for i:=0 to lstcat.Count-1 do
begin
Index := clCat.Items.IndexOf(lstcat.Items[i])
if Index > -1 then
begin
clCat.Checked[Index] := True;
end;
end;
finally
lstcat.Free;
end;
end;
Not sure I understand your question 100%
TCheckListBox can check or uncheck items using the Checked Property.
CheckListBox.Checked[Index] := True/False;
Since you sound like your comparing strings you may need to determine the index in the TCheckListBox based on the string this can be done like this:
CheckListBox1.Items.IndexOf('StringToFind')
If the string is not found then the result is -1;
If you want to check lines in a TMemo Control and see if they exists as rows in a table you can do the following.
While not Table.EOF do
begin
if Memo1.lines.IndexOf(Table.FieldByName('MyField').AsString) = -1 then
begin
// What you want to do if not found
end
else
begin
// what you want to do if it is found.
end;
Table.Next;
end;

Resources