I am developing an application and I have to upload data from CSV files into a DB tables. Problem is, I don’t have CSV files but I have flat text files to be converted into CSV.
An additional problem is, as the application is used by several customers who have different systems, I have different flat text files with different layouts.
What I want to achieve is to create an application that loads “rules” from a special file; these rules will be processed with the flat text file in order generate the CSV file. The application that converts from flat file to CSV would be the same, just the set of rules would be different.
How can I achieve this? What is the best practice you recommend?
It depends on the complexity of the rules. If the only varying input is the names of the columns and the separator used, then it's pretty easy, but if you want to be able to parse completely different formats (like XML or so) as well, then it's a different story.
I myself would choose to implement a base class for a 'record' reader that reads records from a file and outputs them to a dataset or CSV.
Then, you can implement child classes that implement reading different source formats.
If you like, you can then add specific rules for those format, so you can make a generic XMLReader that descends from BaseReader, but which allows for configurable column names. But I would start with a bunch of hard-coded readers for the formats you got, until it's more clear which dialects of those formats you may encounter.
Edit: On request, an example of how it could look like.
Note, this example is far from ideal! It reads a custom format, transfers it to one specific table structure and saves that as an CSV file. You may want to split it a little further, so you can reuse the code for different table structures. Especially the field defs, you may want to be able to set in a descendant class or maybe a factory class.
But for the sake of simplicity I have taken a more rigid approach and put a little too much intelligence in one single base class.
The base class has the logic needed to create an in-memory dataset (I used a TClientDataSet). It can 'Migrate' a file. In practice, this means it reads, validates and exports the file.
The reading is abstract and must be implemented in a child class. It should read the data to the in memory dataset. That allows you to do all necessary validation in the client dataset. This allows to you enforce field types and sized and do any additional checking if you need to, in a database/file format agnostic way.
The validating and writing is done using the data in the dataset. From the moment where the source file is parsed to a dataset, no knowledge about the source file format is required anymore.
Declaration:
Don't forget to use DB, DBClient.
type
TBaseMigrator = class
private
FData: TClientDataset;
protected
function CSVEscape(Str: string): string;
procedure ReadFile(AFileName: string); virtual; abstract;
procedure ValidateData;
procedure SaveData(AFileName: string);
public
constructor Create; virtual;
destructor Destroy; override;
procedure MigrateFile(ASourceFileName, ADestFileName: string); virtual;
end;
Implementation:
{ TBaseReader }
constructor TBaseMigrator.Create;
begin
inherited Create;
FData := TClientDataSet.Create(nil);
FData.FieldDefs.Add('ID', ftString, 20, True);
FData.FieldDefs.Add('Name', ftString, 60, True);
FData.FieldDefs.Add('Phone', ftString, 15, False);
// Etc
end;
function TBaseMigrator.CSVEscape(Str: string): string;
begin
// Escape the string to a CSV-safe format;
// Todo: Check if this is sufficient!
Result := '"' + StringReplace(Result, '"', '""', [rfReplaceAll]) + '"';
end;
destructor TBaseMigrator.Destroy;
begin
FData.Free;
inherited;
end;
procedure TBaseMigrator.MigrateFile(ASourceFileName, ADestFileName: string);
begin
// Read the file. Descendant classes need to override this method.
ReadFile(ASourceFileName);
// Validation. Implemented in base class.
ValidateData;
// Saving/exporting. For now implemented in base class.
SaveData(ADestFileName);
end;
procedure TBaseMigrator.SaveData(AFileName: string);
var
Output: TFileStream;
Writer: TStreamWriter;
FieldIndex: Integer;
begin
Output := TFileStream.Create(AFileName,fmCreate);
Writer := TStreamWriter.Create(Output);
try
// Write the CSV headers based on the fields in the dataset
for FieldIndex := 0 to FData.FieldCount - 1 do
begin
if FieldIndex > 0 then
Writer.Write(',');
// Column headers are escaped, but this may not be needed, since
// they likely don't contain quotes, commas or line breaks.
Writer.Write(CSVEscape(FData.Fields[FieldIndex].FieldName));
end;
Writer.WriteLine;
// Write each row
FData.First;
while not FData.Eof do
begin
for FieldIndex := 0 to FData.FieldCount - 1 do
begin
if FieldIndex > 0 then
Writer.Write(',');
// Escape each value
Writer.Write(CSVEscape(FData.Fields[FieldIndex].AsString));
end;
Writer.WriteLine;
FData.Next
end;
finally
Writer.Free;
Output.Free;
end;
end;
procedure TBaseMigrator.ValidateData;
begin
FData.First;
while not FData.Eof do
begin
// Validate the current row of FData
FData.Next
end;
end;
An example child class: TIniFileReader, which reads inifile sections as if they were database records. As you can see, you only need to implement the logic to read the file.
type
TIniFileReader = class(TBaseMigrator)
public
procedure ReadFile(AFileName: string); override;
end;
{ TIniFileReader }
procedure TIniFileReader.ReadFile(AFileName: string);
var
Source: TMemIniFile;
IDs: TStringList;
ID: string;
i: Integer;
begin
// Initialize an in-memory dataset.
FData.Close; // Be able to migrate multiple files with one instance.
FData.CreateDataSet;
// Parsing a weird custom format, where each section in an inifile is a
// row. Section name is the key, section contains the other fields.
Source := TMemIniFile.Create(AFileName);
IDs := TStringList.Create;
try
Source.ReadSections(IDs);
for i := 0 to IDs.Count - 1 do
begin
// The section name is the key/ID.
ID := IDs[i];
// Append a row.
FData.Append;
// Read the values.
FData['ID'] := ID;
FData['Name'] := Source.ReadString(ID, 'Name', '');
// Names don't need to match. The field 'telephone' in this propriety
// format maps to 'phone' in your CSV output.
// Later, you can make this customizable (configurable) if you need to,
// but it's unlikely that you encounter two different inifile-based
// formats, so it's a waste to implement that until you need it.
FData['Phone'] := Source.ReadString(ID, 'Telephone', '');
FData.Post;
end;
finally
IDs.Free;
Source.Free;
end;
end;
This is very similar to the problems faced by "screen scrapers". If end users are intended to be able to use this, I would avoid regular expressions (except as an internal implementation detail, if needed) and not expose raw regular expression editing to end users.
Instead, I would let them load up samples of their data files, and construct their rules visually, with a drag, and drop style.
Click a "Match text" button, click and drag to select a rectangular region on the screen. Have options so that it might be allowed to move a certain amount up or down or left or right, if the format isn't precise or repeatable. Establish limits on how far you can go outside the original box.
Click a "grab text" button, click and drag to a rectangular or non-rectangular (flow) area on the screen. Name the output with a field, and give it a type (integer, string[x], etc). Similar limits apply as step 1.
Click save and the template rules are written to disk. Load a different file and see if the rules still apply nicely.
Relevant wikipedia topic.
Related
Overview
This question is a second attempt based on this one I recently asked: How can I make a TList property from my custom control streamable?
Although I accepted the answer in that question and it worked, I soon realized that TCollection is not the solution or requirement I was looking for.
Requirements
To keep my requirements as simple and clear to understand as possible, this is what I am attempting to:
Derive a new custom control based on TCustomListBox
Replace the Items property with my own Items type, eg a TList.
The TList (Items property) will hold objects, each containing a caption and a image index property etc.
Ownerdraw my listbox and draw its icons and text etc.
Create a property editor to edit the Items at design-time.
With that in mind, I know how to create the custom control, I know how to work with TList or even TObjectList for example, I know how to ownerdraw the control and I also know how to create the property editor.
Problem
What I don't know is how to replace the standard listbox Items type with my own? well I kind of do (publishing my own property that shares the same name), only I need to make sure it is fully streamable with the dfm.
I have searched extensively on this subject and have tried studying code where TListView and TTreeView etc publishes its Items type but I have found myself more confused than ever.
In fact I came across this very old question asked by someone else on a different website which asks very much what I want to do: Streaming a TList property of a component to a dfm. I have quoted it below in the event the link is lost:
I recently wrote a component that publishes a TList property. I then created a property editor for the TList to enable design-time editing. The problem is that the TList doesn't stream to the dfm file, so all changes are lost when the project is closed. I assume this is because TList inherits from TObject and not from TPersistant. I was hoping there was an easy work around for this situation (or that I have misunderstood the problem to begin with). Right now all I can come up with is to switch to a TCollection or override the DefineProperties method. Is there any other way to get the information in the TList streamed to and from the dfm?
I came across that whilst searching keywords such as DefineProperties() given that this was an alternative option Remy Lebeau briefly touched upon in the previous question linked at the top, it also seemed to be the answer to that question.
Question
I need to know how to replace the Items (TStrings) property of a TCustomListBox derived control with my own Items (TList) or Items (TObjectList) etc type but make it fully streamable with the dfm. I know from previous comments TList is not streamable but I cannot use TStrings like the standard TListBox control does, I need to use my own object based list that is streamable.
I don't want to use TCollection, DefineProperties sounds promising but I don't know how exactly I would implement this?
I would greatly appreciate some help with this please.
Thank you.
Override DefineProperties procedure in your TCustomListBox (let's name it TMyListBox here). In there it's possible to "register" as many fields as you wish, they will be stored in dfm in the same way as other fields, but you won't see them in object inspector. To be honest, I've never encountered having more then one property defined this way, called 'data' or 'strings'.
You can define 'normal' property or binary one. 'Normal' properties are quite handy for strings, integers, enumerations and so on. Here is how items with caption and ImageIndex can be implemented:
TMyListBox = class(TCustomListBox)
private
//other stuff
procedure ReadData(reader: TReader);
procedure WriteData(writer: TWriter);
protected
procedure DefineProperties(filer: TFiler); override;
//other stuff
public
//other stuff
property Items: TList read fItems; //not used for streaming, not shown in object inspector. Strictly for use in code itself. We can make it read-only to avoid memory leak.
published
//some properties
end;
that's DefineProperties implementation:
procedure TMyListBox.DefineProperties(filer: TFiler);
begin
filer.DefineProperty('data', ReadData, WriteData, items.Count>0);
end;
fourth argument, hasData is Boolean. When your component is saved to dfm, DefineProperties is called and it's possible to decide at that moment is there any data worth saving. If not, 'data' property is omitted. In this example, we won't have this property if there is no items present.
If we expect to ever use visual inheritance of this control (for example, create a frame with this listBox with predefined values and then eventually change them when put to form), there is a possibility to check, is value of this property any different than on our ancestor. Filer.Ancestor property is used for it. You can watch how it's done in TStrings:
procedure TStrings.DefineProperties(Filer: TFiler);
function DoWrite: Boolean;
begin
if Filer.Ancestor <> nil then
begin
Result := True;
if Filer.Ancestor is TStrings then
Result := not Equals(TStrings(Filer.Ancestor))
end
else Result := Count > 0;
end;
begin
Filer.DefineProperty('Strings', ReadData, WriteData, DoWrite);
end;
This would save a little bit of space (or lots of space if image is stored within) and sure is elegant, but in first implementation it can well be omitted.
Now the code for WriteData and ReadData. Writing is much easier usually and we may begin with it:
procedure TMyListBox.WriteData(writer: TWriter);
var i: Integer;
begin
writer.WriteListBegin; //in text dfm it will be '(' and new line
for i:=0 to items.Count-1 do begin
writer.WriteString(TListBoxItem(items[I]).caption);
writer.WriteInteger(TListBoxItem(items[I]).ImageIndex);
end;
writer.WriteListEnd;
end;
In dfm it will look like this:
object MyListBox1: TMyListBox
data = (
'item1'
-1
'item2'
-1
'item3'
0
'item4'
1)
end
Output from TCollection seems more elegant to me (triangular brackets and then items, one after another), but what we have here would suffice.
Now reading it:
procedure TMyListBox.ReadData(reader: TReader);
var item: TListBoxItem;
begin
reader.ReadListBegin;
while not reader.EndOfList do begin
item:=TListBoxItem.Create;
item.Caption:=reader.ReadString;
item.ImageIndex:=reader.ReadInteger;
items.Add(item); //maybe some other registering needed
end;
reader.ReadListEnd;
end;
That's it. In such a way rather complex structures can be streamed with ease, for example, two-dimensional arrays, we WriteListBegin when writing new row and then when writing new element.
Beware of WriteStr / ReadStr - these are some archaic procedures which exist for backward compatibility, ALWAYS use WriteString / ReadString instead!
Other way to do is to define binary property. That's used mostly for saving images into dfm. Let's say, for example, that listBox has hundreds of items and we'd like to compress data in it to reduce size of executable. Then:
TMyListBox = class(TCustomListBox)
private
//other stuff
procedure LoadFromStream(stream: TStream);
procedure SaveToStream(stream: TStream);
protected
procedure DefineProperties(filer: TFiler); override;
//etc
end;
procedure TMyListBox.DefineProperties(filer: TFiler);
filer.DefineBinaryProperty('data',LoadFromStream,SaveToStream,items.Count>0);
end;
procedure TMyListBox.SaveToStream(stream: TStream);
var gz: TCompressionStream;
i: Integer;
value: Integer;
item: TListBoxItem;
begin
gz:=TCompressionStream.Create(stream);
try
value:=items.Count;
//write number of items at first
gz.Write(value, SizeOf(value));
//properties can't be passed here, only variables
for i:=0 to items.Count-1 do begin
item:=TListBoxItem(items[I]);
value:=Length(item.Caption);
//almost as in good ol' Pascal: length of string and then string itself
gz.Write(value,SizeOf(value));
gz.Write(item.Caption[1], SizeOf(Char)*value); //will work in old Delphi and new (Unicode) ones
value:=item.ImageIndex;
gz.Write(value,SizeOf(value));
end;
finally
gz.free;
end;
end;
procedure TMyListBox.LoadFromStream(stream: TStream);
var gz: TDecompressionStream;
i: Integer;
count: Integer;
value: Integer;
item: TListBoxItem;
begin
gz:=TDecompressionStream.Create(stream);
try
gz.Read(count,SizeOf(count)); //number of items
for i:=0 to count-1 do begin
item:=TListBoxItem.Create;
gz.Read(value, SizeOf(value)); //length of string
SetLength(item.caption,value);
gz.Read(item.caption[1],SizeOf(char)*value); //we got our string
gz.Read(value, SizeOf(value)); //imageIndex
item.ImageIndex:=value;
items.Add(item); //some other initialization may be needed
end;
finally
gz.free;
end;
end;
In dfm it would look like this:
object MyListBox1: TMyListBox1
data = {
789C636260606005E24C86128654865C064386FF40802C62C40002009C5607CA}
end
78 is sort of signature of ZLib, 9C means default compression, so it works (there are only 2 items actually, not hundreds). Of course, this is just one example, with BinaryProperties any possible format may be used, for example saving to JSON and putting it into stream, or XML or something custom. But I'd not recommend to use binary unless it's absolutely inevitable, because it's difficult to see from dfm, what happens in component.
It seems like good idea to me to actively use streaming when implementing component: we can have no designer at all and set all values by manually editing dfm and see if component behaves correctly. Reading/loading itself can be tested easily: if component is loaded, then saved and text is just the same, it's all right. It's so 'transparent' when streaming format is 'human-readable', self-explaining that it often overweighs drawbacks (like file size) if there are any.
I am aware that Ini Files are meant for single lines of information, needless to say I am trying to read/write multi lines to and from the Ini - without much success (I always seem to do things the hard way!)
Lets say my Ini File when saved, looks like this:
[richardmarx]
Filenames=hazard
children of the night
right here waiting
Suppose the Ini File is built dynamically (ie, the richardmarx and the Filenames are not know, but unique - they could literally be anything).
How would I be able to read the Ini File?
In this example then, how could I put richardmarx into a TEdit, and the Filenames associated with richardmarx section into a memo?
Many thanks in advance.
Do not store multi-line strings into an INI file to begin with. Escape the line breaks, like #RobertFrank suggested. I would not use an asterik for that, though, as that is a valid text character. I would use something like this instead:
[richardmarx]
Filenames=hazard%nchildren of the night%nright here waiting
You can then read the string and replace the %n sequences with the value of the sLineBreak global variable. If you needed to store an actual % character, escape it as %%, eg:
[sales]
value=Sale! 50%% off%nat Macy's
You're not using a valid .ini format, so it's not going to be easy. It's much easier if you use a properly formed .ini file.
A valid ini file is of the format
[section]
akey=value
bkey=value
ckey=value
Here's a sample of reading multiple lines from an ini file. While it uses a TListBox instead of a TEdit, it should be enough to get you started.
The code below will work with an improperly formatted file as well, but you'll probably have to change the code in the ListBox1Click event to use ReadSectionValues instead and do some manual parsing for each item before displaying them; in that case, create another TStringList in the event handler and pass it instead of Memo1.Lines.
With a properly formatted ini file, you can use TIniFile.ReadSection or TMemIniFile.ReadSections to load all of the sections into a TStrings descendant, and then use ReadSection(SectionName) to get each section's values.
Here's an example - save this ini file somewhere (I've used d:\temp\sample.ini:
[A Section]
Item1=Item A1
Item2=Item A2
Item3=Item A3
Item4=Item A4
[B Section]
Item1=Item B1
Item2=Item B2
Item3=Item B3
Item4=Item B4
[C Section]
Item1=Item C1
Item2=Item C2
Item3=Item C3
Item4=Item C4
Here's a sample of the form's code:
unit Unit2;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, IniFiles;
type
TForm2 = class(TForm)
ListBox1: TListBox;
Memo1: TMemo;
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure ListBox1Click(Sender: TObject);
private
{ Private declarations }
FIni: TMemIniFile;
public
{ Public declarations }
end;
var
Form2: TForm2;
implementation
{$R *.dfm}
const
IniName = 'd:\Temp\Sample.ini';
procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);
begin
FIni.Free;
end;
procedure TForm2.FormCreate(Sender: TObject);
begin
FIni := TMemIniFile.Create(IniName);
Memo1.Lines.Clear;
FIni.ReadSections(ListBox1.Items);
end;
procedure TForm2.ListBox1Click(Sender: TObject);
var
Section: string;
begin
if ListBox1.ItemIndex > -1 then
begin
Section := ListBox1.Items[ListBox1.ItemIndex];
FIni.ReadSection(Section, Memo1.Lines);
end;
end;
end.
Clicking on each section name in the ListBox displays the items that are in that section, as seen below:
EDIT: OK. I got curious to see how it would work with the ini file content you posted in your question.
So I made the following change:
Copied and pasted your sample ini content verbatim as a new section to the end of the Sample.ini I created above.
Ran the code, and clicked the new richardmarx item. Here's what I got:
Obviously, that wouldn't work. So I made the following additional changes:
Changed the ListBox1Click event to use FIni.ReadSectionValues instead of ReadSection.
Ran the modified application, and clicked on the C Section item to see how it displayed, and then the new richardmarx item to see how it displayed. The results are as follows:
Pre-process the .ini file! Change all line-breaks between ] and [ to some character that will never appear in a filename (like asterisk). Then use TInifile to access the file you just preprocessed, changing the asterisks back to line breaks after you retrieve the strings. (Use StringReplace)
It's a little more complicated than that if you have more than one identifier in a section. In that case, you could use the equals sign as a flag that the preceding line break should not be removed. Maybe you read the file from the end towards the beginning.
You could even create a descendant of TIniFile that did the astrerisk-to-linebreak change for you.
No, this is certainly not an elegant solution. But, sometimes brute force like this works if you're stuck! The other solutions here are probably better, but thought I'd share this anyway in case it gives you a direction to think about heading...
Using any TStrings descendent object you can just use the CommaText property to read and write all the lines as a single string.
MyStrings.CommaText := IniFile.ReadString('Section', 'Ident');
and
IniFile.WriteString('Section', 'Ident', MyStrings.CommaText);
CommaText is smart enough to handle lines containing commas by automatically embracing them in quotes.
this code show you how to write and read multi lines with INI file
procedure TForm1.SaveButtonClick(Sender: TObject);
begin
IniFile.WriteString('Name', 'FirtName', FirstName.Text);
IniFile.WriteString('Name', 'LastName', LastName.Text);
IniFile.WriteInteger('Alpha Blend', 'Alpha Blend Value', Form1.AlphaBlendValue);
//Here start save Memo Lines
LinesCount := AboutMemo.Lines.Count;
IniFile.WriteInteger('About', 'Lines Count', LinesCount);
for I := 0 to LinesCount-1 do
IniFile.WriteString('About', 'About'+IntToStr(I), AboutMemo.Lines[i]);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin;
GetCurrentDir;
IniFile := TIniFile.Create(ChangeFileExt(Application.ExeName, '.ini'));
FirstName.Text := IniFile.ReadString('Name', 'FirstName', '');
LastName.Text := IniFile.ReadString('Name', 'LastName', '');
Form1.AlphaBlendValue := IniFile.ReadInteger('Alpha Blend', 'Alpha Blend Value', 255);
//Here Start Read Memo Lins From INI File
LinesCount := IniFile.ReadInteger('About', 'Lines Count', 0);
for I := 0 to LinesCount-1 do
AboutMemo.Lines.Insert(I, IniFile.ReadString('About', 'About'+IntToStr(I), ''));
end;
end.
I wanted something similar, to include the body of an automated email in the ini file.
But I also wanted blank lines in the body. Here is part of the ini file
[EmailBody]
This is the weekly Survey Status notification email.
.
It lists Survey Reports that may need some attention.
.
The purpose of this information is to improve conduct-of-operations in our tracking,
recordkeeping, and maintenance of Survey Reports. This report will also enable
us to identify any changes made to electronic Surveys following the normal QA review.
.
Please review your information weekly and determine if you need to take corrective action.
Here is what I ended up doing
var
Config: TMemIniFile;
sl: TStringList;
...
sl := TStringList.Create;
Config.ReadSectionValues('EmailBody', sl);
for i := 0 to sl.Count - 1 do
if sl[i] = '.' then
sl[i] := '';
#Ken White
FIni.ReadSectionValues get full string after same key, like "key=value".
In .ini like
[B Section]
Item1=Item B1
Item B2
Item B3
Item4=Item B4
and after
FIni.ReadSectionValues('B Section',Memo1.Lines);
Memo1.Lines will be {'Item1=Item B1', 'Item4=Item B4'}
Unfortunately, we can't read values directly, without any key.
But in fact, it's not too difficult:
procedure TForm2.FormCreate(Sender: TObject);
var i:longint;
strtemp: TStringList;
begin
FIni := TMemIniFile.Create(IniName);
Memo1.Lines.Clear;
Fini.ReadSection('B Section',strtemp);
for i:=0 to strtemp.Count-1 do
Memo1.Items.Add(Fini.ReadString('B Section',strtemp[i],''));
end;
Memo1.Lines would be {'Item B1', 'Item B4'}
Upper method will read all values in section, without keys.
How to read not-keyed values - I don't know (if at all it possible in .ini).
As a solution for huge non-format .ini, just numerate all strings - read file like text file, find needed section and add same count index like a key, schematically(!):
FileString[i]:=inttostr(i)+'='+FileString[i];
If need mixed format, will require more complicated parser.
P.S. Sorry for my english, I'm not strong in it.
You can use ReadSections method to get all the section names in ini file.
And perhaps you can use TMemIniFile's ReadSectionValues method to read "multiline values" (ie to read whole section into stringlist). If that doesn't work then perhaps you could use GetStrings to get the content of the ini file and parse it "semy manually" - IIRC it returns you a stringlist with section names where each item's object holds another stringlist with section data.
For a simulation program I'm working in Delphi 2010. The simulation isn't a problem but I need to use large collection of data which gives a problem. The data is available in excel sheets, so there is no need to edit this data in Delphi, but collecting this data from the excel sheets takes around 10min. This isn't a problem as long as you don't need to collect the data every time the program runs. So I made a program which collects all the data makes it visible, not problems here,and then store it. However I can't store it to a "Delphi format" , without losing the structure, so it can be loaded in a few seconds.
I'm not that experienced in Delphi and I searched a long time for the solution but couldn't understand what was best. I think my way of structuring the data is wrong but it was simple and worked. However if there are better ways of storing the data please say so, but remember that I need some more explanation than just use 'a xml file', 'generict, or 'Ttreeview'. (have read it but wasn't able to use it).
The data is for: I made this product, The next product I make is this, so do I need to clean? True or false.
The data is stores as a class(TObject) with Productnumber (integer) and a List which contains all products that could be made next.This list contains another class(TObject) with an Productnumber (integer) and a do I need to clean(boolean). I want to save this structure in a file, without losing the data and read it back to the same structure.
I hope someone could help. Thank you in advance.
Update: The code to provide a little more information (modified to English)
Clean_from = class(TObject)
public
myfromNumber : Integer;
mylist : TList;
published
constructor Create;
End
Clean_To = class(TObject)
public
myToNumber : Integer;
Clean : Boolean;
End;
constructor Clean_from.Create;
begin
inherited Create;
myList := Tlist.Create;
end;
For i = 0 to 100 do
begin
From:= Clean_from.create;
for j := 0 to 10 do
begin
To := Clean_To.create;
To.clean := true or false;
From.myList.add(To);
end;
GlobalList.add(from);
end;
And now I want to save the global list with all the content so I could load it with the same structure.
What you need is the so-called "serialization" mechanism.
1. The standard way
1.1 SaveToStream
In Delphi, we usually implement a SaveToStream method, which will save the content of each object in a destination TStream (either a TFileStream or a TMemoryStream).
You'll have to write the serialization by hand.
1.2 DFM-like streaming
See TWriter / TReader classes.
If you define your data in published properties, you are able to serialize them using those standard Delphi classes.
For some methods able to serialize any TCollection to and from JSON content, see this blog article.
2. The RTTI
See for instance this SO question.
In particular, the new enhanced RTTI (available since Delphi 2010) opens new opportunities to serialization.
3. Use records instead of classes
If each item does not store a lot of content (some integer/boolean), it may make sense to use records instead of objects. For speed and memory consumption/fragmentation, it may be worth it.
Here is some wrapper able to serialize any dynamic array, even containing nested records or dynamic arrays.
4. Use a database engine
Perhaps the better approach is not to have your data stuck in a non-evolving binary form, proprietary to your application. If you want to add a property, you'll have to manage it by hand. Or if you want to access your data from other applications, it may be difficult.
There are a lot of database solutions around - instead of using an external database (like MS SQL, FireBird or Oracle), it could be a good idea to embed the database inside your application (much easier to install). Worth mentioning SQLite which has a lot of wrappers, including our version (which will allow you to change to any other database if you want to use MS SQL or Oracle instead).
You have other solutions around - see this SO question - and if you need performance, take a look at our Big Table library.
Add SaveToStream() and LoadFromStream() methods to your data object which, well, save the data to a stream and load data from a stream.
type
TMyData = class(TObject)
private
FChildProducts: TList;
FProductnumber : integer;
FClean: boolean;
public
procedure LoadFromStream(const aStream: TStream);
procedure SaveToStream(const aStream: TStream);
published
property Productnumber: Integer read FProductnumber write FProductnumber;
property Clean: Boolean reas FClean write FClean;
end;
procedure TMyData.LoadFromStream(const aStream: TStream);
var x, cnt: Integer;
cD: TMyData;
begin
aStream.Read(FProductnumber, SizeOf(FProductnumber));
aStream.Read(FClean, SizeOf(FClean));
// read number of child products
aStream.Read(cnt, SizeOf(cnt));
// load child objects
for x := 1 to cnt do begin
cD := TMyData.create;
cD.LoadFromStream(aStream);
FChildProducts.Add(cD);
end;
end;
procedure TMyData.SaveToStream(const aStream: TStream);
var x: Integer;
begin
aStream.Write(FProductnumber, SizeOf(FProductnumber));
aStream.Write(FClean, SizeOf(FClean));
// save number of child products
x := FChildProducts.Count;
aStream.Write(x, SizeOf(x));
// save child objects
for x := 0 to FChildProducts.Count - 1 do
(FChildProducts[x] as TMyData).SaveToStream(aStream);
end;
I assume you have some list of "root objects" so you can make an function or method which saves/loads them to/from stream ie
function SaveDataList(const List: TList;const aFileName: string);
var x: Integer;
FS: TFileStream;
begin
FS := TFileStream.Create(aFileName, ...);
try
// save file version
x := 1;
FS.Write(x, SizeOf(x));
// save number of products
x := List.Count;
FS.Write(x, SizeOf(x));
// save objects
for x := 0 to List.Count - 1 do
(List[x] as TMyData).SaveToStream(FS);
finally
FS.Free;
end;
end;
This is the general idea... how to load data back should be clear too. The file version thing is there so that when the data object changes (ie you add some property) you can increment the version number so that in the loading code you can load data into right version of the data object.
I would like to have a playlist for my own music player in Delphi / Pascal.
I thought that it would be the best solution to have a TStringList with the path of the MP3 file and - additionally - a TListBox with the song names. The matching strings in both lists must be at the same position. So if the user chooses item 5 in TListBox I can just take the path at position 5 in the TStringList.
This works fine.
But now I need a playlist with two columns: "artist" and "song title". You should be able to sort the playlist by artist (ascending and descending) as well as by song title (ascending and descending) - alphabetically, of course.
How could I do this? Having two objects of TStringList - one sorted by artist and one sorted by song title?
I would do a TSong class containing at least the Artist and Title properties, and a TSongList providing 1 or more sort methods (can be generic) using the proper sort Field(s.
Certainly not maintaining 2 separate StringLists that you have to manage, keep in sync and reshuffle when sorting...
One cheap way to kinda implement that, could be to have an in memory DataSet with a record containing Artist and Path displayed in a grid that you can sort on different columns.
The current row will give both informations directly.
One simple solution would be to implement your song list/song information as a TCollection.
By using collections you can let the VCL handle the loading and saving to disk.
For example:
Please note this is not functionally complete, I'll leave that up to you, and since I wrote this from the top of my head I might have messed something up. It is only an example to get you started.
{...}
interface
Type
TSongCollectionItem = class(TCollectionItem)
public
constructor create(Owner:TCollection); override;
procedure assign(source : TPersistent); override;
published
property FileName : String read fFileName Write fFileName;
property Artist : string read fArtist write fArtist;
property Title : string read fTitle write fTitle;
{...}
property Album : string read fAlbum write fAlbum;
end;
TSongCollection = class(TOwnedCollection)
private
function GetItem(Index: Integer): TSongCollectionItem;
procedure SetItem(Index: Integer; Value: TSongCollectionItem);
public
constructor Create(AOwner: TPersistent);
function Add: TSongCollectionItem;
property Songs[Index: Integer]: TSongCollectionItem read GetItem write SetItem; default;
end;
procedure SaveSongList(Songs : TSongCollection; FileName:string; Binary:boolean);
procedure LoadSongList(Songs : TSongCollection; FileName:string; Binary:boolean);
{...}
implementation
{...}
type
TSongComponent = class(TComponent)
published
property SongList : TSongCollection read fsonglist write SetSongList;
end;
procedure SaveSongList(Songs : TSongCollection; FileName:string; Binary:boolean);
var
wFile : TFileStream;
wConvert : TMemoryStream;
wSongList : TSongComponent;
begin
RegisterClass(TSongComponent);
Try
wConvert := TMemoryStream.Create;
wFile := TFileStream.Create(filename, fmcreate);
wSongList := TSongComponent.create(nil);
try
wSongList.SongList.Assign(Songs);
if not Binary then
begin
wConvert.WriteComponent(wSongList);
wConvert.Position := 0;
ObjectBinaryToText(wConvert, wFile);
end
else
wFile.WriteComponent(wSongList);
finally
wConvert.Free;
wFile.Free;
wSongList.free;
end;
finally
Unregisterclass(TSongComponent);
end;
end;
procedure LoadSongList(Songs : TSongCollection; FileName:string; Binary:boolean);
var
wFile : TFileStream;
wConvert : TMemoryStream;
wSongList : TSongComponent;
begin
RegisterClass(TSongComponent);
Try
wConvert := TMemoryStream.Create;
wFile := TFileStream.Create(filename, fmOpenRead);
try
if not Binary then
begin
ObjectTextToBinary(wFile, wConvert);
wConvert.Position := 0;
wSongList := TSongComponent(wConvert.ReadComponent(Nil));
end
else
wSongList := TSongComponent(wFile.ReadComponent(Nil));
if assigned(Songs) and assigned(wSongList) then
Songs.Assign(wSongList.Songs);
if assigned(wSongList) then
wSongList.free;
finally
wConvert.Free;
wFile.Free;
end;
finally
Unregisterclass(TSongComponent);
end;
end;
I've done a few of these "lists" over time and in the end I've always found making the classes rather easy, but storing and especially reading the lists from disk has proven "challenging" to say the least.
The challenge has been in cases were users actually manipulate the lists with external editors, thus making reading the lists error prone.
For a universally accepted playlist format (M3U) have a look at http://schworak.com/programming/music/playlist_m3u.asp.
A VCL component with source that reads multiple formats is available at Torry's called "PlayList v.0.5.1". http://www.torry.net/quicksearchd.php?String=PlayList+v.0.5.1&Title=Yes
If you don't want to build an global object structure, you can allways use TlistView structure in report mode.
You have there a list, with subitems.
You can sort by column, and save to csv or whatever format.
you can easily add icons etc.....
and you have the right events to trigger.
How do I save a Tlistviews layout in Delphi 2007?
I have been asked to write some code to allow users to re-order columns in a TListview (well all TListviews in our application), I have the code working (by manipulating the columns index and setting width to zero to hide columns not needed) but now I need a way to save the state of the view when to form exits.
What is the best way to do this? I thought about serialization, but I dont need the data or sort order so that seamed a bit overkill to me.
Some things to ponder
It needs to be on a per user basis
It needs to be flexible, in-case we add a new column in the middle of the listview
There is no garantee that the Column headding will be unique
The listview name may not be unique across the application
Any ideas?
If you only want to save and load a certain part of the data you can store it n an ini or xml file.
General data can be written to the file. Columns is another problem. You need to find an unique identification for each column. The ini could be something like:
[Settings]
[Col_1]
position=1
width=500
title=hello world
align=left
sort=ascending
.. etc for more fields and more columns.
If you uses a listview helper class, you only need to write the code once:
TListviewHelper = class helper for TListView;
public
procedure SaveToFile(const AFilename: string);
procedure LoadFromFile(const AFileName: string);
end;
procedure TListviewHelper.SaveToFile(const AFilename: string);
var
ini : TIniFile;
begin
ini := TIniFile.Create(AFileName);
try
// Save to ini file
finally
ini.Free;
end;
end;
procedure TListviewHelper.LoadFromFile(const AFileName: string);
var
ini : TIniFile;
begin
ini := TIniFile.Create(AFileName);
try
// Load from ini file
finally
ini.Free;
end;
end;
If TListviewHelper is within scope, you have access to the extra methods.
I suggest you inherit from Tlistview (or is there a TCustomListView) to create your own component, class helpers are nice but unofficial.
Perhaps the easiest way to store the order of the columns would be to define a ID for each as a meaningfull string, and store the list in the right order in the registry.
For instance, let's suppose your columns were ordered like:
Name | First name | Age | Job title
Then the stored string in the registry could be:
"Name,FName,Age,JTitle"
To be stored in the appropriate registry entry, under the appropriate key (typically HCKU\SOFTWARE\MyApplication, under the key ColumnOrder for instance)