[Solved]Using a description instead of an enumerated type
lazarus / 31-LCL
----------------------------------------------------------------------
Knipfty · 2012-08-27 16:53
Hi,
I have an enumerated type defined as
Code: [Select]Type TBookReverse = (brNew, brReverse);
In the DB table, the enumerated values are stored as integers, so
using the type above that would be a 0 or 1.
On a form I have a DBGrid that is connected to the TTable object. The
DBGrid's column shows the 0 or 1, however, I would like it to display
'New' or 'Reverse' instead.
I looked at the OnGetText/OnSetText event handlers that are associated
with TField, however, the documentation states:QuoteIt is not allowed
to change the state of the dataset or the contents of the field during
the execution of this event handler.
Is there another way to do this? The user is not allowed to edit the
data directly on the DBGrid.
----------------------------------------------------------------------
ezlage · 2012-08-27 16:58
You can do this way:
Code: [Select]var
t:string;
begin
WriteStr(t,brNew); //same of t:='brNew';
end;[code]
----------------------------------------------------------------------
Knipfty · 2012-08-27 17:16
Hi ezlage,
I'm trying to display 'New' in cell in a DBGrid. I don't understand
how a Writeln will work or where to put it if it does.
If I could use the onGetText event, I would expect the code to look
something like this:
OnGetTExt procedure definition
Code: [Select] procedure enumBR(Sender: TField; var aText: string;
DisplayText: boolean);
begin
case integer(aText) of
0: aText := 'New';
1: aText := 'Reverse'
end
end;
And then later on its assigned
Code: [Select] AdsTable1.FieldByName('EntryType').OnGetText:=@enumBR;
BTW, the above code does not compile. I get the following error msg:
Quotemain.pas(239,49) Error: Incompatible types: got "
" expected
""
So, I'm not even sure how to get OnGetText to work.
----------------------------------------------------------------------
ludob · 2012-08-27 17:24
OnGetText is the correct way of changing the text displayed in a
dbcontrol. You don't change the dataset when modifying the text
displayed. In your case you would do something like
Code: [Select]procedure TForm1.SQLQuery1idGetText(Sender: TField; var
aText: string;
DisplayText: Boolean);
begin
writestr(aText,TBookReverse(Sender.AsInteger));
end;This will display 'brNew' or 'brReverse'. If you want to to
customise the string do something like:
Code: [Select]const
REVSTRINGS:array [TBookReverse] of string = ('New','Reverse');
...
aText:=REVSTRINGS[TBookReverse(Sender.AsInteger)];
----------------------------------------------------------------------
taazz · 2012-08-27 17:30
Quote from: Knipfty on August 27, 2012, 07:16:49 pmHi ezlage,
I'm trying to display 'New' in cell in a DBGrid. I don't understand
how a Writeln will work or where to put it if it does.
If I could use the onGetText event, I would expect the code to look
something like this:
OnGetTExt procedure definition
Code: [Select] procedure enumBR(Sender: TField; var aText: string;
DisplayText: boolean);
begin
case integer(aText) of
0: aText := 'New';
1: aText := 'Reverse'
end
end;
type casting a string to an integer does not return the value of the
string as integer you need to use strtoint function
or simple
if aText = '0' then atext := 'New'
also although you are not allowed to change the state of the dataset
or fields data it doesn't mean you can read the data from inside this
event eg
if Sender.AsInteger = 0 then atext := 'New';
Quote from: Knipfty on August 27, 2012, 07:16:49 pmAnd then later on
its assigned
Code: [Select] AdsTable1.FieldByName('EntryType').OnGetText:=@enumBR;
BTW, the above code does not compile. I get the following error msg:
Quotemain.pas(239,49) Error: Incompatible types: got "" expected
""
So, I'm not even sure how to get OnGetText to work.
well this error tells me that the enumBR procedure is not a method of
an object or it has been declared with a call type other than register
eg
procedure enumBR(Sender: TField; var aText: string; DisplayText:
boolean); cdecl;
from your code I assume that the procedure is not part of an object.
----------------------------------------------------------------------
ezlage · 2012-08-27 17:38
Sorry, I didn't understood well.
WriteStr(not WriteLn) stores name of enumerated value in a var as
string.
So, your field must be ftString.
But, my suggestion maybe not helpful...
----------------------------------------------------------------------
Knipfty · 2012-08-27 18:02
Working off if taazz and ludob, I created a procedure as part of teh
TForm1 object:
Code: [Select]procedure TForm1.AdsTable1OnGetText(Sender: TField; var
aText: string; DisplayText: boolean);
begin
case Sender.AsInteger of
0: aText := 'New';
1: aText := 'Reverse';
otherwise aText := 'undefined'
end
end;
That seems to work as far as the compiler is concerned.
But now when I try to assign the procedure to the event handler with
this code in the TForm1.ForShow method: (And where should I make this
assignment?)
Code: [Select]AdsTable1.FieldByName('EntryType').OnGetText:=@TForm1.Ad
sTable1GetText;
I ge tthe following compiler error msg
Quotemain.pas(284,59) Error: identifier idents no member
"AdsTable1GetText"
----------------------------------------------------------------------
typo · 2012-08-27 18:09
You have mistyped the method name: AdsTable1OnGetText.
----------------------------------------------------------------------
Knipfty · 2012-08-27 18:13
Well now, that's embarrassing...
And it now compiles...
I'll test this all when I get home this evening. Thanks
----------------------------------------------------------------------
taazz · 2012-08-27 18:21
Quote from: Knipfty on August 27, 2012, 08:02:04 pmCode: [Select]AdsTa
ble1.FieldByName('EntryType').OnGetText:=@TForm1.AdsTable1GetText;
I ge tthe following compiler error msg
Quotemain.pas(284,59) Error: identifier idents no member
"AdsTable1GetText"
You do know that you can't use the class to assign a non class method
to an event right?
You need to to use a created object (or the variable that hold the
reference to that object) in this case I assume that form1 is the
global variable of this test case and if the code above is part of the
form code then self would be a better choice.
----------------------------------------------------------------------
Knipfty · 2012-08-27 18:41
Hi taazz,
In my test code that I set up to figure this out, I used the
TForm1.FormShow event handler
Code: [Select]procedure TForm1.FormShow(Sender: TObject);
begin
AdsTable1.FieldByName('EntryType').OnGetText:=@TForm1.AdsTable1OnG
etText;
end;
Is this incorrect?
Are you suggesting:
Code: [Select]procedure TForm1.FormShow(Sender: TObject);
begin
AdsTable1.FieldByName('EntryType').OnGetText:=@Self.AdsTable1OnGet
Text;
end;
----------------------------------------------------------------------
Knipfty · 2012-08-27 18:52
One more question:
Do I need to also create an OnSetText event handler? Such as
Code: [Select]procedure TForm1.AdsTable1OnSetText(Sender: TField; var
aText: string; DisplayText: boolean);
begin
case Sender.AsString of
'New': aText := '0';
'Reverse': aText := '1'
end
end;
The user cannot edit directly in the DBGrid, so I don;t know if its
needed. I believe I have the DBGrid set to nto allow editting.
Thanks
----------------------------------------------------------------------
taazz · 2012-08-27 19:48
Quote from: Knipfty on August 27, 2012, 08:41:08 pmHi taazz,
In my test code that I set up to figure this out, I used the
TForm1.FormShow event handler
Code: [Select]procedure TForm1.FormShow(Sender: TObject);
begin
AdsTable1.FieldByName('EntryType').OnGetText:=@TForm1.AdsTable1OnG
etText;
end;
Is this incorrect?
I am not sure how FPC handles this kind of things but in my mind
Tform1 is a class not an object I do not know if an object has been
created for that class unless I use a variable that has a reference to
an object of TForm1 class. This means that it needs extra checking in
order to make sure that everything is in place or you will get various
SIGSEGV errors.
Quote from: Knipfty on August 27, 2012, 08:41:08 pmAre you suggesting:
Code: [Select]procedure TForm1.FormShow(Sender: TObject);
begin
AdsTable1.FieldByName('EntryType').OnGetText:=@Self.AdsTable1OnGet
Text;
end;
although self in this context is not needed it makes more sense to me
and does not raise any red flags when I read it.
So yes its a lot better from my point of view
Quote from: Knipfty on August 27, 2012, 08:52:32 pmOne more question:
Do I need to also create an OnSetText event handler? Such as
Code: [Select]procedure TForm1.AdsTable1OnSetText(Sender: TField; var
aText: string; DisplayText: boolean);
begin
case Sender.AsString of
'New': aText := '0';
'Reverse': aText := '1'
end
end;
The user cannot edit directly in the DBGrid, so I don;t know if its
needed. I believe I have the DBGrid set to nto allow editting.
Thanks
You do create a OnSetText event if you want to allow the user to enter
something other than the expected value for example
if you want the end user to be able to type in New, N or Reverse, R
and translate it to 0 or 1 then you use the OnSetText event.
If you use a DBcomboBox for the selection then you ignore the
onsetText and work with the combobox it self. In any case when the
OnSetText event is fired Sender.asstring will contain the existing
value not the new one so you can't use it to check for the new value
in this case you need to use the aText variable.
eg
Code: [Select]procedure TForm1.AdsTable1OnSetText(Sender: TField; var
aText: string; DisplayText: boolean);
begin
case UpperCase(aText) of
'NEW', 'N' : aText := '0';
'REVERSE', 'R' : aText := '1';
end;
end;
In my defense I have only used those 2 events on password fields where
I encrypt and decrypt them on the fly on the user edit form and the
ongettext only to show some data inside a grid for a memo fields.
Regards.
----------------------------------------------------------------------
Knipfty · 2012-08-27 20:06
Hi taazz,
I see what you are saying about TForm1 vs. Self. I've changed the
code to 'Self' and will test it out this evening when I am home. (I
don't have my project here at work to play with ).
Since the user is not going to be entering any data on the DBGrid, I
will skip the OnSetText for now. I have them coded, just in case, and
can bring them in pretty quickly.
And finally, no need to get defensive, it compiles in my little test
environment and that was a major hurdle. Now I can at least test it
and see what happens.
From where I am sitting, if you want to make good use of Enumerated
types, then you need a good way to make it transparent all the way
thru to the DB. So hopefully this will work and I can expand my usage
of them.
I really appreciate your (really everyone's) help and will post what I
find over the next couple of days,
Knipfty
----------------------------------------------------------------------
taazz · 2012-08-27 20:19
Enumerated types are great and you can use a number of visual
components to help the user choose (combo boxes, radio groups etc)
which can be automated using rtti.
In most cases I prefer a master detail relationship where I can define
any number of enumeration after the application has been written and
delivered but this makes the code a lot trickier to write and test for
various conditions.
Glad I could be of help.
----------------------------------------------------------------------
Knipfty · 2012-08-27 20:38
I've used them with DBComboLookup and DBRadioGroups where it was
straight forward and natural (at least to me). This is a bit more
work on a DBGrid.
I actually have a similar requirement on creating a fee schedule in my
application, but have to leave it to the user to define at a later
date. Not sure yet how I will implement it, but it could be via
enumerate types if I get this working. Or perhaps a simple DB look
up.
Thanks again
Knipfty
----------------------------------------------------------------------
Knipfty · 2012-08-28 01:24
OK, here is what I ended doing:
1. I created two TField OnGetText event handlers (one for each column
I wanted to affect)
Code: [Select]procedure
TTransRevenueBrowser.AdsRevenueTableBROnGetText(Sender: TField; var
aText: string; DisplayText: boolean);
begin
aText := BookReverseDesciption(TBookReverse(Sender.AsInteger))
end;
procedure TTransRevenueBrowser.AdsRevenueTableRTOnGetText(Sender:
TField; var aText: string; DisplayText: boolean);
begin
aText := RevenueDesciption(TRevenue(Sender.AsInteger))
end;
2. Then in the table's data source DataChange event handler I added
the following:
Code: [Select]procedure
TTransRevenueBrowser.DatasourceRevenueDataChange(Sender: TObject;
Field: TField);
begin
AdsRevenueTable.FieldByName('EntryType').OnGetText:=@AdsRevenueTable
BROnGetText;
AdsRevenueTable.FieldByName('RevenueType').OnGetText:=@AdsRevenueTab
leRTOnGetText
end;
I tried the above code in a number of event handlers, this one seems
to work in all cases. I am open to other suggestions.
In the TForm object definition I added:
Code: [Select] procedure AdsRevenueTableBROnGetText(Sender: TField;
var aText: string; DisplayText: boolean);
procedure AdsRevenueTableRTOnGetText(Sender: TField; var aText:
string; DisplayText: boolean);
And that was it. The code replaces the enumerated values with the
desired strings. I suspect, but can't figure out, that I should be
able to use the Sender and/or the Field in step two rather than the
code I used. TField is always nil. My guess is that its because But
it is working.
Thanks everyone for your help!
Knipfty
----------------------------------------------------------------------
taazz · 2012-08-28 02:00
I wouldn't use the datasource.datachange event for that what you can
use depends heavily on how your application is designed.
Why you do not use the designer to select the events on the fields and
you are assigning them through code?
----------------------------------------------------------------------
Knipfty · 2012-08-28 08:54
Where? I couldn't find any other place that worked. Certainly not on
the field level.
----------------------------------------------------------------------
avra · 2012-08-28 09:07
Knipfty,
you do not need to assign OnGetText when ever data is changed. It is
enough if you do it just once, like after creation. Just think about
it - the same value is reassigned over and over again.
In this particular case using Form Designer and Object Inspector is
more productive then assigning events programmaticaly as you do. Just
click on your Dataset component, and in Object Inspector choose Events
tab and find OnDataChange. Double clicking to it's dropdown box is a
standard way to assign events in IDE.
Also, if you have zillion rows to show you will find that
FieldByName() is very slow, and you use it twice for each row. If you
want to speed it up then you can also do it just once and use the
result where ever needed. For more info take a look at this thread:
http://stackoverflow.com/questions/4765931/why-is-my-code-so-slow.
----------------------------------------------------------------------
taazz · 2012-08-28 09:22
In general if the dataset is on the form where the event is written
and the fields are persistent then use the object inspector to select
the events directly from the fields.
if the dataset is on the same form but with out persistent fields use
the datasets afteropen event.
if the dataset is not on the same form and there are persistent fields
then use the forms onCreate event making sure that the container that
holds the dataset is created before the form.
if the dataset is not on the same form and there are no persistent
fields then things start to get interesting, there are a number of
things here as well is the datasource (not dataset) on the form or
not, what is the dataset's container?
Well as you can see there are to many variables for me to give a
specific answer so it would be beneficial to both of us a small test
project which duplicates your design as close as possible.
Regards
----------------------------------------------------------------------
Knipfty · 2012-08-28 10:33
Hi taazz,
No need for a sample project. The dataset is on the same form as the
DBGrid AND the fields are not persistent. I'll recode this evening
and see what I end up with. DO I use the same line I currently have?
Code: [Select]procedure
TTransRevenueBrowser.DatasourceRevenueDataChange(Sender: TObject;
Field: TField);
begin
AdsRevenueTable.FieldByName('EntryType').OnGetText:=@AdsRevenueTable
BROnGetText;
AdsRevenueTable.FieldByName('RevenueType').OnGetText:=@AdsRevenueTab
leRTOnGetText
end;
Just with the different procedure definition.
Or, do I somehow use TField and Sender? I played with these in other
event handlers, but could not get them to work.
Knipfty
----------------------------------------------------------------------
taazz · 2012-08-28 17:15
To be sure you need to double click the AfterOpen event editor on your
object inspector but I think it should create the following procedure
for you
Code: [Select]procedure TForm1.AdsRevenueTableAfterOpen(DataSet:
TDataSet);
begin
AdsRevenueTable.FieldByName('EntryType').OnGetText:=@AdsRevenueTable
BROnGetText;
AdsRevenueTable.FieldByName('RevenueType').OnGetText:=@AdsRevenueTab
leRTOnGetText
end;
In the rare occasion where the dataset sql or underline table is
changed you will get a sigsegv error if the fields are not found in
the result set. I usually code it like this
Code: [Select]procedure TForm1.AdsRevenueTableAfterOpen(DataSet:
TDataSet);
var
FldType1, fldType2 : TField;
begin
FldType1 := AdsRevenueTable.FieldByName('EntryType');
FldType2 := AdsRevenueTable.FieldByName('RevenueType');
if assigned(FldType1) then
FldType1.OnGetText:=@AdsRevenueTableBROnGetText
else Showmessage('Field < EntryType > Not found in the underline
dataset');
if Assigned(FldType2) then
FldType2.OnGetText:=@AdsRevenueTableRTOnGetText
else Showmessage('Field < RevenueType > Not found in the underline
dataset');
end;
well not exactly the message is a resource string, I'm using format
and I"m raising a exception instead of show message but you get the
main point.
----------------------------------------------------------------------
Knipfty · 2012-08-28 17:24
Hi taazz,
Instead of hard coding the table name, could I use DataSet? So
something like this"
Code: [Select]procedure TForm1.AdsRevenueTableAfterOpen(DataSet:
TDataSet);
begin
DataSet.FieldByName('EntryType').OnGetText:=@AdsRevenueTableBROnGetT
ext;
DataSet.FieldByName('RevenueType').OnGetText:=@AdsRevenueTableRTOnGe
tText
end;
I do like you second example and will probably follow it.
Thanks
Knipfty
----------------------------------------------------------------------
taazz · 2012-08-28 19:03
As long as you are sure that the dataset you attach this event has the
required fields I see no problem to use the Dataset instead of the
table it self.
----------------------------------------------------------------------
Knipfty · 2012-08-28 19:09
Cool. I'll play with it this evening and see what happens.
Thanks, your help is greatly appreciated
Knipfty
----------------------------------------------------------------------
Knipfty · 2012-08-28 23:38
Hi taazz,
worked like a charm!
Code: [Select]procedure
TTransRevenueBrowser.AdsRevenueTableAfterOpen(DataSet: TDataSet);
begin
AdsRevenueTable.FieldByName('EntryType').OnGetText:=@AdsRevenueTable
BROnGetText;
AdsRevenueTable.FieldByName('RevenueType').OnGetText:=@AdsRevenueTab
leRTOnGetText
end;
Thanks!
----------------------------------------------------------------------
Knipfty · 2012-08-30 13:30
avra,
I finally had a chance to look at the link you provided. THANKS!
Gave me something to think about. It would appear that FieldByName
loops thru the table structure until it finds the field I am looking
for. Using Field[some index value] would be more efficient program
performance wise, but less efficient my time wise as my table
structures are still changing. Ugh!
I would agree though that FieldByName should be banished from loops.
Knipfty