Refresh colors in cxGrid - delphi

I have a cxGrid where I change the background color of some fields based on values in some of the fields.
This is all working very fine. But if I cahnge something in the grids data, the colors aren't updated before I close an reopen my form.
What procedure to call to get this updated if the record is changing?

To my experience it does update when you switch the row. But i used it in DB-mode with TClientDataSet.
Check methods like
TcxControl.InvalidateRect
TcxControl.InvalidateRgn
TcxControl.InvalidateWithChildren
You can also invalidate node:
TcxGrid.ActiveView.Invalidate;
TcxGrid.ViewData.Records[0].Invalidate;
TcxGridViewData.Rows[0].Invalidate
TcxCustomGridTableController.FocusedRecord.Invalidate;
Events like
TcxCustomGridTableViewStyles.OnGetContentStyle
TcxCustomGridTableItem.OnCustomDrawCell
also exposes those items (with their Invalidate methods) among or inside parameters, like
ARecord: TcxCustomGridRecord;
ViewInfo -> TcxGridTableCellViewInfo.GridRecord
In other words - open the cxTL unit and grep for "invalidate" word and note every match.

If your grid is attached to a data set, and the data in the dataset changes, the OnGetContentStyle events are called automatically. Make sure that your dataset knows that the data is updated. It sounds like your editing form isn't telling the grid dataset to refresh itself. You can do that either with a callback procedure or implementing the Observer Pattern.
The following code demonstrates how to implement an OnGetContentStyle event for a grid column.
procedure TFormWithGrid.cxGrid1DBTableView1LASTNAMEStylesGetContentStyle(
Sender: TcxCustomGridTableView; ARecord: TcxCustomGridRecord;
AItem: TcxCustomGridTableItem; var AStyle: TcxStyle);
begin
if ARecord.Values[cxGrid1DBTableView1FIRSTNAME.Index] = 'test' then
begin
AStyle := TcxStyle.Create(nil);
AStyle.Color := clRed;
AStyle.Font.Style := [fsBold];
end;
end;

in my situation, this will works
cxGridDBTblVwContenido.DataController.Refresh;

Related

Delphi 10.4 - Sort Memtable by clicking Stringgrid Header

I feel like an idiot because my question seams so simple but I don't get it done :D
My Settings is that:
One Dataset (Memtable), One Stringgrid. The Grid is bind via live Bindungs.
I would like to sort my Columns by clicking on the GridHeader. In the OnHeaderClick Event I get an tColumn Object. I only can read the Column.Header String, but I changed the Text from the Header to a more speakable Text. When I put Column.header into Memtable.Indexfieldsname Memtable says that field does not exist, what is right, but I don't know how to get the right Fieldname from the column.
What you want is quite straightforward to do. In the example below, which uses the demo data from
the Biolife demo, I've linked the StringgRid to the FDMemTable entirely by binding objects
created in code so that there is no doubt about any of the binding steps or binding properties,
nor the method used to establish the bindings.
procedure TForm2.FormCreate(Sender: TObject);
var
BindSourceDB1 : TBindSourceDB;
LinkGridToDataSourceBindSourceDB1 : TLinkGridToDataSource;
begin
// Note : You need to load FDMemTable1 at design time from the sample Biolife.Fds datafile
// The following code creates a TBindSourceDB which Live-Binds FDMemTable1
// to StringGrid1
//
// As a result, the column header texts will be the fieldnames of FDMemTable1's fields
// However, the code that determines the column on which to sort the StringGrid does not depend
// on this
BindSourceDB1 := TBindSourceDB.Create(Self);
BindSourceDB1.DataSet := FDMemTable1;
LinkGridToDataSourceBindSourceDB1 := TLinkGridToDataSource.Create(Self);
LinkGridToDataSourceBindSourceDB1.DataSource := BindSourceDB1;
LinkGridToDataSourceBindSourceDB1.GridControl := StringGrid1;
end;
procedure TForm2.StringGrid1HeaderClick(Column: TColumn);
// Sorts the STringGrid on the column whose header has been clicked
var
ColIndex,
FieldIndex : Integer;
AFieldName : String;
begin
ColIndex := Column.Index;
FieldIndex := ColIndex;
AFieldName := FDMemTable1.Fields[FieldIndex].FieldName;
Caption := AFieldName;
// Should check here that the the field is a sortable one and not a blob like a graphic field
FDMemTable1.IndexFieldNames := AFieldName;
end;
Note that this answer assumes that there is a one-for-one correspondence between grid columns and fields of the bound dataset, which will usually be the case for bindings created using the default methods in the IDE. However, Live Binding is sophisticated enough to support situations where this correspondence does not exist, and in those circumstances it should not be assumed that the method in this answer will work.

How to know when the USER changed the text in a TMemo/TEdit?

I was always bugged by the fact that TMemo (and other similar controls) only have the OnChange event. I would like to know when the USER changed the text, not when the text was changed programmatically.
I know two methods to discriminate between the user changed text and programmatically changed text:
Put OnChange:= NIL before you change the text programmatically. Then restore the OnChange. This is error prone as you need to remember to do it every time you change text from the code (and to which memos/edits needs this special treatment to be applied). Now we know that every time the OnChange is called, the control was edited by user.
Capture the OnKeyPress, MouseDown, etc events. Decide if the text was actually changed and manually call the code that needs to be called when user edited the ext. This could add a big amount of procedures to an already large file.
There is a more elegant way to do it?
You can write a helper procedure to do your option 1, and use it in your framework whenever you want to ensure no OnChange event is triggered when you set the text. e.g.:
type
TCustomEditAccess = class(TCustomEdit);
procedure SetEditTextNoEvent(Edit: TCustomEdit; const AText: string);
var
OldOnChange: TNotifyEvent;
begin
with TCustomEditAccess(Edit) do
begin
OldOnChange := OnChange;
try
OnChange := nil;
Text := AText;
finally
OnChange := OldOnChange;
end;
end;
end;
TMemo has also the Lines property which also triggers OnChange, so you can make another similar procedure that accepts Lines: TStrings argument.
How about using the Modified property?
procedure TForm1.MyEditChange(Sender: TObject);
begin
if MyEdit.Modified then
begin
// The user changed the text since it was last reset (i.e. set programmatically)
// If you want/need to indicate you've "taken care" of the
// current modification, you can reset Modified to false manually here.
// Otherwise it will be reset the next time you assign something to the
// Text property programmatically.
MyEdit.Modified := false;
end;
end;

Delphi how to save 3 datasources with one save button?

I got a problem with saving all values from 3 datasources into a SMDBGrid with another datasouce.
I got AdressID, ContactpersonID and RelationID.
Those all dont match each others.
The problem is that my SMDBGrid has another datasource then those 3.
I wanna save them with one button.
Tried many ways but can't find a good result.
this is the code i use right now for my Insert button:
procedure TFRelatiebeheer.ToolButton1Click(Sender: TObject);
begin
DRelatiebeheer.ContactpersonID.Insert;
DRelatiebeheer.RelationID.Insert;
DRelatiebeheer.AdressID.Insert;
end;
This is the code i use for my save button right now
if (DRelatiebeheer.ContactpersonID.State in dsEditModes) then
if not (DRelatiebeheer.ContactpersonID.State in [dsInsert]) then
begin
KJSMDBGrid1.RefreshData;
KJPanel4.Visible := True;
end
else
begin
if (DRelatiebeheer.ContactpersonID.State IN dsEditModes) then
DRelatiebeheer.ContactpersonID.Post;
if (DRelatiebeheer.AdressID.State IN dsEditModes) then
DRelatiebeheer.AdressID.Post;
end;
Hope you have a good sight for what I am doing right now, if not please notify.
I got the problem with the datasources that need to be saved on 1 click and then be refreshed in the database and in the Grid.
That means that when I insert a Contactperson there needs to be a AdressID and a RelationID coupled with it.
After that the grid needs to reload all of the data.
Focusing on the given problem
Depending on the intended behavior (should it be possible posting only one or two table(s) or is it necessary to post all tables) the first thing to do would be to ensure that the tables can be posted. You coulds create a function for each table e.g. CanAdressIDBePosted:Boolean to check if required fields are already entered. The condition of the table ContactpersonID would contain additional conditions: needed fields are entered AND CanAdressIDBePosted AND CanRelationIDBePosted. You could create an Action which would be bound on your button with an OnUpdate event which could look like this:
procedure TForm1.PostActionUpdate(Sender: TObject);
begin
TAction(Sender).Enabled := CanAdressIDBePosted and CanContactpersonIDBePosted and CanRelationIDBePosted;
// depending on your requirements (e.g. no need to post RelationID if not entered) it also could be
TAction(Sender).Enabled := CanAdressIDBePosted or CanContactpersonIDBePosted ;
end;
procedure TForm1.PostActionExecute(Sender: TObject);
begin
if CanAdressIDBePosted then AdressID.Post; // ensure ID fields will be generated
if CanRelationIDBePosted then RelationID.Post; // ensure ID fields will be generated
if CanContactpersonIDBePosted then
begin
ContactpersonID.FieldByName('AdressID').value := AdressID.FieldByName('ID').Value;
ContactpersonID.FieldByName('RelationID').value := RelationID.FieldByName('ID').Value;
end;
DateSetBoundToTheGrid.Requery;
// furthor actions you need
end;
Function TForm1.CanAdressIDBePosted:Boolean;
begin
// example implementation
Result := (AdressID.State in [dsEdit,dsInsert]) and (not AdressID.FieldByName('NeededField').IsNull);
end;
Function TForm1.CanContactpersonIDBePosted:Boolean;
begin
// example implementation
Result := (ContactpersonID.State in [dsEdit,dsInsert]) and (not ContactpersonID.FieldByName('NeededField').IsNull)
and CanAdressIDBePosted and CanRelationIDBePosted;
end;
An addidtional Action should be created to cancel if needed:
procedure TForm1.CancelActionExecute(Sender: TObject);
begin
AdressID.Cancel;
RelationID.Cancel;
ContactpersonID.Cancel;
end;
procedure TForm1.CancelActionUpdate(Sender: TObject);
begin
TAction(Sender).Enabled := (AdressID.State in [dsEdit,dsInsert])
or (RelationID.State in [dsEdit,dsInsert])
or (ContactpersonID.State in [dsEdit,dsInsert]);
end;
In general I am not sure if the approach you took ist the best which can be taken, since from the structure given IMHO it should be possible to assign already existing relations and adresses to new generated contactpersons, but that would be another question.
This code just looks somewhat random to me. What SHOULD happen there ?
if (DRelatiebeheer.ContactpersonID.State in dsEditModes) then
// remember this check (1)
if not (DRelatiebeheer.ContactpersonID.State in [dsInsert]) then
// this check better written as "...State = dsInsert"
begin
// why don't you call DRelatiebeheer.ContactpersonID.Post to save chanegs ?
KJSMDBGrid1.RefreshData;
KJPanel4.Visible := True;
end
else
begin
if (DRelatiebeheer.ContactpersonID.State IN dsEditModes) then
// you already checked this above (1), why check again ?
DRelatiebeheer.ContactpersonID.Post;
if (DRelatiebeheer.AdressID.State IN dsEditModes) then
DRelatiebeheer.AdressID.Post;
end;
// so what about DRelatiebeheer.RelationID ?
For what i may deduce, you don't have to make any complex if-ladders, you just have to literally translate your words to Delphi. You want to save three tables and then refresh the grid. Then just do it.
procedure TFRelatiebeheer.SaveButtonClick(Sender: TObject);
begin
DRelatiebeheer.ContactpersonID.Post;
DRelatiebeheer.RelationID.Post;
DRelatiebeheer.AdressID.Post;
DatabaseConnection.CommitTrans;
KJSMDBGrid1.RefreshData;
KJPanel4.Visible := True;
end;
Just like you was told in your other questions.
Delphi save all values from different datasources with 1 save button
Delphi set Panel visible after post
PS. ToolButton1Click - plase, DO rename the buttons. Believe me when you have 10 buttons named Button1, Button2, ...Button10 you would never be sure what each button should do and would mix everything and make all possible program logic errors.

DataSetProvider - DataSet to ClientDataSet

EDIT: It seems as if the DataSetProvider doesn't have the functionality I need for this project, so I'll be implementing a custom class for loading the data into the ClientDataSet.
I am trying to take data from a TMSQuery which is connected to my DB and populate a ClientDataSet with some of that data using a DataSetProvider.
My problem is that I will need to modify some of this data before it can go into my ClientDataSet. The ClientDataSet has persistent fields that will not match up with the raw DB data. I can't even get a string from the DB into a memo field in ClientDataSet.
The ClientDataSet is a part of my data tier so I will need to conform the data from the DB to the ClientDataSet field by field (well most will be able to go right through, but many will require routing and/or conversion).
Does anyone have experience with this?
You're looking for the TDataSetProvider.BeforeUpdateRecord event. Write an event handler for this event and you can take manual control of how the data is applied back to the data base.
Something like this
procedure TDataModule1.DataSetProvider1BeforeUpdateRecord(Sender: TObject; SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind; var Applied: Boolean);
begin
{ Set applied to tell DataSnap that you have applied this record yourself }
Applied := True;
case UpdateKind of
ukModify:
begin
Table1.Edit;
{ set the values of the fields something like this }
if not VarIsEmpty(DeltaDS.FieldByName('NewValue')) then
Table1['SomeField'] := DeltaDS.FieldByName('SomeField').NewValue;
Table1.Post;
end;
ukInsert:
begin
Table1.Insert;
{ set the values of the fields }
Table1['SomeField'] := DeltaDS['SomeField']
Table1.Post;
end;
ukDelete:
if Table1.Locate('PrimaryKeyField', DeltaDS['PrimaryKeyField'], []) then
Table1.Delete;
end; // case
end;
You can modify the data going to the ClientDataSet by implementing the TDataSetProvider.OnGetData event.
procedure TDataModule1.DataSetProvider1GetData(Sender: TObject; DataSet: TCustomClientDataSet);
begin
DataSet.First;
while not DataSet.Eof do begin
DataSet.Edit;
DataSet['Surname'] := UpperCase(DataSet['Surname']);
DataSet.Post;
DataSet.Next;
end; // while
end;
When applying updates from the ClientDataSet you can use the TDataSetProvider.OnUpdateData event. Like the OnGetData event you are operating on the whole dataset rather than a single record.
procedure TDataModule1.DataSetProvider1UpdateData(Sender: TObject; DataSet: TCustomClientDataSet);
begin
DataSet.First;
while not DataSet.Eof do begin
DataSet.Edit;
DataSet['Surname'] := LowerCase(DataSet['Surname']);
DataSet.Post;
DataSet.Next;
end; // while
end;
This OnUpdateData event is called before the OnBeforeUpdateRecord event. Also the OnGetData and OnUpdateData events operate on the whole dataset while OnBeforeUpdateRecord is called once for each modified record.
If I need a ClientDataSet to have data that doesn't exactly match the database schema, I write a query for the TQuery component that returns the data in the format that I want. I then write my own, separate, Delete, Insert, Refresh, and Update queries for the TQuery component.
Alternatively, you could create a view on the database and use the view in your TQuery component.
If you want a custom ClientDataSet that is independent of the database, what you need is an in-memory dataset. If you don't have an in-memory dataset component, Google for "TClientDataSet as in-memory dataset". What you end up with though, is basically a glorified list view component. Of course, you can hook into the OnUpdateRecord of the in-memory dataset to know when to update your real dataset.

How can I detect if ApplyUpdates will Insert or Update data?

In the AfterPost event handler for a ClientDataSet, I need the information if the ApplyUpdates function for the current record will do an update or an insert.
The AfterPost event will be executed for new and updated records, and I do not want to declare a new Flag variable to indicate if a 'update' or ' insert' operation is in progress.
Example code:
procedure TdmMain.QryTestAfterPost(DataSet: TDataSet);
begin
if IsInserting(QryTest) then
// ShowMessage('Inserting')...
else
// ShowMessage('Updating');
QryTest.ApplyUpdates(-1);
end;
The application will write a log in the AfterPost method, after ApplyUpdate has completed. So this method is the place which is closest to the action, I would prefer a solution which completely can be inserted in this event handler.
How could I implement the IsInserting function, using information in the ClientDataSet instance QryTest?
Edit: I will try ClientDataSet.UpdateStatus which is explained here.
ApplyUpdates doesn't give you that information - since it can be Inserting, updating and deleting.
ApplyUpdates apply the change information stored on Delta array. That change information can, for example, contain any number of changes of different types (insertions, deletions and updatings) and all these will be applied on the same call.
On TDatasetProvider you have the BeforeUpdateRecord event (or something like that, sleep does funny things on memory :-) ). That event is called before each record of Delta is applied to the underlying database/dataset and therefore the place to get such information... But Showmessage will stop the apply process.
EDIT: Now I remembered there's another option: you can assign Delta to another clientdataset Data property and read the dataset UpdateStatus for that record.
Of course, you need to do this before doing applyupdates...
var
cdsAux: TClientDataset;
begin
.
.
<creation of cdsAux>
cdsAUx.Data := cdsUpdated.Delta;
cdsAux.First;
case cdsAux.UpdateStatus of
usModified:
ShowMessage('Modified');
usInserted:
ShowMessage('Inserted');
usDeleted:
ShowMessage('Deleted'); // For this to work you have to modify
// TClientDataset.StatusFilter
end;
<cleanup code>
end;
BeforeUpdateRecord event on TDataSetProvider is defined as:
procedure BeforeUpdateRecord(Sender: TObject; SourceDS: TDataSet; DeltaDS:
TCustomClientDataSet; UpdateKind: TUpdateKind;
var Applied: Boolean);
Parameter UpdateKind says what will be done with record: ukModify, ukInsert or ukDelete. You can test it like this:
procedure TSomeRDM.SomeProviderBeforeUpdateRecord(Sender: TObject;
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;
var Applied: Boolean);
begin
case UpdateKind of
ukInsert :
// Process Insert;
ukModify :
// Process update
ukDelete :
// Process Delete
end;
end;
Note: this event signature is from Delphi 7. I don't know if it changed in later versions of Delphi.
Set the ClientDataSet.StatusFilter to an TUpdateStatus value, and then read ClientDataSet.RecordCount
for example,
ClientDataSet1.StatusFilter := [usDeleted];
ShowMessage(IntToStr(ClientDataSet1.RecordCount));
will return the number of Delete queries that will be executed.
Note two things, however. Setting StatusFilter to usModified always includes both the modified and unmodified records, so you take half of that value (a value of 4 means 2 Update queries will be executed). Also, setting StatusFilter to [] (an empty set) is how you restore to default view (Modified, Unmodified, and Inserted)
Make sure that any unposted changes have been posted before doing this, otherwise unposted changes may not be considered.

Resources