Graphing empty input boxes using TChart - delphi

I'm getting the user to enter coefficients (as strings) for terms from a constant (e.g. 2) all the way up to a sextic (e.g. 3X^6).
The user enters each coefficient without the X-term attached to it, so 3X^6 is entered just as 3.
Upon clicking an okay button, the idea is to transfer the coefficients which are valid (checked using a RegEx expression) to a procedure ('CreateGraph') which actually creates the graph using TChart.
Upon clicking the okay button, a loop for i = 1 to 7 (number of coefficients) is used to check if each term is valid. The issue I am having is that I get a string conversion error when converting the coefficients in the CreateGraph procedure as the other coefficients except those with data in, are set to '', which TChart won't accept.
Here's the current procedure:
procedure TfrmGetFunction.btnAddFunctionClick(Sender: TObject);
var
i : integer; // Loop counter.
begin
for i := 1 to 7 do
begin
if CheckCoefficientBoxesValidInput(CoefficientEdit[i].CoEditBox) then
frmGraphingMode.CreateGraph(CoefficientEdit[i].CoEditBox);
end;
end;
Is the best way to just set the inactive coefficient edit boxes to '0'?
The (potential) issue I have with that is that when the user wants to enter another set of coefficients or comes back to the 'Enter Function' form, the values are all set to 0 which may be confusing.
The next issue (related to the first) that I am having, is that the CreateGraph procedure is called each time i is incremented, which means that a load of empty input boxes are passed, which TChart doesn't like.
Here's my current procedure:
var
i : integer; // Loop counter.
Coeff : array[1..7] of string;
begin
for i := 1 to 7 do
begin
Coeff[i] := CoefficientEdit[i].CoEditBox.Text;
if Coeff[i] = '' then
Coeff[i] := '0';
frmGraphingMode.CreateGraph(Coeff[i]);
end;
end;
What is the easiest way to solve this problem? I was thinking of having a boolean variable which is set to true when the loop is complete (i.e. i = 7 is when all the empty (if so) boxes would be filled in); is there a better way?

Simply check for the empty string and replace with '0'.
var
Coeff: string;
....
Coeff := CoefficientEdit[i].CoEditBox.Text;
if Coeff = '' then
Coeff := '0';
I'm assuming that CoEditBox is an edit control. If so then I would comment that you should not pass an edit control to CheckCoefficientBoxesValidInput and CreateGraph. Those functions should receive string arguments. You are making them needlessly coupled to a particular GUI design.
Perhaps this is the root cause of your problems. You comment that it is confusing for the edit controls to be changed to contain zeros. Well, you don't need to, and should now, change the edit controls. Separate the GUI from the charting. Don't pass around edit controls. Pull the contents from the controls, optionally process it, and pass it on.
Finally, instead of using a regex to check if a value is a number, call TryStrToFloat.

Related

How to search a FMX.TListView header as well as items

I have a LiveBindings databound FMX.TListView with the FieldName being Stage and the FillHeaderFieldName being Production. When the app is running, I see a list of Productions using the HeaderAppearance, and within each Production, there is a list of Stages using the ItemAppearance. I've turned on SearchVisible to get the components search panel to show at the top of the list.
Currently, typing into the search box only filters on the Stage, and not the Production.
I'd like to be able to do both, and I'd like to be able to do it without making another REST call with filter parameters. I understand I would probably need to write an event handler for the OnSearchChange event, and I have this bit of code to get the search text entered:
List := Sender as TListView;
for I := 0 to List.Controls.Count-1 do
if List.Controls[I].ClassType = TSearchBox then
begin
SearchBox := TSearchBox(List.Controls[I]);
break;
end;
And I think I need to set the Items.Filter property, and I used this bit of code:
Lower := LowerCase(SearchBox.Text.Trim);
List.Items.Filter :=
function(X: string): Boolean
begin
Result:= (Lower = EmptyStr) or LowerCase(X).Contains(Lower);
end;
One of the problems is that the ListView component is applying its filtering as soon as a character is typed, while the OnSearchChange event only fires when the searchbox loses focus.
The second problem is that, even after the event is fired and the new filter function set, nothing happens to the list.
I've confirmed that the List.Items collection in my "36" example does actually contain all 6 items - the 3 header items and the 3 detail items - so I'm unsure why the filter is not applying to the header items as it does the detail items.
I tried this out and found a solution. Keep in mind I don't have access to Delphi 10.3 Rio. I'm using 10.1 Berlin. Also keep in mind that what I usually do is bind in the code and not visually. But for this I stuck to visual binding.
As a dataset I have used a TFDMemoryTable (mt1) with 2 data fields (fmt1Prod and fmt1Stage) and 1 calculated field (fmt1Search). I have the following handler to calculate the Search field:
Procedure TForm2.mt1CalcFields(DataSet: TDataSet);
Begin
fmt1Search.AsString := fmt1Prod.AsString + '|' + fmt1Stage.AsString;
End;
I put some random data in the memory table OnFormCreate:
Procedure TForm2.FormCreate(Sender: TObject);
Var
i1, i2: Integer;
s1, s2: String;
Begin
mt1.CreateDataSet;
For i1 := 1 To 13 Do Begin
s1 := 'Prod' + FormatFloat('00', i1);
For i2 := Random(6) To Random(14) Do Begin
s2 := 'Stage' + FormatFloat('00', i2);
mt1.Append;
fmt1Prod.AsString := s1;
fmt1Stage.AsString := s2;
mt1.Post;
End;
End;
End;
I have put on Form2 a TGrid and a TListView. Both are bound to the dataset. Data and calculated fields show up properly in the TGrid (just to check).
The TListView is bound to the dataset as follows:
Synch <-> *
ItemHeader.Text <- Prod
ItemHeader.Break <- Prod
Item.Text <- Search
Item.Detail <- Stage
I did this because I cannot find a way to have the TListView searchbox work on anything but the Text of the items. Ok then... but this can be worked around though:
Set TListView.ItemAppeance to Custom
Find the TListView/ItemAppearance/Item/Text object in the structure and set Visible to False
Find the TListView/ItemAppearance/Item/Detail object in the structure and set Visible to True
I'm not sure all of the above is necessary, but it works. If your TListView is editable then you will probably need to fiddle with the ItemEditAppearance too.
Remember that with custom item appearance you can actually set the list view items to look just about anyway you want. You can add and remove labels, images and other things. It's not as powerful as designing a form, but you can do a lot with it. But all you really need here is to hide the search text and show the stage text somewhere in the item.
And... for more sophisticated item appearance you may have to do some code binding (non sure of this though).
If use bind visually and ItemAppearance (Dynamic Appearance) you can one column from data source assign to header and text item (visible = false). In this situation in header and item we have the same value and search work fine.

Which way is the "correct" one to define a shortcut in Delphi?

There are many examples on how to define a ShortCut in a Delphi program, but
they boil down to just two different ways:
Add any of the scCtrl, scShift and scAlt constants to Ord() of the key
Use the Menus.ShortCut function
e.g.
Action.ShortCut := scCtrl + scShift + Ord('K');
// vs
Action.ShortCut := Menus.ShortCut(Word('K'), [ssCtrl, ssShift]);
Is one of these two ways preferable? If yes, which one and why?
The code is almost identical, but ShortCut has some additional checks:
function ShortCut(Key: Word; Shift: TShiftState): TShortCut;
begin
Result := 0;
if HiByte(Key) <> 0 then Exit; // if Key is national character then it can't be used as shortcut
Result := Key;
if ssShift in Shift then Inc(Result, scShift); // this is identical to "+" scShift
if ssCtrl in Shift then Inc(Result, scCtrl);
if ssAlt in Shift then Inc(Result, scAlt);
end;
Because RegisterHotKey function uses Virtual key codes (which has values from $00 to $FE) this additional check is significant.
Note that instead of Ord documentation, real Ord function returns smallint (signed Word), so using national characters can change modificators, that contained in Hi-byte of ShortCut value.
So, more preferably is use ShortCut function.
I'd say that whenever there is a function that does the job, it is better to use the function.
Because given the chance in the future that something changes, having a function gives you a "hard link" to the call, so if the function becomes deprecated, you are notified, and if the function logic changes, you get the update silently.
Otherwise, you will not benefit from this.
Now in this particular case, what are the chances for the definition of a shortcut to change in the next 10-20 years?
Probably none. But I'd still advocate the function call (if not for anything, but you don't have to remember the logic (was it addition or was it logical ORing? one might ask himself later on)

Delphi TValueListEditor Strings prop-ed quirk?

Investigating odd behaviour of a TValueListEditor being used to generate a filter
expression for a ClientDataSet, I've traced it to a situation where if the first entry
in it apparently had nothing in the Value column, it returned #13#10 as the Value, rather than
''.
In the following, the TStringlist TL is initialized with the same contents as the ValueListEditor
Strings property has in my app. The Assert does not fail for the TStringlist, but it does for the
ValueListEditor. These results occurred with D7 and XE4.
procedure TDefaultForm.ApplyFilter;
var
i,
Max : Integer;
Key,
Value : String;
TL : TStringlist;
begin
TL := TStringlist.Create;
try
TL.Add('Country=');
TL.Add('Class=CON');
for i:= 0 to TL.Count - 1 do begin
Key := TL.Names[i];
Value := TL.Values[Key];
Assert(Value <> #13#10); // succeeds for all i
end;
Max := ValueListEditor1.RowCount;
for i:= 1 to Max do begin
Key := ValueListEditor1.Keys[i];
Value := ValueListEditor1.Values[Key];
// Value := ValueListEditor1.Strings.ValueFromIndex[i-1];
Assert(Value <> #13#10); //Fails for i = 1!
end;
finally
TL.Free;
end;
end;
Btw, the TVLE was set up entirely in the Object Inspector: I simply dragged a TVLE off the palette, clicked Strings in the OI, clicked in the LH cell and typed 'Country' (sans quotes), pressed the Down key and typed 'Class' then right-arrow and typed 'CON'.
Obviously, I could avoid this by Value := Trim(Value), but was curious where the #13#10 was coming from.
Update: Prompted by #Deltic's answer and helpful comments, I decided to re-trace my steps and added another TVLE to my form. The following extracts from the DFM are revealing:
object ValueListEditor1: TValueListEditor
Left = 16
Top = 224
Width = 306
Height = 135
KeyOptions = [keyEdit, keyAdd]
Strings.Strings = (
'Country='#13#10
'Class=CON')
TabOrder = 2
end
[...]
object ValueListEditor2: TValueListEditor
Left = 440
Top = 192
Width = 306
Height = 246
KeyOptions = [keyEdit, keyAdd]
Strings.Strings = (
'A='
'B=ValueOfB')
TabOrder = 5
end
So, with hindsight, my question really boils down to how did the #13#10 get into the DFM? And then it came back to me ...
With no previous experience of the TVLE, when I set up the form, I got stuck at the point where I needed to add a second row. I tried pressing [Enter], but that did nothing, so then I tried Ctrl-Enter and that did nothing either. But repeating the exercise now has confirmed that that's how the CR/LF got into the TVLE's Strings.
So, it seems that the answer to my q is "No, the TVLE isn't broken, but its Strings property editor
has a quirk regarding Ctrl-Enter". In other circs, I would consider deleting my q, seeing as it's at least partly caused by operator aberration, but perhaps it's better left to assist any others who trip over the same point.
Update #2 I see that my curiousity has earned me a -1. Fair enough, but I'm still inclined to leave this q & a in place, if only as an illustration of the fact that problems have deterministic causes, which can often be identified by simple things such as re-tracing one's steps, particularly with someone obviously knowledgeable looking over one's shoulder, as it were. Perhaps the down-voter would care to enlighten readers what help to future readers such a silent -1 is.
You have not shown how your value list editor is initialised, and I suspect that this is where your problem is. Behind a TValueListEditor is nothing more than a TStringList (strictly speaking a subclass of one, but the subclass doesn't change the fundamental behaviour w.r.t named values).
If your apparently empty value in the value list is yielding a value of #13#10 then it must be because that is the actual value that it has.
This simple test snippet verifies this:
var
i:Integer;
k, v:String;
begin
ed.InsertRow('Country', '', TRUE);
ed.InsertRow('Class', 'CON', TRUE);
for i:= 1 to ed.RowCount - 1 do
begin
k := ed.Keys[i];
v := ed.Values[k];
ASSERT(v <> #13#10); // Never fails
end;
end;
Where ed is a TValueListEditor on the form.
Replace the first line of code in the above snippet with this however:
ed.InsertRow('Country', #13#10, TRUE);
And the ASSERT() fails.
I suggest you investigate the initialisation of your value list editor. My guess is that it is being populated by reading from a file using a mechanism which is reading the entire line into a string, including the line end sequences, and the code that is adding the values for each read line is not stripping the #13#10 line terminators, resulting in the values being added as <name>=<value>#13#10 in each case.

How to prevent wrong inputs (only numbers) in Delphi?

I am trying to build a function/check to prevent wrong inputs from keyboard and I am a little bit lost here.
function ReadInputs : integer;
var
number : integer;
begin
repeat
Write('Set random number (1-10): ');
Readln(number);
if NOT((number <= 10) AND (number >= 1)) then
begin
Writeln('Error! Type 1-10!');
end;
until (number >= 1) AND (number <= 10);
result := column;
end;
How to prevent from any other character to be input except numbers 1-10? Why only numbers define in my function is not enough even when I set integer? When I type for example "A" it crash, so what is the right way? Thank you.
As it stands your program will fail with an error if the user inputs something that cannot be converted to an integer. That's because the variable that you passed to Readln is typed as an Integer. That is effectively an assertion that the user enters a number. But you want to be more flexible than that and allow the user to recover from non-numeric input.
What you need to do is read a string. This will always succeed. Then you can decide how to handle that string. For example you would try to convert to integer, and if that succeeded, perform further validity checks.
Perhaps like this:
var
Input: string;
Num: Integer;
....
Readln(Input);
if TryStrToInt(Input, Num) then
// perform checks on Num, etc.
else
// handle error: the value input was not numeric
You've already had a good answerfrom David H, but a little more explanation might help.
The ReadLn() procedure dates from before applications had GUIs and doesn't really restrict what the user can type in; the user might just press [return] or type characters that aren't digits (or +/-). ReadLn(AnInteger) will succeed if what the user types happens to convert to an integer, otherwise it fails.
On the other hand, Readln(AString) will always succeed, and the problem then is just how to check that it represents an integer, and DH's answer shows you how to do that.
In case you're wondering, a GUI application, you can control what characters an edit control will accept, e.g. by using a TMaskEDit, which allows you specify what character patterns are acceptable (e.g 6 digits and nothing else) - if the user types something which doesn't match the mask, the edit control doesn't accept it. However, even if you use a TMaskEdit, it's best to check that what's been typed in actually converts to the number type you're wanting.
Or you could use this on the OnKeyPress event:
if NOT(key in['0'..'9', #8]) then
key := #0;

How can I implement "writable calculated" fields in a TDataSet?

I am in the need to add additional fields to a TDataSet that don't exist in the underlying database but can be derived from existing fields. I can easily do this with caclulated fields and that works perfectly.
Now I want to edit these fields and write the changed data back. I can reverse the calculation to write the data back into the existing fields, but the DB controls just don't let me edit calculated fields.
Is there any approach that allows me to do this?
Update:
Ok, some more details about the background.
The dataset has a blob field, which is a TBytes representation. Some of the bytes are identified to contain information that can be represented in a convenient way with existing DB edit fields. Not all of the bytes are known, though, so the TBytes representation has to be kept as it is for processing through another application that knows about it. This app also modifies existing and inserts new records.
The TBytes of different records in the dataset often map to different fields representations, although setting a filter or range on the dataset will ensure that they have the same mapping.
As I said, extracting the known bytes and convert it into strings, dates, numbers and so on via calculated fields is no problem. Reconverting those values into the TBytes is also possible. The problem is making those extra fields editable, while keeping the dataset navigation intact.
If it helps: We have classes that do the bidirectional mapping, exposing the fields as published properties.
The answer depends on a data access components you are using. I am using Anydac and it support fkInternalCalc fields, which may be as calculated as manually edited.
I think calculated fields are by definition read-only, with values calculated on the client.
What you want could probably be implemented by an updatable view. You could define the view with calculated fields already - these would be calculated in SQL on the server - and an update trigger, maybe an insert trigger, too - to perform the reverse calculation. Then from the client you could use the view transparently like a table.
I had similar Issue with a ClientDataSet, i have solved this issue using dummy fileds on the SQL-Stmt so i could simulate the Fields in the Database.
See my Question
You can use TDatasetProvider.OnGetRecords (doesn't remember if this is the correct name of the event) and modify the datapacket sent to the Clientdataset.
Of course, you'll have to deal with them on an ApplyUpdates handler, as TOndrej said.
Woll2Woll's infopower components (I just tested their TwwDBEdit) allow to do such thing. So I would think whatever blocks you is at the TDBEdit level (or at the TFieldDataLink level).
What exactly differs in TwwDBEdit, I don't know. (And I'm not sure the license agreement would allow me to post here...).
In our database design, some values are percentages relative to another column (called oMean below), whereas other float values are stored as absolutes. Our customers later wanted both options (rel. and abs.) for all fields, so we came up with the following class derived from TFloatField. It should work for all TDataSet descendants.
unit EditableCalcFloatField;
interface
uses
db, classes;
type
TEditableCalcFloatField = class(TFloatField)
public
oAbs, oRel, oMean: TField;
protected
function GetCanModify: Boolean; override;
procedure SetAsFloat(Value: Double); override;
end;
implementation
function TEditableCalcFloatField.GetCanModify: Boolean;
begin
Result := oMean.AsFloat <> 0;
if not Result then Exit;
Result := (oAbs <> nil) or (oRel <> nil);
end;
procedure TEditableCalcFloatField.SetAsFloat(Value: Double);
var
fMean : Double;
begin
inherited;
if DataSet.State in [dsEdit, dsInsert] then begin
fMean := oMean.AsFloat;
if fMean = 0 then Exit;
if oAbs <> nil then
oAbs.AsFloat := Value / 100 * fMean
else
oRel.AsFloat := Value / fMean * 100;
end;
end;
end.
To use it without a package, you have to create the field in FormCreate, before the dataset is opened:
with TEditableCalcFloatField.Create(Self) do
begin
oAbs := sqlMerkmaleYourAbsoluteColumn;
DisplayLabel := sDisp;
oMean := sqlMerkmaleAssignedValue_Mean;
Calculated := True;
FieldName := 'R' + oAbs.FieldName;
DataSet := sqlMerkmale;
end;
And of course, its contents can be set either in the OnCalcFields event or by the user.
Use a TQuery descendant (MyQuery) with 'Select *, 0 as TempField from MyTable'
Procedure MyQueryAfterOpen(Dataset:TDataSet);
Begin
DataSet.FieldByName('TempField').ReadOnly := False;
End;
It is now an editabe temporary field

Resources