HOME - - - - - Lazarus Tutorials TOC - - - - - - Other material for programmers
Delicious.Com Bookmark this on Delicious     StumbleUpon.Com Recommend to StumbleUpon

Using StringGrids in Lazarus (or Delphi) programming

General issues, and an example of data validation, preparation

This tutorial will use a StringGrid to look at a text file full of data which has been prepared to be appended to a database. You may not have that exact want!... but in the course of discussing an answer to that, many useful things about working with StringGrids come up.

The application (full sourcecode provided, as usual.) loads the text into a StringGrid, and then performs various checks. Data which do not meet certain criteria are highlighted. The user can then fix the bad data (by hand), and with the click of a button re-save the data, for subsequent appending to the database... with fewer errors than would have arisen, and the data not been edited first. (A full listing of the sourcecode is given at the bottom of this page.)

At this time I am working on giving you TWO sourcecode .zip files. The one the link in the previous paragraph will download is the most up to date one. IT may BE AT ODDS WITH THE LISTING AT THE BOTTOM OF THE PAGE. For a .zip with earlier sourcecode, use this link. The one the link in the previous paragraph gives you is the "latest and best" sourcecode, sourcecode that MAY include patches to the old code, particularly patches, if any, suggested at the bottom of the page. (Apologies for all the "confusion"... I hope you can see why I've inflicted it? Keeping text, sourcecode, listings of sourcecode, and zips of sourcecode all "in step" is not easy. One aid you should always consult: The "vers" constant in the sourcecode. It may help you to know that generally, I "do it" in my copy of the sourcecode before I revise the webpage, and updates to the .zip happen after the previous steps... not always immediately!)

Before we tackle the BIG job...

The tutorial you are looking at now is large. And has many parts, one of which is loading a CSV into a StringGrid.

I have created a separate demo application to show JUST how to load a CSV to a StringGrid. That may be all you want, in which case save yourself the headache of the rest of this tutorial... but if you are interested in the bigger project, and have some Lazarus experience, read on. (You can go to the "Just how to load..." tutorial later, if you find what's included here too superficial.)

You will also find an "How To" guide, over at my Open Office tutorials, which covers using a CSV file consisting of multiple records to append those records to a database table.

Bad news/ Good News

I thought this project was "finished", but then some problems surfaced. It meant extensive revisions of the code, and some very tedious re-working of this page.

At this stage, I think I have finished a re-write of the code, and most of the relevant parts of this page. Work needs to be done to make the page more "elegant", and tidy. (There are some things that are only explained at the end in what is sort of a "corrections" section.

The .zip you can download may or may not have been updated to incorporate all of the (latest) corrections. Sorry. But I wanted to leave the "old", reliable .zip, even with its flaws, until I can do a proper and comprehensive "new" .zip for you. (The .zip (and the comprehensive listing at the end of the page) have already been through several update cycles.)



Wow!

The page at http://wiki.freepascal.org/TStringGrid is brilliant! (Much more than the welcome, but unadorned, technical reference that constitute some such pages.) It puts my humble attempt at a "tutorial" to shame... but I'll press on, if only for the use it will be to me as a place I can go to find things I've taken time to figure out... and subsequently forgotten.

This application created during this tutorial lets you load a CSV file into a StringGrid, edit what's there, and then re-save the file, with your changes. The application highlights cells in the StringGrid which do not pass certain tests, to help the user find the bits that need editing.

Why I wrote this: I have a database in OpenOffice's "Base". I add records to it in batches. The data to be added is in a text file, with commas separating the values, i.e. a "CSV" file. (Besides my Lazarus tutorials, I offer similar tutorials on using the splendid database supplied with OpenOffice... probably also relevant to LibreOffice.)

Base's provisions for adding records in batches works well, unless a record doesn't meet constraints defined in the database. There must be a way to deal with what bothers me, in Base, but I haven't found it.

What bothers me is that if Base rejects one of the records I am trying to add, I don't know how, easily, to determine WHICH was rejected.

Let's say my database is for...

names
where a person lives (just the state they live in), and
their telephone number.

That's a little artificial, but it will do to illustrate the relevant issues.

For the sake of this discussion, the only constraints on data in the database are...

1) The name must be no more than 10 characters. (Longer in real life, of course... but 10 will work better for this discussion.)

2) The value in the "state person lives in" field, hereafter called LivesInState, must be in the standard set of USPS abbreviations.... NY,CA,PA,MA, etc. (Everyone lives in the USA, for the sake of this discussion.)

So.... let's say I've typed up the following as a batch of records to be appended to the database....

860-787-5555, MA, Fred
608-878-6666, CA, Alice
111-222-3333, XX, Henry
113-555-1212, NY, Euphiagenia
-

The first two would be fine, would be appended to the database by the "append" mechanism without challenge.

Henry's state is not in the set that field is constrained to.

"Euphiagenia" would be unacceptable to the database as it is too long. (And unacceptable to any child I think I'd want to know, just on general principles!

Basic plan...

Given those wants, I said to myself: "Time to learn about the TStringGrid component". Time write a little app which will....

* Load my CSV file into a StringGrid

* Check the contents of the cells to see if they match the constraints of the database concerned, maybe highlighting any that do NOT match the constraints.

* Allow the user to edit the cells, re-check them.

* When the user feels he/ she has done enough to "clean up" the file, save the new data back to the hard-drive.

* A nice "frill"??? Something in the code to "shuffle" names in respect of the source file. (The plans for the frill given, code would allow it to be added with little fuss. Job not done, though.)

Let's say you load "DataToBeAppended-August16Revisions.txt".

I would never over-write the source! You never know when you might want to go back to it.

Let's try to write the application so that when you say "Yes, now that's how I want it", the application does the following, if you happen to invoke the "backup and save" at 17:40 on Sep-28-2016. (The day this tutorial was started.)

Renames...
    DataToBeAppended-August16Revisions.txt
  .... as....
    DataToBeAppended-August16Revisions-bef Sep-18-2016-17-40.txt
Saves the revised data as...
    DataToBeAppended-August16Revisions as at Sep-18-2016-17-40.txt

Should be easy enough. Sigh.

Please note... there are better ways to do MANY of the things shown here. I have done "peripheral" things in the most "basic" way, in hopes of helping make the "fancy stuff", the stuff central to what the tutorial is about, more obvious.

Sidebar: Notes on what a CSV file is, and what they are good for.

Preliminary TStringGrid application

We're going to start with an app that load a .txt file containing CSV data. The name of the file will be hard-coded into the application. Then we can edit what is in different cells ("mess with it"!), and then, when told to, it will save the contents of the StringGrid to a file, with the name something along the lined proposed above.

At this stage, apart from obvious things, note that we are not (yet) doing any checking of the values in the StringGrid, and we are not renaming the file the csv data comes from. (The checking IS done, later in this page. The renaming is NOT... yet.)

I am for now ducking the "frills" of letting you choose what file to load, and of being clever with the name of the destination file, and of preserving the original data in a file with a modified name. If these features are important to you, I hope that from the way the application was done, it is easy to add them? (My contact details are at the bottom)

Establish a new project. (I always commend setting up a new folder for a new project.)

I've called my project "LDN191", and if you use the same name, it may make "copying" what I've done easier.

Put the following on the form. The first three go across the top, left to right, the last goes in the bottom part of the form, mostly filling it.

button: buLoad
button: buSaveWithNewName
button: buQuit
stringgrid: sgData

Use the Object Inspector to change the FixedCols and FixedRows properties to 0... we won't be using column or row headings, and change the ColCount property to 3. Don't worry about how many rows there, don't worry that the "image" of the string grid doesn't match the area of the form you have dedicated to the component.

Make buQuit's OnClick handler do "close".

In the "options" for the string grid (in the Object Inspector", for now change only (maybe) "goEditing". (Make it "true".)

I said "change only (maybe)...", because you MAY want to change goAlwaysShowEditor to "true" also... but you don't "need" to.

(goEditing, goAlways..): "go" for Grid Options.)

(The goAlways.. property affects what you need to do to start editing a cell's contents. Both versions work fine, and have their own pros and cons. (It is set true in versions of the sourcecode which are newer than the one of 1 Oct 16.))

With that done, when you run what's done so far, you should find that you can edit what is in the StringGrid.

Good so far!

Now, as a preliminary step along the road to our goal, we are going to make buSaveWithNewName a very crude "just save, fixed name". (The name will be TmpLDN191SavedFile.txt)

I'm very new to programming StringGrids. I hope I am not leading you astray. And approach that made sense to me was given in a DelphiGroups.info article I found.

(Alternatively.... at....
http://wiki.lazarus.freepascal.org/Grids_Reference_Page#procedure_AutoSizeColumn.28aCol:_Integer.29.3B

... there's discussion of a "LoadFromCSVFile" procedure. This, clearly, would be another answer to the "I want to load a CSV file to a StringGrid"... but I'm not exploring that alternative here. Sorry. (I looked onto it, and didn't like aspects of what I found. It has the same problems as the CommaText version of what I provide here.))

At the heart of the DelphiGroups answer were the following two procedures. Forget the "Load" part for now. We'll come back to it. The example uses a StringGrid called "StringGrid". In my code, the StringGrid's name is "sgData".


The "Load" procedure....

{This procedure loads a CSV file into a TStringGrid.
   Author: Gary Williams}

procedure LoadStringGridFromCSVFile(const StringGrid: TStringGrid;
                                    const FileName: String);
var
  I: Cardinal;
  CSV: TStrings;
begin
  CSV := TStringList.Create;
  try
    CSV.LoadFromFile(FileName);
    StringGrid.RowCount := CSV.Count;

    for I := 1 to CSV.Count do
    begin
      StringGrid.Rows[I-1].CommaText := CSV[I-1];
    end;
  finally
    CSV.Free;
  end;
end;// ----------------------------------------

... and the "Save" procedure...

{This procedure saves a TStringGrid into a CSV file.
  Author: Gary Williams}

procedure SaveStringGridToCSVFile(const StringGrid: TStringGrid;
                                  const FileName: String);
var
  I: Integer;
  CSV: TStrings;
begin
  CSV := TStringList.Create;
  try
    for I := 1 to StringGrid.RowCount do
    begin
      CSV.Add(StringGrid.Rows[I-1].CommaText);
    end;
    CSV.SaveToFile(FileName);
  finally
    CSV.Free;
  end;
end;//----------------------------------------

What I eventually ended up with started with what you see above. Credit where due! The above gave me the start I needed... and may help you see the overall "scheme" of things.

Let's do it!...

Make the OnClick handler for buSaveWithNewName simply...

procedure Tldn191f1.buSaveWithNewNameClick(Sender: TObject);
begin
   SaveStringGrid('TmpLDN191SavedFile.txt');
end;

.... and then build SaveStringGrid. The big block that follows is a bit hard to "digest", I know. Sorry. There was a problem in the part of the code above which saves the StringGrid.

If, when you come to save the contents of the StringGrid back to a CSV file after working on it, there are cells with field separators in the datum, or a space (other than at either end), even though the "save" will still "work"... problems will arise if you subsequently try to re-load the result into the application for further work on the data in the CSV file.

Simple edits to the code for SaveStringGrid presented below allow you to convert it to use the CommaText function... with the consequences documented in the code.

procedure Tldn191f1.SaveStringGrid(sFilename:string);
//Doing it by the "CommaText" solution causes quotation
//  marks to be inserted around fields with spaces
//  in them... which will cause
//  problems if the CSV is subsequently re-loaded
//  into this app.
//
//It might also be a good idea to check for field
//  separators within cells just before attempting
//  a save. If present, they will
//  cause a problem if you try to reload the product
//  of the save, unless fairly significant changes
//  are made to how things are saved, how they are
//  loaded. (One answer would be to enclose such
//  data in quotation marks... but that gives rise
//  to other issues... not least providing for
//  things IN quotation marks being loaded as
//  special cases.
//
//  If you use the "CommaText" version which can be
//  created from what follows, the contents of
//  cells with a field separator (if it is a comma?)
//  in it WILL be enclosed in quotes. As will
//  data with internal spaces. But, given that
//  the "load" process would have to be adapted
//  anyway, that wouldn't(?) be a problem. (But
//  I would still be inclined to avoid the
//  "CommaText" answer, if only because I know
//  that "funny things" happen when you rely
//  on the system's idea of the field separator,
//  e.g. if the system has been told it is somewhere
//  other than the USA, the field separator may
//  be something else.

var
  cardCounter:Cardinal;
  stringsTmp:TStrings;

    //Next is SR of SaveStringGrid...
    function boIsFieldSeparatorInACell(var cardRowTmp:cardinal):boolean;
    var cardRowCounter:cardinal;
        bColCounter:byte;
        boTmpL:boolean;
    begin
      boTmpL:=false;
      for bColCounter:=0 to sgData.ColCount-1 do
        for cardRowCounter:=0 to sgData.RowCount-1 do begin
           if pos(kFieldSep,sgData.cells[bColCounter,cardRowCounter])>0
              then begin
                boTmpL:=true;
                cardRowTmp:=cardRowCounter;
                end;
        end;//for bColCounter
      result:=boTmpL;
    end;// boIsFieldSeparatorInACell, SR of SaveStringGrid

    //Next is SR of SaveStringGrid...
    function sFetchRowOfCells({sgTmp:stringgrid;}cardRowL:cardinal):string;
    var bTmpL:byte;
        sTmpL:string;
      begin
        sTmpL:='';
        for bTmpL:=0 to sgData.ColCount-1 do begin
          sTmpL:=sTmpL+sgData.Cells[bTmpL,cardRowL];
          if bTmpL<>sgData.ColCount-1 then sTmpL:=sTmpL+kFieldSep;
          end;//for...
        result:=sTmpL;
      end;//sFetchRowOfCells, SR of SaveStringGrid

begin //SaveStringGrid
  //"cardCounter" used, in first line, as a "temporary"
  //   variable. What is in it, if anything, on entry
  //   to boIsFieldSeparatorInACell DOESN'T MATTER,
  //   nor do we still want what is in it afterwards.
  if boIsFieldSeparatorInACell(cardCounter) then begin
    sTmp:=kFieldSep;
    showmessage('At least one cell contains the '+
      'field separator. the field separator is '+
      sTmp+chr(13)+'The cell is in row '+
      inttostr(cardCounter));
    end;

stringsTmp:=TStringList.Create;
try
    for cardCounter := 1 to sgData.RowCount do
    begin
      //One "solution".... see sheepdogguides.com/lut/ltn4c.htm
      //   for discussion of the pros and cons of the alternatives.
      (*stringsTmp.Add(sgData.Rows[cardCounter-1].CommaText);*)
      //This is where the first solution finishes

      //Alternative solution...
      //BEFORE WE START ON THAT...
      //      a *** G E N E R A L  P O I N T ***
      //Beware how the references to the two indices of the
      //  StringGrid are managed. Where we merely need to do
      //  something once for each row or column, I've sometimes
      //  used For Counter=1 to NumberOfThem. Other times, used
      //  For Counter=0 to NumberOfThem-1. When we need to refer
      //  to a particular cell, the fact that they are "numbered"
      //  FROM 0 needs to be remembered... and sometimes the
      //  associated "-1" to "translate" from the "name" of the
      //  row or column is done in the variable driving the "for"
      //  loop, sometimes not. Sigh.
      sTmp:=sFetchRowOfCells({sgData,}cardCounter-1);
      //   if cardCounter-1<>sgData.ColCount then sTmp:=sTmp+kFieldSep;
      stringsTmp.Add(sTmp);//"Add" appends the EOL code.
      end;//for
      //stringsTmp.Add(kCRLFcode);
      //This is where the alternative solution finishes

    stringsTmp.SaveToFile(sFilename);
finally
    stringsTmp.free;
end;//try

showmessage('CSV re-saved, to '+sFilename+'. (If a '+
   'file of that name existed previously, it was '+
   'over-written.');
end;//SaveStringGrid

Related to the above: If you choose NOT to use a SaveStringGrid (and corresponding Load procedure) that can put data which contain the field string separator as an "ordinary" character, then you should probably watch for the field separator value whenever users change the contents of a cell.

The sgDataSetEditText event handler seems to be the place to do this- it is called every time that a character is added or removed from a cell's contents. **N.B.**: kFieldSep has to be made a global. (It is a local "const" in the code that was in the zip, and the full listing above, when I wrote this. (2 Oct 16). (If I seem to have edited the other things without editing this, please get in touch!)

The following takes care of that...

procedure Tldn191f1.sgDataSetEditText(Sender: TObject; ACol, ARow: Integer;
  const Value: string);
//This is called every time a character is changed in a cell.
//Maybe there's another event for "leaving cell, editing done"... but
//  I don't think the overheads of checking as we go along are
//  too terrible. Maybe during the load-whole-grid process they
//  will be insupportable, when more columns are being tested.
//As long as part of the reason for calling this is to check
//  that a field separator character hasn't been entered, for
//  inclusion in a cell's contents, it (the call of DataSetEditText)
//  should remain.

begin
 //Next is a just scrap of debugging code...
 //showmessage('hi.. col/row: '+inttostr(acol)+'  '+inttostr(aRow));
 //.... but note!  v v v v v

  //sCellContentsWhenFSWarningIssued "works" to solve a problem...
  //   but it feels like a nasty kludge
  //It was introduced to deal with the following problem:
  //Unless the SaveStringGrid and corresponding Load are re-
  //   written to allow and deal with the field separation
  //   character appearing in the "ordinary" text of any cell,
  //   we have to watch, make sure that users do not put one
  //   in.
  //A simpler version of the following "worked"... but it also
  //   "repeated itself", sometimes at unhelpful, and always
  //   at irritating moments. (The sgDataSetEditText procedure
  //   is triggered when you leave a cell, for instance... even
  //   if you are just leaving it to click the "Quit" button.)

  //If you want to make it impossible for a user to leave a cell
  //   while a field separator character remains in it, you
  //   will have to arrange that for yourself... I don't like
  //   creating that sort of situation. If you want to prevent
  //   a user from saving a "bad" StringGrid, i.e. one with
  //   field separator values in the cells, I would suggest you
  //   add (it would be easy... most of what you need is in the
  //   code already) a test at the start of the SaveStringGrid
  //   procedure, and "talk" to the user THEN if bad data present.

  if ((pos(kFieldSep,Value)>0) and
     (sCellContentsWhenFSWarningIssued<>Value))then begin
       showmessage('You have used the field separator inside '+
       'a cell. This will "work", but any CSV that you save '+
       'will be re-loaded with that cell''s contents split '+
       'across two cells, if changes haven''t been made to '+
       'the application in respect of how things are '+
       'saved and loaded to provide for field separators' +
       'in cells.');
       sCellContentsWhenFSWarningIssued:=Value;
     end;
  if aCol=bLengthTestedInWhichCol then
       CheckOneCellDataLength(Value,bLengthLimitInTestedColumn,aCol,aRow);

... etc, as before.

You may wish to supplement (or replace) the "showmessage" with a label to announce the problem of the presence of the field separator character.

Try running the application. Put some data into the grid's cells "by hand". Click the SaveWithNewName button, remembering that the "new name" part is yet to come.

You should get a "File Saved" message.

You can leave the application running, but take a moment to look, see that the file that should be there is there, complete with commas!


Loading data to the StringGrid

For now, the application will NEED a file already available called "TmpDataToLoad.txt". It is hard coded to load THAT file. Eventually, of course, it will let the user choose what file to load. Adding that would be trivial.

Because of limitations in our code, as it stands so far, the file should have three fields per line, separated by commas. (The test data already described would be fine.)

Make the buLoad button simply do...

procedure Tldn191f1.buLoadClick(Sender: TObject);
var sTmp:string;
begin
LoadFileToStringGrid('TmpDataToLoad.txt');
end;

... and then create LoadFileToStringGrid, and the subroutine it calls, StringToSGRow ("String To StringGrid Row")...

procedure Tldn191f1.LoadFileToStringGrid(sFileName:string);
(*The file to be loaded should consist of one or more
  RECORDS, each consisting of one or more FIELDS.
  The file should consist printable characters, plus
  (maybe) the code for TAB, and a FEW others like that.
  The RECORDS are separated by CR/LF terminators,
  FIELDS are separated by COMMAS. A line ending in
  a comma is considered to indicate that the final
  field of that record had nothing in it, e.g.....
    Three fields: Fred,Bloggs,860-767-5555
    Four fields: Fred,Bloggs,860-767-5555,

  ---
  See https://sheepdogguides.com/lut/lt4Nd.htm
  for details of how this part of the application
  works.

  ---
  An annoying little rough edge... not a day wrecker....
  After a load, if the StringGrid's option
    goAlwaysShowEditor is set true, the contents
    a cell in the grid may look funny. It is in
    "edit" mode, but the text is un-selected.
    This does little harm, but is annoying.
  "Look funny": If the things that were once with
    this code which change the color of cell
    contents are still in place, the text/ background
    in the first cell may not be "the right" colors.
  If a way to select the text in the first cell...
    select THE TEXT, not just the cell... could be
    found, and added just after the LoadFileToStringGrid
    this annoying "feature" would go away.
  Or just set goAlwaysShowEditor to "false"... which
    gives you a different set of good things and bad.


  *)

var
  cardCounter:Cardinal;
  stringsTmp:TStrings;
begin //LoadFileToStringGrid
  stringsTmp:=TStringList.Create;
  try
    stringsTmp.LoadFromFile(sFileName);
    sgData.RowCount:=stringsTmp.Count;//Yes: .count, not .count-1
    sgData.ColCount:=7;//This CLUMSY... But could fairly easily
      //be replaced by code to count the fields in the first
      //line, and make the StringGrid have that many columns.
      //The "built in" LoadFromCSVFile does that. Both routines
      //ignore the fact that a data file might have more
      //fields in a subsequent record. (The extra fields are
      //just thrown away in the LoadFromCSVFile. At present,
      //they cause a problem (good! You'll know about them!)
      //in this answer. (That can be fixed.)
      //What's done here, for the moment, is to set, by hand, something
      //that limits how many fields there can be in any record
      //in the CSV file. Exceed that limit, and the application
      //shuts down, in a messy way. Fall short of that on a given
      //line, and you just have some empty cells at the right hand
      //end of the line.

      //For the future: Add a parameter to LoadFileToStringGrid
      //If zero: Use the "answer" used by LoadFromCSVFile, if
      //> zero, set ColCount from that. (If this is done, also
      //provide a "scan file, find out longest (most fields) record"
      //function.)
      //
      //In any case, add a "var" variable to return error codes
      //to user.... "file not found","too many fields encountered",
      //etc.

      //Also add a parameter to specify the separator character.
      //Add a way to use....  "Bloggs, Fred",123 to allow separator
      //character inside a field? Make it optional, and tell
      //users that the application runs faster if they don't
      //use the feature? (If that change made, changes also have
      //to be made to the sub-routine for saving the contents
      //of the StringGrid to a CSV file.)

    for cardCounter:= 0 to stringsTmp.Count-1 do begin
          StringToSGRow(sgData,stringsTmp[cardCounter],cardCounter,true);
          end;

      //To be checked... what does app do when a)there is / b) there
      //  is not a CR at the end of the file? (Prelim checks: It doesn't
      //  matter.  (^_^)
      //If there is a last line consisting just of spaces, the Lazarus
      //  version does NOT add a row to the StringGrid, the
      //  SheepdogGuides version DOES add a row

  finally
    stringsTmp.Free;
    end;//Try... finally

  //Put....  sgData.AutoSizeColumns;
  //   ... in your code, after the call of LoadFileToStringGrid,
  //   if you would like all of the columns' widths adjusted,
  //   leaving each column just wide enough for the widest datum
  //   in the column.

end;//LoadFileToStringGrid

procedure Tldn191f1.StringToSGRow(sgLocal:TStringGrid;
                             sRawData:string;iRow:integer;boTrimFields:boolean);
var bColIndexL:byte;
  sOneField:string;
  boDoneIt:boolean;
//This procedure is used within "LoadFileToStringGrid". You
//  wouldn't be likely to want to call it directly.

//Takes, from sRawData, a string like...
//  860-555-1912,CT,Joe Smith
//... and puts the three fields in the first three
//columns of StringGrid sgLocal.
//What's in sRawData is "eaten away" over the course of the procedure.
//If boTrimFields is true, then spaces at either end of the field datum
//  are trimmed off....
//  "   sample datum   "
//  ... would become...
//  "sample datum"

  function boChomp(var sSource:string;
        var sDest:string):boolean;//SR of StringToSGRow
  //N.B. BOTH PARAMS ARE *var* PARAMS... contents of the
  //variables used to "feed" this SR will be changed by
  //the execution of the procedure.
  //
  //The contents of sDest before the call are irrelevant.
  //
  //If the string passed to sSource has a comma at it's
  //  right hand end, then the routine assumes that to mean
  //  that there is a field after the comma, but that the
  //  contents of that field just happen to be "nothing",
  //  i.e. ''. In the processing of such a string, sSource
  //  will be passed back to the calling program with a
  //  rogue value, just before the call of Chomp which
  //  "reads" the "empty" field.
  //In other words... string "a" has three fields,
  //  string "b" has FOUR...
  //     a) Fred,Bloggs,860-767-5555
  //     b) Fred,Bloggs,860-767-5555,
  //                             ---^---
  var iPosOfComma:integer;
  //This routine uses (read only) kFieldSep. You might
  //  want to re-write things so that it is supplied
  //  to the routine as a parameter, to avoid using
  //  a global inside the routine.

  begin //main block of boChomp, SR of StringToSGRow
    if sSource='' then result:=true//no ; here
         //(See "N.B. bChomp returns...", below. THIS "true"
         //arises if you call Chomp when there was no point...
         //you already knew there was no more data to parse.
    else begin //1  (Use last field in string that started
         //String1, String2, .... , StringLast
      //(There will be no comma after last field.)

      //N.B. boChomp returns false until there is no more
      //  data to be harvested by a further call of Chomp.
      //Note especially... the first time it
      //  returns TRUE, there is STILL one field to be
      //  USED by the calling program. It has been
      //  returned to the calling program in sDest,
      //  as usual.

      if sSource='tkbRogueToIndicateNullFieldAtEndOfRaw' then
            sSource:='';
      iPosOfComma:=pos(kFieldSep,sSource);
      if iPosOfComma=0 then begin //2
           //The "=1" case arises if the string you are
           //chomping ends with a comma, which is interpreted
           //as meaning that there IS one more field in the
           //string, but it just happens to consist of nothing,
           //just happens to be ''}
        sDest:=sSource;
        sSource:='';
        result:=true;//(See "N.B. bChomp returns..." THIS "true"
          //is for the case where you have just harvested the last
          //field from the string you have been "chomping" through.
        end//no ; here. End of "then 2"
      else begin //2
          if iPosOfComma=length(sSource) then
               sSource:=sSource+'tkbRogueToIndicateNullFieldAtEndOfRaw';
          sDest:=copy(sSource,1,iPosOfComma-1);
          sSource:=copy(sSource,iPosOfComma+1,length(sSource));
          result:=false;
          end;//of "else 2"
        end;// of "else 1"
  end;//boChomp, SR of StringToSGRow

begin //main block of StringToSGRow
   bColIndexL:=0;
   boDoneIt:=false;
   repeat
     boDoneIt:=boChomp(sRawData,sOneField);//N.B: These are "var"
          //parameters. A bit is chopped off of sRawData, and
          //sOneField is filled with a new value. Also: You can
          //ignore compiler warning about sOneField not being
          //initialized.
     if boTrimfields then sOneField:=trim(sOneField);

     //If you want to "see something" when a field in the
     //  CSV says "nothing, here", de-rem the following...
     //if sOneField='' then sOneField:='null';
     sgLocal.cells[bColIndexL,iRow]:=sOneField;

     inc(bColIndexL);

   until (boDoneIt) OR (bColIndexL>8);//second term to be refined in
     //due course... at present, it is a kludge... but it WILL have
     //a role to play one day. Keep note at top of code in step.
     //(Eventually, before LoadFileToStringGrid,
     //is called, somehow a determination will be made as to how many
     //columns are to be filled. If, by chance or error, the CSV file
     //has a record with too many fields, at the moment problems arise.
     //When the "8" is replaced by a variable, and that is loaded to
     //reflect how big the StringGrid available is, then steps can be
     //taken to deal nicely with a "too big for the row" record from
     //the CSV file.
end;//StringToSGRow

Note: The code doesn't automatically detect from the CSV file's first line how many fields exist in the data. That is crudely set by hand.

If you set it too high, no problem. (Although you'll get strings of commas on the right hand end of the records in the saved edited data.)

Set it too low, and you'll get a crude crash when you try to run the application. (By the way, it is generally a good idea to turn on range checking for your projects. (Put {$R+} in the sourcecode.) Once something is running smoothly, in some cases turning it off again will speed the application's execution up... but the range checks are good, and can stay in the final version, if not too "expensive".)

The issue of how many columns to provide is tedious, of course. Things which can be done to make it go away are discussed in comments in the sourcecode.

I should mention an annoying little thing, which is, at least, not a day wrecker:

After a load, if the StringGrid's option goAlwaysShowEditor is set true, the contents a cell in the grid may look funny. That cell is in "edit" mode, but the text is un-selected. This does little harm, and, on balance, I like the advantages of having goAlways.... set "true".

By "look funny", I mean that the text/ background colors in the affected cell may not be "right". There's more on this minor matter in the rems in the sourcecode. You can set goAlwaysShowEditor to "false" without day wrecking consequences. The data in your StringGrid will be safer, as you will have to double-click on a cell (or press F2) to start editing its contents.

Column widths:

1) They are expressed in pixels.

2) The width of ALL the columns may be adjusted at design time with the StringGrid's DefaultColWidth property.

Sidebar: Maybe the following will reassure you that you are not alone in wasting time fighting with STUPID things?

I was looking for a way to adjust the width of columns. I soon came across "ColWidths" (with the "s", note), and tried to use it thus...

sgData.ColWidths(2):=15;

That, in my hopes, "should" have set the width of the third column to 15 pixels. It didn't work.

The following DOES do just that...

sgData.ColWidths[2]:=15;

See the difference? Yes... the type of brackets around the 2. I was working at the end of a long day, missed that, and went down many irrelevant cul de sacs trying to "make" ColWidths work, or find an alternative. Sigh.

(End sidebar.)

Maybe the time spent wasn't entirely wasted, though. Along the way I found, at....

http://wiki.lazarus.freepascal.org/Grids_Reference_Page#procedure_AutoSizeColumn.28aCol:_Integer.29.3B

procedure AutoSizeColumn(aCol: Integer);

The AutoSizeColumn procedure sets the column width to the size of the widest text it finds in all rows for the specified column.

The page also said: "Tip: see the goDblClickAutoSize option to allow columns to be automatically resized when doubleClicking the column border." I failed to get into that. Maybe it is something that you can only use at design time?

And then there's....

procedure AutoSizeColumns;

... which automatically resizes all columns by adjusting them to fit in the longest text in each column. This is a quick method of applying AutoSizeColumn() for every column in the grid.

To see that in action, add a button to what we are doing...

procedure Tldn191f1.buResizeColsClick(Sender: TObject);
begin
  sgData.AutoSizeColumns;
end;

The button may be helpful from time to time once you get to using the app on "real" data. If you get to the point of using the application "for real", the best width for the various columns will change as the editing process proceeds.

Ready to make early tests

Create a little text file for the app to "chew on"....

860-787-5555, MA, Fred
608-878-6666, CA, Alice
111-222-3333, XX, Henry
113-555-1212, NY, Euphiagenia

The above will do just fine. Save it as "TestDataLDN191.txt", in the folder you are doing all the rest in.

Fixed Columns / Fixed Rows

By the way... sorry... until I can get to addressing all the "issues" of Fixed Columns (or rows) in a StringGrid, I have to ask you to set those properties to zero, if using my Load and Save. (In a nutshell: those columns ARE in the "cells[x,y] world, just like all the other cells of the StringGrid... but they are OUTSIDE of what you can "reach" at run time with mouse clicks or arrow keys.

The related properties are usually set to zero or 1. When set to 1, you get the things where column titles/ row numbers typically go, at the top and on the left.

If you don't mind loading and saving all of the body of your StringGrid AND the fixed columns when you do a load or save, you will be mostly okay. But if you access the cells directly, by code, you'll have to be careful, or you may "do things" you didn't mean to do things in the (supposed to be) "fixed" columns/ rows.)


Ta da! Major elements in place!

Moving on....

Checking a column

Now that we have a way to load some data to a StringGrid, and a way to save that data after any modifications are made, we'll turn to getting the computer to help us spot invalid data.

We will start by checking that the data in the third column is never longer than ten characters. (The third column is "column 2" internally, of course, because computer people count from zero, don't they? (And all of this is based on the assumption that you have NOT created any "fixed columns" (or rows), i.e. that you have left the StringGrid's FixedCols property set to zero)(Remember that I've already conceded that 10 is not "realistic"... but if you can check for 10, you can check for any length, can't you? Ten made illustrations easier to do, without inflicting long lines on your screen.)

So far in this tutorial, we've provided buttons, tied them to as- yet- uncreated procedures, and then written the procedure.

For the data vetting we are going to attack from the other direction. We will build the procedure, and then look at triggering it.

The procedure will be called CheckDataLength, and will be supplied with two parameters, one to specify which column is being checked, and one to say what the maximum acceptable length is.

Eventually, any too-long datum will be flagged by a color highlight. When the length rule is satisfied, the highlight will go away.

That goal, the highlighting part, proved "challenging", but it is accomplished eventually.

The first stage is simply to scan a column, and use a showmessage each time an invalid datum is found.

Create a temporary button to invoke CheckDataLength(2,10);, and don't click that button until you've loaded data into the grid. With the test data provided, you should get messages for row "0" (the first row) and row "7".

procedure Tldn191f1.CheckDataLength(bCol,bLen:byte);
//"Bad Programming": Makes direct access to a global
//object, the StringGrid sgData. I am comfortable
//that in this case, "the rule" has been broken for
//sensible reasons, and in a manner which is not
//likely to be regretted.
var bLoopCounterCDL:byte;
begin
 for bLoopCounterCDL:=0 to sgData.RowCount-1 do begin
   if length(sgData.Cells[bCol,bLoopCounterCDL])>bLen then begin
     showmessage('Too long: '+inttostr(bLoopCounterCDL));
   end;//if length...
 end;//for...
end;//CheckDataLength

So.... that illustrates aspects of supplying answers for our wants.

Coloring cells in StringGrid

Wouldn't it be nice, if there were a procedure that let us say...

sgData.ColorCell(1,2,clBlack,clRed);

... and, presto/ chango, cell 1,2 in the grid would suddenly have its text in black on a red background?

Sadly, there is no such thing. But we can make what it would do happen.

(My thanks to webmaster Guido for the good tutorial at DelphiLand (Festra.com), which is where I found what I needed for what follows. Note: I have "dumbed down" the better answer to be found there, in that I haven't used re-sizable arrays for the color tables. (Were his parents prescient, or is "Guido" a nom de plume?))

To understand the "color the cells" process, you have to understand a bit about how a modern GUI (Graphical User Interface), in a multi-tasking operating system, works.

"When I was a boy", text appeared on a screen... TEXT... one row at a time, top of screen to bottom. When you got to the bottom, the screen scrolled. Stuff that went off the top was gone forever. Actually... that was when I was a young man. Scrolling was "a big leap forward". When I was a BOY, we didn't have VDUs, we had teletypes. One ink color. And once something was on the page, it stayed there. If you were VERY clever, you could do a * by first printing an + and then backing up (!) and printing an x over the top of it. (But those programs tended to be printer specific, i.e. needed tweaks to run on a different printer.

ANYWAY....

Today we have VDUs. And we have multi-tasking operating systems. In the course of writing this for you, I have, hundreds (literally) of times switched back and forth between my trusty text editor (Textpad, from Textpad.com) and the Lazarus IDE.

Each time I switch, thousands of pixels on the screen have to be changed, to show me "the other application".

If you want to color cells in a string grid, you're going to have to go a little way into the world of how things are drawn on the screen.

("Drawn": A broad term. Often we are "drawing" letters. "Drawing", in this context, isn't restricted to, say, lines. In this context, letters are "drawn".)

When drawing a cell of the grid, we have a foreground color and a background color. The parallels with "real world" drawing break down a bit. In the real world, we take a piece of paper (probably white) and a pen (let's say blue). And we make marks on the paper where we want the color, and leave the rest alone, and that draws what we want... be it a flower or some letters.

In "drawing" a cell of a grid (or pretty well anything else, at the lowest levels) we not only do the "blue" bits, but also we "paint" over all of the OTHER bits. So, for our blue flower on a white background, we "draw" the blue bits AND the white bits. This may seem a bit tedious, but it means we don't have to worry about what color our paper is. We don't have to have supplies of different colors of paper. The "paper" can be any color, because after we have drawn the cell, no part of the paper is left showing. Every part of the whole area has been painted either in the foreground color (blue) or in background color (white).

(Fear not... for some things, there are ways to JUST specify where you want "the lines", i.e. the "blue" bits. But they can only be used in specific cases where you've "set up" a "piece of paper", first. (You can choose the color of the "paper" at that time. See my MoveTo/ LineTo tutorial for that kind of drawing.)

Coloring the cell, and text, in a Lazarus (and probably Delphi) StringGrid

"The secret" of coloring a cell of a StringGrid comes in two parts.

The following is at the heart of things, but don't struggle too valiantly with it yet!...

  //Paint the "paper" to establish background color...
  sgData.canvas.brush.color :=clWhite;
  sgData.canvas.FillRect(aRect);
  sgData.canvas.font.color :=clBlack;
  sgData.canvas.TextOut(aRect.Left + 2, aRect.Top + 2,'Hi');

(That's a much stripped-down and inflexible version of where we're going. Among other things, it would lead to only black text on a white background. And it would always put "Hi" in a cell... eventually.)

We'll come back to "the first secret" in a moment.

The second part of the secret is the matter of WHEN the above happens.

In the Good Old Days, you could put something on the screen, and it would stay there. Now... not so easy. But! Our Modern Times have provided some solutions to some of the problems the "progress" has given rise to.

I said that the above would put "Hi" in a cell, eventually.

The good news is that you do not need to micro-manage the "put it there" timing.

The above code, with some modifications, is going to be put in the event handler for the drawing of a cell in the grid. If you want to do something as a step towards our final "answer", you could add the following to the application we have so far. You need to add it to the application as follows... and don't be alarmed. It will be changed before we are done. At the moment it pretty well destroys any sensible functionality in the StringGrid... but it does illustrate something interesting.

Go to the form, in your Lazarus IDE. Click on the StringGrid. Press f11, to get into the Object Inspector, looking at the properties of the StringGrid. And then click on the "Events" tab.

Double-click on the OnDrawCell event. That will take you back to the sourcecode editor, with an empty procedure Tldn191f1.sgDataDrawCell(Sender: TObject; aCol, aRow: Integer; aRect: TRect; aState: TGridDrawState); sketched in for you.

Between the "begin" and the "end;", insert....

  //Paint the "paper" to establish background color...
  sgData.canvas.brush.color :=clWhite;
  sgData.canvas.FillRect(aRect);
  sgData.canvas.font.color :=clBlack;
  sgData.canvas.TextOut(aRect.Left + 2, aRect.Top + 2,'Hi');

... and run the now crippled application. But it is crippled in an interesting way!

All of the cells of the string-grid are filled with "Hi".

Even though you didn't, explicitly, at any time say "Fill the cells". Before we discuss what is going on, just for a jolly, try clicking the Load button we created earlier. It should look as though (almost) nothing happened. But something did, I promise! ("Almost"? The column widths change, don't they.)

Here's what's going on:

When we modified the OnDrawCell event handler, we told the machine that whenever any cell of the StringGrid is drawn, or re-drawn, then put "Hi" (in black, on white) in that cell. (I won't keep saying "drawn or re-drawn". Consider "drawn" to imply both.)

We don't have to worry very much about WHEN cells are drawn... the system takes care of doing it for us at all sorts of times... for instance when a window is re-sized. What we DO need to be careful about is taking steps to cause it to happen at "odd" times that the system wouldn't normally worry about. What were we working towards? We want to "re-draw" a cell, with the old text, but in a new color scheme, if, during our tests of the data validity, we find an invalid datum somewhere. But don't worry... there's a neat, simple to execute (if not so simple to understand!) trick to take care of this for us.

Change the OnDrawCell event handler as follows. What we are left with is "whenever a cell is drawn, make a note of what text is in it before we Do Things, Do Them, and then reinstate the text that was there before.

For the moment, the Things that we do will be inconsequential... don't worry about that... we're just on a journey, and this is but one step. When you run the application now, things should, apparently, be back to where we were before. The "extra stuff" is happening; it just doesn't change the appearance of things... yet. You should even be able to use the Load button again, and get sensible results.

procedure Tldn191f1.sgDataDrawCell(Sender: TObject; aCol, aRow: Integer;
  aRect: TRect; aState: TGridDrawState);
//Thanks to http://www.festra.com/wwwboard/messages/12892.html
//  for the information used to implement this approach to coloring
//  a cell in my StringGrid.
var sTmpL:string;//                                      **
begin
  //Take a note of what is in the cell at the moment...  **
  sTmpL:= sgData.Cells[aCol, aRow];//                    **
  //Paint the "paper" to establish background color...
  sgData.canvas.brush.color :=clWhite;
  sgData.canvas.FillRect(aRect);
  sgData.canvas.font.color :=clBlack;
  sgData.canvas.TextOut(aRect.Left + 2, aRect.Top + 2,sTmpL);//**
end;//sgDataDrawCell

There are four new lines; each has a "**" at its right-hand end.

I'm not going to analyze that too closely... that's up to you. I will help you with a couple of things which should be bothering you.

This bit of code is about drawing the contents of ONE cell in the grid. The cell at aCol, aRow, it would seem from....

sTmpL:= sgData.Cells[aCol, aRow];

But! We never put anything into the variables aCol, aRow, did we??

No... "we" didn't... but "beneath the surface", the (operating) system did, along the way of calling the event handler which we have been modifying. See the "aCol, aRow: Integer;" in the header of the subroutine? The things that call this subroutine pass values for "which column", "which row" whenever they use the subroutine.

Ah! And that also explains the (perhaps) "mysterious" aRect in...

sgData.canvas.FillRect(aRect);

... and the TextOut line. I hope. (It does. I hope you see!)

Moving on...

AGAIN, not something to be part of our final application, but to help you see how things work:

Create a global variable iPass, type integer.
In the FormCreate handler, set it to 0.
Revise DrawCell as shown below.

Now when you run the application, and click "Load", things go as before. But click on the upper left cell (aRow=0/ aCol=0), and then click somewhere else... and the backgrounds are cyan ("Aqua", in Windows-speak)!

Not "useful", except if, after studying the code below, you come to understand what is going on.

Why all the messing about with iPass? Why not a simple boolean, boFirstPass? Because this is the DrawCell handler. For a 3x5 grid, it is called 15 times, just to put the initial StringGrid on the screen. (I could probably tapped into sgData's handler for the OnClick event handler, watched "passes" there... but that seemed more distracting than the approach I used.)

begin
  //Take a note of what is in the cell at the moment...
  sTmpL:= sgData.Cells[aCol, aRow];
  //Paint the "paper" to establish background color...
  if iPass<3 then begin
     sgData.canvas.brush.color :=clWhite;
     if ((aCol=0) and (aRow=0)) then inc(iPass);;
     end// no ; here
  else begin
     sgData.canvas.brush.color :=clAqua;
     end;//else
  sgData.canvas.FillRect(aRect);
  sgData.canvas.font.color :=clBlack;
  sgData.canvas.TextOut(aRect.Left + 2, aRect.Top + 2,sTmpL);
end;//sgDataDrawCell

In summary: You can't "directly" set the color of text or background in a cell of a StringGrid. But we can "hijack" an event handler, and cause things to go our way the next time a cell is re-drawn... and we can trigger a re-draw.

We may also be able to have cells checked any time we alter their contents. Wouldn't that be nice... they could be checked during the initial fill, and the grid would be filled with data AND indications of validity as soon as the grid was filled!

Either would be served by a subroutine...

SetCellFgBg(iRow,iCol,clFg,clBg)

... where iRow and iCol say WHICH cell. (0,0, as usual, meaning the cell at the upper left.) clFg and clBg would be data of type TColor, and would specify the color for the text ("Foreground"), and the color for the background.

It is Bad Programming, but I am going to use globals to "pass" data between the subroutine we are going to write, and a modified version of the sgDrawCell event handler we have already been working with.

Not only am I going to use globals, but those globals will be two quite large two-dimensional arrays... one for the current "correct" foreground color, one for the current "correct" background color for each cell in the StringGrid.

Which gives rise to a tedious problem. There are "clever", dynamic ways around it. I'm going to use something more pedestrian...

Those arrays will be declared with lines similar to...

clFgG,clBgG:array [0..15,0..15] of TColor;

The "problem" is that as soon as we've used the 15, many other parts of the code are affected. We can't, if clFgG[x] is declared as above, work with a StringGrid of 17 rows.

One thing we can do to reduce the tedium is to use constants for the upper limits of the array indices. We'll use kGridXMaxIndex and kGridYMaxIndex to replace the "15"s in the first draft of the clFgG, clBgG declaration above. Before that will compile, we need to put...

const kGridMaxXIndex=15;
   kGridMaxYIndex=15;

... into the code. It goes just after the "Uses" clause, up at the top of the unit.

The "15" I used is arbitrary... you will need to use different numbers, depending on the number of columns and rows you want in your StringGrid. Small numbers will be restrictive, big numbers will make demands on your system's memory when the application runs.

And once you've established kGridMaxXIndex, kGridMaxYIndex, you need to be disciplined... there are no "crutches" to aid you.

As an example of where you will use your new constants, there is the following, which needs to be present in your FormCreate handler...

procedure Tldn191f1.FormCreate(Sender: TObject);
var bLoopCountL,bLoopCountL2:byte;
begin
  for bLoopCountL:= 0 to kGridMaxXIndex do
   for bLoopCountL2:= 0 to kGridMaxYIndex do begin
     clFgG[bLoopCountL,bLoopCountL2]:=clBlack;
     clBgG[bLoopCountL,bLoopCountL2]:=clWhite;
   end;//for bLoopCountL2
  //(the first "for" needs no end, as it had no begin)
end;

The question of how big the grid, and associated variables is not the only bit of tedium... but alternatives could be even worse. (Certainly the one I tried was! There's a great "chicken and egg" battle going on, if you aren't careful.)

The other tedious bit on my mind at this stage is that we need, as implied by the arrays we have set up, to "manually" keep track of the foreground and background colors we want for each cell in the array. Sigh. But! Having put the means for that in place, other things become fairly straight forward.

I'll start with the code for the new subroutine, then show you the tweaked sgDrawCell, and then show you a way to see if it is working!

Should I apologize? The names Lazarus throws up when you ask for a skeleton DrawCell event handler include "aRow" and "aRect", among others. Rather than fight that, I went with the flow. As aRow is of type "Integer", and aRect of type "TRect" (not Rex!), I would have preferred the names "iRow" and "rRect". I have gone with my preferences in my own subroutine. I think the Hungarian notation is a Good Practice generally. (The link will take you to Wikipedia. You may note that my way of using the notation varies somewhat from what is described there, although I follow the basic idea.)

ORIGINALLY, I was using the following version of SetCellFgBg. It "works".

procedure Tldn191f1.SetCellFgBg(iCol,iRow:integer;clFg,clBg:TColor);
//There has GOT to be a better way. In particular, I don't like the
//  globals clFgG and clBgG... but this WORKS, and is the best I
//  can manage for the moment.
//N.B. This depends for it's functioning upon the DrawCell event
//  handler "sgDataDrawCell".
var sTmpL:string;
begin
 sTmpL:=sgData.Cells[iCol,iRow];//Yes... we do need this and the
   //"sgData.Cells[iCol,iRow]:=sTmpL;" line below...
   //both here and in DrawCell
 clFgG[iCol,iRow]:=clFg;
 clBgG[iCol,iRow]:=clBg;
 sgData.Cells[iCol,iRow]:=sTmpL;//See note on...
   //sTmpL:=sgData.Cells[iCol,iRow]; line
 end;//SetCellFgBg

That (above) is the new routine. As yet not invoked. And, (below), the tweaked DrawCell handler, which, as before, is called by the system from time to time, behind the scenes...

procedure Tldn191f1.sgDataDrawCell(Sender: TObject; aCol, aRow: Integer;
  aRect: TRect; aState: TGridDrawState);
//Thanks to http://www.festra.com/wwwboard/messages/12892.html
//  for the information used to implement this approach to coloring
//  a cell in my StringGrid.
var sTmpL:string;
  clFgPrev,clBgPrev:TColor;
begin
  //Take a note of what is in the cell at the moment...
  sTmpL:=sgData.Cells[aCol,aRow];//Yes... we do need this and the
   //".. TextOut..." line below... both here AND in SetCellFgBg

  //Paint the "paper" to establish background color...
  sgData.canvas.brush.color:=clBgG[aCol,aRow];
  sgData.canvas.FillRect(aRect);
  sgData.canvas.font.color:=clFgG[aCol,aRow];
  sgData.canvas.TextOut(aRect.Left + 2, aRect.Top + 2,sTmpL);//See
    //note on "sTmpL:=sgData...." line above.
end;//sgDataDrawCell

The application should run with the changes indicated above... but you won't (yet) see anything interesting going on. In fact, the application is now back to being less interesting, as the "experiment" which changed the cell backgrounds has been taken out.

Add a temporary button, which just does...

procedure Tldn191f1.Button1Click(Sender: TObject);
begin
  SetCellFgBg(0,2,clGreen,clGray);
end;

I hope that is self-explanatory? Check that it does what it should... change (persistently) the foreground and background colors of the cell in the first ("0") column, third row.

A detail...

My routine, which you will use in various ways, SetCellFgBg, expects the "x" coordinate of the cell you are interested in first, and the "y" coordinate (row, counted from first line called "row 0") second.

The "internal", though modified by us, sgDataDrawCell expects the coordinates in the other order, for reasons which escape me. Maybe it is legacy from the days when paper was being cranked up and down in a mechanical, ink-on-paper printer?

Whew! Take a break! You've earned it... but at least we have in place the "tools" for changing the colors in cells. At last. Next we will use those tools...

Checking the length of the data we loaded

Now that we know a bit more about using colors in StringGrid cells, we can go back to what we wanted in the first place....

... which was, if you cast your mind back, to highlight cells which did not meet some criterion.

Cast your mind back to the crude "CheckDataLength" we created earlier. It, in pseudo code, did...

Check each cell in turn
If the contents do not satisfy the criteria,
   then perform a showmessage.

We're now in a position to modify that, create something that will...

Check each cell in turn
If the contents do not satisfy the criteria,
   then change the relevant entries in the
      clFgG and clBgG arrays

As we have seen, if the DrawCell code draws on what's in those arrays, things more or less "take care of themselves" from there.

A long time ago, we took care of the code to load a CSV file into our StringGrid. This was attached to the buLoad button's OnClick handler.

Once the load was done, besides the data in the StringGrid, we had, in xx and yy respectively, the number of columns used and the number of rows used.

If we wanted to be "clever", we could do the data checks as the data is being loaded. But I find "clever" usually costs time in figuring out "What Did I Do??"

So I'm taking a pedestrian approach: I will call an improved "CheckDataLength" AFTER the "load" is completed.

The routine is "CheckDataLength" and you pass it the number of the column you want to check (using, as ever, "0" for the first column) and the maximum length allowed for the data in that column. The routine checks the data in the column, changing the color of cells as it goes along. You can see the color scheme from the code. (In a "finished" program, I would put the four colors in constants, define them up at the top of the code where the color setting would be easy to find. Or I might even set them via an .ini file.

The revised CheckDataLength is...

procedure Tldn191f1.CheckDataLength(bCol,bLen:byte);
//"Bad Programming": Makes direct access to a global
//object, the StringGrid sgData. I am comfortable
//that in this case, "the rule" has been broken for
//sensible reasons, and in a manner which is not
//likely to be regretted.
var bLoopCounterCDL:byte;
begin
 for bLoopCounterCDL:=0 to sgData.RowCount-1 do
   if length(sgData.Cells[bCol,bLoopCounterCDL])>bLen then begin
     SetCellFgBg(bCol,bLoopCounterCDL,clBlack,clRed);
     end//no ; here
   else begin
     SetCellFgBg(bCol,bLoopCounterCDL,clGreen,clWhite);
     end//no ; here. End of "else"
 //no "end" needed for "for bLoopCountL..."
end;//CheckDataLength

Besides using the CheckDataLength procedure to service the CheckLengths button, if....

CheckDataInList(bInListTestedColumn);

... is inserted into buLoadClick, just after the "LoadFileToStringGrid('TmpDataToLoad.txt');", then the data gets that check, too, as soon as it is loaded.

Alas.

All of the above about changing the foreground and background colors was a lot of work, but it seemed to be working. A little while later, after I'd gone on to other things, I discovered an Unintended Consequence. Sigh.

The stuff described DOES work! But when your try to edit the contents of a cell AND check what you are doing as you go along AND keep the colors up to date, problems arise. The cursor keeps getting moved down to the right hand end of what's in the cell. Annoying.

We're going to leave the "check cell contents valid" stuff where it is. And as we check it, we are going, if need be, to update the values in the Fg and Bg arrays. But, we won't immediately use the code that forces the cell to be re-drawn, which is how we have been "changing" the colors seen on the screen. You can't really see the colors when the cell contents are being edited, so postponing the color update won't "cost" anything, anyway! Whew!

Just taking two lines out of SetCellFgBg fixed the problem we had! There is a slight downside: cells are no longer colored to reflect their validity AS we edit them any more, but that wasn't very obvious, anyway. As soon as we leave the cell, another place where the cell contents are drawn (sgDataDrawCell) kicks in, and it checks the Fg and Bg arrays... which we've recently updated... to see what colors to use. Whew.

(A label could be set up to carry messages about any "bad" cell, and be immediately responsive, as we edited it. I think. (Something to try one day!))

The line that caused the problem with the editing was...

sgData.Cells[iCol,iRow]:=sTmpL;

If that's coming out, it makes sense also to remove...

sTmpL:=sgData.Cells[iCol,iRow];

... which is only there to prepare the way for the other one.

Done?

Alas, no. We've achieved amazing things. But our application doesn't yet really do what we wanted. We haven't even got to the point where only minor tidies remain. What we HAVE accomplished...

We can load a file. As soon as it is loaded, the third column is checked, and any too-long data are highlighted. (But we aren't yet checking the second column, to see that all of the entries are valid USA state abbreviations. No big deal... but no yet implemented.)

We can then edit the data. And, if we click the "Check Lengths" button, the StringGrid colors (foreground and background) are updated, and made right for the data as it now stands.

Not good enough! The cell colors should change as soon as we change the cell contents from "too long" to "not too long"... or vice versa!

Quite easily done, at this point, thank heavens.

On the road to that, we are going to start by taking the heart of CheckDataLength, and putting it in a discrete subroutine. (You'll see why in a moment!) So, what we had before, becomes...

procedure Tldn191f1.CheckDataLength(bCol,bLen:byte);
var iLoopCounterCDL:integer;
begin
 for iLoopCounterCDL:=0 to sgData.RowCount-1 do
   CheckOneCellDataLength(bLen,bCol,iLoopCounterCDL);
end;//CheckDataLength

procedure Tldn191f1.CheckOneCellDataLength(bLen,bCol:byte;iRow:integer);
//"Bad Programming": Makes direct access to a global
//object, the StringGrid sgData. I am comfortable
//that in this case, "the rule" has been broken for
//sensible reasons, and in a manner which is not
//likely to be regretted.
begin
  if length(sgData.Cells[bCol,iRow])>bLen then
    SetCellFgBg(bCol,iRow,clBlack,clRed)//no ; here
  else
    SetCellFgBg(bCol,iRow,clGreen,clWhite);//no "end" here. End of "else"
end;//CheckOneCellDataLength

After that little re-arrangement, we can use the SetEditText handler of sgData to check just an edited cell, just after it is edited. (JUST ONCE something has been simple! Hurrah and head shaking in amazement!)

procedure Tldn191f1.sgDataSetEditText(Sender: TObject; ACol, ARow: Integer;
  const Value: string);
begin
  CheckOneCellDataLength(10,aCol,aRow);
end;

(Sidebar: Here and elsewhere in the code, until now, the length limit for the data in the column has been hard-coded as 10. At this point, I established a global variable, bLengthLimitTestedColumn, put 10 into it during FormCreate, and replaced all the 10s scattered through the code with bLengthLimitTestedColumn. Just a "tidy", and it makes it much easier (and hence less subject to error) to "tweak" the code when requirements change.) And I made mistakes... which I've since fixed, at least in the sourcecode you can download. At least I think I fixed all of them. There was a little confusion between places where WHICH column gets the testing for "is the datum too long", and the definition of "too long". The relevant variables are....

bLengthTestedInWhichCol:=2;//WHERE the data to be
    //checked for length resides.
 bLengthLimitInTestedColumn:=10;//HOW LONG a datum may
    //be, without being considered unacceptable.

(End sidebar.)

Wow! I'm exhausted... I hope you are impressed? A lot of what we set out to do has been done. What remains is mostly minor tidies, apart from setting up doing the OTHER sort of data vetting on the second column. But even for that, most of what we need is in place. Oh, and we haven't attempted the user-friendly shuffling of file names... but at least the application doesn't recklessly overwrite your source data with the revised data, as some applications are prone to do, if you aren't careful. (It WILL overwrite any previous data SAVES... but that is easily made more user friendly. Oh, and speaking of user friendliness, if you fail to save your edits before quitting the application, you won't be asked "Don't you want to save your work first?")

The other data validation check- is "State" in the list?

Again... I could probably be "clever", combine both sorts of data check in one "do all" routine. And elements may have to be combined. But as far as I can, I will keep the checks separate, for sanity's sake.

Our data, remember, looks like...

860-787-5555, MA, Fred
608-878-6666, CA, Alice
111-222-3333, XX, Henry
113-555-1212, NY, Euphiagenia

In the second column (column "1"), we are supposed only to have MA, CA, NY (or other state abbreviations, as sanctioned by the US Post Office). "XX" is invalid data, which should be flagged in a manner very like the manner used previously to flag "too long" data in the "name" column. Easy... now that we've done what we've done previously. Well. Easier.

As before, we'll start with a crude "checker", and call it within buLoadClick, just after the LoadFileToStringGrid('TmpDataToLoad.txt');

We'll call it "CheckDataInList", and it will be similar to CheckDataLength. As with the latter, for now, we will hard-code which column is tested against the list, so that later (not in this tutorial), we can use the routine more flexibly.

With our previous experience, getting started is easy enough...

procedure Tldn191f1.CheckDataInList(bCol:byte);
var iLoopCounterCDL:integer;
begin
 for iLoopCounterCDL:=0 to sgData.RowCount-1 do
   CheckOneCellDataInList(bCol,iLoopCounterCDL);
end;//CheckDataInList

Of course, that won't run until we've written CheckOneCellDataInList. Here's what my first version looked like, as an example of how code can be built in stages...

procedure Tldn191f1.CheckOneCellDataInList(bCol:byte;iRow:integer);
//"Bad Programming": Makes direct access to a global
//object, the StringGrid sgData. I am comfortable
//that in this case, "the rule" has been broken for
//sensible reasons, and in a manner which is not
//likely to be regretted.
begin
  if (sgData.Cells[bCol,iRow])<>'MA' then
    SetCellFgBg(bCol,iRow,clBlack,clRed)//no ; here
  else
    SetCellFgBg(bCol,iRow,clGreen,clWhite);//no "end" here. End of "else"
end;//CheckOneCellDataInList

As soon as we've put a CheckDataInList(1); in buLoadClick after the LoadFileToStringGrid..., the new code works just fine to highlight any state that isn't "MA" as "wrong". That's a good start! Easily leveraged into a finished routine....

procedure Tldn191f1.CheckOneCellDataInList(bCol:byte;iRow:integer);
//"Bad Programming": Makes direct access to a global
//object, the StringGrid sgData. I am comfortable
//that in this case, "the rule" has been broken for
//sensible reasons, and in a manner which is not
//likely to be regretted.
var boTmpL:boolean;
  sTmpL:string;
begin
  boTmpL:=false;//Start with idea that datum is NOT valid.
  sTmpL:=sgData.Cells[bCol,iRow];
  if sTmpL='MA' then boTmpL:=true;
  if sTmpL='CA' then boTmpL:=true;
  if sTmpL='NY' then boTmpL:=true;
  //Etc. As many "if sTmpL='....' then boTmpL:=true;"
  //  as you wish, and then....

  if boTmpL=false then
    SetCellFgBg(bCol,iRow,clBlack,clRed)//no ; here
  else
    SetCellFgBg(bCol,iRow,clGreen,clWhite);//no "end" here. End of "else"
end;//CheckOneCellDataInList

As bLengthLimitTestedColumn, I needed to set up a bInListTestedColumn, which is set to 1 in the FormCreate handler. Once that was in place I revised...

procedure Tldn191f1.sgDataSetEditText(Sender: TObject; ACol, ARow: Integer;
  const Value: string);
begin
  if aCol=bLengthLimitTestedColumn then
       CheckOneCellDataLength(bLengthLimitTestedColumn,aCol,aRow);
  if aCol=bInListTestedColumn then
       CheckOneCellDataInList(aCol,aRow);
end;

(Without the "if aCol..." elements, very strange things, happened!)

Done! Hurrah!

Clear a scrap, etc...

Long ago, we created a button, buChkWidth, while we were working towards better things. That, and the associated Click handler can be removed at this point, as it no longer serves a purpose.

An argument could be made for moving the sgData.AutoSizeColumns; currently invoked by the button, to somewhere inside the sgDataSetEditText handler. That would "work", but it would mean the user would find the column widths changing during the editing process, perhaps at unhelpful moments.

Well.. done, except....

Inside the procedure which saves the contents of the StringGrid, you may want to use the Lazarus "trim" function, to strip leading and trailing spaces off of data while they are in transit to the CSV file.

In summary

The application we have developed is by no means a good, general purpose, answer to a general want. It has too many things hard-coded. For instance, which columns get checks, and what is checked cannot be changed easily. But the application here does illustrate how various things can be done.

I hope to write a second tutorial, covering an enhanced version of this, with that flexibility built into it. The "better" code won't do much "new"... but it will use some arrays and probably an ini file, to make configuring instances of the application possible.

But... does anyone read these things? Is it worth me doing that second tutorial?

Out of sorts?

If you were wondering if there is a way to make the application sort the rows, the answer is "yes". It is even really quite simple... in and of itself. However, to add that to this application requires bringing in some "stuff" (A fixed row at the top of the StringGrid.) It isn't even very hard to bring that in... but there are a bunch of consequences to be considered, dealt with. Sigh. I have made a note to do a tutorial for you, in due course.

Finally, before we go...

Here's a listing of the whole of the sourcecode...

unit ldn191u1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls,
  Grids,ClipBrd;

const vers='3 Oct 16';
   //started 26 Sep 16, on road to an application to
   //   vett DVD records data
   kGridMaxXIndex=15;
   kGridMaxYIndex=25;

   //Following determines color scheme for the contents
   //of the cells in the StringGrid...
   //    FG / BG: The color of the text (ForeGround) /
   //               the color of the BackGround
   //    ok/ bad: Data in the cell OK (valid) or not (BAD)
   kClBGok=clWhite;
   kClFGok=clGreen;
   kClBGbad=clRed;
   kClFGbad=clBlack;

   kFieldSep=',';//You might want to put this in a variable,
     //set it during FormCreate, maybe from an ini file. That
     //would make the application more flexible. There is a
     //"system defined" Field Separator, which varies depending
     //on what locale the machine has been set up for. You
     //could tell the application to fill kFieldSep from that.


type

  { Tldn191f1 }

  Tldn191f1 = class(TForm)
    buQuit: TButton;
    buLoad: TButton;
    buSaveWithNewName: TButton;
    buResizeCols: TButton;
    buAbout: TButton;
    buLinkToWebClick: TButton;
    sgData: TStringGrid;
    procedure buAboutClick(Sender: TObject);
    procedure buLoadClick(Sender: TObject);
    procedure buQuitClick(Sender: TObject);
    procedure buResizeColsClick(Sender: TObject);
    procedure buSaveWithNewNameClick(Sender: TObject);
    procedure buLinkToWebClickClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure sgDataDrawCell(Sender: TObject; aCol, aRow: Integer;
      aRect: TRect; aState: TGridDrawState);
    procedure sgDataSetEditText(Sender: TObject; ACol, ARow: Integer;
      const Value: string);

  private
    { private declarations }
    clFgG,clBgG:array [0..kGridMaxXIndex,0..kGridMAxYIndex] of TColor;
    bLengthLimitInTestedColumn,bLengthTestedInWhichCol,
      bInListTestedColumn:byte;
    sTmp:string;
    sCellContentsWhenFSWarningIssued:string;//This is a nasty little
      //bit of Kludging, perhaps. See notes where it is used.
    procedure SaveStringGrid(sFilename:string);
    procedure LoadFileToStringGrid(sFileName:string);
    procedure CheckDataLength(bCol,bLen:byte);
    procedure SetCellFgBg(iCol,iRow:integer;clFg,clBg:TColor);
    procedure CheckOneCellDataLength(sTmpL:string;bLen,bCol:byte;iRow:integer);
    procedure CheckDataInList(bCol:byte);
    procedure CheckOneCellDataInList(bCol:byte;iRow:integer);
    procedure StringToSGRow(sgLocal:TStringGrid;
                             sRawData:string;iRow:integer;boTrimFields:boolean);
  public
    { public declarations }
  end;

var
  ldn191f1: Tldn191f1;

implementation

{$R *.lfm}
{$R+}
// {$R+} turns on range-checking.

{ Tldn191f1 }

procedure Tldn191f1.FormCreate(Sender: TObject);
var bLoopCountL,bLoopCountL2:byte;
begin
  sCellContentsWhenFSWarningIssued:='';
  ldn191f1.caption:='LDN191- Use StringGrid to vett data- ver: '+vers;
  application.title:='LDN191';
  for bLoopCountL:= 0 to kGridMaxXIndex do
   for bLoopCountL2:= 0 to kGridMaxYIndex do begin
     clFgG[bLoopCountL,bLoopCountL2]:=kClFGok;
     clBgG[bLoopCountL,bLoopCountL2]:=kClBGok;
   end;//for bLoopCountL2
  //(the first "for" needs no end, as it had no begin)

  bLengthTestedInWhichCol:=2;//WHERE the data to be
    //checked for length resides.
  bLengthLimitInTestedColumn:=10;//HOW LONG a datum may
    //be, without being considered unacceptable.
  bInListTestedColumn:=1;//WHERE the data to be checked
    //for being in the list of acceptable values resides.

end;

procedure Tldn191f1.SaveStringGrid(sFilename:string);
//Doing it by the "CommaText" solution causes quotation
//  marks to be inserted around fields with spaces
//  in them... which will cause
//  problems if the CSV is subsequently re-loaded
//  into this app.
//
//It might also be a good idea to check for field
//  separators within cells just before attempting
//  a save. If present, they will
//  cause a problem if you try to reload the product
//  of the save, unless fairly significant changes
//  are made to how things are saved, how they are
//  loaded. (One answer would be to enclose such
//  data in quotation marks... but that gives rise
//  to other issues... not least providing for
//  things IN quotation marks being loaded as
//  special cases.
//
//  If you use the "CommaText" version which can be
//  created from what follows, the contents of
//  cells with a field separator (if it is a comma?)
//  in it WILL be enclosed in quotes. As will
//  data with internal spaces. But, given that
//  the "load" process would have to be adapted
//  anyway, that wouldn't(?) be a problem. (But
//  I would still be inclined to avoid the
//  "CommaText" answer, if only because I know
//  that "funny things" happen when you rely
//  on the system's idea of the field separator,
//  e.g. if the system has been told it is somewhere
//  other than the USA, the field separator may
//  be something else.

var
  cardCounter:Cardinal;
  stringsTmp:TStrings;

    //Next is SR of SaveStringGrid...
    function boIsFieldSeparatorInACell(var cardRowTmp:cardinal):boolean;
    var cardRowCounter:cardinal;
        bColCounter:byte;
        boTmpL:boolean;
    begin
      boTmpL:=false;
      for bColCounter:=0 to sgData.ColCount-1 do
        for cardRowCounter:=0 to sgData.RowCount-1 do begin
           if pos(kFieldSep,sgData.cells[bColCounter,cardRowCounter])>0
              then begin
                boTmpL:=true;
                cardRowTmp:=cardRowCounter;
                end;
        end;//for bColCounter
      result:=boTmpL;
    end;// boIsFieldSeparatorInACell, SR of SaveStringGrid

    //Next is SR of SaveStringGrid...
    function sFetchRowOfCells({sgTmp:stringgrid;}cardRowL:cardinal):string;
    var bTmpL:byte;
        sTmpL:string;
      begin
        sTmpL:='';
        for bTmpL:=0 to sgData.ColCount-1 do begin
          sTmpL:=sTmpL+sgData.Cells[bTmpL,cardRowL];
          if bTmpL<>sgData.ColCount-1 then sTmpL:=sTmpL+kFieldSep;
          end;//for...
        result:=sTmpL;
      end;//sFetchRowOfCells, SR of SaveStringGrid

begin //SaveStringGrid
  //"cardCounter" used, in first line, as a "temporary"
  //   variable. What is in it, if anything, on entry
  //   to boIsFieldSeparatorInACell DOESN'T MATTER,
  //   nor do we still want what is in it afterwards.
  if boIsFieldSeparatorInACell(cardCounter) then begin
    sTmp:=kFieldSep;
    showmessage('At least one cell contains the '+
      'field separator. the field separator is '+
      sTmp+chr(13)+'The cell is in row '+
      inttostr(cardCounter));
    end;

stringsTmp:=TStringList.Create;
try
    for cardCounter := 1 to sgData.RowCount do
    begin
      //One "solution".... see sheepdogguides.com/lut/ltn4c.htm
      //   for discussion of the pros and cons of the alternatives.
      (*stringsTmp.Add(sgData.Rows[cardCounter-1].CommaText);*)
      //This is where the first solution finishes

      //Alternative solution...
      //BEFORE WE START ON THAT...
      //      a *** G E N E R A L  P O I N T ***
      //Beware how the references to the two indices of the
      //  StringGrid are managed. Where we merely need to do
      //  something once for each row or columm, I've sometimes
      //  used For Counter=1 to NumberOfThem. Other times, used
      //  For Counter=0 to NumberOfThem-1. When we need to refer
      //  to a particular cell, the fact that they are "numbered"
      //  FROM 0 needs to be remembered... and sometimes the
      //  associated "-1" to "translate" from the "name" of the
      //  row or column is done in the variable driving the "for"
      //  loop, sometimes not. Sigh.
      //
      //All of the above true as it stands when the StringGrid's
      //  FixedCols and FixedRows properties set to zero. When
      //  they are NOT, you must deal with the fact that the
      //  fixed rows/ columns are "ordinary columns/ rows" as
      //  far as the cells[x,y] array is concerned.

      sTmp:=sFetchRowOfCells({sgData,}cardCounter-1);
      stringsTmp.Add(sTmp);//"Add" appends the EOL code, too.
      end;//for
      //This is where the alternative solution finishes

    stringsTmp.SaveToFile(sFilename);
finally
    stringsTmp.free;
end;//try

showmessage('CSV re-saved, to '+sFilename+'. (If a '+
   'file of that name existed previously, it was '+
   'over-written.');
end;//SaveStringGrid

procedure Tldn191f1.buLoadClick(Sender: TObject);
//Note: After a load, if the StringGrid's option
//  goAlwaysShowEditor is set true, the contents
//  of the first cell in the grid (upper left)
//  may look funny. It is in "edit" mode, but the
//  text is un-selected. This does little harm,
//  but is annoying.
//"Look funny": If the things that were once with
//  this code which change the color of cell
//  contents are still in place, the text/ background
//  in the first cell may not be "the right" colors.
//If a way to select the text in the first cell...
//  select THE TEXT, not just the cell... could be
//  found, and added just after the LoadFileToStringGrid
//  this annoying "feature" would go away.
//Or just set goAlwaysShowEditor to "false"!
begin
LoadFileToStringGrid('TestDataLDN191.txt');
//It would be nice if a way could be found to send the cursor
//to cell 0,0 at this point... it would make the user experience
//constent. (At the momemt, after a Load, the cursor remains
//where it was previously. Seems better to me if after a load
//it is always in the same cell. ALSO, if the "go to 0,0" could
//be achieved, I hope it would go to that cell AND select the
//contents. At the moment, we are in whatever cell we are in,
//ready to edit (because we set option goAlwaysShowEditor true),
//but the text isn't selected, and the insertion point isn't flashing,
//so one cell, for no apparent reason, is black text on white
//while everything else is green on white, or red on black.
sgData.AutoSizeColumns;
CheckDataLength(bLengthTestedInWhichCol,bLengthLimitInTestedColumn);
CheckDataInList(bInListTestedColumn);
end;

procedure Tldn191f1.buAboutClick(Sender: TObject);
begin
  showmessage('This is one of many applications '+
    'available from https://sheepdogguides.com/Lut/'+chr(13)+
    '... a collection of Lazarsus and Delphi tutorials.');
end;

procedure Tldn191f1.sgDataDrawCell(Sender: TObject; aCol, aRow: Integer;
  aRect: TRect; aState: TGridDrawState);
//Thanks to http://www.festra.com/wwwboard/messages/12892.html
//  for the information used to implement this approach to coloring
//  a cell in my StringGrid.
var sTmpL:string;

begin
  //Take a note of what is in the cell at the moment...
  sTmpL:=sgData.Cells[aCol,aRow];//Yes... we do need this and the
   //".. TextOut..." line below... both here AND in SetCellFgBg

  //Paint the the "paper" to establish background color...
  sgData.canvas.brush.color:=clBgG[aCol,aRow];
  sgData.canvas.FillRect(aRect);
  sgData.canvas.font.color:=clFgG[aCol,aRow];
  sgData.canvas.TextOut(aRect.Left + 2, aRect.Top + 2,sTmpL);
    //See the note on "sTmpL:=sgData...." line above.

end;//sgDataDrawCell

procedure Tldn191f1.SetCellFgBg(iCol,iRow:integer;clFg,clBg:TColor);
//There has GOT to be a better way. In particular, I don't like the
//  globals clFgG and clBgG... but this WORKS, and is the best I
//  can manage for the moment.
//N.B. This depends for it's functioning upon the DrawCell event
//  handler "sgDataDrawCell".
var sTmpL:string;
begin
 (* 02 Oct 16, 21:13>> This (and the one below) taken out... fixes problems
       which arose during the editing of cell contents! (^_^)

       sTmpL:=sgData.Cells[iCol,iRow];

  the old comment:
       //Yes... we do need this and the
       //"sgData.Cells[iCol,iRow]:=sTmpL;" line below...
       //both here and in DrawCell

  ... appears to be WRONG. We ONLY (I hope) need it in DrawCell.
  AND it seems that it causes no problems there!!

  The slight downside is that cells are not colored to
  reflect their validity AS we edit them any more.

  A label could be set up to carry messages about any
  "bad" cell, and be immediately responsive, as we
  edited it. I think.*)

 clFgG[iCol,iRow]:=clFg;
 clBgG[iCol,iRow]:=clBg;

 (* 02 Oct 16, 21:13>> This (and the one above) taken out... fixes problems
       which arose during the editing of cell contents! (^_^)

    sgData.Cells[iCol,iRow]:=sTmpL;

    ... see note above about whys, wherefors. *)

 end;//SetCellFgBg

procedure Tldn191f1.CheckDataLength(bCol,bLen:byte);
var iLoopCounterCDL:integer;
begin
 for iLoopCounterCDL:=0 to sgData.RowCount-1 do
   CheckOneCellDataLength(sgData.Cells[bCol,iLoopCounterCDL],bLen,bCol,iLoopCounterCDL);
end;//CheckDataLength

procedure Tldn191f1.CheckDataInList(bCol:byte);
var iLoopCounterCDL:integer;
begin
 for iLoopCounterCDL:=0 to sgData.RowCount-1 do
   CheckOneCellDataInList(bCol,iLoopCounterCDL);
end;//CheckDataInList

procedure Tldn191f1.CheckOneCellDataInList(bCol:byte;iRow:integer);
//See "Bad Programming" note in CheckOneCellDataLength
var boTmpL:boolean;
  sTmpL:string;
begin
  boTmpL:=false;//Start with idea that datum is NOT valid.
  sTmpL:=sgData.Cells[bCol,iRow];
  if sTmpL='MA' then boTmpL:=true;
  if sTmpL='CA' then boTmpL:=true;
  if sTmpL='NY' then boTmpL:=true;
  //Etc. As many "if sTmpL='....' then boTmpL:=true;"
  //  as you wish, and then....

  if boTmpL=false then
    SetCellFgBg(bCol,iRow,kClFGbad,kClBGbad)//no ; here
  else
    SetCellFgBg(bCol,iRow,kClFGok,kClBGok);//no "end" here. End of "else"
end;//CheckOneCellDataInList

procedure Tldn191f1.CheckOneCellDataLength(sTmpL:string;
    bLen,bCol:byte;iRow:integer);
//"Bad Programming": Makes direct access to a global
//object, the StringGrid sgData. I am comfortable
//that in this case, "the rule" has been broken for
//sensible reasons, and in a manner which is not
//likely to be regretted. Hmmm... a very complex
//(hard to diagnose) problem arose... not(?) because
//THIS was accessing the global object(?),
//but because SetCellFg/Bg causes a repaint
//of one of the cells. Would I have noticed that
//sooner, had I not been going to the global?
//My guess? No.

//THIS NOT incorp by ref in CheckOneCellDataInList,
//  also.
begin

  if length(sTmpL)>bLen then
    SetCellFgBg(bCol,iRow,kClFGbad,kClBGbad)//no ; here
  else
    SetCellFgBg(bCol,iRow,kClFGok,kClBGok);//no "end" here. End of "else"

end;//CheckOneCellDataLength

procedure Tldn191f1.sgDataSetEditText(Sender: TObject; ACol, ARow: Integer;
  const Value: string);
//This is called every time a character is changed in a cell.
//Maybe there's another event for "leaving cell, editing done"... but
//  I don't think the overheads of checking as we go along are
//  too terrible. Maybe during the load-whole-grid process they
//  will be insupportable, when more columns are being tested.
//As long as part of the reason for calling this is to check
//  that a field separator character hasn't been entered, for
//  inclusion in a cell's contents, it (the call of DataSetEditText)
//  should remain.

begin
 //Next is a just scrap of debugging code...
 //showmessage('hi.. col/row: '+inttostr(acol)+'  '+inttostr(aRow));
 //.... but NOTE FOLLOWING!  v v v v v

  //sCellContentsWhenFSWarningIssued "works" to solve a problem...
  //   but it feels like a nasty kludge
  //It was introduced to deal with the following problem:
  //Unless the SaveStringGrid and corresponding Load are re-
  //   written to allow and deal with the field separation
  //   character appearing in the "ordinary" text of any cell,
  //   we have to watch, make sure that users do not put one
  //   in.
  //A simpler version of the following "worked"... but it also
  //   "repeated itself", sometimes at unhelpful, and always
  //   at irritating moments. (The sgDataSetEditText procedure
  //   is triggered when you leave a cell, for instance... even
  //   if you are just leaving it to click the "Quit" button.)

  //If you want to make it impossible for a user to leave a cell
  //   while a field separator character remains in it, you
  //   will have to arrange that for yourself... I don't like
  //   creating that sort of situation. If you want to prevent
  //   a user from saving a "bad" StringGrid, i.e. one with
  //   field separator values in the cells, I would suggest you
  //   add (it would be easy... most of what you need is in the
  //   code already) a test at the start of the SaveStringGrid
  //   procedure, and "talk" to the user THEN if bad data present.

  if ((pos(kFieldSep,Value)>0) and
     (sCellContentsWhenFSWarningIssued<>Value))then begin
       showmessage('You have used the field separator inside '+
       'a cell. This will "work", but any CSV that you save '+
       'will be re-loaded with that cell''s contents split '+
       'across two cells, if changes haven''t been made to '+
       'the application in respect of how things are '+
       'saved and loaded to provide for field separators' +
       'in cells.');
       sCellContentsWhenFSWarningIssued:=Value;
     end;
  if aCol=bLengthTestedInWhichCol then
       CheckOneCellDataLength(Value,bLengthLimitInTestedColumn,aCol,aRow);
  if aCol=bInListTestedColumn then
       CheckOneCellDataInList(aCol,aRow);
  //You COULD have sgData.AutoSizeColumns;  here,
  //... but that would result in columns frequently re-sizing
  //    as the user entered data.... and as the program loaded
  //    the grid in the first place from the CSV file. Probably
  //    not the way to go.
end;//sgDataSetEditText

procedure Tldn191f1.LoadFileToStringGrid(sFileName:string);
(*The file to be loaded should consist of one or more
  RECORDS, each consisting of one or more FIELDS.
  The file should consist printable characters, plus
  (maybe) the code for TAB, and a FEW others like that.
  The RECORDS are separated by CR/LF terminators,
  FIELDS are separated by COMMAS. A line ending in
  a comma is considered to indicate that the final
  field of that record had nothing in it, e.g.....
    Three fields: Fred,Bloggs,860-767-5555
    Four fields: Fred,Bloggs,860-767-5555,

  ---
  See https://sheepdogguides.com/lut/lt4Nd.htm
  for details of how this part of the application
  works.

  ---
  An annoying little rough edge... not a day wrecker....
  After a load, if the StringGrid's option
    goAlwaysShowEditor is set true, the contents
    a cell in the grid may look funny. It is in
    "edit" mode, but the text is un-selected.
    This does little harm, but is annoying.
  "Look funny": If the things that were once with
    this code which change the color of cell
    contents are still in place, the text/ background
    in the first cell may not be "the right" colors.
  If a way to select the text in the first cell...
    select THE TEXT, not just the cell... could be
    found, and added just after the LoadFileToStringGrid
    this annoying "feature" would go away.
  Or just set goAlwaysShowEditor to "false"... which
    gives you a different set of good things and bad.
  *)

var
  cardCounter:Cardinal;
  stringsTmp:TStrings;
begin //LoadFileToStringGrid
  stringsTmp:=TStringList.Create;
  try
    stringsTmp.LoadFromFile(sFileName);
    sgData.RowCount:=stringsTmp.Count;//Yes: .count, not .count-1
    sgData.ColCount:=7;//This CLUMSY... But could fairly easily
      //be replaced by code to count the fields in the first
      //line, and make the StringGrid have that many columns.
      //The "built in" LoadFromCSVFile does that. Both routines
      //ignore the fact that a data file might have more
      //fields in a subsequent record. (The extra fields are
      //just thrown away in the LoadFromCSVFile. At present,
      //they cause a problem (good! You'll know about them!)
      //in this answer. (That can be fixed.)
      //What's done here, for the moment, is to set, by hand, something
      //that limits how many fields there can be in any record
      //in the CSV file. Exceed that limit, and the application
      //shuts down, in a messy way. Fall short of that on a given
      //line, and you just have some empty cells at the right hand
      //end of the line.

      //For the future: Add a parameter to LoadFileToStringGrid
      //If zero: Use the "answer" used by LoadFromCSVFile, if
      //> zero, set ColCount from that. (If this is done, also
      //provide a "scan file, find out longest (most fields) record"
      //function.)
      //
      //In any case, add a "var" variable to return error codes
      //to user.... "file not found","too many fields encountered",
      //etc.

      //Also add a parameter to specify the separator character.
      //Add a way to use....  "Bloggs, Fred",123 to allow separator
      //character inside a field? Make it optional, and tell
      //users that the application runs faster if they don't
      //use the feature? (If that change made, changes also have
      //to be made to the sub-routine for saving the contents
      //of the StringGrid to a CSV file.)

    for cardCounter:= 0 to stringsTmp.Count-1 do begin
          StringToSGRow(sgData,stringsTmp[cardCounter],cardCounter,true);
          end;

      //To be checked... what does app do when a)there is / b) there
      //  is not a CR at the end of the file?
      //(Prelim checks give the "right answer": It doesn't
      //  matter.  (^_^) (Not tested exhaustively)
      //If there is a last line consisting just of spaces, the Lazarus
      //  version does NOT add a row to the StringGrid, the
      //  SheepdogGuides version DOES add a row. ("Lazarus version":
      //  the one that uses CommaText.)

  finally
    stringsTmp.Free;
    end;//Try... finally

  //Put "sgData.AutoSizeColumns;" in your code, outside this
  //   routine, after you call LoadFileToStringGrid,
  //   if you would like all of the columns' widths adjusted,
  //   leaving each column just wide enough for the widest datum
  //   in the column.

end;//LoadFileToStringGrid

procedure Tldn191f1.StringToSGRow(sgLocal:TStringGrid;
                             sRawData:string;iRow:integer;boTrimFields:boolean);
var bColIndexL:byte;
  sOneField:string;
  boDoneIt:boolean;
//This procedure is used within "LoadFileToStringGrid". It
//  is unlikely that you would want to call it directly.

//Takes, from sRawData, a string like...
//  860-555-1912,CT,Joe Smith
//... and puts the three fields in the first three
//colums of StringGrid sgLocal.
//What's in sRawData is "eaten away" over the course of the procedure.
//If boTrimFields is true, then spaces at either end of the field datum
//  are trimmed off....
//  "   sample datum   "
//  ... would become...
//  "sample datum"

  function boChomp(var sSource:string;
        var sDest:string):boolean;//SR of StringToSGRow
  //N.B. BOTH PARAMS ARE *var* PARAMS... contents of the
  //variables used to "feed" this SR will be changed by
  //the execution of the procedure.
  //
  //The contents of sDest before the call are irrelevant.
  //
  //If the string passed to sSource has a comma at it's
  //  right hand end, then the routine assumes that to mean
  //  that there is a field after the comma, but that the
  //  contents of that field just happen to be "nothing",
  //  i.e. ''. In the processing of such a string, sSource
  //  will be passed back to the calling program with a
  //  rogue value, just before the call of Chomp which
  //  "reads" the "empty" field.
  //In other words... string "a" has three fields,
  //  string "b" has FOUR...
  //     a) Fred,Bloggs,860-767-5555
  //     b) Fred,Bloggs,860-767-5555,
  //                             ---^---
  var iPosOfComma:integer;
  //This routine uses (read only) kFieldSep. You might
  //  want to re-write things so that it is supplied
  //  to the routine as a parameter, to avoid using
  //  a global inside the routine.

  begin //main block of boChomp, SR of StringToSGRow
    if sSource='' then result:=true//no ; here
         //(See "N.B. bChomp returns...", below. THIS "true"
         //arises if you call Chomp when there was no point...
         //you already knew there was no more data to parse.
    else begin //1  (Use last field in string that started
         //String1, String2, .... , StringLast
      //(There will be no comma after last field.)

      //N.B. boChomp returns false until there is no more
      //  data to be harvested by a further call of Chomp.
      //Note especially... the first time it
      //  returns TRUE, there is STILL one field to be
      //  USED by the calling program. It has been
      //  returned to the calling program in sDest,
      //  as usual.

      if sSource='tkbRogueToIndicateNullFieldAtEndOfRaw' then
            sSource:='';
      iPosOfComma:=pos(kFieldSep,sSource);
      if iPosOfComma=0 then begin //2
           //The "=1" case arises if the string you are
           //chomping ends with a comma, which is interpreted
           //as meaning that there IS one more field in the
           //string, but it just happens to consist of nothing,
           //just happens to be ''}
        sDest:=sSource;
        sSource:='';
        result:=true;//(See "N.B. bChomp returns..." THIS "true"
          //is for the case where you have just harvested the last
          //field from the string you have been "chomping" through.
        end//no ; here. End of "then 2"
      else begin //2
          if iPosOfComma=length(sSource) then
               sSource:=sSource+'tkbRogueToIndicateNullFieldAtEndOfRaw';
          sDest:=copy(sSource,1,iPosOfComma-1);
          sSource:=copy(sSource,iPosOfComma+1,length(sSource));
          result:=false;
          end;//of "else 2"
        end;// of "else 1"
  end;//boChomp, SR of StringToSGRow

begin //main block of StringToSGRow
   bColIndexL:=0;
   boDoneIt:=false;
   repeat
     boDoneIt:=boChomp(sRawData,sOneField);//N.B: These are "var"
          //parameters. A bit is chopped off of sRawData, and
          //sOneField is filled with a new value. Also: You can
          //ignore compiler warning about sOneField not being
          //initialized.
     if boTrimfields then sOneField:=trim(sOneField);

     //If you want to "see something" when a field in the
     //  CSV says "nothing, here", de-rem the following...
     //if sOneField='' then sOneField:='null';
     sgLocal.cells[bColIndexL,iRow]:=sOneField;

     inc(bColIndexL);

   until (boDoneIt) OR (bColIndexL>8);//second term to be refined in
     //due course... at present, it is a kludge... but it WILL have
     //a role to play one day. Keep note at top of code in step.
     //(Eventually, before LoadFileToStringGrid,
     //is called, somehow a determination will be made as to how many
     //columns are to be filled. If, by chance or error, the CSV file
     //has a record with too many fields, at the moment problems arise.
     //When the "8" is replaced by a variable, and that is loaded to
     //reflect how big the StringGrid available is, then steps can be
     //taken to deal nicely with a "too big for the row" record from
     //the CSV file.
end;//StringToSGRow

procedure Tldn191f1.buResizeColsClick(Sender: TObject);
begin
  showmessage('This button does resize the columns, but '+
  'it would be nice if the panel they appeared on was also '+
  'resized. Haven''t cracked that bit fully yet. '+
  'Currently.. and for the future, probably, that '+
  'can be resized by resizing the application''s window. '+
  'Not central to application.');
  sgData.AutoSizeColumns;//Leave this statement. Ref'd in ltN4c.htm
  //Next line: Crude, and not discussed in turtorial... but "works"
  //  to make width of stringgrid right for columns on it.
  //Doing same for height possible, more tedious.
  //The code for both needs to be in a better place, too... this
  //  just shows how it could be done.

  //The following DOES resize the PANEL the grid is displayed on,
  //  but there are "issues"... not only does it need to be
  //  generalized for the current number of columns, but there
  //  needs to be interaction between this code and the current
  //  window size, ESPECIALLY if the anchor editor has "turned on"
  //  "fit panel to window". (It might be easiest to turn OFF the
  //  anchor editor's "help", for this object, and "do it by hand".
  //
  //sgData.width:=sgData.ColWidths[0]+sgData.ColWidths[1]+
  //        sgData.ColWidths[2];

end;

procedure Tldn191f1.buQuitClick(Sender: TObject);
begin
  close;
end;

procedure Tldn191f1.buSaveWithNewNameClick(Sender: TObject);
begin
   SaveStringGrid('TmpLDN191SavedFile.txt');
end;

procedure Tldn191f1.buLinkToWebClickClick(Sender: TObject);
begin
  Clipboard.AsText:='https://sheepdogguides.com/lut/lt4Nc.htm';
end;

end.






Search across all my sites with the Google search...

Custom Search
            powered by FreeFind
  Site search Web search
Site Map    What's New    Search This search merely looks for the words you enter. It won't answer "Where can I download InpOut32?"
Ad from page's editor: Yes.. I do enjoy compiling these things for you. I hope they are helpful. However... this doesn't pay my bills!!! Sheepdog Software (tm) is supposed to help do that, so if you found this stuff useful, (and you run a Windows or MS-DOS PC) please visit my freeware and shareware page, download something, and circulate it for me? Links on your page to this page would also be appreciated!
Click here to visit editor's freeware, shareware page.

Link to Lazarus Tutorials main page
How to contact the editor of this page, Tom Boyd


Please consider contributing to the author of this site... and if you don't want to do that, at least check out his introduction to the new micro-donations system Flattr.htm....



Valid HTML 4.01 Transitional Page tested for compliance with INDUSTRY (not MS-only) standards, using the free, publicly accessible validator at validator.w3.org. Mostly passes. There were two "unknown attributes" in Google+ button code. Sigh.


If this page causes a script to run, why? Because of things like Google panels, and the code for the search button. Why do I mention scripts? Be sure you know all you need to about spyware.

....... P a g e . . . E n d s .....