Delphi - NOVICE programmer - (meaning don't expect advanced concepts to be already known )
I am working on an application, part of which is a scheduling function. I look at a group of appointments. These appointments fall on a specific date. I need to create a consolidated view of all the appointments on each date. By this, I mean that I create a "grouping" of data about Jan 17th, this is going to be how many appointments are on that date, when they start, when they stop, etc. This may be an array, a Record, a class, don't know at this point. I may have one for the 17th, then the next one is for the 22nd, then the next is for the 24, and then I may have one every day for the next 35 days... I will have either 0 or 1 structures/containers per day, and I can see having 3 to 5 months months of these...Which ones exist and which ones don't exist will be very fluid. This structure is a consolidated view, meaning that I get some of the information from one appointment, and then some of the information from the next appointment, so as I am reading each appointment, I need to be able to find this structure quickly.
I need these structure to be memory based, so they are fast. They MAY be of different sizes (depending on the number of appointments on that day.
REQUIREMENTS
So, I need to be able to create these structures on the fly. (for example, I have just read an appointment for Jan 22, and need to update the structure, but it doesn't exists yet, so I need to make it on the fly).
I need to be able to find them them quickly. (Hash on TDate maybe).
Each one of the structures will hold multiple data types (boolean, TDateTime, TStringlist, etc).
I have Delphi 2010 if that helps...
What structure am I looking for? Easy, fast, already included with D2010 (or free) are all important.
Thanks
GS
UPDATED INFO:
So it appears the TDictionary or TObjectDictionary is the way to go...
I have decided (I think...) to use a record to hold my base information, and then store those records in either TDictionary or TObjectDictionary. I am running into a challenge in both of them. In TDictionary, I cannot figure out how to free the records when I am done with them (since they are pointers), and with TObjectDictionary, I cannot create it with a record type... Any help appreciated. Code samples are...
// Create the base record definition that will be put in the TObjectDictionary/TDictionary
type
TSummaryAppt = record
Date : TDate;
SA_ID : Integer;
BusyFlag : Array[1..36] of Boolean; // 15 minute periods...
PCTFree: Double;
LargestFreeMinutes : integer;
end;
If I go TObjectDictionary, this will not work...
var
Dic : TObjectDictionary<Integer,TSummaryAppt>;
begin
Dic := TObjectDictionary<Integer,TSummaryAppt>.Create([doOwnsKeys, doOwnsValues]);
The Create line fails on execution (compiles fine) with Invalid Class Typecast.
TDictionary appears to be a little friendlier, but I have to deallocate my memory....
var
Dic : TDictionary<Integer,TSummaryAppt>;
rec : ^TSummaryAppt;
p : TSummaryAppt;
i : Integer;
begin
Dic := TDictionary<Integer,TSummaryAppt>.Create;
// Now add some records. THese have to be created dynamically
// because I dont know at compile time how many there are.
new(rec);
rec.Date := now;
rec.SA_ID := 3;
Dic.Add(1, rec^);
new(rec);
rec^.Date := now;
rec^.SA_ID := 5;
Dic.Add(2, rec^);
new(rec);
rec^.Date := now;
rec^.SA_ID := 7;
Dic.Add(3, rec^);
// Test ...
for p in Dic.Values do begin
ShowMessage(IntToStr(p.SA_ID));
end;
// Now free everything. HERE IS WHERE I AM HAVING PROBLEMS...
// What should I be doing?
for p in Dic.values do
p.dispose;
Dic.Values.Free;
Dic.Keys.Free;
Dic.Free;
end;
Any and all help appreciated. What should I be doing different?
Thanks so much!
GS
Regarding your update to the question (which really needs to be a new question), here's what you need to do.
First of all, since both your key and value for the dictionary are value types you need to use TDictionary<K,V>. When you add items to the dictionary, a copy is made of both key and value so this means you need not do any dynamic allocation.
Your code should look like this:
type
TSummaryAppt = record
Date: TDate;
SA_ID: Integer;
BusyFlag: Array[1..36] of Boolean; // 15 minute periods...
PCTFree: Double;
LargestFreeMinutes: Integer;
end;
....
var
Dic: TDictionary<Integer, TSummaryAppt>;
rec: TSummaryAppt;
....
// create the dictionary
Dict := TDictionary<Integer, TSummaryAppt>.Create;
....
// initialise rec, in your code you would put real values in
FillChar(rec, SizeOf(rec), 0);
rec.Date := now;
rec.SA_ID := 3;
Dict.Add(1, rec);
rec.Date := now;
rec.SA_ID := 5;
Dict.Add(2, rec);
//etc.
When you have finished with the dictionary all you need to do is free it. The dictionary owns all the contents and will clean up.
Dict.Free;
You may well prefer to wrap up the functionality of the dictionary and expose it through a higher-level interface. So you may have an Add method that received as parameters all the fields of the value to be added. And you may want an update method that received just the mutable fields.
Do you know the generic TDictionary collection?
class TAppointmentCalendar = TDictionary<TDate, TAppointments>
end;
TAppointments also can be a generic class, based on Generics.Collections.TObjectList:
class TAppointments = TObjectList<TAppointment>
end;
These classes can be extended as needed, for example to add properties or data aggregation methods.
Then instantiate a Calendar
Cal := TAppointmentCalendar.Create;
Cal.Add(MyDate, AppointmentsForThisDay);
or retrieve Appointments
var
Appointments: TAppointments
begin
Cal.TryGetValue(ADate, Appointments);
...
TDictionary performs hash based Key lookups.
First of all, as a novice in data structures, it is worth buying and reading this great book: The Tomes of Delphi: Algorithms and Data Structures - By Julian Bucknall.
Here are some potential layout to implement your application.
A. NoSQL database. For instance, take a look at our BigTable Open Source components.
The root component TSynBigTableis available to records within a file-based database. It is light, and very optimized for speed.
Two of their children do handle fields within records. The field layout can change on the fly. See TSynBigTableRecord and TSynBigTableMetaData.
B. Use a regular SQL database, either like TClientDataSet or direct via SQL (you'll find some Open Source components on a static SQLite3 engine (static, i.e. with no external dll needed).
Using SQLite3 as application data, is a very good idea. It is the main purpose of this library - it is used by a lot of programs, like FireFox or Chrome, or even in most Cell phones OS.
In order to make queries fast, you'll have to create indexes on some columns (e.g. date field), and therefore query results will be immediate.
I do not recommend "to be able to create these structures on the fly". It is not a good programming practice IMHO - or it will become very complex: it would need to store the field layout within the records, so it is not a good path for a novice Delphi programmer.
You should better make your data structures open enough to handle any kind of data. With SQLite3 you can serialize your data as BLOB or text (e.g. with JSON). This is e.g. what we allow in our Open Source mORMot ORM - it is client-server, but can be used stand alone. I'd recommend taking a look at our framework documentation, especially the SAD document which tries to present some design approach, like test-driven, ORM or SOA.
C. If you want a TDictionary kind of storage, take a look at our TDynArray wrapper, which handle the same methods but has some unique features like automated serialization or multiple indexes (not handled by TDictionary), which are mandatory for your request.
AFTER QUESTION UPDATE
Some code using TDynArray:
type
TSummaryAppt = record
Date : TDate;
SA_ID : Integer;
BusyFlag : Array[1..36] of Boolean; // 15 minute periods...
PCTFree: Double;
LargestFreeMinutes : integer;
end;
TSummaryApptDynArray = array of TSummaryAppt;
var rec: TSummaryAppt;
SAs: TSummaryApptDynArray;
SA: TDynArray;
F: TFileStream;
begin
SA.Init(TypeInfo(TSummaryApptDynArray),SAs);
rec.Date := now;
rec.SA_ID := 3;
SA.Add(rec); // rec is now added in SAs[]
assert(length(SAs)=1); // or SA.Count=1
assert(SAs[0].SA_ID=3);
for rec in SAs do // will work like any dynamic array
ShowMessage(IntToStr(p.SA_ID));
F := TFileStream.Create('datafile',fmCreate);
SA.SaveToStream(F); // a TDictionary won't do that
F.Free;
SA.Clear;
assert(length(SAs)=0); // or SA.Count=0
F := TFileStream.Create('datafile',fmOpenRead);
SA.LoadFromStream(F); // a TDictionary won't do that
F.Free;
assert(length(SAs)=1); // or SA.Count=1
assert(SAs[0].SA_ID=3);
for rec in SAs do // will work like any dynamic array
ShowMessage(IntToStr(p.SA_ID));
// you need nothing to free the memory, since both are handled by the compiler
end;
Of course, your array may be stored directly in one block, since it contains only plain data (double, integer, booleans); but our TDynArray wrapper is able to handle any string or other dynamic array within.
Some code using our ORM:
type
TBusyFlag = set (1..36);
TSummaryAppt = class(TSQLRecord)
private
fDate : TDate;
BusyFlag : TBusyFlag; // 15 minute periods...
PCTFree: Double;
LargestFreeMinutes : integer;
published
// already contains an ID: integer field
property Date : TDate read fDate write fDate;
property BusyFlag : TBusyFlag read fBusyFlag write fBusyFlag ; // 15 minute periods...
property PCTFree: Double read fPCTFree write fPCTFree;
property LargestFreeMinutes : integer read fLargestFreeMinutes write fLargestFreeMinutes;
end;
// then initialize the database model and use your database:
Model := TSQLModel.Create([TSummaryAppt]);
Client := TSQLRestClientDB.Create(Model,nil,'FileName',TSQLRestServerDB);
Client.Server.CreateMissingTables(0); // will create the database if needed
...
rec := TSummaryAppt.Create;
rec.Date := Now;
rec.ID := 3; // but the ORM may create one unique ID for you
Client.Add(rec);
rec.Date := 0;
Client.Retrieve(3,rec);
...
rec.Free;
Client.Free;
Model.Free;
Our ORM is here used locally, all in one executable, creating a SQLite3 database for data storage. But if you change TSQLRestClientDB into TSQLite3HttpClient and TSQLRestServerDB+TSQLite3HttpServer, you'll be able to use your data remotely, via standard JSON (and also from an AJAX application). Without modifying your client code. And if you want to store your data with something else than SQLite3 (even up to Oracle, or an in-memory database), you can.
Related
How can I use record in TDictionary?
TMyRec = record
a: Integer;
b: Integer;
end;
...
dictionary = TDictionary<String, TMyRec>.create();
...
dictionary[key].a := 30;<<<
Here the compiler gives an error: "Left side cannot be assigned to". How can I solve this problem without creating a separate function for writing myFunc(a, b: Integer): TMyRec?
dictionary[key] returns a copy of the record held by the dictionary. The compiler prevents you from modifying that because it would serve no purpose.
As an aside, older versions of the program would accept your code and it was very confusing that the modification to the record would be lost. You'd make an assignment but nothing visible changed because what you assigned was a nameless local variable.
Clearly you intend to modify the record held in the collection. In order to do that you need to assign the entire record. Read the record from the collection into a local variable. Modify the local variable. Write the updated value back to the collection. Like so:
var
rec: TMyRec;
...
rec := dictionary[key];
rec.a := 30;
dictionary[key] := rec;
One of the frustrating aspects of this is that the code needs to perform two dictionary lookups, even though we know that the second one will find the same record as the first one. Not even the mighty Spring4d dictionary can do this with a single lookup.
David Heffernans answer is what you're after, but I would like to offer an additional warning. Records can have properties just like classes, with getters and setters, and if your record has such properties your code will compile, but it will still not change the actual record value.
TMyRec = record
private
FA : integer;
procedure SetA(const Value: integer);
function GetA : integer;
public
{ Warning: When used on result from dictionary lookup, only the COPY will be
altered, not the actual record in the dictionary! }
property A : integer read GetA write SetA;
end;
A very simple workaround is to use the List property of the record.
You can say:
dictionary.list[key].a := 30;
This will access the dynamic array that backs up the TList via the List property. The compiler already supports direct access to a dynamic array.
If you can login to quality.embarcadero.com, you can see the full discussion of this issue raised as: RSP-23136: We should be able to assign a value to one element in a list of records - posted Dec 18, 2018 and resolved Nov 21, 2019.
The issue was closed with the comment:
"This works as expected. Alternative coding style was provided."
On my form I have a number of TMyQuery Components. Their names identify which MySQL Tables they work with. For example, COMPONENTSTABLE works with the COMPONENTS TABLE, etc.
There are about 30 tables, but that might change in the future.
I also use a basic String List to read field names from a Table called TIMESTAMPS. This table is updated via triggers when an UPDATE, INSERT, or DELETE occurs. Each field within the TIMESTAMPS Table refers to which Table was modified. There's only one record in the table! Based on the field values I can see which table changed so I can refresh it rather than refreshing all of them.
I don't want to do this;
If fieldbyname['COMPONENTSTABLE'] <> CurrentTimeStamp
then ComponentsTable.Refresh;
If fieldbyname['ORDERSTABLE'] <> CurrentTimeStamp
then OrdersTable.Refresh;
{ and so on forever }
What I want to do is;
Right now I have a String List with "Names / Values". Each "Name" is the Fieldname within the Table and "Value" is the TIMESTAMP provided by MySQL Triggers.
I've got the following;
For Idx := 0 to MyStringList.Count -1 do
Begin
If MyStringlist.ValueFromIndex[Idx] <> SomethingElse then
Begin
with (MyStringList.Names[Idx] as tMyQuery).Refresh;
End;
End;
I've got the String List functioning, the Names, the Values etc are all correct.
My question is this;
Is there a way I can use a String ("Names" column in the list) to refer to an Object if that Object exists?
I already have a function I use to refresh individual tables by passing an Object to it, but that's an Object and easy to work with. I'd like to pass the "Object" based on it's name retrieved from a String.
I hope this makes sense and you can follow what I'm after.
I am not sure what your question actually is. In the first part of the answer I assume that you don't really care about names of the objects but rather want some automated way of getting all the tables available refer to a field in another table. Below that, I answer your question about referring to an object if you know its name.
Automated way of handling all tables
It depends on what class your objects are.
From your description, I assume your TMyQuery are TComponent descendants owned by the form. Then the solution is very simple, as each TComponent has both a public Name and a list of owned components Components. You can then use something like this:
var
i: integer;
MyQuery: TMyQuery;
begin
for i := 0 to Pred(MyForm.ComponentCount) do
if MyForm.Components[i] <> TimeStampsTable then
if MyForm.Components[i] is TMyQuery then
begin
MyQuery := TMyQuery(MyForm.Components[i]);
if TimeStampsTable.FieldByName(MyQuery.Name).AsDateTime >= LastAccess then ...
end;
end;
Note that you may want to add extra checks, e.g. to make sure that MyQuery.Name is not empty or that it exists as a field in TimeStampsTable.
If your objects are only TObjects, then there is no "standard" name property and no standard registration of these objects. Name can be handled, apparently your component already has one so it's just a question of a proper type coercion, but object registration is a different matter. You may have to create some kind of a global list for all your created TMyQuery instances.
Getting an object instance based on that object's name
function TMyForm.GetQueryByName(const Name: string): TMyQuery;
var
Obj: TObject;
begin
Result := nil;
Obj := Self.FindComponent(Name);
if Obj <> nil then
if Obj is TMyQuery then
Result := TMyQuery(Obj);
end;
Or you could simply loop over all Components and use your own Name matching.
While the first part of the accepted Answer from #pepak isn't what I was looking for ( I've used similar code in the app previously and found it slow ), the second part of the Answer pointed my in the right direction.
My (thanks to Pepak) eventual solution was;
Function RefreshQueryByName(Const Name: String): Boolean;
Var
Obj: TComponent;
Begin
Result := False;
Obj := Self.FindComponent(Name);
If Obj <> nil Then
If Obj Is TMyQuery Then
With Obj As TMyQuery Do
If Active Then
Begin
Refresh;
Result := True;
End;
End;
Which I use by by passing a String I get from a Field Value that identifies which table I want to refresh.
Now, my Database App automatically refreshes a table changed by other users. It will now refresh any of the 30 tables of they are modified by another user without refreshing all tables.
Thanks for your help Pepak, I've accepted your answer and hope it is useful to others.
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'm new to Stack Overflow but I find myself seeking some of the best programming solutions on this site. So I have a question to ask.
I am writing a program in Delphi that is a TUI menu-driven program for a local business client. They have asked me to keep the user interface the same as in the old program (written in BASIC for MS-DOS, dated in 1982) so it is all menu driven with global data being stored in files and reloaded by the program. Each sub-menu is a program in and of itself run by the active menu (also a program).
I have written my own TUI framework and UI manager for displaying menus and sub-menus. The UI manager contains an overridden method called "Draw" to display the menu and another overridden method called "OnEvent" which handles keyboard events in the UI. My first question is would you consider this to be an appropriate method for making a menu-driven program containing sub-menus? An example of how this works is such:
type
TMenu1 = class(TExtendedUIManager)
private
procedure OnEvent (c: Char); override;
end;
type
TSubMenu1 = class(TExtendedUIManager)
end;
procedure TMenu1.OnEvent (c: Char);
var
Next: TExtendedUIManager;
begin
if c = '2' then begin
Next := TSubMenu1.Create;
Self.Start(Next);
Next.Free;
end;
end;
My other question is what would be an appropriate way of sharing data between menus? For example, if I wanted my TSubMenu1 class to return a string when a method is called, how would I make it accessible to other sub-menus that do not interact with it? (Sorry if the question is vague). I have the Singleton pattern in mind but I've also thought of having the UI manager store a reference to some object for data storage and each time a new sub-menu is run, pass in the reference to the new sub-menu (UI manager). The conundrum is finding out which one works best. Or even if my menu-driven framework is decent.
Opinions are welcomed and any advice is appreciated. Thanks for your time and help!
--Todd
"My other question is what would be an appropriate way of sharing data between menus?"
You can share the data by using class methods and properties. By using these, you can even access them even without create instance of the class. For more info go through this Link.
Below is sample code which will share List.
type
TForm1 = class(TForm)
---
---
private
{ Private declarations }
class var List: TStringList;
---
end;
var
Form1, Form2: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
Form1.List.Add('4');
Form2.List.Add('5');
ShowMessage(TForm1.List.Text);
end;
initialization
StrList := TStringList.Create;
TForm1.List := TStringList.Create;
TForm1.List.Add('1');
TForm1.List.Add('2');
TForm1.List.Add('3');
ShowMessage(TForm1.List.Text);
finalization
FreeAndNil(TForm1.List);
end.
I'll freely admit this is thinking aloud territory for me, but the first thing that caught my eye was this:
procedure TMenu1.OnEvent (c: Char);
var
Next: TExtendedUIManager;
begin
if c = '2' then begin
Next := TSubMenu1.Create;
Self.Start(Next);
Next.Free;
end;
end;
It feels like the end condition of this sort of programming is going to be huge decision trees of if c = '2' then ... else if c = '3' then ... else and so on. This does get tedious to write and maintain; if feasible, an array mapping input character with a function to execute is often far easier to maintain.
[['2', foo_create],
['3', foo_delete],
['4', foo_ship],
['d', foo_destroy_all],
['`', foo_return_to_previous_menu]]
When a new character comes in, you would look up the corresponding function in the table and execute it. Once you're done, return to waiting.
You could further extend this array to keep track of arguments that are required and values returned from each function. When you execute a function, store its return value in a global variable somewhere, and keep track of what types were returned. (Perhaps Delphi's type system is sophisticated enough that it is trivial, and not even worth mentioning.) When you execute a new function, check to see if the type of the 'current' returned result is suitable to pass to the desired function. (You could even grey-out the menu entries if the types are wrong, to indicate to the user that this combination won't work.)
/* key function arg ret types */
[['2', foo_create, NULL, FOO],
['3', foo_delete, FOO, NULL],
['4', foo_ship, FOO, NULL],
['d', foo_destroy_all, NULL, NULL],
['`', foo_return_to_previous_menu, NULL, NULL]]
I hope this is useful in some fashion. :)
I've given up on the Delphi 7 debugger and am pretty much relying on outputdebugstrings. Is there a standard function I can call to get the contents of an object as a string like the debugger would if I set a breakpoint?
Not exactly what your looking for, but you can use RTTI to get access to the values of various published properties. The magical routines are in the TypInfo unit. The ones you are probably most interested in are GetPropList which will return a list of the objects properties, and GetPropValue which will allow you to get the values of the properties.
procedure TForm1.DumpObject( YourObjectInstance : tObject );
var
PropList: PPropList;
PropCnt: integer;
iX: integer;
vValue: Variant;
sValue: String;
begin
PropCnt := GetPropList(YourObjectInstance,PropList);
for iX := 0 to PropCnt-1 do
begin
vValue := GetPropValue(YourObjectInstance,PropList[ix].Name,True);
sValue := VarToStr( vValue );
Memo1.Lines.Add(PropList[ix].Name+' = '+sValue );
end;
end;
for example, run this with DumpObject(Self) on the button click of the main form and it will dump all of the properties of the current form into the memo. This is only published properties, and requires that the main class either descends from TPersistent, OR was compiled with {$M+} turned on before the object.
Rumor has it that a "reflector" like ability will be available in a future version of Delphi (possibly 2010).
Consider something like Codesite which is a much more complete tracing solution. It allows you to output much more complex info, and then search, print, and analyse the data. But for your purposes, you can simply send an object to it with Codesite.Send('Before', self); and you get all the RTTI available properties in the log. Do an "After" one too, and then you can compare the two in the Codesite output just by selecting both. It's saved me many times.
if delphi 7 is the .NET version, then you could do (some of) that with reflection. (not easy, but not terribly hard). if it's the normal, compiled thing, then it's a hard problem and the debugger is you best bet, apart from specialized printing functions/methods.