In Delphi get value from lookup field - delphi

I have database Paradox 7.
Two tables:
Order
--------
OCode
ODate // Buy date
OCount // Product count
PPrice // Price of product in current moment
PCode // Product code
CCode //Client code
Product
---------
PCode
PName
PPrice
On OrderTable(TTable) I add lookup field Product(relation PCode(Order)<->PCode(Product)) which returns PName. I need when I choose on DBGrid Product in field Order.PPrice store Product.PPrice
P.S>Sorry for my bad english.

Declare a HandleProductSelection method in your form (or datamodule):
procedure HandleProductSelection(Sender: TField);
Attach an AfterOpen handler to the OrderTable. In that event handler, find the Product code field and attach the HandleProductSelection method to the field's OnChange event.
procedure TForm1.OrderTableAfterOpen(DataSet: TDataSet);
var
Field: TField;
begin
Field := OrderTable.FindField('PCode');
Field.OnChange := HandleProductSelection;
end;
This ensures that whenever your OrderTable is opened, the event handler will be attached to the proper field and that in turn will ensure that the HandleProductSelection method will be called whenever the contents of the Product Code field are changed.
Implement the HandleProductSelection method. If you defined the lookup field for the product code using the field editor, you have a Product dataset on your form (or datamodule). The dataset with the product information will then be positioned according to the value of the Product code field in the current record of your OrderTable.
You can take advantage of this in your HandleProductSelection method by simply transferring the information from the Product dataset to the OrderTable:
procedure TForm1.HandleProductSelection(Sender: TField);
begin
OrderTable.FieldByName('PPrice').AsCurrency :=
DataSetProduct.FieldByName('PPrice').AsCurrency;
end;

Related

How to find out which field was changed in a delphi 6 TZTable via ZeosLib

I have a TZTable (ZEOSlib) bound to a DBGrid now I need to know which particular TField was changed by the user.
I tried with
if NOT (taPositionenArtNrGH.NewValue = taPositionenArtNrGH.OldValue) then
ShowMessage('ArticleNumber changed');
I placed the code in
BeforePost, OnUpdateRecord, AfterPost
But in the Debugger OldValue is always NewValue. How do I check which field was changed?
You can use UpdateStatus : TUpdateStatus for this. For example:
Set ZTable.CachedUpdates to true;
Create new calculated field named "Status".
To show old value for example of field "FNAME" create new calculate field named "FNameOldValue"
In OnCalcFields event use:
procedure TDM1.ZTable1CalcFields(DataSet: TDataSet);
begin
if ZTable1.UpdateStatus in [usModified] then
begin
ZTable1Status.value := 'Modified';
ZTable1FNameOldValue.value := ZTable1FNAME.OldValue;
end
else
ZTable1Status.value := 'UnModified'
end;
Result :
Edit:
You can detect field level changes like:
if ZTable1.UpdateStatus in [usModified] then
begin
for I := 0 to ZTable1.Fields.Count - 1 do
begin
if ZTable1.Fields[i].OldValue <> ZTable1.Fields[i].NewValue then
-- do something with this field
end;
end;
As per documentation:
The NewValue property is only usable when the data is accessed using
a TClientDataSet component or cached updates is enabled.
If you just want to know what fields have been changed, why not use TField.OnChange event? You could fill a list of field names in this event and clear it in OnAfterPost. But the Modified property would be very useful indeed; it's odd that it haven't been implemented yet.

How can I use a DBLookupComboBox to select a foreign key value without immediately changing the target table? (Delphi 5 Pro)

Basic Scenario:
User clicks 'edit item'
Edit dialog opens
A combobox 'item type' should be populated with items form a table 'item_type'. The combobox should display 'item_type.name', but also know about 'item_type.id'
User edits other item stuff and selects an item type, then clicks ok
My program does some input validation
if everything is ok, get the 'item_type.id' from the selected combo item and save it to the foreign key column of the item table ('item.fk_item_type').
If I understand this component correctly, I should set DataSource to point to the destination table 'item' and DataField to 'item.fk_item_type'. But that would immediately edit my item table before I get a chance the validate all the other input.
It feels like I am missing something here. Somewhere I read that I need to use a classic ComboBox and fill it manually. But then I don't understand how to get to the id of the selected item.
Thank you for any pointers.
Edit:
I am starting to suspect that maybe I am missing a fundamental thing. All these DB* components, do they load values from that database automatically, but I have to call Post() myself? Meaning they do not automatically change values in the database?
If I understand you correctly, you want to use a DBLookupComboBox. You have to supply values for the following properties
datasource - linked to the table which you are editing, presumably 'items'
datafield - the name of the field in the table which you are editing, presumably 'item_type'
listsource - linked to the table which populated the combobox, presumably 'item_types'
list field - the name of the field from 'item_types' which you want to display, presumably 'name'
key field - the name of the field from 'item_types' which will be inserted into the items record, presumably 'item_type'
The table which populated the combobox is never edited.
You can validate the values before posting the new/edited 'items' record.
I can show you how to use a non-data aware combobox if necessary, but it's easier to use the data aware version.
Regarding validation, I use the following code template in edit dialogs.
Function TEditQuestion.Execute (n: longint): boolean;
var
gen: longint;
begin
sdsEditQuestion.params[0].asinteger:= n; // TSQLDataSet
with qEditQuestion do // TClientDataSet
begin
open;
if n = -1 then // new record
begin
caption:= 'New record';
insert;
fieldbyname ('alive').asinteger:= 1;
// initialise necessary fields in the dataset
end
else caption:= 'Editing record ' + inttostr (n);
edit;
if showmodal = mrOK then
begin
// validation code comes here. Set 'result' to true if everything is ok
if result then
begin
if n = -1 then
begin
with qGenID do
begin
open;
gen:= fields[0].asinteger; // get new value from generator
close
end;
FieldByName ('id').asinteger:= gen;
end;
post;
applyupdates (0)
end
else cancel // showmodal = OK, result = false
else // showmodal cancelled
begin
cancel;
result:= false
end;
end // with qEditQuestion
end;

it's possible to create a fake data field in a delphi dataset?

I want to create a 'fake' data field in a DataSet (not ClientDataSet):
the field should not be stored in the db
it's not a calculated field (the user should be allowed to enter input data)
the field has business logic meaning, so after the user updates its value it should update other fields (with the OnFieldChange event)
I know I can have a simple no-dbaware control, capture it's OnChange event and perform the calculations there (or call a DataModule function, where the DataSet resides) but I think it's more clean if I can reutilize the dataset automatic binding with db-ware controls and dataset events..
Also this way the unique connection between the Form (Presentation) and the DataModule (Model) it's the DataSet (less coupling)..
PD: I'm using fibplus, with I think the solution (if any) will be at the VCL level..
Thanks!
Have you tried using an InternalCalc field? Your data aware controls will let you edit an InternalCalc field's value, and the value is stored in the dataset.
If you create an InternalCalc field in the dataset (TClientDataSet, TQuery, etc.) at design time, it's almost exactly what you're asking for.
You could create an updatable view (with before insert/update/delete triggers) in your Interbase/Firebird database. This would look like a "virtual table" to the client (the select statement of the view's declaration can also contain "virtual fields") and it's totally up to you how you implement the triggers.
You can make a Calcfield writeable:
type
TCalcStringField = class(TWideStringField)
function GetCanModify: Boolean; override;
end;
function TCalcStringField.GetCanModify: Boolean;
begin
// Makes Calcfield editable
//if FieldNo > 0 then
if DataSet.State <> dsSetKey then
Result := not ReadOnly and DataSet.CanModify
else
Result := IsIndexField
//else
// Result := False;
end;
Set Calculated to true, and in OnSetText of this Field you can then write the Text to any other place
procedure TformXX.XXOnSetText(Sender: TField; const Text: string);
begin
...
end;

DBGrid showing "(MEMO)" as the value of string fields

I'm trying to write a simple SQLite application using Lazarus and the SQLdb components.
I connect to the database and populate a TDBGrid. Problem is that all columns that are text fields display the value "(MEMO)" rather then the string in database.
I have found a simple solution:
The property dgDisplayMemoText from the DBGrid must be enabled.
I forgot the source of this but this is what I am doing with memo fields in tdbgrid.
bluish is right about the gettext event, this is how to implement it in the code:
Create a class called MemoDifier:
MemoDifier = class
public
procedure DBGridOnGetText(Sender: TField; var aText: string;
DisplayText: boolean);
end;
At the implementation section of your code, put this:
procedure MemoDifier.DBGridOnGetText(Sender: TField; var aText: string;
DisplayText: boolean);
begin
if (DisplayText) then
aText := Sender.AsString;
end;
Then click the tdbgrid control in your form and at the Object Inspector(Lazarus IDE), click the Events tab, scroll below to find the OnPrepareCanvas event. Double click it to generate the code. Then modify the code to suit to your needs such as the name of your tdbgrid control:
procedure Tmainui.TDBGrid1PrepareCanvas(sender: TObject;
DataCol: Integer; Column: TColumn; AState: TGridDrawState);
var
MemoFieldReveal: MemoDifier;
begin
if (DataCol = 1) then
begin
try
TDBGrid1.Columns.Items[0].Field.OnGetText := #MemoFieldReveal.DBGridOnGetText;
TDBGrid1.Columns.Items[1].Field.OnGetText := #MemoFieldReveal.DBGridOnGetText;
TDBGrid1.Columns.Items[2].Field.OnGetText := #MemoFieldReveal.DBGridOnGetText;
except
On E: Exception do
begin
ShowMessage('Exception caught : ' + E.Message);
end;
end;
end;
end;
The variable MemoFieldReveal points to the class MemoDifier. Don't forget to modify the index (Items[x]) to point to your index number of the tdbgrid items/fields which shows the (MEMO) text.
Another option
If you are using TZConection add this line to you code when you Connect you database
TZConnection).Properties.Add('Undefined_Varchar_AsString_Length=100');
As said on IRC, you probably need to add the fields of your query to the form, (so that "field" components are generated for them) and then implement the TMemoField.GetText event.
See if entering the "fields" field in the object inspector brings up an editor to generate the components (it does so in Zeos iirc).
Memo fields cannot be shown in the TDBGrid. Add TDBMemo to the form and connect it to the same TDataSource. You will see the text in your memo in this case.
I have the same in MySQL and Tgrid so I'm hoping the answer is the same as it is quite simple :-)
Say if s is the field causing the problem then Instead of writing
SELECT s
write
SELECT LEFT(s,200) AS s
An apparently simple solution is to limit the length of the TEXT in the field using something like VARCHAR(n) in the column type where n is the maximum number of allowed characters.
Old question but I came across it while looking for a similar issue, but on the data retrieved by code rather than with a DBGrid; When I retrieved a string fields with the methods DisplayText or Text, I got "(memo)" instead of the correct value.
The answer is actually simpleā€¦ The TSQLQuery class owns a set of methods called AsXxx to get the data according to a type of data. Here use AsString to assign a variable inline.
SQLQuery1.Open;
//some check to make sure there are fields
name:=SQLQuery1.Fields[1].AsString; //gives "English" for example
code:=SQLQuery1.Fields[2].DisplayText; //gives "(Memo) instead of "en"
More, if you don't know at design time of which type the variable is (for example, you want to design a generical function for any kind of table) with its FieldDefs property of your TSqlQuery object can help you.
It owns a DataType property that allows to choose which AsXxx function to use. Here for example.
SQLQuery1.Open;
//some check to make sure there are fields
with SQLQuery1.FieldDefs[1].DataType of
ftMemo: SQLQuery1.Fields[1].DisplayText; //gives "(Memo)"
ftString: name:=SQLQuery1.Fields[1].AsString; //gives "English" for example
...
end;
And if you look at the datatype while debugging, you will notice that with SQLite, any text is seen as a ftMemo, not a ftString for there is only one type of text there.
after some tests, I could see that although the fields are treated as "MEMO", for "string" fields, just keep the "VARCHAR" option instead of "TEXT" when creating the table.
This article gives a solution: Displaying and editing MEMO fiels in Delphi's TDBGrid.
Here I summarize what you have to do:
in the .dfm add OnGetText = MyDataSetMyFieldGetText to the TMemoField (here named MyField) belonging to your data set (for example a TTable, here named MyDataSet)
in the .pas > interface > type > inside your form definition, add
procedure MyDataSetMyFieldGetText(Sender: TField; var Text: string; DisplayText: Boolean);
in the .pas > implementation > add this method
procedure TDM.WorkVisiteNoteGetText(Sender: TField; var Text: string; DisplayText: Boolean);
begin
Text := Copy(WorkVisiteNote.AsString, 1, 100);
end;

Adding a calculated field to a Query at run time

I'm getting data using a query in Delphi, and would like to add a calculated field to the query before it runs. The calculated field is using values in code as well as the query so I can't just calculate it in SQL.
I know I can attach an OnCalcFields Event to actually make the calculation, but the problem is after adding the calculated field there are no other fields in the query...
I did some digging and found that all of the field defs are created but the actual fields are only created
if DefaultFields then
CreateFields
Default Fields is specified
procedure TDataSet.DoInternalOpen;
begin
FDefaultFields := FieldCount = 0;
...
end;
Which would indicate that if you add fields you only get the fields you added.
I would like all the fields in the query AS WELL AS the ones I Add.
Is this possible or do I have to add all the fields I'm using as well?
Nothing prevents you from creating all the fields first in your code,
then add your calculated fields.
You can either use a "hacked type" to use the protected CreateFields:
type
THackQuery = class(TADOQuery)
end;
[...]
MyQuery.FieldDefs.Update;
THackQuery(MyQuery).CreateFields;
or borrowing some code from CreateFields:
MyQuery.FieldDefs.Update;
// create all defaults fields
for I := 0 to MyQuery.FieldDefList.Count - 1 do
with MyQuery.FieldDefList[I] do
if (DataType <> ftUnknown) and not (DataType in ObjectFieldTypes) and
not ((faHiddenCol in Attributes) and not MyQuery.FIeldDefs.HiddenFields) then
CreateField(Self, nil, MyQuery.FieldDefList.Strings[I]);
then create your calculated fields:
MyQueryMyField := TStringField.Create(MyQuery);
with MyQueryMyField do
begin
Name := 'MyQueryMyField';
FieldKind := fkCalculated;
FieldName := 'MyField';
Size := 10;
DataSet := MyQuery;
end;
Delphi now has the option to combine automatic generated fields and calculated fields : Data.DB.TFieldOptions.AutoCreateMode an enumeration of type TFieldsAutoCreationMode. This way you can add your calculated fields at runtime. Francois wrote in his answer how to add a field at runtime.
Different modes of TFieldsAutoCreationMode :
acExclusive
When there are no persistent fields at all, then automatic fields are created. This is the default mode.
acCombineComputed
The automatic fields are created when the dataset has no persistent fields or there are only calculated persistent fields. This is a convenient way to create the persistent calculated fields at design time and let the dataset create automatic data fields.
acCombineAlways
Automatic fields for the database fields will be created when there are no persistent fields.
You need to add all fields in addition to your calculated field.
Once you add a field, you have to add all of the fields that you want in the data set.
Delphi calls this persistent fields versus dynamic fields. All fields are either persistent or dynamic. Unfortunately, you can't have a mixture of both.
Another thing to note, from the documentation is
Persistent fields component lists are
stored in your application, and do not
change even if the structure of a
database underlying a dataset is
changed.
So, be careful, if you later add additional fields to a table, you will need to add the new fields to the component. Same thing with deleting fields.
If you really don't want persistent fields, there is another solution. On any grid or control that should show the calculated field, you can custom draw it. For example, many grid controls have a OnCustomDraw event. You can do your calculation there.
If you have know your to be calculated fields names at runtime, you can use something like that.
var
initing:boolean;
procedure TSampleForm.dsSampleAfterOpen(
DataSet: TDataSet);
var
i:integer;
dmp:tfield;
begin
if not initing then
try
initing:=true;
dataset.active:=false;
dataset.FieldDefs.Update;
for i:=0 to dataset.FieldDefs.Count-1 do
begin
dmp:=DataSet.FieldDefs.Items[i].FieldClass.Create(self);
dmp.FieldName:=DataSet.FieldDefs.Items[i].DisplayName;
dmp.DataSet:=dataset;
if (dmp.fieldname='txtState') or (dmp.FieldName='txtOldState') then
begin
dmp.Calculated:=true;
dmp.DisplayWidth:=255;
dmp.size:=255;
end;
end;
dataset.active:=true;
finally
initing:=false;
end;
end;
procedure TSampleForm.dsSampleAfterClose(
DataSet: TDataSet);
var
i:integer;
dmp:TField;
begin
if not initing then
begin
for i:=DataSet.FieldCount-1 downto 0 do
begin
dmp:=pointer(DataSet.Fields.Fields[i]);
DataSet.Fields.Fields[i].DataSet:=nil;
freeandnil(dmp);
end;
DataSet.FieldDefs.Clear;
end;
end;
procedure TSampleForm.dsSampleCalcFields(
DataSet: TDataSet);
var
tmpdurum,tmpOldDurum:integer;
begin
if not initing then
begin
tmpDurum := dataset.FieldByName( 'state' ).AsInteger;
tmpOldDurum:= dataset.FieldByName( 'oldstate' ).AsInteger;
dataset.FieldByName( 'txtState' ).AsString := State2Text(tmpDurum);
dataset.FieldByName( 'txtOldState' ).AsString := State2Text(tmpOldDurum);
end;
end;
procedure TSampleForm.btnOpenClick(Sender: TObject);
begin
if dsSample.Active then
dsSample.Close;
dsSample.SQL.text:='select id,state,oldstate,"" as txtState,"" as txtOldState from states where active=1';
dsSample.Open;
end;

Resources