问题
Note
Sorry in advance for the long post, I though it would be best to put as much information on as possible rather than fill the gaps when needed.
Note although I have tagged this as Delphi as well and do own and still use Delphi XE I am now using Lazarus as my primary IDE, I simply cannot afford to purchase the newer Delphi versions and now Lazarus is becoming more stable it makes sense to me to make the switch to Lazarus.
For this question I have included a zip attachment with project source, although written in Lazarus it will really help with the question I have, hence the comments in the first paragraph.
Overview
Onto the question, I have a Object that owns several classes as TLists.
I represent this data in a Treeview and there is no way of knowing how many levels and nodes will be present in the tree as they are dynamically created at runtime. One limitation I have set is that the Top level nodes will be fixed, meaning they cannot be deleted or renamed - these are what I will call RootGroups.
The Treeview will be populated with items and groups, every node added to the Treeview will have its own Object assigned to the data to identify each item correctly. I am going to show an example screenshot now to give a better idea before carrying on:
As you can see I have the two top most nodes, Object1Root and Object2Root. If you notice the buttons on the right, they allow adding group and items to the Treeview but they become disabled if they don't belong in that part of the Treeview. For example you cannot add Object2Group or Object2Item under Object1Root.
Basically everything in the Treeview has its own Pointer to a Object. Each Object I am deriving from a Base Object. This Base Object has properties to store the position of where it is found in the Treeview, like this:
type
TBaseObject = class
private
FName: string;
FGroup: string;
FNodeLevel: Integer;
FNodeIndex: Integer;
public
constructor Create(AName: string);
destructor Destroy; override;
published
property Name: string read FName write FName;
property Group: string read FGroup write FGroup;
property NodeLevel: Integer read FNodeLevel write FNodeLevel;
property NodeIndex: Integer read FNodeIndex write FNodeIndex;
end;
I can then derive my other classes from the Base Object, like this:
type
TObject1RootGroup = class(TBaseObject)
public
constructor Create(AName: string);
destructor Destroy; override;
procedure ToSave(const XMLDoc: IXMLDocument; var Root, Node: IXMLNode);
end;
TObject1Group = class(TBaseObject)
public
constructor Create(AName: string);
destructor Destroy; override;
procedure ToSave(const XMLDoc: IXMLDocument; var Root, Node: IXMLNode);
end;
TObject1Item = class(TBaseObject)
private
FSomeVal1: string;
FSomeVal2: string;
public
constructor Create(AName: string);
destructor Destroy; override;
procedure ToSave(const XMLDoc: IXMLDocument; var Root, Node: IXMLNode);
published
property SomeVal1: string read FSomeVal1 write FSomeVal1;
property SomeVal2: string read FSomeVal2 write FSomeVal2;
end;
The Main Object that holds all these classes looks like this:
type
TMyObject = class(TObject)
private
FName: string;
FObject1Groups: TList;
FObject1Items: TList;
FObject2Groups: TList;
FObject2Items: TList;
protected
procedure FreeObjects;
public
constructor Create(AName: string);
destructor Destroy; override;
procedure Save(FileName: string);
function Load(Filename: string): Boolean;
published
property Name: string read FName write FName;
property Object1Groups: TList read FObject1Groups;
property Object1Items: TList read FObject1Items;
property Object2Groups: TList read FObject2Groups;
property Object2Items: TList read FObject2Items;
end;
When I save the Main Object to XML I first iterate the whole TreeView and then assign to each Object the Node data such as Parent, Level, Index etc. The output XML File based on the first image would look like this:
Note: The SomeVal parts are not important as I never bothered writing anything to the Objects.
Really what I should do is Save to the XML just as the Treeview is represented. I am not too familar with XML as I am still getting to grips with it, but I think the output should look something like this: (written in Notepad)
<XML Name="test.xml">
<Counts Object1Groups="3" Object1Items="5" Object2Groups="2" Object2Items="1" />
<TObject1RootGroup Name="Object1Root" Group="" NodeLevel="0" NodeIndex="0"
<TObject1Item Name="Item1" Group="Object1Root" NodeLevel="1" NodeIndex="0" SomeVal1="" SomeVal2="" />
<TObject1Item Name="Item2" Group="Object1Root" NodeLevel="1" NodeIndex="1" SomeVal1="" SomeVal2="" />
<TObject1Group Name="Group1" Group="Object1Root" NodeLevel="1" NodeIndex="2" />
<TObject1Item Name="Item3" Group="Object1Root" NodeLevel="1" NodeIndex="3" SomeVal1="" SomeVal2="" />
<TObject1Group Name="Group2" Group="Object1Root" NodeLevel="1" NodeIndex="4" />
<TObject1Item Name="Item1" Group="Group2" NodeLevel="2" NodeIndex="0" SomeVal1="" SomeVal2="" />
<TObject1Group Name="Group1" Group="Group2" NodeLevel="2" NodeIndex="1" />
<TObject1Item Name="Item1" Group="Group1" NodeLevel="3" NodeIndex="0" SomeVal1="" SomeVal2="" />
<TObject2RootGroup Name="Object2Root" Group="" NodeLevel="0" NodeIndex="1"
<TObject2Group Name="Group1" Group="Object2Root" NodeLevel="1" NodeIndex="0" />
<TObject2Group Name="Group2" Group="Object2Root" NodeLevel="1" NodeIndex="1" />
<TObject2Item Name="Item1" Group="Group2" NodeLevel="2" NodeIndex="0" SomeVal1="" SomeVal2="" />
</XML>
Then I could load the TreeView from the XML. The problem is I only really know how to save the XML as I currently am, I know some kind of recursion etc is needed and this is where I would struggle, and particularly rebuilding the Tree from the XML File.
Attachment
It has taken me a few hours to strip down my actual project code into an example that is easier to read and understand, it is written in Lazarus and uses the OmniXML library, I have only included the source units no project file.
Download it here (the password is stackoverflow): http://www34.zippyshare.com/v/16401041/file.html
Ultimately my question is:
- How to save to XML with correct hierarchy structure.
- How to load the XML and rebuild the Treeview to exactly how it was before save.
Many thanks.
回答1:
As a raw draft for further development.
unit TreeXML;
interface
uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, xmldom, XMLIntf, msxmldom, XMLDoc, ActiveX, ComObj, ComCtrls;
Type
TTreeToXML = Class
private
FDOC: TXMLDocument;
FRootNode: IXMLNode;
FTree: TTreeView;
procedure IterateRoot;
procedure WriteNode(N: TTreeNode; ParentXN: IXMLNode);
Public
Constructor Create(Tree: TTreeView);
Procedure SaveToFile(const fn: String);
Destructor Destroy; override;
End;
TXMLToTree = Class
private
FTree: TTreeView;
procedure IterateNodes(xn: IXMLNode; ParentNode: TTreeNode);
Public
Procedure XMLToTree(Tree: TTreeView; Const FileName: String);
End;
implementation
{ TTreeToXML }
constructor TTreeToXML.Create(Tree: TTreeView);
begin
FTree := Tree;
FDOC := TXMLDocument.Create(nil);
FDOC.Options := FDOC.Options + [doNodeAutoIndent];
FDOC.Active := true;
FDOC.Encoding := 'UTF-8';
FRootNode := FDOC.CreateElement('Treeview', '');
FDOC.DocumentElement := FRootNode;
IterateRoot;
end;
Procedure TTreeToXML.WriteNode(N: TTreeNode; ParentXN: IXMLNode);
var
CurrNode: IXMLNode;
Child: TTreeNode;
begin
CurrNode := ParentXN.AddChild(N.Text);
CurrNode.Attributes['NodeLevel'] := N.Level;
CurrNode.Attributes['Index'] := N.Index;
Child := N.getFirstChild;
while Assigned(Child) do
begin
WriteNode(Child, CurrNode);
Child := Child.getNextSibling;
end;
end;
Procedure TTreeToXML.IterateRoot;
var
N: TTreeNode;
begin
N := FTree.Items[0];
while Assigned(N) do
begin
WriteNode(N, FRootNode);
N := N.getNextSibling;
end;
end;
procedure TTreeToXML.SaveToFile(const fn: String);
begin
FDOC.SaveToFile(fn);
end;
destructor TTreeToXML.Destroy;
begin
if Assigned(FDOC) then
FDOC.Free;
inherited;
end;
{ TXMLToFree }
Procedure TXMLToTree.XMLToTree(Tree: TTreeView; const FileName: String);
var
Doc: TXMLDocument;
begin
FTree := Tree;
Doc := TXMLDocument.Create(Application);
try
Doc.LoadFromFile(FileName);
Doc.Active := true;
IterateNodes(Doc.DocumentElement, NIL);
finally
Doc.Free;
end;
end;
Procedure TXMLToTree.IterateNodes(xn: IXMLNode; ParentNode: TTreeNode);
var
ChildTreeNode: TTreeNode;
i: Integer;
begin
For i := 0 to xn.ChildNodes.Count - 1 do
begin
ChildTreeNode := FTree.Items.AddChild(ParentNode,
xn.ChildNodes[i].NodeName);
IterateNodes(xn.ChildNodes[i], ChildTreeNode);
end;
end;
end.
Example call
procedure TForm1.Button1Click(Sender: TObject);
begin
With TTreeToXML.Create(TreeView1) do
try
SaveToFile('C:\temp\test.xml');
finally
Free;
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
With TXMLToTree.Create do
try
XMLToTree(TreeView2, 'C:\temp\test.xml')
finally
Free;
end;
end;
The XML used would look like:
<?xml version="1.0" encoding="UTF-8"?>
<Treeview>
<Object1Root NodeLevel="0" Index="0">
<Item1 NodeLevel="1" Index="0"/>
<Item2 NodeLevel="1" Index="1"/>
<Group1 NodeLevel="1" Index="2"/>
<Group2 NodeLevel="1" Index="3">
<Item1 NodeLevel="2" Index="0"/>
<Group1 NodeLevel="2" Index="1">
<Item1 NodeLevel="3" Index="0"/>
</Group1>
</Group2>
</Object1Root>
<Object2Root NodeLevel="0" Index="1">
<Group1 NodeLevel="1" Index="0"/>
<Group2 NodeLevel="1" Index="1">
<Item1 NodeLevel="2" Index="0"/>
</Group2>
</Object2Root>
</Treeview>
回答2:
An attempt of my own. Uses the MSXML 6.0 type library. Not too fancy, but seems to do the job.
unit ttreexml;
// treeview to XML, XML to treeview by Glenn1234,
// may be used with proper credit given
interface
uses msxml2_tlb, comctrls, dialogs, sysutils;
type
// saves TTreeView as XML file.
TTreeViewToXML = class
private
doc: IXMLDOMDocument;
FTree: TTreeView;
procedure XMLPopulate(BaseNode: TTreeNode; DataItem: IXMLDOMelement);
Public
Constructor Create(Tree: TTreeView);
procedure SaveToFile(filename: string);
end;
// loads TTreeView from XML file
TXMLToTreeView = class
private
doc: IXMLDOMDocument;
FTree: TTreeView;
procedure XMLLoad(BaseItem: TTreeNode; DataItem: IXMLDOMNode);
Public
Procedure XMLToTree(Tree: TTreeView; Const FileName: String);
end;
implementation
constructor TTreeViewToXML.Create(Tree: TTreeView);
begin
FTree := Tree;
end;
procedure TTreeViewToXML.XMLPopulate(BaseNode: TTreeNode; DataItem: IXMLDOMelement);
var
SubItem: IXMLDOMElement;
selnode: TTreeNode;
begin
SelNode := BaseNode;
while selnode <> nil do
begin
if SelNode.HasChildren then
begin
SubItem := doc.CreateElement('Group');
SubItem.setAttribute('Value', SelNode.Text);
DataItem.AppendChild(SubItem);
XMLPopulate(SelNode.GetFirstChild, SubItem);
end
else
begin
SubItem := doc.CreateElement('Item');
SubItem.setAttribute('Value', SelNode.Text);
DataItem.AppendChild(SubItem);
end;
SelNode := SelNode.GetNextChild(SelNode);
end;
end;
procedure TTreeViewToXML.SaveToFile(filename: string);
var
topnode: IXMLDOMElement;
selnode: TTreeNode;
begin
//create DOM document instance
doc := CoDOMDocument.Create;
doc.async := false;
//------------------------------------------------------------------------------
topnode := doc.createElement('TreeView');
doc.appendChild(topnode);
selnode := FTree.Items.GetFirstNode;
XMLPopulate(SelNode, topnode);
doc.save(FileName);
end;
procedure TXMLToTreeView.XMLLoad(BaseItem: TTreeNode; DataItem: IXMLDOMNode);
var
item1, item2: IXMLDOMNode;
attr: IXMLDOMNamedNodeMap;
CurrItem: TTreeNode;
begin
Item1 := DataItem;
CurrItem := nil; // compiler complains if I don't do this
while Item1 <> nil do
begin
attr := item1.attributes;
item2 := attr.nextNode;
while item2 <> nil do
begin
CurrItem := FTree.Items.AddChild(BaseItem, Item2.NodeValue);
item2 := attr.nextNode;
end;
if item1.nodename = 'Group' then
XMLLoad(CurrItem, Item1.Get_firstChild);
Item1 := Item1.Get_nextSibling;
end;
end;
Procedure TXMLToTreeView.XMLToTree(Tree: TTreeView; Const FileName: String);
var
item1: IXMLDOMNode;
begin
//create DOM document instance
doc := CoDOMDocument.Create;
doc.async := false;
FTree := Tree;
//------------------------------------------------------------------------------
if doc.load(FileName) then
begin
FTree.Items.BeginUpdate;
FTree.Items.Clear;
Item1 := doc.documentElement.Get_firstChild;
XMLLoad(nil, Item1);
FTree.Items.EndUpdate;
end
else
begin
MessageDlg(Format ('Error loading XML document.'#13 +
'Error number: %d'#13 +
'Reason: %s'#13 +
'Line: %d'#13 +
'Column: %d', [doc.parseError.errorCode,
doc.parseError.reason,
doc.parseError.line,
doc.parseError.linePos]), mtError, [mbOK], 0);
end;
end;
end.
Quick sample XML output:
- <Group Value="Delphi 3">
- <Group Value="BIN">
<Item Value="BOWF520.DLL" />
<Item Value="BOWFVC.DLL" />
<Item Value="BRC32.EXE" />
<Item Value="BRCC32.EXE" />
. . .
<Item Value="DELPHI32.EXE" />
<Item Value="DELPHIMM.DLL" />
. . .
</Group>
<Item Value="DeIsL1.isu" />
- <Group Value="Demos">
- <Group Value="ACTIVEX">
- <Group Value="DELCTRLS">
<Item Value="ABOUT1.DFM" />
来源:https://stackoverflow.com/questions/18578683/saving-and-loading-treeview-using-xml