Delphi - restore actual row in DBGrid - delphi

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.

Related

Delphi Grid: How to get selectedrows count "on the fly"

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.

How to find a table in word and update values in the table using delphi

I am new to Ole word automation using Delphi. I have a sample document which have many tables inside it. I am able to insert an image by finding a shape in word and insert values inside it. But I am not able to find a particular table and update some values into it using delphi. Is there a way? Thanks !
I assume you're asking mainly how to find the table, rather than how to change the contents of the table afterwards. How to do this depends on the criteria you want to use to find the table of interest.
On the face of it, you should be able to navigate to a given table using the Goto method of MS Word's Selection object. However, there is a problem with that (see at the end of this answer) in detecting when the operation has failed because Goto didn't locate the correct table.
If the table of interest is preceded in the document by an identifying text label, you could simply search for the label and, if found, navigate forwards from that, like this example which finds the table after the label 'Table3':
procedure TForm1.Button4Click(Sender: TObject);
var
AFileName : String;
MSWord,
Document : OleVariant;
Found : WordBool;
begin
AFileName := 'd:\aaad7\officeauto\Tables.Docx';
MSWord := CreateOleObject('Word.Application');
MSWord.Visible := True;
Document := MSWord.Documents.Open(AFileName);
MSWord.Selection.Find.Text :='Table3';
Found := MSWord.Selection.Find.Execute;
if Found then begin
MSWord.Selection.MoveDown( Unit:=wdLine, Count:=1);
end;
end;
As written, the "if Found ..." block merely places the cursor on the first character of the first cell of the table. Once in the table, you can alter its contents however you like.
If you want to find out how to do something like insert an image in a table cell, go to the Developer tab on Word's ribbon, record a macro that does what you want and then use Edit from the Macros pop-up to look at it - it's usually fairly easy then to cut'n paste it into Delphi and edit it into the equivalent Delphi code. Same goes for other methods of finding the table you want - record a macro then translate it.
To find the Nth table in a document and plant the cursor in its top left cell, you can do this:
procedure TForm1.Button2Click(Sender: TObject);
var
AFileName : String;
MSWord,
Document,
Tables,
Table : OleVariant;
TableNo : Integer;
begin
AFileName := 'd:\aaad7\officeauto\Tables.Docx';
MSWord := CreateOleObject('Word.Application');
MSWord.Visible := True;
Document := MSWord.Documents.Open(AFileName);
TableNo := 3;
Tables := Document.Tables;
if TableNo <= Tables.Count then begin
Table := Tables.Item(TableNo);
Table.Select;
MSWord.Selection.MoveLeft( Unit:=wdCharacter, Count:=1);
end;
end;
Btw, in Word's Find dialog, on the Goto tab, there is Table entry in the Go to what listbox. You can call that in code using something like
MSWord.Selection.GoTo(What:= wdGoToTable, Which:=wdGoToFirst, Count:=3);
The problem with it is how to check in code whether it succeeded. Unlike Find, which returns a WordBool, Goto returns a Range object. If you try to use it to go to the 10th table in a document which only contains 2 tables, there is no error raised, but the returned range is the last table in the document. I haven't yet found a way to check from the returned Range whether Goto succeeded without checking some text associated with the table which could have been found using Find in the first place. Of course, if the document is guaranteed to contain the table you're looking for, this problem with Goto probably needn't concern you.
Maybe something like:
Word.ActiveDocument.Tables.Item( 1 ).Cell( 1, 1 ).Range.Text := 'some text';

Delphi reaching a DBGrid's rows

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

TListView: Last Column's caption lost after Columns.Delete(index)

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;

How best to present my Delphi database table to FastReport for lists and aggregates

I have spent several days so far laying the ground work to use FastReport in my Application. The Application stores device test result data in the form of a DBF file comprising several fixed fields(DeviceID, Passed etc) plus a variable number of result fields, each of which correspond to the type of measurement data available. There can be as few as one of these fields and as many as 100. Each field has a letter code name such as OV and RV. Total record counts can be from zero up to some 10's of thousands.
A specific report template will have already included in its design the field names that it will display. Missing fields will be empty on the report.
My question involves the best way of designing the report and the data supplied to the report so that the report construction is as simple as possible - I'm going to allow my users to generate their own reports - and I need two kinds of report output - list of results and aggregates. It is the aggregates that are giving me the headache. I need not only MIN, MAX, COUNT etc (as provided internally in FastReport) but Standard Deviation as well. Further, I would like to use the FastReport 'drill down' feature where you can click on a group header and the data table is revealed or hidden. My aggregates should ideally be in the header, not the footer so that they appear all the time.
I have found that SQL in a TQuery gives me a lot of flexibility since it provides the 'StDev' aggregrate (FastREport does not) but as far as I can see I would need a fixed TQuery for each of my fields. So far, the nicest solution that I can come up with involves using a filter on the main table for 'Passed' (so that the user can view passe, failed or all) and then to build a separate 'stats' table with the same field name columns, but with MIN, MAX, COUNT, MEAN, STDEV as individual records. I would then use a TfrxDBDataSet to expose this table to FastReport. I see that I can also use FastReport's own ADODatabase and ADOQuery to directly access my DBF file. This works well but again I did not want to expose this access layer to my user in the report if possible.
This just seems so messy when aggregate functions must be a fundamental database requirement. Am I missing a much easier way of doing this? I've worked my way through the (excellent) demos supplied with FastReport (professional) and I'm using XE2. I'm also aware of the useful functions in the MATH unit if I need to calculate StDev myself.
I would appreciate any guidance, thanks.
For anything you could calculate in code, lists of array values, aggregate or functional calculation results, I prefer to use the TfrxUserDataSet and implement the TfrxReport.OnGetvalue event.
Although it might initially be confusing, the user datasets simply declare a data set name, and the list of fields available through that data set name and use events which fire to let you "navigate" (first, next record) and declare when you've reached the end of your calculated data. This allows you to build a "generator" or else, just a normal virtual-data-provider set of logic for your calculations.
Here's what my OnGetValue events look like:
procedure TfrmReport.frxReportGetValue(const VarName: string; var Value: Variant);
begin
Value := GetReportValue(VarName);
end;
// INPUT: VarName = '(<GlobalArea."hdReportTitle">)'
// OUTPUT: tableName = 'GlobalArea', fieldName = 'hdReportTitle'
function ParseVar(const VarName:String; var tableName,fieldName:String; var ParenFlag:Boolean):Boolean;
var
paVarName:String;
angleBracketFlag:Boolean;
dotPos:Integer;
fieldQuoteFlag:Boolean;
procedure RemoveOuter(var str:String; initialChar,finalChar:Char; var flag);
var
n:Integer;
begin
n := Length(str);
if n>2 then begin
ParenFlag := (str[1]=initialChar) and (str[n]=finalChar);
if ParenFlag then begin
str := Copy(str,2,n-2);
end;
end;
end;
begin
result := false;
fieldQuoteFlag := false;
paVarName := SysUtils.Trim(VarName);
ParenFlag := false;
tableName := '';
fieldName := '';
RemoveOuter(paVarName, '(',')',parenFlag);
RemoveOuter(paVarName,'<','>',angleBracketFlag);
dotPos := Pos('.',paVarName);
if dotPos >0 then begin
tableName := Copy(paVarName,1,dotPos-1);
fieldName := Copy(paVarName,dotPos+1,Length(paVarName));
RemoveOuter(fieldName, '"','"',fieldQuoteFlag);
result := true;
end else begin
tableName := '';
fieldName := paVarName;
end;
end;
function TfrmProfitAnalysisReport.GetReportValue(const VarName:String):Variant;
var
tableName:String;
fieldName:String;
parenFlag:Boolean;
begin
ParseVar(VarName,tableName,fieldName,parenFlag);
result := NULL;
{ Global Area - Header Values }
if sameText(tableName,'GlobalArea') then begin
if fieldName='hdReportTitle' then
result := GetTitle; { A function that calculates a title for the report }
else if fieldName='hdReportSubtitle' then
result := 'Report for Customer XYZ'
else if fieldName='....' then begin
...
end;
if Variants.VarIsNull( result) then
result := '?'+fieldName+'?';
end;
Well, a lot of questions with a lot of possible answers:
1) About the datasets, I really recommend put them in your application (DataModule or Form) instead of using them inside the report. It will give you more flexibility;
2) You can have one query for each aggregation, but this will affect performance if your data tables grows in tons of records. Some alternatives:
2.1) calculate the values in your FastReport script, but this will also expose the logic to the report;
2.2) Iterate through the record on the Delphi code, and pass the results as variables to your report. Example:
frxReport.Variables['MIN'] := YourMinVariableOrMethod;
frxReport.Variables['MAX'] := YourMaxVariableOrMethod;
2.3) Using a ClientDataSet associated with your query and implement TAggregateFields on the ClientDataSet.
I, personally, prefer the 2.2 approach, with all logic in the Delphi code, which is simple and powerful.

Resources