I have two StringList that are loaded (from a file) with users and users + password respectivally. I'm comparing these lists to determine what user (of first list) already have a password (in second list) and then insert on ListView who have and also who still not have.
But exists a problem here that from second ListItem.Caption (user) is repeting two times.
How i can solve this?
My files that are loaded on lists are:
users.dat
User01
User02
User03
logins.dat
User01|test01
User01|test01
And this was my last attempt of code:
type
TForm1 = class(TForm)
Button1: TButton;
ListView1: TListView;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var
L1, L2, LSplit: TStringList;
L: TListItem;
I, J: Integer;
begin
L1 := TStringList.Create;
L2 := TStringList.Create;
LSplit := TStringList.Create;
L1.LoadFromFile('users.dat');
L2.LoadFromFile('logins.dat');
for I := 0 to L1.Count - 1 do
begin
for J := 0 to L2.Count - 1 do
begin
LSplit.Clear;
ExtractStrings(['|'], [], PChar(L2[J]), LSplit);
if L1[I] = LSplit[0] then
begin
L := ListView1.Items.Add;
L.Caption := LSplit[0];
L.SubItems.Add(LSplit[1]);
Break;
end;
L := ListView1.Items.Add;
L.Caption := L1[I];
end;
end;
L1.Free;
L2.Free;
LSplit.Free;
end;
end.
Your inner loop is broken. It is adding items to the ListView even when the 2 StringList items being compared don't match each other. For each user in the first list, you are adding it to the ListView as many times as there are entries in the second list.
Your code should look more like this instead:
procedure TForm1.Button1Click(Sender: TObject);
var
L1, L2, LSplit: TStringList;
L: TListItem;
I, J: Integer;
begin
L1 := TStringList.Create;
try
L2 := TStringList.Create;
try
LSplit := TStringList.Create;
try
L1.LoadFromFile('users.dat');
L2.LoadFromFile('logins.dat');
for I := 0 to L1.Count - 1 do
begin
L := ListView1.Items.Add;
L.Caption := L1[I];
for J := 0 to L2.Count - 1 do
begin
LSplit.Clear;
ExtractStrings(['|'], [], PChar(L2[J]), LSplit);
if L1[I] = LSplit[0] then
begin
L.SubItems.Add(LSplit[1]);
Break;
end;
end;
end;
finally
LSplit.Free;
end;
finally
L2.Free;
end;
finally
L1.Free;
end;
end;
That being said, you don't need 3 TStringList objects and 2 loops. 2 TStringList objects and 1 loop will suffice:
procedure TForm1.Button1Click(Sender: TObject);
var
L1, L2: TStringList;
L: TListItem;
I: Integer;
begin
L1 := TStringList.Create;
try
L2 := TStringList.Create;
try
L1.LoadFromFile('users.dat');
L2.LoadFromFile('logins.dat');
L2.NameValueSeparator := '|';
for I := 0 to L1.Count - 1 do
begin
L := ListView1.Items.Add;
L.Caption := L1[I];
L.SubItems.Add(L2.Values[L1[I]]);
end;
finally
L2.Free;
end;
finally
L1.Free;
end;
end;
for I := 0 to L1.Count - 1 do
begin
found := false;
L := ListView1.Items.Add;
for J := 0 to L2.Count - 1 do
begin
LSplit.Clear;
ExtractStrings(['|'], [], PChar(L2[J]), LSplit);
if L1[I] = LSplit[0] then
begin
L.Caption := LSplit[0];
L.SubItems.Add(LSplit[1]);
found := true;
Break;
end;
end;
if not found then
L.Caption := L1[I];
end;
Also note that dictionary approach is much faster for large lists
The problem with your code is that you are always adding the username from your first list to the result regardles of whether you aready added it from the second list with included password or not.
To avoid this you need to write your code in a way that adding username from first list happen only if it hasn't been added from the second one.
//If password exist in second list add username and password from second list
if L1[I] = LSplit[0] then
begin
L := ListView1.Items.Add;
L.Caption := LSplit[0];
L.SubItems.Add(LSplit[1]);
Break;
end
//Else add username from first list
else
begin
L := ListView1.Items.Add;
L.Caption := L1[I];
end;
Also note that your approqach Will fail in case if your second list contains username that is not present in the first list.
For instance let us check next scenario:
users.dat
User01
User02
User03
logins.dat
User01|test01
User02|test01
User04|test04
In the above scenario your final result won't include any data from User04 becouse it doesn't exist in your first list.
So I would recomend you to use different approach where you iterate the second list and for each entry search the first list to see if the username exists in it.
If it does you edit that entry on the first list to add the pasword infromation from the second list. And in case if you can't find username on the first list then you add both username and password to it.
This will guarantee you that final result will unclude all usernames from both lists no matter on which they were found.
Related
I have a problem with Text inside of a found TEdit.
This is my code:
function TfrmGenerateExam.zlicz_liczby(Component: TControl): integer;
var
i, j: integer;
begin
Result := 0;
for i := 0 to Component.ComponentCount - 1 do
begin
for j := 0 to Panel.ComponentCount - 1 do
begin
if Components[j] is TEdit then
begin
Result := Result + ???;
end;
end;
end;
end;
In a nutshell:
I create dynamic panels with ComboBoxes, Edits, Buttons etc.
When I have some panels, I want to count the edits which are in panels, which are in ScrollBox:
What do I need to put here?
if Components[j] is TEdit then
begin
Result := Result + ???;
end;
The code provided does not match the screenshot shown. What is being passed as the Component parameter to zlicz_liczby()? Is it the Form itself? The ScrollBox? A specified Panel?
Let's just iterate the Panels in the ScrollBox directly. Try something more like this:
function TfrmGenerateExam.zlicz_liczby: Integer;
var
i, j: integer;
Panel: TPanel;
begin
Result := 0;
for i := 0 to ScrollBox1.ControlCount - 1 do
begin
Panel := ScrollBox1.ControlCount[i] as TPanel;
for j := 0 to Panel.ControlCount - 1 do
begin
if Panel.Controls[j] is TEdit then
Result := Result + StrToIntDef(TEdit(Panel.Controls[j]).Text, 0);
end;
end;
end;
That being said, as #AndreasRejbrand stated in comments, you should use an array instead. When you create a new TPanel with a TEdit on it, put its TEdit into a TList<TEdit>, for instance. If you destroy the TPanel, remove its TEdit from the list. And then you can simply loop through that list whenever needed, without having to hunt for the TEdit controls at all. For example:
private
Edits: TList<TEdit>;
procedure TfrmGenerateExam.FormCreate(Sender: TObject);
begin
Edits := TList<TEdit>.Create;
end;
procedure TfrmGenerateExam.FormDestroy(Sender: TObject);
begin
Edits.Free;
end;
function TfrmGenerateExam.FillScrollBox;
var
Panel: TPanel;
Edit: TEdit;
begin
...
Panel := TPanel.Create(Self);
Panel.Parent := ScrollBox1;
...
Edit := TEdit.Create(Panel);
Edit.Parent := Panel;
...
Edits.Add(Edit);
...
end;
function TfrmGenerateExam.zlicz_liczby: Integer;
var
i: integer;
begin
Result := 0;
for i := 0 to Edits.Count - 1 do
Result := Result + StrToInt(Edits[i].Text);
end;
I have an ini file which contains the following:
[Colours]
1 = Red
2 = Blue
3 = Green
4 = Yellow
In my app I have a TComboBox which I would like to populate with the colours in the ini file.
Does anyone know how I'd go about this?
Thanks,
You can get a list of names in a section by using TIniFile.ReadSection() and then iterate to get the values:
procedure TForm1.LoadFile(const AFilename: String);
var
I: TIniFile;
L: TStringList;
X: Integer;
N: String;
V: String;
begin
I:= TIniFile.Create(AFilename);
try
L:= TStringList.Create;
try
ComboBox1.Items.Clear;
I.ReadSection('Colours', L);
for X := 0 to L.Count-1 do begin
N:= L[X]; //The Name
V:= I.ReadString('Colours', N, ''); //The Value
ComboBox1.Items.Add(V);
end;
finally
L.Free;
end;
finally
I.Free;
end;
end;
As an alternative, you could also dump the name/value pairs within the section into a single TStringList and read each value using the string list's built-in capabilities...
procedure TForm1.LoadFile(const AFilename: String);
var
I: TIniFile;
L: TStringList;
X: Integer;
N: String;
V: String;
begin
I:= TIniFile.Create(AFilename);
try
L:= TStringList.Create;
try
ComboBox1.Items.Clear;
I.ReadSectionValues('Colours', L);
for X := 0 to L.Count-1 do begin
N:= L.Names[X]; //The Name
V:= L.Values[N]; //The Value
ComboBox1.Items.Add(V);
end;
finally
L.Free;
end;
finally
I.Free;
end;
end;
On a side-note, Ini files do not have spaces on either side of the = sign, unless of course you want that space as part of the actual name or value.
try this, without reading the file twice:
uses IniFiles;
procedure TForm1.Button1Click(Sender: TObject);
var
lIni : TIniFile;
i: Integer;
begin
lIni := TIniFile.Create('c:\MyFile.ini');
try
lIni.ReadSectionValues('Colours', ComboBox1.Items);
for i := 0 to ComboBox1.Items.Count - 1 do
ComboBox1.Items[i] := ComboBox1.Items.ValueFromIndex[i];
finally
FreeAndNil(lIni);
end;
end;
I have stringlist with comments (like Ini file section content):
;comment c
c=str1
;comment b
b=str2
;comment a
a=str3
Any ideas how to sort this list by names to:
;comment a
a=str3
;comment b
b=str2
;comment c
c=str1
Comment for pair should be linked with pair during sorting
One option would be to parse the TStringList content into a second list that separates and groups the name, value, and comment strings together, then sort that list on the names as needed, then repopulate the TStringList with the sorted groups. For example:
uses
...
System.Classes,
System.SysUtils,
System.Generics.Defaults,
System.Generics.Collections,
System.StrUtils,
System.Types;
type
ItemInfo = record
LeadingText,
Name,
Value: string;
end;
ItemInfoComparer = class(TComparer<ItemInfo>)
public
function Compare(const Left, Right: ItemInfo): Integer; override;
end;
function ItemInfoComparer.Compare(const Left, Right: ItemInfo): Integer;
begin
if (Left.Name <> '') and (Right.Name <> '') then
Result := AnsiCompareStr(Left.Name, Right.Name)
else if (Left.Name <> '') then
Result := -1
else
Result := 1;
end;
procedure SortMyList(List: TStringList);
var
Compare: IComparer<ItemInfo>;
Items: TList<ItemInfo>;
Info: ItemInfo;
I: Integer;
InText: Boolean;
S: String;
begin
Compare := ItemInfoComparer.Create;
Items := TList<ItemInfo>.Create(Compare);
try
Items.Capacity := List.Count;
InText := False;
for I := 0 to List.Count-1 do
begin
S := Trim(List[i]);
if (S = '') or (S[1] = ';') then
begin
if InText then
Info.LeadingText := Info.LeadingText + #13 + List[i]
else
begin
Info.LeadingText := List[i];
InText := True;
end;
end else
begin
Info.Name := List.Names[I];
Info.Value := List.ValueFromIndex[I];
Items.Add(Info);
Info := Default(ItemInfo);
InText := False;
end;
end;
if InText then
Items.Add(Info);
Items.Sort;
List.Clear;
for I := 0 to Items.Count-1 do
begin
Info := Items[I];
if Info.LeadingText <> '' then
begin
for S in SplitString(Info.LeadingText, #13) do
List.Add(S);
end;
if Info.Name <> '' then
List.Add(Info.Name + '=' + Info.Value);
end;
finally
Items.Free;
end;
end;
Here is a simple procedure that will sort and also deal with spaces as cargo. I also added code to handle comments at the end of the file.
This will work with older versions of Delphi that do not have generics or advanced types as in Remy's answer (provided as convenience for those using older versions)
function SortKeys(List: TStringList; Index1, Index2: Integer): Integer;
begin
result := CompareText(List.Names[Index1], List.Names[Index2]);
end;
Procedure SortStringListWithComments(AStrings: TStrings);
var
LCargoText: TStringList;
LSortedText : TStringList;
s: string;
i : integer;
begin
LCargoText := nil;
LSortedText := TStringList.Create;
try
for i := 0 to AStrings.count-1 do
begin
s := Trim(AStrings[i]);
if (s='') or (s[1] = ';') then //LCargoText and blank lines attached to sorted strings (Boolean short circuit assumed here)
begin
if LCargoText = nil then
LCargoText := TStringList.Create;
LCargoText.Add(AStrings[i]);
end
else
begin
LSortedText.AddObject(AStrings[i], LCargoText);
LCargoText := nil; //set nil to deal with cases where we have no comments for a following key value pair
end;
end;
LSortedText.CustomSort(SortKeys);
// LSortedText.sort - will cause a1=x to be sorted before a=x
AStrings.clear;
for i := 0 to LSortedText.count-1 do
begin
if LSortedText.objects[i] <> nil then
begin
AStrings.AddStrings(TStringList(LSortedText.Objects[i]));
LSortedText.Objects[i].Free;
end;
AStrings.Add(LSortedText[i]);
end;
if LCargoText <> nil then
begin
AStrings.AddStrings(LCargoText) ; //comments orphaned at the end of the file
LCargoText.Free;
end;
finally
LSortedText.Free;
end;
end;
I've found a good component to implement a Caption<->Value List for ComboBox:
Is there a ComboBox that has Items like a TcxRadioGroup?
The only problem is: It has a Sorted property, but that doesn't work.
So, how do I sort the Items of a TcxImageComboBox?
Quick and dirty method, should work fine for most cases:
function CompareItems(AFirst: TcxImageComboBoxItem; ASecond: TcxImageComboBoxItem): Integer;
begin
Result := AnsiCompareText(AFirst.Description, ASecond.Description);
end;
procedure SortCxComboBoxItems(AItems: TcxImageComboBoxItems);
var
I : Integer;
J : Integer;
PMin : Integer;
begin
AItems.BeginUpdate;
try
// Selection Sort (http://en.wikipedia.org/wiki/Selection_sort)
for I := 0 to AItems.Count - 1 do
begin
PMin := I;
for J := I + 1 to AItems.Count - 1 do
begin
if CompareItems(AItems[J], AItems[PMin]) < 0 then begin
PMin := J;
end;
end;
if PMin <> I then
begin
AItems[PMin].Index := I;
end;
end;
finally
AItems.EndUpdate;
end;
end;
We have a combo box with more than 100 items.
We want to filter out the items as we enter characters in combo box. For example if we entered 'ac' and click on the drop down option then we want it to display items starting with 'ac' only.
How can I do this?
Maybe you'd be happier using the autocompletion features built in to the OS. I gave an outline of how to do that here previously. Create an IAutoComplete object, hook it up to your combo box's list and edit control, and the OS will display a drop-down list of potential matches automatically as the user types. You won't need to adjust the combo box's list yourself.
To expand on Rob's answer about using the OnChange event, here is an example of how to do what he suggests.
procedure TForm1.FormCreate(Sender: TObject);
begin
FComboStrings := TStringList.Create;
FComboStrings.Add('Altair');
FComboStrings.Add('Alhambra');
FComboStrings.Add('Sinclair');
FComboStrings.Add('Sirius');
FComboStrings.Add('Bernard');
FComboStrings.Sorted := True;
ComboBox1.AutoComplete := False;
ComboBox1.Items.Text := FComboStrings.Text;
ComboBox1.Sorted := True;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FreeAndNil(FComboStrings);
end;
procedure TForm1.ComboBox1Change(Sender: TObject);
var
Filter: string;
i: Integer;
idx: Integer;
begin
// Dropping down the list puts the text of the first item in the edit, this restores it
Filter := ComboBox1.Text;
ComboBox1.DroppedDown := True;
ComboBox1.Text := Filter;
ComboBox1.SelStart := Length(Filter);
for i := 0 to FComboStrings.Count - 1 do
if SameText(LeftStr(FComboStrings[i], Length(ComboBox1.Text)), ComboBox1.Text) then
begin
if ComboBox1.Items.IndexOf(FComboStrings[i]) < 0 then
ComboBox1.Items.Add(FComboStrings[i]);
end
else
begin
idx := ComboBox1.Items.IndexOf(FComboStrings[i]);
if idx >= 0 then
ComboBox1.Items.Delete(idx);
end;
end;
My brief contribution working with objects in the combobox:
procedure FilterComboBox(Combo: TComboBox; DefaultItems: TStrings);
function Origin: TStrings;
begin
if Combo.Tag = 0 then
begin
Combo.Sorted := True;
Result := TStrings.Create;
Result := Combo.Items;
Combo.Tag := Integer(Result);
end
else
Result := TStrings(Combo.Tag);
end;
var
Filter: TStrings;
I: Integer;
iSelIni: Integer;
begin
if(Combo.Text <> EmptyStr) then
begin
iSelIni:= Length(Combo.Text);
Filter := TStringList.Create;
try
for I := 0 to Origin.Count - 1 do
if AnsiContainsText(Origin[I], Combo.Text) then
Filter.AddObject(Origin[I], TObject(Origin.Objects[I]));
Combo.Items.Assign(Filter);
Combo.DroppedDown:= True;
Combo.SelStart := iSelIni;
Combo.SelLength := Length(Combo.Text);
finally
Filter.Free;
end;
end
else
Combo.Items.Assign(DefaultItems);
end;
You can handle the combo box's OnChange event. Keep a master list of all items separate from the UI control, and whenever the combo box's edit control changes, adjust the combo box's list accordingly. Remove items that don't match the current text, or re-add items from the master list that you removed previously.
As Rob already answered, you could filter on the OnChange event, see the following code example. It works for multiple ComboBoxes.
{uses}
Contnrs, StrUtils;
type
TForm1 = class(TForm)
ComboBox1: TComboBox;
ComboBox2: TComboBox;
procedure FormCreate(Sender: TObject);
procedure ComboBoxChange(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FComboLists: TList;
procedure FilterComboBox(Combo: TComboBox);
end;
implementation
{$R *.dfm}
procedure TForm1.ComboBoxChange(Sender: TObject);
begin
if Sender is TComboBox then
FilterComboBox(TComboBox(Sender));
end;
procedure TForm1.FilterComboBox(Combo: TComboBox);
function Origin: TStrings;
begin
if Combo.Tag = 0 then
begin
Combo.Sorted := True;
Result := TStringList.Create;
Result.Assign(Combo.Items);
FComboLists.Add(Result);
Combo.Tag := Integer(Result);
end
else
Result := TStrings(Combo.Tag);
end;
var
Filter: TStrings;
I: Integer;
begin
Filter := TStringList.Create;
try
for I := 0 to Origin.Count - 1 do
if AnsiStartsText(Combo.Text, Origin[I]) then
Filter.Add(Origin[I]);
Combo.Items.Assign(Filter);
Combo.SelStart := Length(Combo.Text);
finally
Filter.Free;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FComboLists := TObjectList.Create(True);
// For Each ComboBox, set AutoComplete at design time to false:
ComboBox1.AutoComplete := False;
ComboBox2.AutoComplete := False;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FComboLists.Free;
end;