Using SelectedRows.Count I can get a count of the number of grid rows selected. For example, if the user selected 3 rows, I can put a button on the form and clicking that can show me the number selected. Fine.
BUT how could I update the number of rows as the user selects them ("on the fly"). I have tried many of the Grid events, like OnColEnter or OnMouseDown. They seem to update the count only when the user clicks just outside the data columns, and not when a row is first selected.
Not seeing events related to changing ROWS in the Grid component, I tried many events in the underlying data query, but they too were inconsistent or often required clicking in certain places. The best result I found (actual code) was after scrolling the query:
procedure TDataHerd10.QuCowsAfterScroll(DataSet: TDataSet);
begin
if MenuOpt = 'UpdtInd' then MainView.NumSelEdit.Text:=
IntToStr(MainView.CowSelGrid.SelectedRows.Count);
end;
This event seems to lag one behind, and adds one more to the count initially when the user abandons the multiselect to go back to a single row.
Seems like with the right event, I should be able to count the selected rows to report to the user as they select/unselect rows?
Update: I found it trickier than I was expecting to modify my original
answer to reliably meet your requirement to have the select count displayed
when the form first shows.
Below are the essentials of the testbed project which I hope reliably behaves
as you asked for. In addition to the DBGrid, the form has a TEdit, which I use
to ensure that the dbgrid is not initially focused (so as to make it easier to observe the dbgrid's behaviour) and 3 TButtons whose
functions should be self-evident from their OnClick handlers.
You'll notice that the code that catches the changing count of the
dbgrid's selection count is only triggered in the dbgrid's OnDrawColumnCell
event. However, this is called rather too frequently (in my case over 700
times before the form is first displayed) to be doing something else
in the gui every time it is triggered. So instead, the form has a variable
which keeps track of the selection count and only updates the display of it
when the count changes (in the SetSelectedCount setter).
type
TForm1 = class(TForm)
[...]
private
FSelectedCount: Integer;
procedure SetSelectedCount(const Value: Integer);
public
procedure ShowSelectedCount;
property SelectedCount : Integer read FSelectedCount write SetSelectedCount;
end;
[...]
procedure TForm1.btnClearSelectedClick(Sender: TObject);
begin
DBGrid1.SelectedRows.Clear;
end;
procedure TForm1.btnGetSelectedClick(Sender: TObject);
begin
ShowSelectedCount;
end;
procedure TForm1.btnSetSelectedClick(Sender: TObject);
begin
DBGrid1.SelectedRows.CurrentRowSelected := True;
end;
procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;
DataCol: Integer; Column: TColumn; State: TGridDrawState);
begin
SelectedCount := DBGrid1.SelectedRows.Count;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
ActiveControl := Edit1; // so the grid does not have focus when the form is first shown
SelectedCount := -1;
end;
procedure TForm1.SetSelectedCount(const Value: Integer);
begin
if FSelectedCount <> Value then begin
FSelectedCount := Value;
ShowSelectedCount;
end;
end;
procedure TForm1.ShowSelectedCount;
begin
Caption := IntToStr(DBGrid1.SelectedRows.Count);
end;
Original answer follows
I usually use DataSet.AfterScroll for doing non-gui things which need to be synchronised with its current row. Unfortunately, it doesn't work so well with a DBGrid, as you've obviously found, not least because the current row's selection state in the grid can be changed (e.g. by clicking it) without the dataset scrolling.
Unfortunately,
procedure TForm1.DBGrid1CellClick(Column: TColumn);
begin
Caption := IntToStr(DBGrid1.SelectedRows.Count);
end;
doesn't quite do the job, either, for the fairly obvious reason that you can extend a selection from the current row without using the mouse - e.g. Shift + Down will do it, too.
However, if you just add
procedure TForm1.DBGrid1KeyUp(Sender: TObject; var Key: Word; Shift:
TShiftState);
begin
Caption := IntToStr(DBGrid1.SelectedRows.Count);
end;
that takes use of the keyboard to change the selection(s) into account and has so far resisted my attempts to wrong-foot it. If you are allowing the user to do in-place editing in the grid, you might want to filter the Key values which are used to update your display of the selection count.
Btw, taking the keyboard wrinkleas well as the problem with AfterScroll into account, your q doesn't seem to deserve (to me at any rate) the downvote it's got , so I've given it a +1.
Adding a little more to the excellent answer from #Martyn...
In order to update the displayed count automatically ("on the fly"), I found the suggestion to use Grid1.KeyUp to update the count very good, but also added the count update on a couple other events. Most critical was Grid1.MouseUp. Without that, the user could click on a new row, outside the currently selected rows and lose all the row selections, BUT the displayed count would remain rather than go back to zero.
Related
which run first, TDataSet OnFilterRecord or OnCalcFields event? Are records set to Accept = false are still visible in OnCalcFields event? if it is, is there a property to check record visibility?
the code situation is like, when dataset has more records like 3k, OnRecordFilter has manual filter on string fields for records visibility on grid (Accept = true / false), OnCalckFields has extra columns lookup to other datasets,
the function that sum the amount columns is so slow with or without filter.
when i disable the OnCalcFields event, the execution was so fast.
DataSet is TFDQuery, loaded initial data is free date range so user can view like 3 or more year date range.
ui looks like this
https://i.stack.imgur.com/0XyrN.png
You can test this for yourself.
Create a new VCL project and add a TFDMemTable, TDataSource, TDBGrid, TCheckBox (called cbUseFilterExpr) and TButton
to the form.
Connect up the FDMemTable, TDataSource and TDBGrid as you normally would.
Add an OnFilterCalls integer form field and event handlers shown below.
Compile and run.
Code
procedure TForm1.FormCreate(Sender: TObject);
var
AField : TField;
i : Integer;
begin
AField := TIntegerField.Create(Self);
AField.FieldName := 'ID';
AField.DataSet := FDMemTable1;
AField := TStringField.Create(Self);
AField.FieldName := 'Name';
AField.DataSet := FDMemTable1;
FDMemTable1.CreateDataSet;
for i := 1 to 100 do
FDMemTable1.InsertRecord([1, 'Name' + IntToStr(i)]);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
OnFilterCalls := 0;
FDMemTable1.Filtered := False;
if cbUseFilterExpr.Checked then
FDMemTable1.Filter := 'Name= ''Name1'''
else
FDMemTable1.Filter := '';
FDMemTable1.Filtered := True;
ShowMessage('OnFilterCalls ' + IntToStr(OnFilterCalls));
end;
procedure TForm1.FDMemTable1FilterRecord(DataSet: TDataSet;
var Accept: Boolean);
begin
Inc(OnFilterCalls);
end;
The app populates FDMemTable1 with 100 records. The OnFilterCalls variable will
count the number of times OnFilterRecord is called when filtering is activated.
Clicking Button1 sets a filter on FDMemTable1 which differs depending on whether
cbUseFilterExpr is checked or not: If it is, filtering uses a filter expression which only
matches record ID=1. The result displayed by ShowMessage is 1, iow, the OnFilterRecord
event is called only once. If cbUseFilterExpr is not checked ShowMessage displays the value 100.
Conclusion: For FDMemTable (and, I confidently predict, other FireDAC dataset types) the OnFilterRecord
event is called once for each record which matches the FDMemTable's Filter expression, if any,
or once for each record in the dataset if the Filter expression is blank. Iow, OnFilterRecord
is only called for records which match the Filter expression, if there is one, so it behaves as if OnFilterRecord
is called "after" filtering via the Filter expression, so the answer to your q in FireDAC's case is "No", expression-filtered records are not visible in the OnFilterRecord event.
As mentioned in a comment, TDataSet does not define how a dataset processes filtering, rather it is
implementation-specific and may differ between different dataset component libraries.
Update You still haven't provided any details of what exactly you are doing in your code
(and on reflection your q should probably have been closed for lacking debugging details), but
I think you can satisfy yourself that what I have said above also applies to your situation. Simply put a debugger breakpoint
on the end in TForm1.FDMemTable1FilterRecord(DataSet: TDataSet. Run the app and check the cbUseFilterExpr checkbox. When the bp triggers, repeatedly single-step the debugger by pressing
F8 until you land in the unit FireDAC.DatS, in the method TFDDatSView.Rebuild. You will see that your are in a for loop,
for i := iBegin to iEnd do begin
...
This is the loop which is executed when the filtering is applied to the dataset, once for each record in the dataset, and from the for-loop's contents
it will be a straightforward matter to satify yourself that the OnFilterRecord event is only called
for any record which is visible because it satifies any filter expression which is in effect.
Here's workaround i did to this OnCalcEvent difficulty while no exact answer to my query.
solution:
a way to skip OnCalcEvent when traversing dataset with huge initial records loaded
create a public variable
set value to public variable before dataset iteration
in OnCalcEvent, check the value to exit to abort the event code execution
after dataset iteration. re-set the dataset bookmark to force run the OnCalcFields event else calcFields column will be empty
it works for me, for now.
another idea is to create pagination (paging to limit query return similar to web apps) instead.
I have a two objects in a form: 1 listbox and 1 memo. I'm trying to delete a item in the listbox1 and the same line index in the memo using this code:
procedure TForm1.ListBox1KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
var i:integer; //seting a variable
begin
if key=vk_delete then //if key = a delete
begin
for i:=0 to listbox1.items.count -1
begin
listbox1.DeleteSelected; //delete the selected line of the listbox
memo2.Lines.Delete(i); //delete the line based on the listbox selected item
end;
end;
end;
But it only work if i add one line to listbox. If i add two lines in the listbox and try to delete a item 2, the memo1 delete the line 1; and if i add more items into listbox and try delete, various lines are deleted in the memo1. I think this is due to the fact that the memo begins index in 0 and listbox begins in 1.. However I can not solve this problem. Can anyone help me delete in both objects, only the row that I select in the object listbox?
The problem is simply that you are deleting multiple lines from the memo. That's because, for some reason, you coded a loop which deleted in each and every iteration of the loop. You don't want to do that. You only want to delete a single line.
You need to use code along these lines:
var
Index: Integer;
....
Assert(ListBox1.Items.Count=Memo2.Lines.Count);
Index := ListBox1.ItemIndex;
if Index<>-1 then
begin
ListBox1.Items.Delete(Index);
Memo2.Lines.Delete(Index);
end;
I have replaced your code which looped over the list box items and deleted multiple items from the list box, and multiple rows from the memo. Instead I obtain the index of the selected item in the list box, and make a single deletion from the list box and remove a single line from the memo.
Your code makes absolutely no sense. It doesn't even come close to doing what I believe you want to do, which is this:
Create a new VCL project. Add a TListBox and a TMemo control. Add the same lines to them in the IDE (e.g., alpha, beta, gamma, delta, and epsilon).
Then add the following event handler:
procedure TForm1.ListBox1KeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if (Key = VK_DELETE) and (ListBox1.ItemIndex <> -1) then
begin
Memo1.Lines.Delete(ListBox1.ItemIndex);
ListBox1.DeleteSelected;
end;
end;
So i have a TDBGrid, my purpose is searching DBGrid's Fieldname and comparing it with my Edit's Text property and if they are equal then,
i want to write the whole column which i've found the match, to a ListBox.
With a for loop with fieldcount, i can compare FieldName, though since there is no rows or rowcount property i can use, i don't know how i would get the index of this whole column.
for i:=0 to DBGrid1.FieldCount-1 do
begin
if DBGrid1.Fields[i].FieldName=Edit1.Text then
for j:=1 to DBGrid1.RowCount-1 do
ListBox1.Items.Add(DBGrid1.Rows.Fields[i].Index.AsString);
end;
This is an imaginary code of what im trying to do...
P.S.:I'm still using Delphi 7, (educational reasons)
You can't get the row values directly from the DbGrid. Instead, you have to navigate through the dataset that's used to feed the DbGrid.
This example assumes you are using a TClientDataSet.
for i := 0 to DBGrid1.FieldCount - 1 do
begin
if DBGrid1.Fields[i].FieldName = Edit1.Text then
begin
ClientDataSet1.DisableControls;
try
ClientDataSet1.First();
while (not ClientDataSet1.Eof) do
begin
ListBox1.Items.Add(ClientDataSet1.FieldByName(Edit1.Text).AsString);
ClientDataSet1.Next();
end;
finally
ClientDataSet1.EnableControls;
end;
end;
end;
As far as DBGrid only displays an excerpt of the data, IMHO you should
get a bookmark of your dataset
disable Controls
use first, while not eof with your dataset, adding
Dataset.FieldbyName(Edit1.text).asString to your list
goto bookmark
enable controls
I think I found a potential bug in TListView.
Steps to reproduce:
Create a new VCL Forms application, add a TListView, set it`s ViewStyle to vsReports.
Add two buttons
button1:
procedure TForm1.Button1Click(Sender: TObject);
var
lCol: TListColumn;
begin
lcol := ListView1.Columns.Add;
lcol.Caption := 'name';
lcol := ListView1.Columns.Add;
lcol.Caption := 'name2';
lcol := ListView1.Columns.Add;
lcol.Caption := 'name3';
end;
button2:
procedure TForm1.Button2Click(Sender: TObject);
begin
ListView1.Columns.Delete(1);
end;
Result:
The column is deleted, but the caption of the last column gets lost. This also happens, when adding more columns and deleting a column that is between others (or deleting the first column). The caption of the last column is always empty.
I'm using XE3. Is there anything I missed?
Thanks
edit:
QC link
potential duplicate
There's more to this then what's reported in the question (more at the end).
This is related with a previous question of yours. That question involved the listview control losing the mapping between columns and items/subitems when you moved a column after adding a column. I proposed a possible fix for comctrls.pas which involved preserving FOrderTags of columns when they are moved. The VCL had 'FOrderTag's built from the ground-up whenever a column is moved - disregarding any current positioning of the columns.
What happens then is, you file a bug report, submit the possible fix as a workaround, and it gets checked-in exactly as is. The problem now you discover is, when we preserve FOrderTag of each column, and then remove a column from the middle, we create a hole - they are not sequential any more (say we have columns 0, 1 and 2 with respective order tags, remove column 1 and now we have 2 columns with order tags 0 and 2). Apparently the native control does not like this.
Again modifying the VCL, we can remove any possible hole when we are removing a column. The below seems to take care of the missing caption and the AV when you resize/move the column with the missing caption mentioned in a comment to the question.
destructor TListColumn.Destroy;
var
Columns: TListColumns;
i: Integer; //+
begin
Columns := TListColumns(Collection);
if TListColumns(Collection).Owner.HandleAllocated then
ListView_DeleteColumn(TListColumns(Collection).Owner.Handle, Index);
//{+
for i := 0 to Columns.Count - 1 do
if Columns[i].FOrderTag > FOrderTag then
Dec(Columns[i].FOrderTag);
//}
inherited Destroy;
Columns.UpdateCols;
end;
Now if we come back to what's not reported in the question, if you had been inserted some subitems, you'd have noticed that they are preserving their positions, IOW the mapping between columns and subitems are lost. There's the probability that your view is different then mine on this, but I think the subitems of the deleted column should get lost. Unfortunately I couldn't figure out a way to achieve this.
edit: I cannot think of anything to easily integrate/fix in the VCL. There's nothing stopping you from deleting the first inserted column. This one corresponds to the items, if we delete the items, all subitems will also be taken out. The current implementation in VCL is that in fact no item data is deleted when you remove a column. You can verify this by adding a column after you remove one, subitems will magically appear under the new column.
Anyway, what I can suggest you to is to delete the subitems of a removed column manually. Below is an example of a utility procedure to delete a column and its corresponding subitems:
procedure ListViewDeleteColumn(ListView: TListView; Col: Integer);
var
i: Integer;
ColumnOrder: array of Integer;
begin
SetLength(ColumnOrder, ListView.Columns.Count);
ListView_GetColumnOrderArray(
ListView.Handle, ListView.Columns.Count, PInteger(ColumnOrder));
Assert(ColumnOrder[Col] <> 0, 'column with items cannot be removed');
for i := 0 to ListView.Items.Count - 1 do
if Assigned(ListView.Items[i].SubItems) and
(ListView.Items[i].SubItems.Count >= Col) then
ListView.Items[i].SubItems.Delete(ColumnOrder[Col] - 1);
ListView.Columns.Delete(Col);
end;
If you decide to delete the first column, decide what you'll do with items/subitems and rebuild them.
If you delete the last column using code below it works ok:
uses
CommCtrl;
procedure TForm1.Button3Click(Sender: TObject);
begin
ListView_DeleteColumn(ListView1.Handle, 2);
end;
D6 prof.
Formerly we used DBISAM and DBISAMTable. That handle the RecNo, and it is working good with modifications (Delete, edit, etc).
Now we replaced with ElevateDB, that don't handle RecNo, and many times we use Queries, not Tables.
Query must reopen to see the modifications.
But if we Reopen the Query, we need to repositioning to the last record.
Locate isn't enough, because Grid is show it in another Row.
This is very disturbing thing, because after the modification record is moving into another row, you hard to follow it, and users hate this.
We found this code:
function TBaseDBGrid.GetActRow: integer;
begin
Result := -1 + Row;
end;
procedure TBasepDBGrid.SetActRow(aRow: integer);
var
bm : TBookMark;
begin
if IsDataSourceValid(DataSource) then with DataSource.DataSet do begin
bm := GetBookmark;
DisableControls;
try
MoveBy(-aRow);
MoveBy(aRow);
//GotoBookmark(bm);
finally
FreebookMark(bm);
EnableControls;
end;
end;
end;
The original example is uses moveby. This working good with Queries, because we cannot see that Query reopened in the background, the visual control is not changed the row position.
But when we have EDBTable, or Live/Sensitive Query, the MoveBy is dangerous to use, because if somebody delete or append a new row, we can relocate into wrong record.
Then I tried to use the BookMark (see remark). But this technique isn't working, because it is show the record in another Row position...
So the question: how to force both the row position and record in DBGrid?
Or what kind of DBGrid can relocate to the record/row after the underlying DataSet refreshed?
I search for user friendly solution, I understand them, because I tried to use this jump-across DBGrid, and very bad to use, because my eyes are getting out when try to find the original record after update... :-(
Thanks for your every help, link, info:
dd
Since 'MoveBy's are working for you, use them.
Get a 'Bookmark' before closing the dataset. Do your work, reopen the dataset and then reposition your record on the grid with 'MoveBy's. When you're done, get another Bookmark and compare it with the previous one with DataSet.CompareBookmarks. If the result is 0 fine, if not, only then issue a 'GotoBookmark' for the previous bookmark.
This way, as long as another user have not deleted/inserted records your grid will not seem to be jumpy, and if this is not the case at least you'd be on the same record.
edit: Here's some code sample that should reposition the selected record in the correct place even when there had been deletes/inserts in the dataset. Note that the code omits disabling/enabling controls, and the special case when there are less records to fill the grid for simplicity.
type
TAccessDBGrid = class(TDBGrid);
procedure TForm1.Button1Click(Sender: TObject);
var
BmSave, Bm: TBookmark;
GridRow, TotalRow: Integer;
begin
GridRow := TAccessDBGrid(DBGrid1).Row;
TotalRow := TAccessDBGrid(DBGrid1).RowCount;
BmSave := DBGrid1.DataSource.DataSet.GetBookmark;
try
// close dataset, open dataset...
if DBGrid1.DataSource.DataSet.BookmarkValid(BmSave) then
DBGrid1.DataSource.DataSet.GotoBookmark(BmSave);
Dec(TotalRow);
if GridRow < TotalRow div 2 then begin
DBGrid1.DataSource.DataSet.MoveBy(TotalRow - GridRow);
DBGrid1.DataSource.DataSet.MoveBy(GridRow - TotalRow);
end else begin
if dgTitles in DBGrid1.Options then
Dec(GridRow);
DBGrid1.DataSource.DataSet.MoveBy(-GridRow);
DBGrid1.DataSource.DataSet.MoveBy(GridRow);
end;
Bm := DBGrid1.DataSource.DataSet.GetBookmark;
try
if (DBGrid1.DataSource.DataSet.BookmarkValid(Bm) and
DBGrid1.DataSource.DataSet.BookmarkValid(BmSave)) and
(DBGrid1.DataSource.DataSet.CompareBookmarks(Bm, BmSave) <> 0) then
DBGrid1.DataSource.DataSet.GotoBookmark(BmSave);
finally
DBGrid1.DataSource.DataSet.FreeBookmark(Bm);
end;
finally
DBGrid1.DataSource.DataSet.FreeBookmark(BmSave);
end;
end;
Store the value(s) of your unique key field(s) before closing and reopening the query, then Locate to the record after reopening. DisableControls/EnableControls to prevent screen updates.
Just simple piece of code that came in my mind:
procedure DoRefresh(Dataset: TDataset);
var
bkm: TBookmark;
begin
Dataset.UpdateCursorPos;
bkm := Dataset.GetBookmark;
Dataset.DisableControls;
try
Dataset.Refresh; //refresh dataset if it's open
if Dataset.BookmarkValid(bkm) then
begin
Dataset.GotoBookmark(bkm);
end;
finally
Dataset.EnableControls;
Dataset.FreeBookmark(bkm);
end;
end;
Record position depends much on the sort order of resultset you got from the Query/Table object.
If you don't order at all, the order you get from the server is implementation defined and such, can't guarantee that records come in the same order when reopen the query, even if no changes happened. At least in MSSQL and Firebird, results come in different orders if no Order By clause is used.
As for repositioning, I think that TOndrej solution is the safest one - using the primary key of your resultset to reposition the grid on the right record.