I feel like an idiot because my question seams so simple but I don't get it done :D
My Settings is that:
One Dataset (Memtable), One Stringgrid. The Grid is bind via live Bindungs.
I would like to sort my Columns by clicking on the GridHeader. In the OnHeaderClick Event I get an tColumn Object. I only can read the Column.Header String, but I changed the Text from the Header to a more speakable Text. When I put Column.header into Memtable.Indexfieldsname Memtable says that field does not exist, what is right, but I don't know how to get the right Fieldname from the column.
What you want is quite straightforward to do. In the example below, which uses the demo data from
the Biolife demo, I've linked the StringgRid to the FDMemTable entirely by binding objects
created in code so that there is no doubt about any of the binding steps or binding properties,
nor the method used to establish the bindings.
procedure TForm2.FormCreate(Sender: TObject);
var
BindSourceDB1 : TBindSourceDB;
LinkGridToDataSourceBindSourceDB1 : TLinkGridToDataSource;
begin
// Note : You need to load FDMemTable1 at design time from the sample Biolife.Fds datafile
// The following code creates a TBindSourceDB which Live-Binds FDMemTable1
// to StringGrid1
//
// As a result, the column header texts will be the fieldnames of FDMemTable1's fields
// However, the code that determines the column on which to sort the StringGrid does not depend
// on this
BindSourceDB1 := TBindSourceDB.Create(Self);
BindSourceDB1.DataSet := FDMemTable1;
LinkGridToDataSourceBindSourceDB1 := TLinkGridToDataSource.Create(Self);
LinkGridToDataSourceBindSourceDB1.DataSource := BindSourceDB1;
LinkGridToDataSourceBindSourceDB1.GridControl := StringGrid1;
end;
procedure TForm2.StringGrid1HeaderClick(Column: TColumn);
// Sorts the STringGrid on the column whose header has been clicked
var
ColIndex,
FieldIndex : Integer;
AFieldName : String;
begin
ColIndex := Column.Index;
FieldIndex := ColIndex;
AFieldName := FDMemTable1.Fields[FieldIndex].FieldName;
Caption := AFieldName;
// Should check here that the the field is a sortable one and not a blob like a graphic field
FDMemTable1.IndexFieldNames := AFieldName;
end;
Note that this answer assumes that there is a one-for-one correspondence between grid columns and fields of the bound dataset, which will usually be the case for bindings created using the default methods in the IDE. However, Live Binding is sophisticated enough to support situations where this correspondence does not exist, and in those circumstances it should not be assumed that the method in this answer will work.
Another question pertaining the devexpress treelists. I have managed to get a cxgrid to colour rows based on a value stored in the grid. My attempts to transfer this knowledge into the treelist have not gone well. I currently have the entire grid being coloured and the children nodes are left as default(black text on a white background).
I think the issue is with regards to when and how children nodes are populated. Currently they are dynamically populated, and deleted, when the selection changes.
var
LNode : TcxTreeListNode;
LListData : TOfferList;
LSiteData : TSiteList;
LContract : TItem;
begin
LNode := TreeTypes.FocusedNode;
if Assigned(LNode) then
begin
if LNode.Level = 0 then
begin
LNode.DeleteChildren; //clear children so that list doesnt get duplicate entries / children
LSiteData := TSiteList(LNode.Data); //root node info
PopulateOfferList; //populate children list with data from DB
InitialiseOffers; // populate treelist with children gathered from the list above
LNode.Expand(True);
end
else
begin
LListData := TOfferList(LNode.Data); //do stuff with selected child node
LContract := TItem.Create(LListData.UnitID);//do stuff with selected child node
end;
end;
end;
This as it stands works fine. I can do what I need to do with all the information. However the entire list is black and white. To help identify any differences in child nodes I'd like them to be coloured based on information gathered during the population process(DB objects).
The current custom draw:
procedure TFrmTestView1.TreeTypesCustomDrawDataCell(Sender: TcxCustomTreeList;
ACanvas: TcxCanvas; AViewInfo: TcxTreeListEditCellViewInfo;
var ADone: Boolean);
var
LState : TStates;
LOffer : TOfferList;
begin
if (Assigned(AViewInfo)) and (Assigned(AViewInfo.Node)) then
begin
if AViewInfo.Node.Level > 0 then
begin
LOffer := AViewInfo.Node.Data;
LState := FGlobals.GetStateFromID(LOffer.StateID);
end;
end;
if AViewInfo.Node.Level > 0 then
begin
if Assigned(LState) then
begin
ACanvas.Brush.Color := LState.Background;
ACanvas.Font.Color := LState.Foreground;
end;
end;
This is causing access violations when changing Parent(root) nodes and is not colouring any of the children nodes. I tried adding a Tree.FullRefresh on nodeselectionchange event and that did not seem to have any effect.
I just want to be able to change the background and text colours depending on data stored in the node. With the parent nodes appearing in black and white and children nodes a whole array of colours.
Thank you,
Any questions or prompt for stuff that I have missed I would be more than happy to add. Note: I add children nodes using the Node.AddChild procedure.
Edit: Think I have added the method signature, never heard it be called that so that's one terminology I can tick off my list.
Devexpress account finally got set up!
The answer is from them but I thought I'd post it here should anyone look for it. Turns out I am an idiot
var
LState : TStates;
LOffer : TOfferList;
begin
if AViewInfo.Node.Level > 0 then
begin
LOffer := TOfferList(AViewInfo.Node.Data);
LState := FGlobals.GetStateFromTag(LOffer.StateTag);
ACanvas.Brush.Color := LState.Background; //integer stored in db
ACanvas.Font.Color := LState.Foreground; //integer stored in db
end;
end;
Turns out I was trying to overcomplecate it by splitting up the procedure into 'gathering data' then apply canvas. By keeping it together it works. For note to future me KISS(Keep it simple stupid)!.
Sorry for wasting your time guys. Feel like I tried to make the problem harder than it should have ever been
I have as simple query, which returns following rows:
Name Value
Peter 1
Peter 2
Peter 3
John 1
John 2
Applying filter:
ADO.Filter := 'Name="John"';
ADO.Filtered := True; // Now, its only 2 rows in dataset
ADO.Locate('Value', 2);
Cursor should point to "John 2", but it points to "Peter 2" (which being filtered out by filter). And locate returns True.
Tried on Delphi 7, Rad studio XE 6. It seems that this error is living there for the last 15 years
Any solution ?
The problem with TCustomADODataSet.Locate is that it's internally using Recordset.Clone and trying to locate a record in the cloned recordset, without setting the filter to the cloned recordset (see ADODB TCustomADODataSet.LocateRecord).
From the Remarks in the docs:
The Filter property of the original Recordset, if any, will not be
applied to the clone. Set the Filter property of the new Recordset
to filter the results. The simplest way to copy any existing Filter
value is to assign it directly, as follows. rsNew.Filter =
rsOriginal.Filter The current record of a newly created clone is set
to the first record.
I have been using my own simple Locate function for filtered ADO DataSets: Basically, storing the current bookmark, moving to the first record and iterating the DataSet until it found a match. If no match found restore the previous bookmark.
Bellow is a really limited implementation that worked for me (a lot of todo tho for a perfect solution):
class function TData.Locate(DataSet: TDataSet; const KeyFields: string;
const KeyValues: Variant; Options: TLocateOptions): Boolean;
{ a very simple Locate function - todo: TLocateOptions & multiple KeyFields/KeyValues }
var
BM: TBookmarkStr;
begin
Result := False;
if DataSet.IsEmpty then Exit;
BM := DataSet.Bookmark;
DataSet.DisableControls;
try
DataSet.First;
while not DataSet.Eof do
begin
if DataSet.FieldByName(KeyFields).Value = KeyValues then
begin
Result := True;
Break;
end;
DataSet.Next;
end;
if not Result then DataSet.Bookmark := BM;
finally
DataSet.EnableControls;
end;
end;
Another option is to patch ADODB.pas TCustomADODataSet.LocateRecord and set the FLookupCursor.Filter to match the current dataset filter. This option is acceptable as long as you patch ADODB.pas as a new copy placed in your project folder.
Yet another option is to use TCustomADODataSet.Recordset.Find method (See also: How to use a RecordSet.Find with TADOQuery?).
Try this:
ADO.Filter := 'Name=' + QuotedStr('John');
ADO.Filtered := True; // Now, its only 2 rows in dataset
ADO.Locate('Value',2,[]);
The third parameter of the Locate function is for locate options, which I've left empty in the example above. You can read up on Locate here
http://docwiki.embarcadero.com/RADStudio/XE7/en/Using_Locate and here http://docwiki.embarcadero.com/Libraries/XE7/en/Data.DB.TDataSet.Locate
Trying again. On advice, adding the piece of code that I do understand. I am fine with the fact that I have to save 4 bits of information in two lines like so:
IniFile.WriteString('TestSection','Name','Country');
IniFile.WriteString('TestSection','City','Street');
My question is more about loading this information back into the form. If in my IniFile I have saved for example the following code
[TestSection]
John=Uk
London=barlystreet
Mike=Spain
Madrid=eduardostrata
Emma=USA
New York=1st Avenue
Made up information in the IniFile. Added through the code above.
Now my question is: How could I load for example, when I type in an edit box Mike, the rest of the belonging information.(Spain, Madrid,eduardostrata).
That's not how an INI file works. You save name=value pairs, and have to have a way to associate them.
Maybe this can help you get started:
Ini := TIniFile.Create(YourIniFileName);
try
Ini.WriteString('Mike', 'Country', 'Spain');
Ini.WriteString('Mike', 'City', 'Madrid');
Ini.WriteString('Mike', 'Street', 'EduardoStrata');
finally
Ini.Free;
end;
Results in your INI file containing:
[Mike]
Country=Spain
City=Madrid
Street=EduardoStrata
To load back:
var
Country, City, Street: string;
Ini: TIniFile;
begin
Ini := TIniFile.Create(YourIniFilename);
try
Country := Ini.ReadString('Mike', 'Country', '<None>');
City := Ini.ReadString('Mike', 'City', '<None>');
Street := Ini.ReadString('Mike', 'Street', '<None>');
finally
Ini.Free;
end;
// Country, City, and Street now equal the values for 'Mike',
// or they contain '<None>' if the section 'Mike' doesn't
// exist or has no values for the variable.
end;
So you can probably figure out how this works. The section (the part in []) is the person's name, and the name/value pairs are the location and it's corresponding value (for instance, 'Country=Spain').
You have clearly not understood how INI files work. What if both John and Dave live in New York? You cannot have two keys with the same name in an INI file. (In addition, you shouldn't rely on the ordering of the lines within each section.)
You thus need to rethink how you save your data. A very simple solution is to use a plain text file in which each line is an item in your database, and the fields are seperated by, for instance, a vertical line (|):
John|Uk|London|barlystreet
Mike|Spain|Madrid|eduardostrata
Emma|USA|New York|1st Avenue.
How to read this file? Well, that is trivial, and you should know how to do that. If you have a very specific question, then feel free to ask.
What INI files are for
But what are INI files for, then? Well, a typical application of a INI file is to save program settings. For instance, when you quit a text editor, it might save the settings to settings.ini:
[Font]
Name=Consolas
Size=10
[Behaviour]
AutoIndent=1
AutoReplace=1
AutoBrackets=1
BracketHighlight=1
SyntaxHighlight=1
[Window]
Width=800
Height=600
Maximized=0
etc. This is done by
WriteString('Font', 'Name', Editor.Font.Name);
WriteInteger('Font', 'Size', Editor.Font.Size);
etc. And when you start the application the next time, it will read the file to restore the settings:
Editor.Font.Name := ReadString('Font', 'Name', 'Consolas');
Editor.Font.Size := ReadInteger('Font', 'Size', 10);
etc., where the last parameters are the default values (in case the field is missing in the INI file). Notice that in each section, the keys are unique (and need to be), and that we don't care about the relative order of the keys inside each section.
The easiest way to create the relationship you want is to have all of a user's details on 1 line of text. This is not a job for INI files. Your issue is how to parse strings.
First of all, why do you need to save the "repeat password"? that doesn't make sense to me. Usually a UI will ask the user to repeat the password as a form of validation, but that's all it's good for. There's no benefit in storing it for later retrieval is there.
I think you need to save the user's first_name, last_name, and password (3 strings). Have a look at the following piece of code.
procedure SaveUserDetails(sFileName: string);
var
sFirstName, sLastName, sPassword: string;
slUsers: TStringList;
begin
sFirstName := txtFirstName.Text; // these could be from TEdit controls for example
sLastName := txtLastName.Text;
sPassword := txtPassword.Text;
slUsers := TStringList.Create;
slUsers.Add(sFirstName + ',' + sLastName + ',' + sPassword);
slUsers.SaveToFile(sFileName); // that has saved your stringlist to a file
slUsers.Free;
end;
The file will look this
Shane,Warne,cricket
Now, how to load it...
procedure LoadUserDetails(sFileName: string);
var
sFirstName, sLastName, sPassword: string;
sTemp: string;
slUsers: TStringList;
iPos: integer; // string position marker we'll use to split the string in 3
begin
slUsers := TStringList.Create;
slUsers.LoadFromFile(sFileName); // this loads the file's contents into stringlist
sTemp := slUsers[0];
if (Length(sTemp) > 0) then // just to check that there is data there
begin
iPos := pos(',', sTemp); // get position of first comma (our "delimiter")
sFirstName := Copy(sTemp, 0, iPos-1); // firstName everything upto 1st comma
sTemp := Copy(sTemp, iPos + 1, Length(sTemp)); // chop off bit we just read
iPos := pos(',', sTemp); // get position of second comma
sLastName := Copy(sTemp, 0, iPos-1); // LastName everything upto 2nd comma
sTemp := Copy(sTemp, iPos + 1, Length(sTemp)); // chop off bit we just read
sPassword := sTemp; // that's it
end;
slUsers.Free;
end;
Now... this far from "good code" but now you know at least 1 way to do your thing. Hope that helps.
I'm trying to work with the class from JosephStyons but I do get an "Invalid Index" Error on the line where the "User ID" should get set.
FRpt.Database.Tables[i].ConnectionProperties.Item['User ID'] := edUserName.Text;
Here's my environment:
WinXP Sp3, Crystal Reports Developer XI Rel.2 SP4, Delphi 5 Update Pack 1
Any help or ideas greatly appreciated!
Thx,
Reinhard
Your value for [i] could be the culprit...I can't remember for sure but I believe the first table will be Table[1] instead of Table[0] as one would expect.
I altered my loop to use:
CrTables := CrDatabase.Tables;
for crTableObj in crTables do
You might try stepping through the table using a for loop as shown above or by starting with 1 instead of 0.
I hope this helps.
Put a break point on that line and use Evaluate/Modify.
It will return an error if you try something invalid.
Examine FRpt.Database.Tables[i] and see if it's valid for what you think are the min and max values for i.
If Tables is an array, one way to avoid that is to use ...Low(Tables) to High(Tables)
If you get your Table Ok, examine FRpt.Database.Tables[i].ConnectionProperties.Item['User ID'] and see if it's valid.
It might be that the Item getter does not like the space embedded in "User ID". Some products need either to surround by special characters like "[User ID]", other to replace by an underscore like "User_ID"
Are you also setting the password, server name and database name?
procedure TReports.LogonToDBTables(cReport:
CrystalDecisions.CrystalReports.Engine.ReportDocument;
ConnInfo: ConnectionInfo);
var
CrDataBase: Database;
CrTables: Tables;
CrTableObj: TObject;
CrTable: Table;
CrTableLogonInfo: TableLogonInfo;
iSubReportIndex: smallint;
begin
CrDataBase := CReport.Database;
CrTables := CrDatabase.Tables;
cReport.DataSourceConnections[0].IntegratedSecurity := False;
for crTableObj in crTables do
begin
crTable := CrystalDecisions.CrystalReports.Engine.Table(crTableObj);
crTableLogonInfo := crTable.LogOnInfo;
crTableLogonInfo.ConnectionInfo := ConnInfo;
crTable.ApplyLogOnInfo(crTableLogonInfo);
end;
end;
function TReports.GetConnectionInfo(): ConnectionInfo;
var
cTemp: ConnectionInfo;
begin
cTemp := ConnectionInfo.Create();
cTemp.AllowCustomConnection := True;
cTemp.ServerName := GetServerName();
cTemp.DatabaseName := GetDBName();
cTemp.UserID := GetDBUserID();
cTemp.Password := GetDBPassword();
Result := cTemp;
end;