0

i want to put a large string in a TStringGrid where every cell contains 4 chars from the string the StringGrid has 16 columns

nc:=1; nr:=1;     //nc = number of column . nr = number of raw
while fs.Length>0  do   // fs is a large string 
 begin
    if nc>16 then nr:=nr+1; nc:=1; 
 stringgrid.Cells[nc,nr]:=copy(fs,1,4);    
 delete(fs,1,4);
 nc:=nc+1;

PeekMessage(M, Handle, 0, 0, PM_NOREMOVE); // it prevents "not responding"
end;

how do i make it faster :=)

  • `fs.Length` tells me that `fs` is in fact *not* a string, your code won't even compile as so. Besides that, when I change it to `Length(fs)`, I don't know what `M` is. If I comment that line out, your code isn't doing the trick anyway in the first place. Is this your real code? – Jerry Dodge Nov 06 '14 at 22:11
  • @JerryDodge There's a helper for strings in newer versions of Delphi that allows Length to be used as shown – Jason Nov 06 '14 at 22:17
  • I see you're forcefully setting `nc:= 1` for every iteration in the loop – Jerry Dodge Nov 06 '14 at 22:17
  • @Jason I see, I'm using XE2, OP did not bother mentioning what version they're using. Besides that, I see many other issues in this code. – Jerry Dodge Nov 06 '14 at 22:17
  • @Jerry Dodge the main idea is to read a file , convert it to hex values , put them in a string , then put that string in the StringGrid , the code works fine it's just too slow whene i handle files larger then 400 KB (sorry for bad englsih) –  Nov 06 '14 at 22:21
  • 2
    Don't mutate the string. And don't put the entire file into a control. Use the virtual paradigm and your program will handle files of any size. Your current approach cannot scale. – David Heffernan Nov 06 '14 at 22:40
  • @DavidHeffernan i would use virtual paradigm if i knew what it is :D lol i'm new in delphi –  Nov 06 '14 at 22:51
  • Stop using string grids. They cannot scale. You cannot see all of the data at once. Don't waste all your time stuffing all that data into the control. Use a control that doesn't contain the data. Use a virtual control that displays just the data that can be seen. Imagine that the user loads up a file and then looks at the first couple of pages, and then quits. If you put the entire file into the control you've wasted all that time processing that which is never seen. Show the data on demand. – David Heffernan Nov 06 '14 at 22:54
  • @DavidHeffernan "Show the data on demand" , i never thaught about that . but i will difintly do in the futur thnx man –  Nov 06 '14 at 23:02
  • If you want to load files on demand you could use http://www.soft-gems.net/index.php/controls/virtual-treeview which is free and fastest but might be not so easy at the begning. Otherwise you could usse any other grid component from known wendors like ehlib.com or tmssoftware.com or devexpress.com. The will allow you to load a large portions of data with decent performance. Of course if TStringGrid is a must, ignore my comment ;) – Wodzu Nov 07 '14 at 06:49

5 Answers5

5

The slowdown comes for the most part from Delete. Delete rewrites the entire string. It is better to save the index where to copy from.

NGLN
  • 43,011
  • 8
  • 105
  • 200
4

You are never going to get this to scale well to large amounts of data. The problem is that trying to get the string grid control to hold huge amounts of data is asking it to do something it was not designed for. Doing so results in an incredibly inefficient storage of the data.

Instead what you really need is the virtual data paradigm. Instead of having the control store the data that it displays, let the control ask you for data on demand. When it needs to know what to display it asks you. That saves you having to load it up in advance with information, most of which it never uses.

Perhaps the ideal control for your needs would be Mike Lischke's famous virtual tree view. As a simpler demonstration of the power of this paradigm, here's a simple example using TListView.

Some initial declarations:

const
  ColCount = 16;
  CharactersPerCell = 4;
  LoremIpsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod '+
    'tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, '+
    'quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. '+
    'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu '+
    'fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in '+
    'culpa qui officia deserunt mollit anim id est laborum. ';

Setting the control's properties, and making a large string:

var
  Data: string;

procedure TForm1.FormCreate(Sender: TObject);
var
  i: Integer;
begin
  while Length(Data)<20*1000*1000 do begin // 20 million characters
    Data := Data + LoremIpsum;
  end;

  ListView1.ViewStyle := vsReport;
  ListView1.OwnerData := True;
  ListView1.OnData := ListViewData;
  ListView1.Items.Count := 1 + (Length(Data)-1) div (ColCount*CharactersPerCell);

  ListView1.Columns.Clear;
  for i := 0 to ColCount-1 do begin
    ListView1.Columns.Add.Caption := IntToStr(i+1);
  end;
end;

Rather than using a global var stuffed full of nonsense, you might load the text from a file.

The code to fetch the data on demand:

procedure TForm1.ListViewData(Sender: TObject; Item: TListItem);
var
  Row: string;
  ColIndex: Integer;
begin
  Row := Copy(Data, 1 + Item.Index*ColCount*CharactersPerCell, ColCount*CharactersPerCell);
  Item.Caption := Copy(Row, 1, CharactersPerCell);
  for ColIndex := 1 to ColCount-1 do begin
    Item.SubItems.Add(Copy(Row, 1 + CharactersPerCell*ColIndex, CharactersPerCell));
  end;
end;

Using a virtual contol gives performance in the display aspect. You will still have an issue with loading the data into memory. If you wish to be able to operate on huge files then you'll need to avoid loading the entire file into memory. Instead load only portions of the file, again on demand.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • i found an answer that i alredy posted . it handled a string with 1.000.000 charcter in about 5 sec . i guess it's great , but i'm always open for new ideas –  Nov 06 '14 at 23:21
  • 5 seconds is horribly slow. The approach I show will deal with arbitrarily sized files instantly. And you did not find an answer. The other answerers did. Your code is just Jerry's. All the other answers say the same thing as your answer. You should delete you answer. – David Heffernan Nov 06 '14 at 23:24
  • why whould i delete my answer ? . putting a 500kb file hex values in a StringGrid in 5 seconds is actually a great deal (faster than hex workshop) and since i tried it and worked i would let it so others get to use it –  Nov 06 '14 at 23:30
  • 1
    I think you should delete it because it because it has the same idea as all the other answers and adds nothing. It seems disrespectful to claim that you had the idea. And 5 seconds is appallingly slow. You should be aiming for a few milliseconds tops. I have demonstrated that is possible. – David Heffernan Nov 06 '14 at 23:37
  • i wrote that peace of code based on @NGLN answer . which i accepted as the answer of the the problem –  Nov 06 '14 at 23:42
  • 1
    Virtual is the way to go with large amounts of data. – LU RD Nov 07 '14 at 00:06
  • +1, and + another 1 for the Lorem ipsum string but SO won't let me :-) – Jerry Dodge Nov 07 '14 at 01:23
  • By the way, `20 million characters` isn't right, you mean `20 million lorem ipsums` which is actually, well I'm not going to count the characters there :-) – Jerry Dodge Nov 07 '14 at 01:25
  • +1 But I think that it would be better to show how to operate on a `TStream` descendant so it could be easly swappable between `TMemoryStream` and `TFileStream`. – Wodzu Nov 07 '14 at 06:56
  • @Wodzu That would be next. Instead of reading the entire file into memory, read just what was needed on demand. Then the file load could be instant too. – David Heffernan Nov 07 '14 at 07:04
  • @DavidHefernan yes, that was what I've meant. I mentioned `TMemoryStream` in case that someone would want to load all the data to memory. – Wodzu Nov 07 '14 at 07:16
  • @JerryDodge No, it's 20 million characters – David Heffernan Nov 07 '14 at 09:08
  • @DavidHeffernan I see, for whatever reason I was thinking you were multiplying that string by 20 million times. – Jerry Dodge Nov 07 '14 at 16:20
2

First of all, I don't know exactly how big this string is. However, there are many other issues in your code which made it not do what you were saying in the first place (It was only putting the last two characters in the first cell).

This is what I believe you're trying to do...

procedure TForm1.BitBtn1Click(Sender: TObject);
var
  i, nc, nr, sp, len: Integer;
  fs: String;
begin
  StringGrid.RowCount:= 2;
  StringGrid.ColCount:= 16;
  for i := 1 to 1000 do
    fs:= fs + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz';
  nc:= 1;
  nr:= 1;
  sp:= 1;
  len:= Length(fs);
  while sp < len  do begin
    if nc >= 16 then begin
      Inc(nr);
      nc:= 1;
      StringGrid.RowCount:= StringGrid.RowCount + 1;
    end;
    StringGrid.Cells[nc,nr]:= Copy(fs, sp, 4);
    Inc(sp, 4);
    Inc(nc);
  end;
end;

A few other notes...

I excluded the PeekMessage line because I don't know where you get M from. But that adds to the performance issues you're experiencing. That's forcing the UI to update and repaint for every single cell you will be placing text in.

The row count should also be pre-calculated and set before the loop starts. Personally my math is not fresh enough to add that to my answer for you.

(Modified from my original answer's code using the information from NGLN's answer which was posted seconds after mine)

Jerry Dodge
  • 26,858
  • 31
  • 155
  • 327
  • about that M . i just copied it from a website and it works absulutly fine http://embarcadero.newsgroups.archived.at/public.delphi.language.delphi.win32/200812/0812171924.html –  Nov 06 '14 at 22:27
  • "StringGrid.RowCount:= 16;" sorry . not what i meant . the raws grow as much as it takes until the fs string is finished –  Nov 06 '14 at 22:29
  • @IslamBouderbala Well you didn't put all your original code to begin with so it was unclear what you were doing. Keep in mind I'm not writing all your code for you, just giving you an idea how to go about it. – Jerry Dodge Nov 06 '14 at 22:31
  • i appriciate your effort to help me –  Nov 06 '14 at 22:36
  • @IslamBouderbala No problem, but other answers here do provide even better solutions than my own, especially David's recommendation of keeping it virtual without a UI, and displaying it only on-demand. Also, make sure you've seen my edits. – Jerry Dodge Nov 07 '14 at 01:05
  • The effect of PeekMessage should be a bit more complicated. A WM_PAINT will not be sent unless the message queue is empty, and the call here is not removing any message - if there's any. So it may or may not cause a paint cycle. I'd think it wouldn't in practice. – Sertac Akyuz Nov 07 '14 at 01:21
  • @SertacAkyuz Indeed, but it still does slow down the process, and the question is all about speed which means getting rid of anything which isn't necessary :-) – Jerry Dodge Nov 07 '14 at 01:29
  • @sertac the peek is just a means to avoid the system ghosting the window. I think. – David Heffernan Nov 07 '14 at 07:06
  • 1
    Whenever filling a stringgrid, start with a StringGrid.BeginUpdate before the loop and a StringGrid.EndUpdate after the loop. – Pieter B Nov 07 '14 at 09:50
  • 1
    @PieterB, there is no such method pair. You can lock only `Rows` or `Cols` (you can lock both). Or optionally lock rendering with `WM_SETREDRAW` message (which I would personally prefer). – TLama Nov 07 '14 at 16:10
  • I still think David's answer is more appropriate for OP. – Jerry Dodge Nov 07 '14 at 16:20
  • @TLama ok, that was a bit of an oops, think I've been working too much with a custom stringgrid class that does have it. Point stands though that preventing the stringgrid from drawing each filled cell increases speed dramatically. I think: http://stackoverflow.com/questions/12854401/how-to-quickly-add-many-rows-to-a-tstringgrid has more info. – Pieter B Nov 08 '14 at 09:01
1

As @NGLN has already explained the easiest to simply make it faster, I'll show an alternative that also avoids Delete and automatically adjusts to any length string input.

Here's how I would do it, calculating the number of rows needed based on the length of the input data. Note that I've included some setup code to allocate a string for testing (I've commented that code as such) that clearly wouldn't need to be used in your application. This correctly handles strings that aren't evenly divided in to 64-byte rows to display in the grid as well.

procedure TForm4.FormCreate(Sender: TObject);
var
  NumRows, CurrRow, CurrCol: Integer;
  Len: Integer;
  StrToParse: string;
  i: Integer;
const
  SplitCount = 16 * 4;  // Number of columns * chars per column
const
  Letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
begin
  // Setup code. Only for demonstration purposes.
  // Grid columns, remove fixed column. Leaves column headers
  StringGrid1.ColCount := 16;
  StringGrid1.FixedCols := 0;
  StrToParse := Letters;
  StringGrid1.ColCount := 16;
  // Allocates 1088 character string for testing
  while StrToParse.Length < 1000 do
    StrToParse := StrToParse + Letters;

  Len := StrToParse.Length;
  NumRows := Len div SplitCount;
  // If it's not evenly divisible, add an extra row for the spillover
  if Len mod SplitCount <> 0 then
    Inc(NumRows);
  {
    Calculate the number of rows we need, allowing
    1 for the fixed header row
  }
  StringGrid1.RowCount := NumRows + 1;
  // Index into string's characters
  i := 1;
  for CurrRow := 1 to NumRows do // Skipping fixed row headers
    for CurrCol := 0 to 15 do
      if i < Len then
      begin
        StringGrid1.Cells[CurrCol, CurrRow] := Copy(StrToParse, i, 4);
        Inc(i, 4);
      end;
end;
Ken White
  • 123,280
  • 14
  • 225
  • 444
0

if possible i would set the stringgrid to invisible because it is more than 10 times faster:

procedure TForm1.Button1Click(Sender: TObject);
(** SLOW VERSION **)
const ABC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
var s:string;
    i,l,r,c:Integer;
    dt:TDateTime;
begin
  s := '';
  for i := 0 to 10000 do begin
    s := s + ABC;
  end;
  l := s.Length;

  StringGrid1.RowCount := 1;
  StringGrid1.ColCount := 16;

  dt := Now;
  i := 1;
  r := 0;
  c := 0;
  while i < l do begin
    StringGrid1.Cells[c,r] := Copy(s,i,4);
    Inc(i,4);
    c := (c+1) mod 17;
    if c=0 then begin
      StringGrid1.RowCount := StringGrid1.RowCount + 1;
      Inc(r);
    end;
  end;
  ShowMessage(Format('Adding strings took %d msec',[MilliSecondsBetween(dt,Now)]));  // ~ 7000 msec
end;

procedure TForm1.Button2Click(Sender: TObject);
(** FASTER VERSION **)
const ABC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
var s:string;
    i,l,r,c:Integer;
    dt:TDateTime;
begin
  s := '';
  for i := 0 to 10000 do begin
    s := s + ABC;
  end;
  l := s.Length;

  StringGrid1.RowCount := 1;
  StringGrid1.ColCount := 16;

  dt := Now;
  i := 1;
  r := 0;
  c := 0;
  StringGrid1.Visible := false;
  while i < l do begin
    StringGrid1.Cells[c,r] := Copy(s,i,4);
    Inc(i,4);
    c := (c+1) mod 17;
    if c=0 then begin
      StringGrid1.RowCount := StringGrid1.RowCount + 1;
      Inc(r);
    end;
  end;
  StringGrid1.Visible := true;
  ShowMessage(Format('Adding strings took %d msec',[MilliSecondsBetween(dt,Now)])); // ~ 700 msec
end;

if you want your app to response you could add Application.ProcessMessages; in the loop.

  while i < l do begin
    StringGrid1.Cells[c,r] := Copy(s,i,4);
    Inc(i,4);
    c := (c+1) mod 17;
    if c=0 then begin
      Application.ProcessMessages;
      StringGrid1.RowCount := StringGrid1.RowCount + 1;
      Inc(r);
    end;
  end;

there are some things you have to be aware when using Application.ProcessMessages;:

  • UI is reacting to user inputs
  • UI gets redrawn
  • timers get fired
  • ...

it depends on your application if it is "safe" to call it. maybe it is enough to set a flag when entering your function like this:

procedure DoSomething;
begin
  if not InDoSomething then begin
    InDoSomething := true;
    while blub do begin
      // ...
      Application.ProcessMessages;
    end;
    InDoSomething := false;
  end;
end;
linluk
  • 1,650
  • 16
  • 33
  • 1
    ProcessMessages has consequences that you need to explain – David Heffernan Nov 07 '14 at 08:02
  • @DavidHeffernan: i added a brief explanation. – linluk Nov 07 '14 at 08:17
  • 1
    Processmessages can change the control flow of your application in strange way. – Pieter B Nov 08 '14 at 09:06
  • `ProcessMessages` has caused me major nightmares, especially when using it in a grid. For example, adding a new row of data in a loop, kept writing data in the wrong row, and would leave some rows empty, and overwrite other rows. Way too dangerous to use, and I stopped using it years ago. – Jerry Dodge Nov 08 '14 at 17:17