Avatar billede nca Juniormester
19. januar 2004 - 19:26 Der er 13 kommentarer og
1 løsning

Listbox.Items.LoadfromFile

Jeg har et program hvor jeg skal indlæse en temmelig stor mængde data i en listbox (ca. 25.000 linier).
Jeg benytter LoadfromFile, men vil gerne ha' en progressbar på, da indlæsningen godt kan ta' op til 2 minutter.
Er det muligt ved fx. LoadFromStream at bestemme den samlede mængde data der skal indlæses og samtidig styre hvor langt indlæsningen er kommet, eller skal jeg i gang med noget BlockRead?
El lille eksempel ville være velkommen.
Avatar billede nca Juniormester
19. januar 2004 - 21:19 #1
Jeg har arbejdet lidt videre med problemet.
Jeg kan bruge en Filestream, hvilket er hurtigt, men har så et problem at få bufferen (array of byte) konverteret til string og samtidig forsvinder mine linieskift.
En anden måde kunne være at finde filen størrelse, læse den første linie med readln, finde længden af 1 linie og ud fra filstørrelsen finde antal linie i filen. Herefter kan jeg indlæse 1 linie ad gangen. Tager ca 15% længere tid end at bruge LoadFromFile, men så har jeg en progressbar.
Tager stadig imod gode råd :-)
Avatar billede fixxxer Nybegynder
19. januar 2004 - 22:02 #2
jeg forstod selv engang at det jo er selve parsingen af tekstenfilen ind i listboxen der tager tid, ikke selve det at "læse" filen. så vidt jeg husker kræver det at du kontruere en ny listbox, som du selv har kontrol over ved LoadFromFile så du kan vise evt. progress i en progressbar.
Avatar billede hrc Mester
25. januar 2004 - 00:12 #3
Jeg har lavet en noget smartere løsning (den er 200 linier men læser 25000 (1,5MB) navne på under 1 sekund...) Jeg har en form med tre TEdits (eName,eAdr,eAge), Add-knap (dele hele i et panel) - og så lige en TListBox. Dertil har jeg lavet to klasser, et liste og et data-objekt.

Humlen i det hele er, at man sætter listboksens style til lbVirtual og bruger de to events OnData og OnDataObject.

unit FMain;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ComCtrls, StdCtrls, ContNrs, ExtCtrls;

type
  TMyList = class;
  TMyData = class;

  TMyList = class(TClassList)
  private
    fListBox : TCustomListBox;
    function GetMyData(aIndex : integer) : TMyData;
  public
    constructor Create(aListBox : TCustomListBox);
    procedure LoadFromFile(const aFileName : string);
    procedure SaveToFile(const aFileName : string);
    property Items[aIndex : integer] : TMyData read GetMyData; default;
    function Add(aMyData : TMyData) : integer;
    procedure Delete(aIndex : integer);
  end;

  TMyData = class
  private
    fName : string;
    fAdr : string;
    fAge : integer;
  public
    constructor Create(aStream : TStream); overload;
    constructor Create(const aName, aAdr : string; const aAge : integer); overload;
    procedure Write(aStream : TStream);
    property Name : string read fName;
    property Adr : string read fAdr;
    property Age : integer read fAge;
  end;

  TfrmMain = class(TForm)
    sbMain: TStatusBar;
    lbData: TListBox;
    eName: TEdit;
    eAdr: TEdit;
    eAge: TEdit;
    btnAdd: TButton;
    pTop: TPanel;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure btnAddClick(Sender: TObject);
    procedure lbDataData(Control: TWinControl; Index: Integer; var Data: String);
    procedure lbDataDataObject(Control: TWinControl; Index: Integer; var DataObject: TObject);
    procedure lbDataClick(Sender: TObject);
  private
    fMyList : TMyList;
  public
  end;

var
  frmMain: TfrmMain;

implementation

uses Math;

{$R *.dfm}

procedure TfrmMain.FormCreate(Sender: TObject);
//var
//  i : integer;
begin
  fMyList := TMyList.Create(lbData);
  if fileexists('c:\BigFile.txt') then
    fMyList.LoadFromFile('c:\BigFile.txt');

// Uncomment to create test data
//  for i := 0 to 24999 do
//    fMyList.Add(TMyData.Create('Eksperten er Gud','Ekspertvejen 12',2));
end;

procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  fMyList.SaveToFile('c:\BigFile.txt');
  fMyList.Free;
end;

procedure TfrmMain.lbDataData(Control: TWinControl; Index: Integer; var Data: String);
begin
  Data := fMyList[Index].Name;
end;

procedure TfrmMain.btnAddClick(Sender: TObject);
begin
  fMyList.Add(TMyData.Create(eName.Text,eAdr.Text,StrToInt(eAge.Text)));
end;

procedure TfrmMain.lbDataDataObject(Control: TWinControl; Index: Integer; var DataObject: TObject);
begin
  DataObject := FMyList[Index];
end;

procedure TfrmMain.lbDataClick(Sender: TObject);
var
  MyData : TMyData;
begin
  with (Sender as TListBox) do
    MyData := TMyData(Items.Objects[ItemIndex]);

  eName.Text := MyData.Name;
  eAdr.Text := MyData.Adr;
  eAge.Text := IntToStr(MyData.Age);
end;

{ TMyList }

function TMyList.Add(aMyData: TMyData): integer;
begin
  result := inherited Add(TClass(aMyData));
  fListBox.Count := Count;
end;

constructor TMyList.Create(aListBox: TCustomListBox);
begin
  inherited Create;
  fListBox := aListBox;
end;

procedure TMyList.Delete(aIndex: integer);
begin
  inherited Delete(aIndex);
  fListBox.Count := Count;
end;

function TMyList.GetMyData(aIndex: integer): TMyData;
begin
  result := TMyData(inherited Items[aIndex]);
end;

procedure TMyList.LoadFromFile(const aFileName: string);
var
  fs : TFileStream;
begin
  fs := TFileStream.Create(aFileName,fmOpenRead);
  try
    while fs.Position < fs.Size do
      Add(TMyData.Create(fs));
  finally
    fs.Free;
    fListBox.Count := Count;
  end; // try
end;

procedure TMyList.SaveToFile(const aFileName: string);
var
  i : integer;
  fs : TFileStream;
begin
  fs := TFileStream.Create(aFileName,fmCreate);
  try
    for i := 0 to Count - 1 do
      Items[i].Write(fs);
  finally
    fs.Free;
  end; // try
end;

{ TMyData }

constructor TMyData.Create(aStream: TStream);

  function GetString(aStream: TStream) : string;
  var
    p : pchar;
    size : integer;
  begin
    aStream.Read(size,sizeof(integer));
    getmem(p,size+1);
    try
      fillchar(p^,size+1,0);
      aStream.Read(p^,size);
      result := string(p);
    finally
      freemem(p);
    end; // try
  end;

begin
  inherited Create;
  fName := GetString(aStream);
  fAdr := GetString(aStream);
  fAge := StrToInt(GetString(aStream));
end;

constructor TMyData.Create(const aName, aAdr: string; const aAge: integer);
begin
  inherited Create;
  fName := aName;
  fAdr := aAdr;
  fAge := aAge;
end;

procedure TMyData.Write(aStream: TStream);

  procedure SetString(const aString : string);
  var
    Size : integer;
    p : pchar;
  begin
    Size := length(aString);
    p := pchar(aString);
    aStream.Write(Size,sizeof(integer));
    aStream.Write(p^,Size);
  end;

begin
  SetString(fName);
  SetString(fAdr);
  SetString(IntToStr(fAge));
end;

end.
Avatar billede hrc Mester
25. januar 2004 - 00:16 #4
Du skal ikke blive afskrækket af at det fylder en del, det meste er "fyldkode" som jeg lavede for at kunne vise dig de virtuelle lister.

Lad høre hvis der er spørgsmål...

N.B. Der er en del komponenter der kan bruge de virtuelle lister, eksempelvis TListView.

N.B.2. I øvrigt er det et spørgsmål om det er god GUI at have 25 linier i en listbox - men det er din beslutning.

N.B.3. Bemærk sidst, at jeg fører listboksen over i TMyList's constructor. Det er nødvendigt fordi jeg skal administrere dens "virtuelle" forestilling om hvor mange enheder der er i listen).
Avatar billede hrc Mester
25. januar 2004 - 00:36 #5
Det slog mig at jeg bare kunne sende dig dfm-filen og så havde du hele eksemplet, køreklart og færdigt...

Opret et projekt, kopiér ovenstående (FMain.pas) og nedenstående (FMain.dfm) ind i kataloget. Fjern projektets form og tilføj denne her.

object frmMain: TfrmMain
  Left = 615
  Top = 556
  Width = 474
  Height = 333
  Caption = 'frmMain'
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'MS Sans Serif'
  Font.Style = []
  OldCreateOrder = False
  OnCreate = FormCreate
  OnDestroy = FormDestroy
  PixelsPerInch = 96
  TextHeight = 13
  object pTop: TPanel
    Left = 0
    Top = 0
    Width = 466
    Height = 33
    Align = alTop
    BevelOuter = bvNone
    TabOrder = 0
    DesignSize = (
      466
      33)
    object eName: TEdit
      Left = 0
      Top = 8
      Width = 121
      Height = 21
      Anchors = [akLeft, akTop, akRight]
      TabOrder = 0
    end
    object eAdr: TEdit
      Left = 128
      Top = 8
      Width = 121
      Height = 21
      Anchors = [akTop, akRight]
      TabOrder = 1
    end
    object eAge: TEdit
      Left = 256
      Top = 8
      Width = 121
      Height = 21
      Anchors = [akTop, akRight]
      TabOrder = 2
    end
    object btnAdd: TButton
      Left = 384
      Top = 8
      Width = 75
      Height = 21
      Anchors = [akTop, akRight]
      Caption = 'Add'
      TabOrder = 3
      OnClick = btnAddClick
    end
  end
  object sbMain: TStatusBar
    Left = 0
    Top = 287
    Width = 466
    Height = 19
    Panels = <
      item
        Width = 50
      end
      item
        Style = psOwnerDraw
        Width = 50
      end
      item
        Width = 50
      end
      item
        Width = 50
      end>
  end
  object lbData: TListBox
    Left = 0
    Top = 33
    Width = 466
    Height = 254
    Style = lbVirtual
    Align = alClient
    ItemHeight = 13
    TabOrder = 1
    OnClick = lbDataClick
    OnData = lbDataData
    OnDataObject = lbDataDataObject
  end
end
Avatar billede nca Juniormester
25. januar 2004 - 11:45 #6
Det er et impornerende arbejde du har lavet, men ...
Når jeg prøver dit program på min tekstfil, går der meeeget lang tid hvorefter programmet stopper med en Out of memory fejl.
Jeg kan heller ikke se, at du har gjort noget ved mit oprindelige problem, nemlig en progress-bar under indlæsningen.
Avatar billede hrc Mester
26. januar 2004 - 13:32 #7
Jeg indlæser en fil i eget format, så det er klart at noget uventet vil forekomme når man høvler din fil igennem. Den opbektorienterede idé i det er, at kun TMyData kender opbygningen af filen. I mit objekt er opbygningen "size name size adr size age" pr. record (klodset, men det virker)

Jeg ved godt at dit problem mht. progressbar'en ikke er løst, men mit eksempel indlæser (på min 800Mhz bærbare) 75000 records på ca. 1 sekund og så er der ikke brug for en progressbar. Prøv at fjerne udkommenteringen i OnCreate. Dette vil generere  25000 records hver gang du starter programmet (dette efter indlæsning af eksisterende fil, så det kommer 25k til hver gang). Disse gemmes når programmet lukkes.
Avatar billede hrc Mester
26. januar 2004 - 13:40 #8
Hvis det er en fil du selv administrerer, så bliver det meget meget lettere, for så skriver du antallet af records først. Så er der ingen problemer med at lave en progressbar og få den til at tælle op korrekt.

procedure TMyList.LoadFromFile(const aFileName : string; aProgrssBar : TProgrssBar);
var
  fs : TFileStream;
  i, Count : integer;
  Skip : integer;
begin
  fs := TFileStream.Create(aFileName,fmOpenRead);
  try
    aStream.Read(Count,sizeof(integer));
 
    aProgressBar.Min := 0;
    aProgressBar.Max := Count;



    Clear; // Clear and free any existing items in the list

    for i := 0 to Count - 1 do begin
      Add(TMyData.Create(fs));
      ProgressBar.Position := i;
Avatar billede hrc Mester
26. januar 2004 - 13:44 #9
... ramte knappen, øv!

procedure TMyList.LoadFromFile(const aFileName : string; aProgrssBar : TProgrssBar);
var
  fs : TFileStream;
  i, Count : integer;
  Skip : integer;
begin
  fs := TFileStream.Create(aFileName,fmOpenRead);
  try
    aStream.Read(Count,sizeof(integer));

    ProgressBar.Position := 0;
    aProgressBar.Min := 0;
    aProgressBar.Max := Count;

    Skip := Count div 100; // Hackup progress do avoid ressource draining...
    Clear; // Clear and free any existing items in the list

    for i := 0 to Count - 1 do begin
      Add(TMyData.Create(fs));

      if i mod Skip = 0 then begin
        ProgressBar.Position := i;
        Application.ProgrssMessages;
      end;
    end; // for
  finally
    fs.Free;
  end; // try
end;
Avatar billede hrc Mester
26. januar 2004 - 14:15 #10
Disse to metoder dur, men medmindre du sender en Message til formen (så den vises), så ser du kun noget når du tilretter programmet, så der er knapper til indlæsning og skrivning.

procedure TMyList.LoadFromFile(const aFileName: string; aProgressBar : TProgressBar);
var
  fs : TFileStream;
  i, Skip, RecCount : integer;
begin
  fs := TFileStream.Create(aFileName,fmOpenRead);
  try
    fs.Read(RecCount,sizeof(integer));

    if assigned(aProgressBar) then begin
      aProgressBar.Position := 0;
      aProgressBar.Min := 0;
      aProgressBar.Max := RecCount;
    end; // if

    Skip := RecCount div 100;

    for i := 0 to RecCount - 1 do begin
      if (i mod Skip) = 0 then
        if assigned(aProgressBar) then begin
          aProgressBar.Position := i;
          Application.ProcessMessages;
        end; // if

      Add(TMyData.Create(fs));
    end; // for
  finally
    fs.Free;
    fListBox.Count := Count;
  end; // try
end;

procedure TMyList.SaveToFile(const aFileName: string; aProgressBar : TProgressBar);
var
  i : integer;
  Skip : integer;
  fs : TFileStream;
begin
  fs := TFileStream.Create(aFileName,fmCreate);
  try
    fs.Write(Count,sizeof(integer));

    if assigned(aProgressBar) then begin
      aProgressBar.Position := 0;
      aProgressBar.Min := 0;
      aProgressBar.Max := Count;
    end; // if

    Skip := Count div 100;

    for i := 0 to Count - 1 do begin
      if (i mod Skip) = 0 then
        if assigned(aProgressBar) then begin
          aProgressBar.Position := i;
          Application.ProcessMessages;
        end; // if

      Items[i].Write(fs);
    end; // for
  finally
    fs.Free;
  end; // try
end;
Avatar billede hrc Mester
26. januar 2004 - 17:13 #11
I øvrigt kan du vinde en del ved følgende:

procedure TMyList.LoadFromFile(const aFileName: string; aProgressBar : TProgressBar);
var
  fs : TFileStream;
  i, Skip, RecCount : integer;
begin
  fs := TFileStream.Create(aFileName,fmOpenRead);
  fListBox.Items.BeginUpdate; // <- Dette hjælper også meget!
  try
    fs.Read(RecCount,sizeof(integer));

    if assigned(aProgressBar) then begin
      aProgressBar.Position := 0;
      aProgressBar.Min := 0;
      aProgressBar.Max := RecCount;
    end; // if

    Skip := RecCount div 100;

    for i := 0 to RecCount - 1 do begin
      if assigned(aProgressBar) then
        if (i mod Skip) = 0 then begin
          aProgressBar.Position := i;
          Application.ProcessMessages; // Update GUI
        end; // if

      Add(TMyData.Create(fs));
    end; // for
  finally
    fs.Free;
    fListBox.Items.EndUpdate;
    fListBox.Count := Count;
  end; // try
end;

Undskyld jeg spammed dig sådan - har hygget mig ved at optimere mit eksempel. Hvordan udveksler man egentlig eMails uden at skrive det her? Kan man mon det i Eksperten?
Avatar billede nca Juniormester
27. januar 2004 - 08:18 #12
>>Undskyld jeg spammed dig sådan
Det er helt fint med mig.
Normalt ville jeg gøre programmet færdigt på én dag, men jeg har rasende travlt for øjeblikket, så jeg har ikke en gang haft tid til at nærlæse din kode. Pointene har du fortjent for lang tid siden.
Avatar billede hrc Mester
27. januar 2004 - 09:13 #13
Det skal da heller ikke afholde mig fra at fortsætte (Odenseaner til Odenseaner):

Idé 1:

Jeg læser en binærfil, mens du læser en tekstfil. Det kræver en lidt anden tilgang. Igen er den tunge del at populere listboksen, ikke at læse selve filen. Derfor vil jeg indlæse filen i en TMemoryStream (den har også en LoadFromFile), tælle <CR/LF> for at få antallet af records. Derefter vil jeg parse data og indsætte linierne i listboksen.

... men man kan jo gå over åen efter vand...

Idé 2:

Først finder du størrelsen af tekstfilen og det tæller du op efter (og det er vel egentlig den løsning som du forespurgte fra starten - vil dog stadig anbefale at bruge det sammen med en virtuel ListBox).

const
  TheFile = 'c:\BigMF.txt';
var
  Step, NextStep : integer;
  fp : TextFile;
  st : string;
begin
  if not FileExists(TheFile) then
    exit;
 
  pbStatus.Min := 0;
  pbStatus.Max := GetFileSize(TheFile);
  pbStatus.Position := 0;

  Step := pbStatus.Max div 100;
  NextStep := 0;

  assignfile(fp,TheFile);
  reset(fp);
  lbData.Items.BeginUpdate;
  Screen.Cursor := crHourGlass;
  try
    lbData.Clear;
    while not eof(fp) do begin
      readln(fp,st);
      lbData.Items.Add(st);
      pbStatus.StepBy(length(st)+2); // remember to count cr/lf
      if NextStep <= pbStatus.Position then begin
        Application.ProgressMessage;
        inc(NextStep,Step);
      end; // if
    end; // while
  finally
    lbData.Items.EndUpdate;
    closefile(fp);
    Screen.Cursor := crDefault;
  end; // try
end;

P.S. Har du i øvrigt prøvet at sætte LoadFromFile mellem BeginUpdate/EndUpdate?
Avatar billede nca Juniormester
15. februar 2004 - 09:51 #14
Der sker vist ikke mere i sagen, så tak for hjælpen.
Avatar billede Ny bruger Nybegynder

Din løsning...

Tilladte BB-code-tags: [b]fed[/b] [i]kursiv[/i] [u]understreget[/u] Web- og emailadresser omdannes automatisk til links. Der sættes "nofollow" på alle links.

Loading billede Opret Preview
Kategori
Kurser inden for grundlæggende programmering

Log ind eller opret profil

Hov!

For at kunne deltage på Computerworld Eksperten skal du være logget ind.

Det er heldigvis nemt at oprette en bruger: Det tager to minutter og du kan vælge at bruge enten e-mail, Facebook eller Google som login.

Du kan også logge ind via nedenstående tjenester