How can I find the order of grid columns? - delphi

I have a grid on a form connected to a database table with 10 fields. The first field (hidden) is the ID. The second field is First_Name, the third Last_Name, etc. These columns are indexed 1 through 10. Now, if the user wants the Last_Name before the First_Name, he can grab that column and slide it over. First_Name now holds index 3 and Last_Name is at index 2.
I need to be able to read the order of the column indices so I can write them to an INI file. Then the next time the user opens the app, I can set the grid back to the preferred state.
I'm doing this with Lazarus 2.0.6 using a TRxDBGrid. I've tried several of its properties, but none of them show the grid column order.

I usually use Delphi rather than Lazarus and have been trying to install the RXDbGrid package into Lazarus 2.0.6 to check my suggested answer to this without any luck so far. However ...
TRxColumn descends from TColumn in the DBGrids source file.
TColumnhas a public property Index which is an integer, which is the index of the column into the GridColumns collection.
Because I can't get the RXDBGrid to install atm, the example below uses a normal TDBGrid, but should work fine with obvious detail changes.
The example has 3 fields, ID integer, Name String[20] and Value integer.
For simplicity, instead of saving and loading an IniFile, the Column order is saved to a TMemo, and to test the LoadColumnInfo you need to change the column order in the memo.
As you'll see, to reload the grid column order, it's easiest to save the column tit;es in left->right order and use a function ColumnByName to find the correct column when reloading the saved info.
uses
Classes, SysUtils, memds, db, Forms, Controls, Graphics, Dialogs, DBGrids,
StdCtrls;
type
TForm1 = class(TForm)
btnSaveColumns: TButton;
btnLoadColumns: TButton;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
MemDataset1: TMemDataset;
Memo1: TMemo;
procedure btnLoadColumnsClick(Sender: TObject);
procedure btnSaveColumnsClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
function ColumnByName(const AName: String): TColumn;
procedure LoadColumnInfo;
procedure SaveColumnInfo;
public
end;
[...]
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
var
i : integer;
begin
MemDataSet1.Open;
for i := 0 to 5 do
MemDataSet1.InsertRecord([i, 'Name' + IntToStr(i), i]);
end;
procedure TForm1.btnSaveColumnsClick(Sender: TObject);
begin
SaveColumnInfo;
end;
procedure TForm1.btnLoadColumnsClick(Sender: TObject);
begin
LoadColumnInfo;
end;
procedure TForm1.SaveColumnInfo;
var
i : Integer;
S : String;
begin
Memo1.Lines.Clear;
for i := 0 to DBGrid1.Columns.Count - 1 do begin
S := DBGrid1.Columns[i].Title.Caption;
Memo1.Lines.Add(S);
end;
end;
function TForm1.ColumnByName(const AName : String) : TColumn;
var
i : integer;
begin
for i := 0 to DBGrid1.Columns.Count - 1 do begin
Result := DBGrid1.Columns[i];
if CompareText(AName, Result.Title.Caption) = 0 then
exit;
end;
Result := Nil;
end;
procedure TForm1.LoadColumnInfo;
var
i : Integer;
Index : Integer;
Column : TColumn;
S : String;
begin
for i := 0 to Memo1.Lines.Count - 1 do begin
S := Memo1.Lines[i];
Column := ColumnByName(S);
Assert(Column <> Nil);
Column.Index := i;
end;
end;
end.

Related

Assigning a value to a Label using procedures (Delphi)

I've been trying to assign a value to a Label (inserted by a TextBox) using procedures. Here's what I have so far:
type
TfrmM8E1 = class(TForm)
Button1: TButton;
txt1: TEdit;
lbl1: TLabel;
procedure Button1Click(Sender: TObject);
procedure Labels(a1: Integer);
procedure DataInput(var a1 : Integer);
private
public
end;
var
frmM8E1: TfrmM8E1;
implementation
{$R *.dfm}
procedure TfrmM8E1.Button1Click(Sender: TObject);
var
a: Integer;
begin
// calls both procedures
DataInput(a);
Labels(a);
end;
Procedure TfrmM8E1.DataInput(var a1 : Integer);
begin
a1 := StrToInt(frmM8E1.txt1.Text);
// Receives a value from txt1(which is a textbox) and stores it in "a1".
end;
Procedure TfrmM8E1.Labels(a1 : Integer);
begin
frmM8E1.lbl1.Caption := IntToStr(a1);
// Assign the value of a1 to the label
end;
end.
Once the program runs, it doesn't show in my Label the value inserted in the TextBox.
Any idea why it is not working?
If you do know how to make the main idea work, assign a value to a Label inserted by a TextBox throughout the usage of procedures, great! Forget my code and let me take a look in yours :).
Otherwise, if you know, or at least have a hint about, what I should change in my code, even better!
Your code works for me, at least as VCL code. There as a lot of non-axiomatic stuff here, like you generally shouldn't reference the form variable from the object's methods. What if you want two forms later? Or what if that variable is not set?
The idiomatic way to do it would be more like:
procedure TForm1.Button1Click(Sender: TObject);
begin
Label1.Caption := Edit1.Text;
end;
You could put some validation in there to make sure it was a number, like
Label1.Caption := Validate(Edit1.Text);
And then Validate could be something like:
function TForm1.Validate(S: String): String;
var I: Integer;
begin
I := StrToIntDef(S, -1);
if I = -1 then Result := 'Invalid positive integer.'
else Result := S;
end;
Just for instance.
EDIT: Word correction.

How can I use multiple TcxDBTextEdit with formatted data stored in a single DB field?

I have a need to store two values in a single database field (yes, I agree that it is bad practice, but this is a legacy database that can't be altered). The data is stored as string1#4string2.
The data needs to be edited using two separate TcxDBTextEdit controls. But how can I connect them to the single database field so that I can edit string1 in one and string2 in the other?
I've tried adding two calculated (fkCalculated) fields to the TADOQuery, extracting/joining their values in OnGetText/OnSetText and reading/writing to the TStringField, but it didn't work.
So I tried creating a TdxMemData component with two fields and using them instead of the calculated fields, but it still doesn't work.
How can I achieve this (without altering the database structure)?
The sample project below does what you seem to want.
Update The code below replaces the code I originally posted and avoids the use
of a dataset type (TClientDataSet) which supports fkInternalCalc fields. It will
work with a TAdoQuery.
Although there is no difficulty in principle in parsing a string field into two subfields
and surfacing them in your gui for editing, the problem with straightforward ways of doing this with
a TAdoQuery is that it only supports fkCalculated calculated fields and db-aware gui controls
treat these as not modifiable by the user.
I'm not sure why this restriction exists, but I imagine that it is related to the fact that
Delphi's db-aware controls were originally developed for the BDE (and in aby case before
fkInternalCalc was added to support TClientDataSet). The code in DB.Pas which enforces
the restriction is in DB.Pas:
function TField.GetCanModify: Boolean;
begin
if FieldNo > 0 then
if DataSet.State <> dsSetKey then
Result := not ReadOnly and DataSet.CanModify else
Result := IsIndexField
else
Result := False;
end;
The code below works by adding an interposer class for TStringField which
removes the restriction for stringfields whose FieldKind is fkCalculated
which are not ReadOnly and belong to a dataset which is modifiable
(though this latter restriction could be removed, I think). The
interposer TStringField overrides GetCanModify like so:
function TStringField.GetCanModify: Boolean;
begin
if (FieldKind = fkCalculated) and DataSet.CanModify and not ReadOnly then
Result := True
else
if DataSet.State <> dsSetKey then
Result := not ReadOnly and DataSet.CanModify else
Result := IsIndexField
end;
The full code of an example project is below. Note that I've used regular
TDBEdits because I have a problem with my current Devex set-up but the code should
work fine with TcxDBEdit as well.
Code:
type
TStringField = class(db.TStringField)
protected
function GetCanModify : Boolean; override;
end;
type
TForm1 = class(TForm)
DBGrid1: TDBGrid;
DBNavigator1: TDBNavigator;
DataSource1: TDataSource;
DBEdit1: TDBEdit;
DBEdit2: TDBEdit;
ADOQuery1: TADOQuery;
cxDBMaskEdit1: TcxDBMaskEdit;
DBEdit3: TDBEdit;
btnDataLinks: TButton;
ADOConnection1: TADOConnection;
ADOQuery1ID: TIntegerField;
ADOQuery1Field1: TWideStringField;
ADOQuery1Field2: TWideStringField;
ADOQuery1SubField1: TStringField;
ADOQuery1SubField2: TStringField;
procedure FormCreate(Sender: TObject);
procedure ADOQuery1BeforePost(DataSet: TDataSet);
procedure ADOQuery1CalcFields(DataSet: TDataSet);
private
procedure UpdateSubFields(DataSet : TDataSet);
procedure UpdateField1(DataSet: TDataSet);
end;
[...]
const
scSeparator = '#4'; // could be a literal #4 instead
procedure TForm1.UpdateField1(DataSet : TDataSet);
var
S : String;
begin
if DataSet.FieldByName('SubField1').IsNull or DataSet.FieldByName('SubField2').IsNull then exit;
S := DataSet.FieldByName('SubField1').AsString + scSeparator +
DataSet.FieldByName('SubField2').AsString;
S := Trim(S);
if Length(S) > DataSet.FieldByName('Field1').Size then
raise exception.Create('tthe combined size of the subfields is too long');
DataSet.FieldByName('Field1').AsString := S;
end;
procedure TForm1.UpdateSubFields(DataSet : TDataSet);
var
S,
SF1,
SF2 : String;
P,
SF2Start : Integer;
begin
S := DataSet.FieldByName('Field1').AsString;
P := Pos(scSeparator, S);
SF1 := Copy(S, 1, P-1);
SF1 := Trim(SF1);
SF2Start := P + Length(scSeparator);
SF2 := Copy(S, Sf2Start, Length(S));
SF2 := Trim(SF2);
DataSet.FieldByName('SubField1').AsString := SF1;
DataSet.FieldByName('SubField2').AsString := SF2;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
AdoQuery1.Open;
end;
procedure TForm1.CDS1CalcFields(DataSet: TDataSet);
begin
UpdateSubFields(DataSet);
end;
function TStringField.GetCanModify: Boolean;
begin
if (FieldKind = fkCalculated) and DataSet.CanModify and not ReadOnly then
Result := True
else
if DataSet.State <> dsSetKey then
Result := not ReadOnly and DataSet.CanModify else
Result := IsIndexField
end;
procedure TForm1.ADOQuery1BeforePost(DataSet: TDataSet);
begin
UpdateField1(AdoQuery1);
end;
procedure TForm1.ADOQuery1CalcFields(DataSet: TDataSet);
begin
UpdateSubFields(DataSet);
end;

Delphi DBGrid conditional dbcombo

I have a table with two columns. ConfigItem and ConfigValue. Now I want to populate this in a dbgrid where ConfigValue should be a dbcombobox.
Sample ConfigItem(First Column)
Product
Product Type
Item Type
Items
ConfigValue should have a dbcombobox and the items of the combobox should be populated on the basis of the values in the first column.
Example.
If user clicks on the first row which has Product as config Item then for the same row ConfigValue column in the grid should contain combobox with list of Products.
Possibly I can use BeforeDrawCell event of grid however I am trying to find a way by which this can be handled using adoquery or dataset component.
Could someone please guide on the solution approach tho this problem.
Thanks in advance,
Divyesh
You can use the AfterScrollEvent to assign a PickList to your Column.
Picklist are here Stringlists assigned to the object of a master StringList.
Depdending on your Delphi version you could use a generic dictionary.
unit Unit2;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, DB, Grids, DBGrids, ADODB;
type
TForm2 = class(TForm)
ADODataSet1: TADODataSet;
DBGrid1: TDBGrid;
DataSource1: TDataSource;
procedure FormCreate(Sender: TObject);
procedure ADODataSet1AfterScroll(DataSet: TDataSet);
private
{ Private-Deklarationen }
FList: TStringList;
public
{ Public-Deklarationen }
end;
var
Form2: TForm2;
implementation
{$R *.dfm}
procedure TForm2.ADODataSet1AfterScroll(DataSet: TDataSet);
var
idx: Integer;
begin
if Assigned(FList) and (DBGrid1.Columns.Count > 1) then
begin
DBGrid1.Columns[1].ButtonStyle := cbsAuto;
idx := FList.IndexOf(DBGrid1.Columns[0].Field.asString);
if idx > -1 then
DBGrid1.Columns[1].PickList := TStringList(FList.Objects[idx])
else
DBGrid1.Columns[1].PickList := nil;
end;
end;
procedure TForm2.FormCreate(Sender: TObject);
var
i: Integer;
begin // some demo filling
FList := TStringList.Create;
FList.AddObject('A1A1', TStringList.Create);
for i := 0 to 10 do
TStringList(FList.Objects[FList.Count - 1]).Add(Format('group1_%d', [i]));
FList.AddObject('A1A2', TStringList.Create);
for i := 0 to 10 do
TStringList(FList.Objects[FList.Count - 1]).Add(Format('group2_%d', [i]));
end;
procedure TForm2.FormDestroy(Sender: TObject);
var
i: Integer;
begin
for I := 0 to FList.Count - 1 do Flist.Objects[i].Free;
FList.Free;
end;
end.

How to create a combobox with two columns (one hidden) in Delphi 7?

How to create a TComboBox with two columns that has one of its columns hidden so that it can keep an id value along with the actual item in it? And then how to get to that id value programmatically?
There's no need for two columns here.
You can take advantage of the fact that TComboBox.Items (like many other things in Delphi, like TStringList, TMemo.Lines, and TListBox.Items) descends from TStrings, which has both the Strings and Objects properties. Objects stores anything the size of a TObject, which is a pointer.
This means you can store your integer value by simply typecasting it to a TObject when adding it, and typecasting it back to an Integer when retrieving it.
Something like this should work:
procedure TForm1.FormCreate(Snder: TObject);
var
i: Integer;
sItem: String;
begin
for i := 0 to 9 do
begin
sItem := Format('Item %d', [i]);
ComboBox1.Items.AddObject(sItem, TObject(i));
end;
end;
To retrieve the value:
procedure TForm1.ComboBox1Click(Sender: TObject);
var
Idx: Integer;
Value: Integer;
begin
Idx := ComboBox1.ItemIndex;
if Idx <> -1 then
begin
Value := Integer(ComboBox1.Items.Objects[Idx]);
// Do something with value you retrieved
end;
end;
Note that, since the Objects property is actually meant to store objects, this gives you a lot of flexibility. Here's an example (intentionally very trivial) of storing a customer's contact information in an associated object instance and displaying it in labels when an item from a listbox is selected.
unit Unit1;
interface
uses
Windows, Messages, Variants, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TCustomer=class
private
FContact: string;
FPhone: string;
public
constructor CreateCustomer(const AContact, APhone: string);
property Contact: string read FContact write FContact;
property Phone: string read FPhone write FPhone;
end;
TForm1 = class(TForm)
ListBox1: TListBox;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
lblContact: TLabel;
lblPhone: TLabel;
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure ListBox1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
{ TCustomer }
constructor TCustomer.CreateCustomer(const AContact, APhone: string);
begin
inherited Create;
FContact := AContact;
FPhone := APhone;
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var
i: Integer;
begin
for i := 0 to ListBox1.Items.Count - 1 do
ListBox1.Items.Objects[i].Free;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
lblContact.Caption := '';
lblPhone.Caption := '';
// Create some customers. Of course in the real world you'd load this
// from some persistent source instead of hard-coding them here.
ListBox1.Items.AddObject('N Company', TCustomer.CreateCustomer('Nancy', '555-3333'));
ListBox1.Items.AddObject('B Company', TCustomer.CreateCustomer('Brad', '555-1212'));
ListBox1.Items.AddObject('A Company', TCustomer.CreateCustomer('Angie', '555-2345'));
end;
procedure TForm1.ListBox1Click(Sender: TObject);
var
Cust: TCustomer;
begin
if ListBox1.ItemIndex <> -1 then
begin
Cust := TCustomer(ListBox1.Items.Objects[ListBox1.ItemIndex]);
lblContact.Caption := Cust.Contact;
lblPhone.Caption := Cust.Phone;
end;
end;
end.
ComboBox controls do not support columns, and you do not need a hidden column anyway to accomplish what you need.
The TComboBox.Items property is a TStrings descendant. It can hold both string values and associated user-defined data values together at the same time, but the user will only see the string values in the UI. Use the Items.AddObject() method to add string+ID values to the list, and then use the Items.Objects[] property to retrieve the ID values when needed.
Alternatively, you could just store your ID values in a separate array that has the same number of elements as the ComboBox and then use the ComboBox item indexes to access the array values. This is especially important if you need to store a value of -1, because that particular value is not retrievable from the Objects[] property of a TComboBox due to the way the getter method is implemented, like Rob said.

How to create an array of controls?

I have to create an array and place all controls there in order to access them.Here's a short example:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
const Test:Array[0..2] of TButton = (Button1,Button2,Button3);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
end.
Undeclarated idenitifier 'Button1' at the line where I declarated my array.But it's declarated three lines above.
Where's the problem,how to put all controls in an array?
EDIT:
Thank you for your answers,but I've got problems:
var TestA:TObjectList<TButton>;
var index:TComponent;
begin
TestA := TObjectList<TButton>.Create(false);
for index in Form7 do
if pos(index.name, 'Button') = 1 then
TestA.add(TButton(index));
TestA[0].Caption := 'Test'; //Exception out of range.
Ben's right. You can't set up a control array in the form designer. But if you have 110 images, for this specific case you can put them into a TImageList component and treat its collection of images as an array.
If you've got a bunch of more normal controls, like buttons, you'll have to create an array and load them into it in code. There are two ways to do this. The simple way, for small arrays at least, is Ben's answer. For large control sets, or ones that change frequently, (where your design is not finished, for example,) as long as you make sure to give them all serial names (Button1, Button2, Button3...), you can try something like this:
var
index: TComponent;
list: TObjectList;
begin
list := TObjectList.Create(false); //DO NOT take ownership
for index in frmMyForm do
if pos('Button', index.name) = 1 then
list.add(index);
//do more stuff once the list is built
end;
(Use a TObjectList<TComponent>, or something even more specific, if you're using D2009.) Build the list, based on the code above, then write a sorting function callback that will sort them based on name and use it to sort the list, and you've got your "array."
You may not be able to reference public properties of your form in an array constant like that. Try doing it in your form constructor/OnCreate event instead.
procedure TForm1.FormCreate(Sender: TObject);
begin
Test[0] := Button1;
Test[1] := Button2;
Test[2] := Button3;
end;
This function will iterate over all the controls on a specified container, like a particular TPanel or even the entire form, and populate a specified TObjectList with your TImage controls.
procedure TForm1.AddImageControlsToList(AParent: TWinControl; AList: TObjectList; Recursive: boolean);
var
Index: integer;
AChild: TControl;
begin
for Index := 0 to AParent.ControlCount - 1 do
begin
AChild := AParent.Controls[Index];
if AChild is TImage then // Or whatever test you want to use
AList.Add(AChild)
else if Recursive and (AChild is TWinControl) then
AddImageControlsToList(TWinControl(AChild), AList, True);
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
// Call like this or similar to get your list of images
// (assumes MyImageList is declared in Form)
MyImageList := TObjectList.Create(False);
AddImageControlsToList(Self, MyImageList, True);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
// Destroy the list
FreeAndNil(MyImageList);
end;
How about this?
procedure TForm1.FormCreate(Sender: TObject);
begin
for b := 1 to 110 do
Test[b] := FindComponent('Button' + IntToStr(b)) as TButton;
end;
You'll have to declare the array as a variable rather than a constant and it will have to go from 1 to 110 rather than 0 to 109 but that's no problem.
I use this all the time - it is simple and fast (despite Mr Wheeler's comment)- declare the maxbuttons as a constant
var
Form1: TForm1;
pbutton:array[1..maxbuttons] of ^tbutton;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
(* Exit *)
var k:integer;
begin
for k:=1 to maxbuttons do dispose(pbutton[k]);
close;
end;
procedure TForm1.FormActivate(Sender: TObject);
var k:integer;
begin
(*note the buttons must be Button1, Button2 etc in sequence or you need to
allocate them manually eg pbutton[1]^:=exitbtn etc *)
for k:=1 to maxbuttons do
begin
new(pbutton[k]);
pbutton[k]^:= tbutton(FindComponent('Button'+IntToStr(k)));
end;
end;
procedure TForm1.ButtonMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var k:integer;
b:boolean;
begin
b:=false;
k:=1;
while (k<= maxbuttons) and (not b) do
begin
if pbutton[k]^ = sender then (Note sender indicates which button has been clicked)
begin
{ found it so do something}
b:=true;
end;
k:=k+1;
end;
end;
Try this
var
TestA:TObjectList;
index:TComponent;
begin
TestA := TObjectList<TButton>.Create(false);
try
for index in Form7 do
if (pos is TButton) OR {or/and} (pos.tag and 8=8) then
TestA.add(TButton(index));
if TestA.Count>0 then //Fix:Exception out of range.
TestA[0].Caption := 'Test';
finally
TestA.Free;
end;
end;

Resources