Is there a way to call a StringGrid OnCellDraw during runtime - delphi

I have a program that tracks the days during the year which are booked. In order to display this I have a StringGrid which I use Colors to display the days booked. The days booked are stored in ar2Booking which is a 2D array which contains the days and months respectively.
procedure TfrmClient.stgYearPlan1DrawCell(Sender: TObject; ACol, ARow: Integer;
Rect: TRect; State: TGridDrawState);
var
k, iMonth, iDay : Integer;
begin
for k := 1 to 31 do
stgYearPlan1.Cells[k,0] := IntToStr(k);
for k := 1 to 12 do
stgYearPlan1.Cells[0,k] := ShortMonthNames[k];
for iDay := 1 to 31 do
for iMonth := 1 to 12 do
begin
if ar2Booking[iDay,iMonth] = 'Y' then
begin
if (ACol = iDay) and (ARow = iMonth) then
begin
stgYearPlan1.Canvas.Brush.Color := clBlack;
stgYearPlan1.Canvas.FillRect(Rect);
stgYearPlan1.Canvas.TextOut(Rect.Left,Rect.Top,stgYearPlan1.Cells[ACol, ARow]);
end;
end;
if ar2Booking[iDay,iMonth] = 'D' then
begin
if (ACol = iDay) and (ARow = iMonth) then
begin
stgYearPlan1.Canvas.Brush.Color := clSilver;
stgYearPlan1.Canvas.FillRect(Rect);
stgYearPlan1.Canvas.TextOut(Rect.Left+2,Rect.Top+2,stgYearPlan1.Cells[ACol, ARow]);
end;
end;
end;
end;
I then want to click a button during runtime which allows a user to book a date. I would then like the date they select to reflect in the StringGrid. If I update the array how would I be able to run the OnCellDraw again in order to reflect the new booked dates?
Thanks

Generally you would invalidate part of the control causing it to be redrawn with the next windows paint message. The methods of a TStringGrid to do this are protected so you need to use a cracker class to access them.
// -- add to the type section
type
TStringGridCracker = class(TStringGrid);
procedure TForm1.Button1Click(Sender: TObject);
begin
TStringGridCracker(StringGrid1).InvalidateCell(1,2);
end;

I discovered after a friend showed me, the StringGrid.Redraw procedure accomplishes what I need. Thanks everyone

Related

How to make the same button run different code everytime it is clicked?

I am currently doing a school project, I am making a Credit Card machine. I need the 'Enter Button' to
run different code when it is clicked. The first click must get the card number from an edit ps... (I clear the edit once the card number has been retrieved), and the second click must get the pin from the same edit.
How would I do this?
procedure TfrmMainMenu.btbtnEnterClick(Sender: TObject);
var
sCvv,sPin:string;
begin
iCount2:=0;
sCardNumber:=lbledtCardInfo.Text;
if (Length(sCardNumber)<>16) AND (iCount2=0) then
begin
ShowMessage('Card number has to 16 digits,please try again!!');
end
else
begin
Inc(iCount2);
lbledtCardInfo.clear;
lbledtCardInfo.EditLabel.Caption:='Enter Pin' ;
btbtnEnter.Enabled:=false;
end; //if
if iCount2=2 then
begin
btbtnEnter.Enabled:=true;
sPin:=lbledtCardInfo.Text;
ShowMessage(sPin);//returns a blank
end;
You could try to do everything in a single event handler. There are several different ways to handle that. However, a different solution would be to use separate event handlers for each task, and then each task can assign a new handler for the next click to perform, eg:
procedure TfrmMainMenu.FormCreate(Sender: TObject);
begin
// you can set this at design-time if desired...
btbtnEnter.OnClick := GetCCNumber;
end;
procedure TfrmMainMenu.GetCCNumber(Sender: TObject);
begin
sCardNumber := lbledtCardInfo.Text;
if Length(sCardNumber) <> 16 then
begin
ShowMessage('Card number has to 16 digits,please try again!!');
Exit;
end;
lbledtCardInfo.Clear;
lbledtCardInfo.EditLabel.Caption := 'Enter Pin' ;
btbtnEnter.OnClick := GetCCPin;
end;
procedure TfrmMainMenu.GetCCPin(Sender: TObject);
var
sPin: string;
begin
sPin := lbledtCardInfo.Text;
if Length(sPin) <> 4 then
begin
ShowMessage('Card Pin has to 4 digits,please try again!!');
Exit;
end;
ShowMessage(sPin);
...
lbledtCardInfo.Clear;
lbledtCardInfo.EditLabel.Caption := 'Enter Number' ;
btbtnEnter.OnClick := GetCCNumber;
end;
A variation of this would be to create multiple buttons that overlap each other in the UI, and then you can toggle their Visible property back and forth as needed, eg:
procedure TfrmMainMenu.FormCreate(Sender: TObject);
begin
// you can set this at design-time if desired...
btbtnCCPinEnter.Visible := False;
btbtnCCNumEnter.Visible := True;
end;
procedure TfrmMainMenu.btbtnCCNumEnterClick(Sender: TObject);
begin
sCardNumber := lbledtCardInfo.Text;
if Length(sCardNumber) <> 16 then
begin
ShowMessage('Card number has to 16 digits,please try again!!');
Exit;
end;
lbledtCardInfo.Clear;
lbledtCardInfo.EditLabel.Caption := 'Enter Pin' ;
btbtnCCNumEnter.Visible := False;
btbtnCCPinEnter.Visible := True;
end;
procedure TfrmMainMenu.btbtnCCPinEnterClick(Sender: TObject);
var
sPin: string;
begin
sPin := lbledtCardInfo.Text;
if Length(sPin) <> 4 then
begin
ShowMessage('Card Pin has to 4 digits,please try again!!');
Exit;
end;
ShowMessage(sPin);
...
lbledtCardInfo.Clear;
lbledtCardInfo.EditLabel.Caption := 'Enter Number' ;
btbtnCCPinEnter.Visible := False;
btbtnCCNumEnter.Visible := True;
end;
Notice that you test iCount2 = 0 immediately after setting iCount2 := 0. Thus, that test will always be True. Furthermore, the later test iCount2 = 2 will always be False because the value starts at 0 and you only have one Inc in between.
Instead try the following.
Add two string fields FCardNumber and FPin to your form class:
private
FCardNumber: string;
FPin: string;
Also create an enumerated type TEntryStage = (esCardNumber, esPin) and add a field of this type. This will make your code look like this:
private
type
TEntryStage = (esCardNumber, esPin);
var
FCardNumber: string;
FPin: string;
FEntryStage: TEntryStage;
In Delphi, class fields (class member variables) are always initialized, so FEntryStage will be esCardNumber (=TEntryStage(0)) when the form is newly created.
Add a TLabeledEdit (I see you use those) and a TButton; name them eInput and btnNext, respectively. Let the labeled edit's caption be Card number: and the caption of the button be Next.
Now add the following OnClick handler to the button:
procedure TForm1.btnNextClick(Sender: TObject);
begin
case FEntryStage of
esCardNumber:
begin
// Save card number
FCardNumber := eInput.Text;
// Prepare for the next stage
eInput.Clear;
eInput.EditLabel.Caption := 'Pin:';
FEntryStage := esPin;
end;
esPin:
begin
// Save pin
FPin := eInput.Text;
// Just do something with the data
ShowMessageFmt('Card number: %s'#13#10'Pin: %s', [FCardNumber, FPin]);
end;
end;
end;
You might notice that you cannot trigger the Next button using Enter, which is very annoying. To fix this, do
procedure TForm1.eInputEnter(Sender: TObject);
begin
btnNext.Default := True;
end;
procedure TForm1.eInputExit(Sender: TObject);
begin
btnNext.Default := False;
end;
Much better!

Updating field in cxGrid acting strange

I have a function to update a cxGrid made with help from answers to Loop through records on a cxgrid and update a field/column
But it is sometimes acting a bit strange. If I open the form with the cxGrid and click the columnheader without doing anything else, the records are updateted OK. But if the 'selectorbar' is moved away from the top, the record marked is not updated.
I am sure it is a property that needs to be changed, but which one.
The variable fSelected is set to False at FormShow and is ther so that the user can unselect records as well.
procedure TfrmContactsSelect.colContactSelectedHeaderClick(Sender: TObject);
var
i: Integer;
Index: Integer;
BookMark : TBookMark;
Contact: variant;
begin
if fMulti = True then
begin
Screen.Cursor := crHourGlass;
fSelected := not fSelected;
BookMark := qryContacts.GetBookmark;
qryContacts.DisableControls;
try
for i := 0 to grdContactsView1.DataController.FilteredRecordCount - 1 do
begin
Index := grdContactsView1.DataController.FilteredRecordIndex[i];
Contact := grdContactsView1.DataController.Values[Index, 4];
if grdContactsView1.DataController.LocateByKey(Contact) then
begin
qryContacts.Edit;
qryContacts.FieldByName('fldcontact_selected').AsBoolean := fSelected;
qryContacts.Post;
end;
end;
finally
qryContacts.EnableControls;
qryContacts.GotoBookmark(BookMark);
qryContacts.FreeBookmark(BookMark);
end;
Screen.Cursor := crDefault;
end;
end;
Delphi XE7, DevExpress 14.2.2, UniDAC 5.5.12 for DB access
Comment:
I have ended up with the following solution based on the answer and input from MartynA
procedure TfrmContactsSelect.colContactSelectedHeaderClick(Sender: TObject);
var
i: Integer;
Index: Integer;
MarkedRecord: variant;
CurrentRecord: variant;
begin
if fMulti = True then
begin
Screen.Cursor := crHourGlass;
fSelected := not fSelected;
Index := grdContactsView1.DataController.FocusedRecordIndex;
MarkedRecord := grdContactsView1.DataController.Values[Index, colContactGuid.ID];
try
for i := 0 to grdContactsView1.DataController.FilteredRecordCount - 1 do
begin
Index := grdContactsView1.DataController.FilteredRecordIndex[i];
CurrentRecord := grdContactsView1.DataController.Values[Index, colContactGuid.ID];
if grdContactsView1.DataController.LocateByKey(CurrentRecord) then
begin
grdContactsView1.DataController.Edit;
grdContactsView1.DataController.SetEditValue(colContactSelected.ID, fSelected, evsText);
grdContactsView1.DataController.Post;
end;
end;
finally
grdContactsView1.DataController.LocateByKey(MarkedRecord);
end;
Screen.Cursor := crDefault;
end;
end;
I can reproduce your problem using the sample project I posted in my answer to your other q.
Try this: Add a TMemo to your form, and inside the 'if grdContactsView1.DataController.LocateByKey(Contact) then' block, write the value of a row-unique datafield and the Selected datafield value to the memo.
Then, what I get when some row other than the top row is selected is that one row is listed twice in the memo, with Selected both false and true, and one of the rows in the filter isn't listed at all, which I think accounts for the behaviour you're seeing. If I then comment out the .Edit .. .Post lines, it correctly lists all the rows in the filter.
So evidently doing the Selected field changes inside a block which iterated the FilteredRecordIndex property of the DBTableView is what's causing the problem.
Personally, I find that it goes a bit against the grain to modify dataset rows in code via a DB-aware control (because you usually end up fighting the DB-awareness of the control), but in this case, it's straightforward to do the processing via the DBTableView of the cxGrid.
procedure TForm1.ProcessFilteredRecords;
var
PrevV,
V : Variant;
i,
Index: Integer;
S : String;
begin
// First, pick up a reference to the current record
// so that we can return to it afterwards
Index := cxGrid1DBTableView1.DataController.FocusedRecordIndex;
PrevV := cxGrid1DBTableView1.DataController.Values[Index, 0];
try
for i := 0 to cxGrid1DBTableView1.DataController.FilteredRecordCount - 1 do begin
Index := cxGrid1DBTableView1.DataController.FilteredRecordIndex[i];
V := cxGrid1DBTableView1.DataController.Values[Index, 0];
if cxGrid1DBTableView1.DataController.LocateByKey(V) then begin
cxGrid1DBTableView1.DataController.Edit;
// 2 is the index of my Selected column in the grid
if cxGrid1DBTableView1.DataController.SetEditValue(2, True, evsText) then
Caption := 'OK'
else
Caption := 'Failed';
cxGrid1DBTableView1.DataController.Post;
end;
end;
finally
if cxGrid1DBTableView1.DataController.LocateByKey(PrevV) then
Caption := 'OK'
else
Caption := 'Failed';
end;
end;
Another way to avoid the problem is to change the Selected states in two steps:
Iterate the FilteredRecordIndex to build a list of rows to change - in your case this would be a list of guids
Then, iterate the list of rows and update their Selected states.
Code:
procedure TForm1.ProcessFilteredRecords;
var
V : Variant;
i,
Index: Integer;
BM : TBookMark;
S : String;
TL : TStringList;
begin
Memo1.Lines.Clear;
TL := TStringList.Create;
try
for i := 0 to cxGrid1DBTableView1.DataController.FilteredRecordCount - 1 do begin
Index := cxGrid1DBTableView1.DataController.FilteredRecordIndex[i];
V := cxGrid1DBTableView1.DataController.Values[Index, 0];
if cxGrid1DBTableView1.DataController.LocateByKey(V) then begin
if CDS1.FieldByName('Selected').AsBoolean then
S := 'True'
else
S := 'False';
S := CDS1.FieldByName('Name').AsString + ' ' + S;
Memo1.Lines.Add(S);
TL.Add(CDS1.FieldByName('Guid').AsString);
end;
end;
try
BM := CDS1.GetBookMark;
CDS1.DisableControls;
for i := 0 to TL.Count - 1 do begin
if CDS1.Locate('guid', TL[i], []) then begin
CDS1.Edit;
CDS1.FieldByName('Selected').AsBoolean := True;
CDS1.Post;
end
end;
finally
CDS1.EnableControls;
CDS1.GotoBookmark(BM);
CDS1.FreeBookmark(BM);
end;
finally
TL.Free;
end;
end;
Like you, I was expecting that changing a property or two of the cxGrid might avoid the problem without any code, but I haven't been able to find anything which does.

Calculation on a hidden field

I am hiding a field so that when it is shown (on checkbox checked) it preforms a certain calculation.
procedure TForm1.cxCheckBox1Click(Sender: TObject);
var
C:TcxGridDBColumn;
begin
if ABSTable1.FieldByName('CENIK_IME').AsString = 'PAK' then begin
C := cxGrid2dbtableview1.GetColumnByFieldName('TT');
if Assigned(C) then C.Visible := not C.Visible;
ABSQuery2.Edit;
ABSQuery2.FieldByName('TOTAL').AsCurrency := (ABSQuery2.FieldByName('TOTAL').AsCurrency) + (ABSQuery2.FieldByName('TT').AsCurrency);
ABSQuery2.Refresh;
end;
end;
Problem is that every time I check or uncheck the checkbox my TOTAL gets bigger and bigger. Any way to prevent the checkbox from summing every time it gets checked or unchecked ?
Also I have this on calculate fields of the query ;
procedure TForm1.ABSQuery2CalcFields(DataSet: TDataSet);
begin
ABSQuery2.FieldByName('TT').Value:= (ABSQuery2.FieldByName('DAYS').AsCurrency) * 1.01 ;
end;
This is all done on a Temp table which is used just for the occassion. Contents get deleted all the time....
Won't you need to subtract ABSQuery2.FieldByName('TT').AsCurrency if the column is hidden?
And also only change Total if the TT is found?
So:
procedure TForm1.cxCheckBox1Click(Sender: TObject);
var
C:TcxGridDBColumn;
begin
if ABSTable1.FieldByName('CENIK_IME').AsString = 'PAK' then begin
C := cxGrid2dbtableview1.GetColumnByFieldName('TT');
if Assigned(C) then
begin
C.Visible := not C.Visible;
ABSQuery2.Edit;
if C.Visible then
ABSQuery2.FieldByName('TOTAL').AsCurrency := (ABSQuery2.FieldByName('TOTAL').AsCurrency) + (ABSQuery2.FieldByName('TT').AsCurrency)
else
ABSQuery2.FieldByName('TOTAL').AsCurrency := (ABSQuery2.FieldByName('TOTAL').AsCurrency) - (ABSQuery2.FieldByName('TT').AsCurrency);
ABSQuery2.Post;
ABSQuery2.Refresh;
end;
end;
end;

Set ComboBox text on selection

I am working on an application where i have a combobox with long text values.Since the text values are large(in term of characters ..20 or more), to display in the combobox, the requirement was to display on the first character after selecting from the drop down.
Like in the image marked in red. if the user selects 3th item 3 0.5 to 1.25 Slight i should only display the 3 in the combobox.
So i tried this
sTheSelectedValue : string;
procedure TForm1.ComboBox1Select(Sender: TObject);
begin
sTheSelectedValue:=TrimTextAndDisplay(ComboBox1.Text); //send theselected value
ComboBox1.Text :=''; //clear the selection
ComboBox1.Text:=sTheSelectedValue; //now assign as text to combo box
Button1.Caption:=ComboBox1.Text; //just show the new value on the button.
end;
function TForm1.TrimTextAndDisplay(TheText : string): string;
var
sTheResult : string;
begin
sTheResult :=copy(TheText,0,1); //extract the first value..
Result :=sTheResult;
end;
The result is
The button seem to show the proper value but not the combobox.
what i want is to get 3 in the combobox, i cant seem set ComboBox1.Text:=
can any one tell me how to do it?
like this on selection of from the combobox the result should be
I would suggest owner-drawing the ComboBox to handle this. Set the TComboBox.Style property to csOwnerDrawFixed, then store just the numbers '1', '2', '3', etc in the TComboBox.Items property itself and use the TComboBox.OnDrawItem event to render the full strings when the drop-down list is visible, eg:
var
sTheSelectedValue : string;
const
ItemStrings: array[0..7] of string = (
'0 to 0.1 Calm (rippled)',
'0.1 to 0.5 Smooth (wavelets)',
'0.5 to 1.25 Slight',
'1.25 to 2.5 Moderate',
'2.5 to 4 Rough',
'4 to 6 Very rough',
'6 to 9 High',
'9 to 14 Very high');
procedure TForm1.FormCreate(Sender: TObject);
var
I: Integer;
begin
ComboBox1.Items.BeginUpdate;
try
for I := Low(ItemStrings) to High(ItemStrings) do begin
ComboBox1.Items.Add(IntToStr(I+1));
end;
finally
ComboBox1.Items.EndUpdate;
end;
end;
procedure TForm1.ComboBox1Select(Sender: TObject);
begin
sTheSelectedValue := IntToStr(ComboBox1.ItemIndex+1);
Button1.Caption := sTheSelectedValue;
end;
procedure TForm1.ComboBox1DrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState);
var
s: String;
begin
if odSelected in State then begin
ComboBox1.Canvas.Brush.Color := clHighlight;
ComboBox1.Canvas.Font.Color := clHighlightText;
end else begin
ComboBox1.Canvas.Brush.Color := ComboBox1.Color;
ComboBox1.Canvas.Font.Color := ComboBox1.Font.Color;
end;
ComboBox1.Canvas.FillRect(Rect);
s := IntToStr(Index+1);
if not (odComboBoxEdit in State) then begin
s := s + ' ' + ItemStrings[Index];
end;
ComboBox1.Canvas.TextRect(Rect, Rect.Left+2, Rect.Top+2, s);
if (State * [odFocused, odNoFocusRect]) = [odFocused] then begin
ComboBox1.Canvas.DrawFocusRect(Rect);
end;
end;
You have to try to save the data in a record, for ex:
type
TMyRec = record
Num:Integer;
Text:String;
end;
TMyRecArray = array of TMyRec;
MyRecArray:TMyRecArray;
then you can set manually the items to be set in the ComboBox (on the OnFromCreate),
SetLength(MyRecArray,9);
MyRecArray[0].Num:=1;
MyRecArray[0].Text:='0 to 0.1 Calm Rippled';
.
.
and so on.
then in the combobox strigns place only the numbers, and
procedure TForm1.ComboBox1Select(Sender: TObject);
var
i:integer;
begin
for i:=0 to 9 do
begin
if ComboBox1.Text=IntToStr(MyRecArray[i].Num) then
Button1.Caption:=MyRecArray[i].Text;
end;
end;

Delphi for loops and StringList Errors

Ok guys, I've been trying to find out every possible mistake i'm making but I give up... I need help! What I'm writing is an app to manage rentals for my job and when the date is past, my app removes the name from 2 text files. I wrote 3 little functions(procedures) to make this work. Here:
This one loads from dates.dat file and remove the line containing the name of the employee.
procedure remDate(emp: String);/// Removes employee from date file
var
pos1, i: integer;
dateList: TStringList;
begin
dateList:=TStringList.Create;
dateList.LoadFromFile('Data\dates.dat');
for i:=0 to dateList.Count-1 do begin
pos1:=AnsiPos(emp, dateList[i]);
if pos1<>0 then begin
dateList.Delete(i);
dateList.SaveToFile('Data\dates.dat');
end;
end;
dateList.Free;
end; //eo remDate
This one removes the line containing the employee name from the perm.dat file.
procedure remPerm(emp: String);/// Removes employee from perm file
var
pos1, i: integer;
permList: TStringList;
begin
permList:=TStringList.Create;
permList.LoadFromFile('Data\perm.dat');
for i:=0 to permList.Count-1 do begin
pos1:=AnsiPos(emp, permList[i]);
if pos1<>0 then begin
permList.Delete(i);
permList.SaveToFile('Data\perm.dat');
end;
end;
permList.Free;
end; //eo remPerm
This one sticks those together. The isDue is a simple function that compares 2 dates and returns a TRUE if date is today or is past.
procedure updatePerms;
var
empList: TStringList;
i: integer;
begin
empList:=TStringList.Create;
empList.LoadFromFile('Data\employes.dat');
for i:=0 to empList.Count-1 do begin
if isDue(empList[i]) then begin
remDate(empList[i]);
remPerm(empList[i]); (*) Here is where the error points.
end;
end;
empList.Free;
end;
The error I get is when it gets to remPerm in the updatePerms procedure.(*)
I get a EStringList Error, out of bound (#). Figured out with many tries that it only happens when an employee's due date is today. Please comment if you need more info!
Thanks in advance, any help is really appreciated!
The problem is that you are using a for loop. The end point of a for loop is only evaluated once when the loop is entered. At that point you may have 100 items, but once you start deleting there will be less. This will then result in a list index out of bounds error.
The simple fix is to reverse the for loop:
procedure remDate(emp: String);
/// Removes employee from date file
var
pos1, i: integer;
dateList: TStringList;
begin
dateList := TStringList.Create;
dateList.LoadFromFile('Data\dates.dat');
for i := dateList.Count - 1 downto 0 do
begin
pos1 := AnsiPos(emp, dateList[i]);
if pos1 <> 0 then
begin
dateList.Delete(i);
dateList.SaveToFile('Data\dates.dat');
end;
end;
dateList.Free;
end; // eo remDate
This will work if the employee occurs more than once.
However if the employee does only occur once, you can use break to exit from the loop early:
procedure remDate(emp: String);
/// Removes employee from date file
var
pos1, i: integer;
dateList: TStringList;
begin
dateList := TStringList.Create;
dateList.LoadFromFile('Data\dates.dat');
for i := 0 to dateList.Count - 1 do
begin
pos1 := AnsiPos(emp, dateList[i]);
if pos1 <> 0 then
begin
dateList.Delete(i);
dateList.SaveToFile('Data\dates.dat');
Break; // <-- early exit
end;
end;
dateList.Free;
end; // eo remDate
Another solution is to use a while loop.

Resources