I need to add functionality to copy a rectangular selection of nodes and columns, but I can't find any way to actually select multiple columns in a Virtual Treeview (beside toFullRowSelect).
Am I just missing something? And if not, is there a descendant out there with grid-like multicolumn select support?
So after some testing I came up with the following, thanks DiGi for the extra push. DrawSelection won't work with this solution so it needs to be disabled. Since I don't think I'll need to do this again soon I didn't write a descendant.
Set toDisableDrawSelection, toExtendedFocus and toMultiSelect to True.
Declare the following variables/properties somewhere suitable:
StartSelectedColumn: integer;
FirstSelectedColumn: integer;
LastSelectedColumn: integer;
Selecting: boolean;
Update the following events:
OnKeyDown
if (not Selecting) and (Key = VK_SHIFT) then
begin
StartSelectedColumn := vtMain.FocusedColumn;
FirstSelectedColumn := StartSelectedColumn;
LastSelectedColumn := StartSelectedColumn;
Selecting := true;
end;
OnKeyUp
if Key = VK_SHIFT then
Selecting := false;
OnFocusChanged
if Selecting then
begin
if column < StartSelectedColumn then
begin
FirstSelectedColumn := column;
LastSelectedColumn := StartSelectedColumn;
end
else if column > StartSelectedColumn then
begin
FirstSelectedColumn := StartSelectedColumn;
LastSelectedColumn := column
end
else
begin
FirstSelectedColumn := column;
LastSelectedColumn := column;
end;
end
else
begin
StartSelectedColumn := column;
FirstSelectedColumn := column;
LastSelectedColumn := column;
end;
OnBeforeCellPaint
if vtMain.Selected[node] and InRange(column, FirstSelectedColumn, LastSelectedColumn) then
begin
if vtMain.Focused then
TargetCanvas.Brush.Color := vtMain.Colors.FocusedSelectionColor
else
TargetCanvas.Brush.Color := vtMain.Colors.UnfocusedSelectionColor;
TargetCanvas.Brush.Style := bsSolid;
TargetCanvas.FillRect(CellRect);
end;
OnPaintText
if vtMain.Selected[node] and InRange(column, FirstSelectedColumn, LastSelectedColumn) then
begin
if vtMain.Focused then
TargetCanvas.Font.Color := clHighlightText
else
TargetCanvas.Font.Color := vtMain.Font.Color;
end;
You can try enable/add toGridExtensions in TreeOptions.MiscOptions. It enables free moving in columns by cursor keys, but VT still deselect column on leaving. But I'm sure that is possible to "fix" it by custom draw and remembering starting node and column.
One more tip - look at OnStateChange event, maybe you can use
procedure TSomeForm.VTreeStateChange(Sender: TBaseVirtualTree; Enter,Leave: TVirtualTreeStates);
begin
if tsDrawSelecting in Enter then
begin
// Save position
end;
end;
Related
Using Delphi XE8 I'm currently testing functionality with Firemonkey TListViews.
One thing I'm trying to do is to load a field of all records from a TFDMemtable component into a Listview Item, specifically into the DetailObject of the ListView Item.
For example, I have 3 records in a table (db field is called 'Name'):
Record 1 = Name 1
Record 2 = Name 2
Record 3 = Name 3
There is only 1 DetailObject property per ListView Item so my question is, would I be able to add all of the fields (Name 1, Name 2, Name 3) into that one DetailObject?
Below is what I've attempted so far but no luck. Not 100% sure what I need to do.
procedure MainForm.BuildList;
var LItem : TListViewItem;
begin
ListView1.BeginUpdate;
try
ListView1.CLearItems;
LItem := ListView1.Items.Add;
LItem.Objects.DetailObject.Visible := True;
with memtable do
begin
while not eof do
begin
LItem.Detail := FieldByName('Name').AsString;
end;
end;
finally
ListView1.EndUpdate;
end;
end;
I'm sorry if this isn't clear enough, please let me know.
Any help would be great.
I think I should warn you that before seeing your q, I'd never done anything with FMX ListViews and Master/Detail datasets. The Following is a little rough around the edges, and the layout isn't ideal, but it shows one way to populate a ListView from Master + Detail datasets. I have no idea whether there are better ways. Personally, I would see if I could use Live Bindings to do the job.
procedure TMasterDetailForm.BuildList;
var
LItem : TListViewItem;
DetailItem : TListViewItem;
ListItemText : TListItemText;
DetailIndex : Integer;
begin
ListView1.BeginUpdate;
ListView1.ItemAppearanceObjects.ItemEditObjects.Text.TextVertAlign := TTextAlign.Leading; // The default
// seems to be `Center`, whereas we want the Master field name to be at the top of the item
try
ListView1.Items.Clear; //Items;
Master.First;
while not Master.eof do begin
LItem := ListView1.Items.Add;
LItem.Text := Master.FieldByName('Name').AsString;
LItem.Height := 25;
Detail.First;
DetailIndex := 0;
while not Detail.Eof do begin
Inc(DetailIndex);
ListItemText := TListItemText.Create(LItem);
ListItemText.PlaceOffset.X := 100;
ListItemText.PlaceOffset.Y := 25 * (DetailIndex - 1);
ListItemText.TextAlign := TTextAlign.Leading;
ListItemText.Name := 'Name' + IntToStr(DetailIndex); //Detail.FieldByName('Name').AsString;
LItem.Data['Name' + IntToStr(DetailIndex)] := Detail.FieldByName('Name').AsString;
Detail.Next;
end;
LItem.Height := LItem.Height * (1 + DetailIndex);
Master.Next;
end;
finally
ListView1.EndUpdate;
end;
end;
TListItemText is one of a number of "drawable" FMX objects that can be added to do the TListViewItem. They seem to need unique names so that they can be accessed via the Names property.
FWIW, I used 2 TClientDataSets as the Master and Detail in my code.
Also FWIW, for FMX newbies like me, populating an FMX TreeView is a lot more like what you'd do in a VCL project:
procedure TMasterDetailForm.BuildTree;
var
PNode,
ChildNode : TTreeViewItem;
begin
TreeView1.BeginUpdate;
try
TreeView1.Clear;
Master.First;
while not Master.eof do begin
PNode := TTreeViewItem.Create(TreeView1);
TreeView1.AddObject(PNode);
PNode.Text := Master.FieldByName('Name').AsString;
Detail.First;
while not Detail.Eof do begin
ChildNode := TTreeViewItem.Create(TreeView1);
ChildNode.Text := Detail.FieldByName('Name').AsString;
PNode.AddObject(ChildNode);
Detail.Next;
end;
Master.Next;
end;
finally
TreeView1.EndUpdate;
end;
end;
Btw, in your code you should have been calling
memtable.Next;
in your while not eof loop, and memtable.First immediately before the loop.
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.
i have a DBGrid and it is linked to client dataset when i assign a SQLQuery at run time
the DBGrid automatically assigns no of column. What i need is when DBGrid automatically assign columns i need to set one of those columns to assign a picklist.
can anyone help me?
the following procedure calls in the forms on show event. the form contains DataSource, ClientDataSet, SQLViewQuery (TSQLQuery), DatasetProvider and DBGridDetails (TDBGrid).
procedure TViewDetailsForm.ViewPendingAndReturnCheques;
var I : Integer;
slPickList:TStringList;
begin
slPickList := TStringList.Create;
slPickList.Add('Pending');
slPickList.Add('Returned');
slPickList.Add('Passed');
SQL := 'SELECT a.CHEQUE_NO, a.BANK, a.CHEQUE_DATE, a.AMOUNT,a.STATUS FROM CHEQUES a';
//refreshisng the DBGrid
SQLViewQuery.SQL.Clear;
SQLViewQuery.SQL.Add(SQL);
ClientDataSet.Active := false;
ClientDataSet.Active := true;
DBGridDetails.Columns[0].Width := _Block;
DBGridDetails.Columns[1].Width := _Block;
DBGridDetails.Columns[2].Width := _Block;
DBGridDetails.Columns[3].Width := _Block;
DBGridDetails.Columns[4].Width := _Block;
for I := 0 to DBGridDetails.Columns.Count - 1 do
begin
if DBGridDetails.Columns[I].FieldName = 'STATUS' then
begin
DBGridDetails.Columns[i].ButtonStyle := cbsAuto;
DBGridDetails.Columns[I].PickList := slPickList;
end;
end;
Show;
end;
Here's a sample app I just created in Delphi 2007 that demonstrates how to accomplish this. Here's all I did to set it up:
Click File->New-VCL Forms Application from the IDE's main menu.
Drop a TClientDataSet, a TDataSource, and a TDBGrid on the form.
Click on the form, and then use the Object Inspector to create a new OnCreate event handler. Add the following code:
procedure TForm1.FormCreate(Sender: TObject);
var
SL: TStringList;
begin
with ClientDataSet1 do
begin
FieldDefs.Clear;
FieldDefs.Add('OrderNo', ftInteger);
FieldDefs.Add('Status', ftString, 10);
CreateDataSet;
end;
ClientDataSet1.Active := True;
// Connect a datasource to the CDS
DataSource1.DataSet := ClientDataSet1;
// Connect the grid to that datasource to create the columns.
DBGrid1.DataSource := DataSource1;
// Create the picklist for the second column (Status)
SL := TStringList.Create;
try
SL.Add('Pending');
SL.Add('Returned');
SL.Add('Passed');
DBGrid1.Columns[1].ButtonStyle := cbsAuto;
DBGrid1.Columns[1].PickList := SL;
finally
SL.Free;
end;
end;
Run the application, click in the Status column in the grid, and you'll see the three choices added to the PickList above.
You can assign values to the dbgrid column picklist during the run time.
Below is the code:
procedure Tfrm1.FormShow(Sender: TObject);
var
slPickList:TStringList;
I: Integer;
begin
slPickList := TStringList.Create;
slPickList.Add('Pending');
slPickList.Add('Returned');
slPickList.Add('Passed');
for I := 0 to 2 do
begin
dbgViewAxiomClaims.Columns1.PickList.add(slPickList[i]);//assigning
end;
end;
Below is the result:
I want to delete multiple selected records which I have displayed at TDBAdvGrid. A number of records are being selected by checking checkbox in front of them. After clicking on Delete button it triggers procedure as follows. It is returning values of selected rows id successfully only problem with deletion of all records. It deletes only first selected record.
procedure TForm5.Button3Click(Sender: TObject);
var
i,j,idn: Integer;
State: Boolean;
begin
j := 0;
for i := 1 to DBAdvGrid1.RowCount - 1 do
begin
if DBAdvGrid1.GetCheckBoxState(1,i,state) then
begin
if state then
begin
idn := StrToInt(DBAdvGrid1.Cells[6,i]);
UniQuery1.SQL.Text := 'Delete from userplays where id = :id';
UniQuery1.ParamByName('id').AsInteger := idn;
UniQuery1.ExecSQL;
end;
end;
end;
end;
It is deleting only first record in lineup. After deleting first record it breaks for loop and control goes back to TDBAdvGrid with updated data after delete.
Final code based on suggestion is as follows
j := 0;
DBAdvGrid1.BeginUpdate;
for i := 1 to DBAdvGrid1.RowCount - 1 do
begin
showmessage('inside rowcount loop');
if DBAdvGrid1.GetCheckBoxState(1,i,state) then
begin
if state then
begin
DBAdvGrid1.DataSource.DataSet.First;
DBAdvGrid1.DataSource.DataSet.MoveBy(i - 1 - j);
DBAdvGrid1.DataSource.DataSet.Delete;
j := j+1;
continue;
end;
end;
end;
This code works perfectly only when PageMode := False
Based on the suggestion above, I would like to make another suggestion...
//DBAdvGrid1.BeginUpdate;
query.First;
for i := 1 to DBAdvGrid1.RowCount - 1 do
begin
if DBAdvGrid1.GetCheckBoxState(1,i,state) then
begin
if state then
begin
//you can do whatever you want with the current selected record
end;
end;
query.Next;
end;
I think this is easier then using the MoveBy...
I even never changed PageMode, it's still on true. No idea if it's coincidence or not.
You can use cyBookmarks in the OnCheck event , with Virtual Table.
It creates a list of all records selected, and you can list them with
cyBookmarks.bookmarklits.count, so my example is like this :
procedure TForm2.cyDBGrid1CheckBoxClick(Sender: TObject);
begin
if cyBookmarks1.AddCurrentRecord then
begin
cyBookmarks1.BookmarkList.InsertBookmark(cyDBGrid1.CheckedList.CurrentRecord);
cyDBGrid1.Invalidate ;
end
else
begin
cyBookmarks1.RemoveCurrentRecord;
cyDBGrid1.Invalidate;
end;
if cyBookmarks1.BookmarkList.Count>0 then
begin
VirtualTable1.GotoBookmark(cyBookmarks1.BookmarkList.Items[0]);
lbl1.Caption:=VirtualTable1.FieldByName('N_ENREGISTREMENT').AsString;
end;
end;
I want to know if it is possible in delphi to populate a combobox component from object Tcollection.
somme codes:
// My product list
procedure TfoMain.InitForm;
begin
FListProduct := TListeDispoProduit.Create(TProduct);
with (FListProduct ) do
begin
with TProduct(Add) do
begin
Name := 'Product 01';
CIP := 'A001';
StockQty := 3;
end;
with TProduct(Add) do
begin
Name := 'Product 02';
CIP := 'A002';
StockQty := 5;
end;
end;
// need to fill a combobox (name's cbxListProduct)
procedure TfoMain.fFillCbxFromProductList(aProductList: FListProduct);
begin
// I don't know how to do this follow
foMain.cbxListProduct.Items.Add()
end;
thank you.
Something like this (change combobox and collection names to reflect your case):
for i := 0 to Collection.Count-1 do
myComboBox.Items.Add(TProduct(Collection.Items[i]).Name);
And by the way, you don't need that "foMain" in
foMain.cbxListProduct.Items.Add()
It's enough to write
cbxListProduct.Items.Add()
When you're inside of TfoMain's procedure, TfoMain's contents is accessible by default.
In newer Delphis you can do
for item in collection do
myComboBox.Items.Add(TProduct(item).Name)