Set ComboBox text on selection - delphi

I am working on an application where i have a combobox with long text values.Since the text values are large(in term of characters ..20 or more), to display in the combobox, the requirement was to display on the first character after selecting from the drop down.
Like in the image marked in red. if the user selects 3th item 3 0.5 to 1.25 Slight i should only display the 3 in the combobox.
So i tried this
sTheSelectedValue : string;
procedure TForm1.ComboBox1Select(Sender: TObject);
begin
sTheSelectedValue:=TrimTextAndDisplay(ComboBox1.Text); //send theselected value
ComboBox1.Text :=''; //clear the selection
ComboBox1.Text:=sTheSelectedValue; //now assign as text to combo box
Button1.Caption:=ComboBox1.Text; //just show the new value on the button.
end;
function TForm1.TrimTextAndDisplay(TheText : string): string;
var
sTheResult : string;
begin
sTheResult :=copy(TheText,0,1); //extract the first value..
Result :=sTheResult;
end;
The result is
The button seem to show the proper value but not the combobox.
what i want is to get 3 in the combobox, i cant seem set ComboBox1.Text:=
can any one tell me how to do it?
like this on selection of from the combobox the result should be

I would suggest owner-drawing the ComboBox to handle this. Set the TComboBox.Style property to csOwnerDrawFixed, then store just the numbers '1', '2', '3', etc in the TComboBox.Items property itself and use the TComboBox.OnDrawItem event to render the full strings when the drop-down list is visible, eg:
var
sTheSelectedValue : string;
const
ItemStrings: array[0..7] of string = (
'0 to 0.1 Calm (rippled)',
'0.1 to 0.5 Smooth (wavelets)',
'0.5 to 1.25 Slight',
'1.25 to 2.5 Moderate',
'2.5 to 4 Rough',
'4 to 6 Very rough',
'6 to 9 High',
'9 to 14 Very high');
procedure TForm1.FormCreate(Sender: TObject);
var
I: Integer;
begin
ComboBox1.Items.BeginUpdate;
try
for I := Low(ItemStrings) to High(ItemStrings) do begin
ComboBox1.Items.Add(IntToStr(I+1));
end;
finally
ComboBox1.Items.EndUpdate;
end;
end;
procedure TForm1.ComboBox1Select(Sender: TObject);
begin
sTheSelectedValue := IntToStr(ComboBox1.ItemIndex+1);
Button1.Caption := sTheSelectedValue;
end;
procedure TForm1.ComboBox1DrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState);
var
s: String;
begin
if odSelected in State then begin
ComboBox1.Canvas.Brush.Color := clHighlight;
ComboBox1.Canvas.Font.Color := clHighlightText;
end else begin
ComboBox1.Canvas.Brush.Color := ComboBox1.Color;
ComboBox1.Canvas.Font.Color := ComboBox1.Font.Color;
end;
ComboBox1.Canvas.FillRect(Rect);
s := IntToStr(Index+1);
if not (odComboBoxEdit in State) then begin
s := s + ' ' + ItemStrings[Index];
end;
ComboBox1.Canvas.TextRect(Rect, Rect.Left+2, Rect.Top+2, s);
if (State * [odFocused, odNoFocusRect]) = [odFocused] then begin
ComboBox1.Canvas.DrawFocusRect(Rect);
end;
end;

You have to try to save the data in a record, for ex:
type
TMyRec = record
Num:Integer;
Text:String;
end;
TMyRecArray = array of TMyRec;
MyRecArray:TMyRecArray;
then you can set manually the items to be set in the ComboBox (on the OnFromCreate),
SetLength(MyRecArray,9);
MyRecArray[0].Num:=1;
MyRecArray[0].Text:='0 to 0.1 Calm Rippled';
.
.
and so on.
then in the combobox strigns place only the numbers, and
procedure TForm1.ComboBox1Select(Sender: TObject);
var
i:integer;
begin
for i:=0 to 9 do
begin
if ComboBox1.Text=IntToStr(MyRecArray[i].Num) then
Button1.Caption:=MyRecArray[i].Text;
end;
end;

Related

Is there a way to call a StringGrid OnCellDraw during runtime

I have a program that tracks the days during the year which are booked. In order to display this I have a StringGrid which I use Colors to display the days booked. The days booked are stored in ar2Booking which is a 2D array which contains the days and months respectively.
procedure TfrmClient.stgYearPlan1DrawCell(Sender: TObject; ACol, ARow: Integer;
Rect: TRect; State: TGridDrawState);
var
k, iMonth, iDay : Integer;
begin
for k := 1 to 31 do
stgYearPlan1.Cells[k,0] := IntToStr(k);
for k := 1 to 12 do
stgYearPlan1.Cells[0,k] := ShortMonthNames[k];
for iDay := 1 to 31 do
for iMonth := 1 to 12 do
begin
if ar2Booking[iDay,iMonth] = 'Y' then
begin
if (ACol = iDay) and (ARow = iMonth) then
begin
stgYearPlan1.Canvas.Brush.Color := clBlack;
stgYearPlan1.Canvas.FillRect(Rect);
stgYearPlan1.Canvas.TextOut(Rect.Left,Rect.Top,stgYearPlan1.Cells[ACol, ARow]);
end;
end;
if ar2Booking[iDay,iMonth] = 'D' then
begin
if (ACol = iDay) and (ARow = iMonth) then
begin
stgYearPlan1.Canvas.Brush.Color := clSilver;
stgYearPlan1.Canvas.FillRect(Rect);
stgYearPlan1.Canvas.TextOut(Rect.Left+2,Rect.Top+2,stgYearPlan1.Cells[ACol, ARow]);
end;
end;
end;
end;
I then want to click a button during runtime which allows a user to book a date. I would then like the date they select to reflect in the StringGrid. If I update the array how would I be able to run the OnCellDraw again in order to reflect the new booked dates?
Thanks
Generally you would invalidate part of the control causing it to be redrawn with the next windows paint message. The methods of a TStringGrid to do this are protected so you need to use a cracker class to access them.
// -- add to the type section
type
TStringGridCracker = class(TStringGrid);
procedure TForm1.Button1Click(Sender: TObject);
begin
TStringGridCracker(StringGrid1).InvalidateCell(1,2);
end;
I discovered after a friend showed me, the StringGrid.Redraw procedure accomplishes what I need. Thanks everyone

How to make the same button run different code everytime it is clicked?

I am currently doing a school project, I am making a Credit Card machine. I need the 'Enter Button' to
run different code when it is clicked. The first click must get the card number from an edit ps... (I clear the edit once the card number has been retrieved), and the second click must get the pin from the same edit.
How would I do this?
procedure TfrmMainMenu.btbtnEnterClick(Sender: TObject);
var
sCvv,sPin:string;
begin
iCount2:=0;
sCardNumber:=lbledtCardInfo.Text;
if (Length(sCardNumber)<>16) AND (iCount2=0) then
begin
ShowMessage('Card number has to 16 digits,please try again!!');
end
else
begin
Inc(iCount2);
lbledtCardInfo.clear;
lbledtCardInfo.EditLabel.Caption:='Enter Pin' ;
btbtnEnter.Enabled:=false;
end; //if
if iCount2=2 then
begin
btbtnEnter.Enabled:=true;
sPin:=lbledtCardInfo.Text;
ShowMessage(sPin);//returns a blank
end;
You could try to do everything in a single event handler. There are several different ways to handle that. However, a different solution would be to use separate event handlers for each task, and then each task can assign a new handler for the next click to perform, eg:
procedure TfrmMainMenu.FormCreate(Sender: TObject);
begin
// you can set this at design-time if desired...
btbtnEnter.OnClick := GetCCNumber;
end;
procedure TfrmMainMenu.GetCCNumber(Sender: TObject);
begin
sCardNumber := lbledtCardInfo.Text;
if Length(sCardNumber) <> 16 then
begin
ShowMessage('Card number has to 16 digits,please try again!!');
Exit;
end;
lbledtCardInfo.Clear;
lbledtCardInfo.EditLabel.Caption := 'Enter Pin' ;
btbtnEnter.OnClick := GetCCPin;
end;
procedure TfrmMainMenu.GetCCPin(Sender: TObject);
var
sPin: string;
begin
sPin := lbledtCardInfo.Text;
if Length(sPin) <> 4 then
begin
ShowMessage('Card Pin has to 4 digits,please try again!!');
Exit;
end;
ShowMessage(sPin);
...
lbledtCardInfo.Clear;
lbledtCardInfo.EditLabel.Caption := 'Enter Number' ;
btbtnEnter.OnClick := GetCCNumber;
end;
A variation of this would be to create multiple buttons that overlap each other in the UI, and then you can toggle their Visible property back and forth as needed, eg:
procedure TfrmMainMenu.FormCreate(Sender: TObject);
begin
// you can set this at design-time if desired...
btbtnCCPinEnter.Visible := False;
btbtnCCNumEnter.Visible := True;
end;
procedure TfrmMainMenu.btbtnCCNumEnterClick(Sender: TObject);
begin
sCardNumber := lbledtCardInfo.Text;
if Length(sCardNumber) <> 16 then
begin
ShowMessage('Card number has to 16 digits,please try again!!');
Exit;
end;
lbledtCardInfo.Clear;
lbledtCardInfo.EditLabel.Caption := 'Enter Pin' ;
btbtnCCNumEnter.Visible := False;
btbtnCCPinEnter.Visible := True;
end;
procedure TfrmMainMenu.btbtnCCPinEnterClick(Sender: TObject);
var
sPin: string;
begin
sPin := lbledtCardInfo.Text;
if Length(sPin) <> 4 then
begin
ShowMessage('Card Pin has to 4 digits,please try again!!');
Exit;
end;
ShowMessage(sPin);
...
lbledtCardInfo.Clear;
lbledtCardInfo.EditLabel.Caption := 'Enter Number' ;
btbtnCCPinEnter.Visible := False;
btbtnCCNumEnter.Visible := True;
end;
Notice that you test iCount2 = 0 immediately after setting iCount2 := 0. Thus, that test will always be True. Furthermore, the later test iCount2 = 2 will always be False because the value starts at 0 and you only have one Inc in between.
Instead try the following.
Add two string fields FCardNumber and FPin to your form class:
private
FCardNumber: string;
FPin: string;
Also create an enumerated type TEntryStage = (esCardNumber, esPin) and add a field of this type. This will make your code look like this:
private
type
TEntryStage = (esCardNumber, esPin);
var
FCardNumber: string;
FPin: string;
FEntryStage: TEntryStage;
In Delphi, class fields (class member variables) are always initialized, so FEntryStage will be esCardNumber (=TEntryStage(0)) when the form is newly created.
Add a TLabeledEdit (I see you use those) and a TButton; name them eInput and btnNext, respectively. Let the labeled edit's caption be Card number: and the caption of the button be Next.
Now add the following OnClick handler to the button:
procedure TForm1.btnNextClick(Sender: TObject);
begin
case FEntryStage of
esCardNumber:
begin
// Save card number
FCardNumber := eInput.Text;
// Prepare for the next stage
eInput.Clear;
eInput.EditLabel.Caption := 'Pin:';
FEntryStage := esPin;
end;
esPin:
begin
// Save pin
FPin := eInput.Text;
// Just do something with the data
ShowMessageFmt('Card number: %s'#13#10'Pin: %s', [FCardNumber, FPin]);
end;
end;
end;
You might notice that you cannot trigger the Next button using Enter, which is very annoying. To fix this, do
procedure TForm1.eInputEnter(Sender: TObject);
begin
btnNext.Default := True;
end;
procedure TForm1.eInputExit(Sender: TObject);
begin
btnNext.Default := False;
end;
Much better!

Hiding items in TListBox while filtering by String

Short Version: Is there any way to control or modify LisBox items individually? for example set their Visible property to False separately.
I found a TListBoxItem class in Fire Monkey when I was searching, but I don't want to use Fire Monkey and want it in VCL.
Detailed Version:
I tried to filter my ListBox using two TStringList and an Edit, one StringList is global to keep the original list (list_files_global) and another StringList to help filtering procedure (list_files_filter) and my primary list of files is my ListBox (list_files).
I created my global StringList on onCreate event while program is starting to store my original list:
procedure Tfrm_main.FormCreate(Sender: TObject);
Begin
list_files_global := TStringList.Create;
list_files_global.Assign(list_files.Items);
End;
and used Edit's onChange event for filtering:
procedure Tfrm_main.edit_files_filterChange(Sender: TObject);
Var
list_files_filter: TStringList;
i: Integer;
Begin
list_files_filter := TStringList.Create;
list_files_filter.Assign(list_files.Items);
list_files.Clear;
for i := 0 to list_files_filter.Count - 1 do
if pos(edit_files_filter.text, list_files_filter[i]) > 0 then
list_files.Items.Add(list_files_filter[i]);
End;
and for switching off the filter, just recover the list from my global list that I created at first:
list_files.Items := list_files_global;
here so far, everything works just fine, but problem is when I'm trying to edit/rename/delete items from filtered list, for example I change an item:
list_files.Items[i] := '-- Changed Item --';
list will be edited, but when I switch off the filter, the original list will be back and all changes are lost.
so I want to know is there any proper way to solve this problem? Something like hiding items individually or change items visibility, etc... so I can change the filtering algorithm and get rid of all this making extra lists.
I searched the internet and looked into Delphi's help file for a whole day and nothing useful came up.
The items of a VCL listbox, List Box in the API, does not have any visibility property. The only option for not showing an item is to delete it.
You can use the control in virtual mode however, where there are no items at all. You decide what data to keep, what to display. That's LBS_NODATA window style in the API. In VCL, set the style property to lbVirtual.
Extremely simplified example follows.
Let's keep an array of records, one record per virtual item.
type
TListItem = record
FileName: string;
Visible: Boolean;
end;
TListItems = array of TListItem;
You can extend the fields as per your requirements. Visibility is one of the main concerns in the question, I added that. You'd probably add something that represents the original name so that you know what name have been changed, etc..
Have one array per listbox. This example contains one listbox.
var
ListItems: TListItems;
Better make it a field though, this is for demonstration only.
Required units.
uses
ioutils, types;
Some initialization at form creation. Empty the filter edit. Set listbox style accordingly. Fill up some file names. All items will be visible at startup.
procedure TForm1.FormCreate(Sender: TObject);
var
ListFiles: TStringDynArray;
i: Integer;
begin
ListFiles := ioutils.TDirectory.GetFiles(TDirectory.GetCurrentDirectory);
SetLength(ListItems, Length(ListFiles));
for i := 0 to High(ListItems) do begin
ListItems[i].FileName := ListFiles[i];
ListItems[i].Visible := True;
end;
ListBox1.Style := lbVirtual;
ListBox1.Count := Length(ListFiles);
Edit1.Text := '';
end;
In virtual mode the listbox is only interested in the Count property. That will arrange how many items will show, accordingly the scrollable area.
Here's the filter part, this is case sensitive.
procedure TForm1.Edit1Change(Sender: TObject);
var
Text: string;
Cnt: Integer;
i: Integer;
begin
Text := Edit1.Text;
if Text = '' then begin
for i := 0 to High(ListItems) do
ListItems[i].Visible := True;
Cnt := Length(ListItems);
end else begin
Cnt := 0;
for i := 0 to High(ListItems) do begin
ListItems[i].Visible := Pos(Text, ListItems[i].FileName) > 0;
if ListItems[i].Visible then
Inc(Cnt);
end;
end;
ListBox1.Count := Cnt;
end;
The special case in the edit's OnChange is that when the text is empty. Then all items will show. Otherwise code is from the question. Here we also keep the total number of visible items, so that we can update the listbox accordingly.
Now the only interesting part, listbox demands data.
procedure TForm1.ListBox1Data(Control: TWinControl; Index: Integer;
var Data: string);
var
VisibleIndex: Integer;
i: Integer;
begin
VisibleIndex := -1;
for i := 0 to High(ListItems) do begin
if ListItems[i].Visible then
Inc(VisibleIndex);
if VisibleIndex = Index then begin
Data := ListItems[i].FileName;
Break;
end;
end;
end;
What happens here is that the listbox requires an item to show providing its index. We loop through the master list counting visible items to find out which one matches that index, and supply its text.
This is something I often do, but with list views instead of list boxes. The basic principles are the same, though.
I tend to store the individual items as objects, which are reference types in Delphi. And I keep them all in one main unfiltered list, which owns the objects, while I maintain a filtered list (which does not own the objects) for display purposes. Like #Sertac, I combine this with a virtual list view.
To see how this works in practice, create a new VCL application and drop a list view (lvDisplay) and an edit control (eFilter) on the main form:
Notice I have added three columns to the list view control: "Name", "Age", and "Colour". I also make it virtual (OwnerData = True).
Now define the class for the individual data items:
type
TDogInfo = class
Name: string;
Age: Integer;
Color: string;
constructor Create(const AName: string; AAge: Integer; const AColor: string);
function Matches(const AText: string): Boolean;
end;
where
{ TDogInfo }
constructor TDogInfo.Create(const AName: string; AAge: Integer;
const AColor: string);
begin
Name := AName;
Age := AAge;
Color := AColor;
end;
function TDogInfo.Matches(const AText: string): Boolean;
begin
Result := ContainsText(Name, AText) or ContainsText(Age.ToString, AText) or
ContainsText(Color, AText);
end;
And let us create the unfiltered list of dogs:
TForm1 = class(TForm)
eFilter: TEdit;
lvDisplay: TListView;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FList, FFilteredList: TObjectList<TDogInfo>;
public
end;
where
function GetRandomDogName: string;
const
DogNames: array[0..5] of string = ('Buster', 'Fido', 'Pluto', 'Spot', 'Bill', 'Rover');
begin
Result := DogNames[Random(Length(DogNames))];
end;
function GetRandomDogColor: string;
const
DogColors: array[0..2] of string = ('Brown', 'Grey', 'Black');
begin
Result := DogColors[Random(Length(DogColors))];
end;
procedure TForm1.FormCreate(Sender: TObject);
var
i: Integer;
begin
FList := TObjectList<TDogInfo>.Create(True); // Owns the objects
// Populate with sample data
for i := 1 to 1000 do
FList.Add(
TDogInfo.Create(GetRandomDogName, Random(15), GetRandomDogColor)
);
FFilteredList := FList;
lvDisplay.Items.Count := FFilteredList.Count;
lvDisplay.Invalidate;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
if FFilteredList <> FList then
FreeAndNil(FFilteredList);
FreeAndNil(FList);
end;
The idea is that the list view control always displays the FFilteredList, which either points to the same object instance as FList, or points to a filtered (or sorted) version of it:
// The list view's OnData event handler
procedure TForm1.lvDisplayData(Sender: TObject; Item: TListItem);
begin
if FFilteredList = nil then
Exit;
if not InRange(Item.Index, 0, FFilteredList.Count - 1) then
Exit;
Item.Caption := FFilteredList[Item.Index].Name;
Item.SubItems.Add(FFilteredList[Item.Index].Age.ToString);
Item.SubItems.Add(FFilteredList[Item.Index].Color);
end;
// The edit control's OnChange handler
procedure TForm1.eFilterChange(Sender: TObject);
var
i: Integer;
begin
if string(eFilter.Text).IsEmpty then // no filter, display all items
begin
if FFilteredList <> FList then
begin
FreeAndNil(FFilteredList);
FFilteredList := FList;
end;
end
else
begin
if (FFilteredList = nil) or (FFilteredList = FList) then
FFilteredList := TObjectList<TDogInfo>.Create(False); // doesn't own the objects
FFilteredList.Clear;
for i := 0 to FList.Count - 1 do
if FList[i].Matches(eFilter.Text) then
FFilteredList.Add(FList[i]);
end;
lvDisplay.Items.Count := FFilteredList.Count;
lvDisplay.Invalidate;
end;
The result:
Notice that there always is only one in-memory object for each dog, so if you rename a dog, the changes will reflect in the list view, filtered or not. (But don't forget to invalidate it!)

Combo Box parameter has no default value in delphi

The program I'm working on uses an if statement to add a line to the SQL for the contents of another combo box
procedure TFmNewGarage.ComboBoxCountryEnter(Sender: TObject);
begin
ADOQueryCountry.SQL.Clear;
ADOQueryCountry.SQL.Add('SELECT DISTINCT Country');
ADOQueryCountry.SQL.Add(' FROM TblBaseCar');
ADOQueryCountry.Open;
while not ADOQueryCountry.Eof do
begin
ComboBoxCountry.Items.Add(ADOQueryCountry['Country']);
ADOQueryCountry.Next;
end;
end;
procedure TFmNewGarage.ComboBoxCountryChange(Sender: TObject);
begin
SelA:=True;
ComboBoxManufacturer.Show;
ComboBoxCountry.Hide;
end;
procedure TFmNewGarage.ComboBoxManufacturerEnter(Sender: TObject);
begin
ADOQueryManufacturer.SQL.Clear;
ADOQueryManufacturer.SQL.Add('SELECT DISTINCT Manufacturer');
ADOQueryManufacturer.SQL.Add(' FROM TblBaseCar');
if SelA=true then
ADOQueryManufacturer.SQL.Add(' WHERE Country=(ComboBoxCountry.seltext)');
ADOQueryManufacturer.Open;
while not ADOQueryManufacturer.Eof do
begin
ComboBoxManufacturer.Items.Add(ADOQueryManufacturer['Manufacturer']);
ADOQueryManufacturer.Next;
end;
end;
At runtime this results in the error ComboBoxCountry.seltext has no default value, can anyone help me to rectify this?
SelText is not the property you should be using. You need the combobox Items value for the chosen ItemIndex:
var
Country: string;
begin
...
if ComboBoxCountry.ItemIndex <> -1 then
begin
Country := ComboBoxCountryItems[ComboBoxCountry.ItemIndex];
ADOQueryManufacturer.SQL.Add('WHERE Country = ' + QuotedStr(Country));
end;
end;

Displaying item when LVItemGetCaption is being called with EasyListView?

I am trying to implement virtual data mode with EasyListview
From the demo :
procedure TForm1.AddItems(Count: Integer);
var
i: Integer;
begin
// Add items to the listview. Actually the items are added to the first
// group. This group is created automatically when the first item is added.
LV.BeginUpdate;
try
for i := 0 to Count - 1 do
LV.Items.AddVirtual;
finally
LV.EndUpdate;
end;
end;
procedure TForm1.LVItemGetCaption(Sender: TCustomEasyListview;
const Item: TEasyItem; Column: Integer; var Caption: WideString);
begin
case Column of
0: Caption := 'Item ' + IntToStr(Item.Index);
1: Caption := 'Detail ' + IntToStr(Item.Index);
end;
end;
If I add some items which are string :
procedure TForm1.AddItems(Count: Integer);
var
i: Integer;
begin
// Add items to the listview. Actually the items are added to the first
// group. This group is created automatically when the first item is added.
LV.BeginUpdate;
try
for i := 0 to Count - 1 do
begin
LV.Items.AddVirtual.Caption := 'DISPLAY ME ' + IntToStr(i);
end;
finally
LV.EndUpdate;
end;
end;
How to get and displaying the stored virtual caption(=string) when LVItemGetCaption is being called?
If I get the caption with Caption := LV.Items.Items[Item.Index].Caption ; then Stack overflow.
You must add your data object to the item. E.g.:
type
TMyData = class
Caption: string;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
i: Integer;
item: TEasyItemVirtual;
MyData: TMyData;
begin
EasyListview1.BeginUpdate;
try
for i := 0 to 100 - 1 do
begin
MyData := TMyData.Create;
MyData.Caption := Format('My Item %D',[i]);
item := EasyListview1.Items.AddVirtual;
item.Data := MyData;
end;
finally
EasyListview1.EndUpdate;
end;
end;
procedure TForm1.EasyListview1ItemGetCaption(Sender: TCustomEasyListview; Item: TEasyItem;
Column: Integer; var Caption: WideString);
begin
case Column of
0: Caption := TMyData(Item.Data).Caption;
1: Caption := TMyData(Item.Data).Caption;
end;
end;
And don't forget to free your object:
procedure TForm1.EasyListview1ItemFreeing(Sender: TCustomEasyListview; Item: TEasyItem);
begin
if Assigned(Item.Data) then
Item.Data.Free;
end;
Virtual nodes are ones that don't store their data. They're just views of data you are expected to already have in some other data structure of your program. When the control needs to display a node, it asks your program what text it should use by firing the OnItemGetCaption event.
In fact, it will call the event any time it needs to know the value of the Caption property, so when you try to handle the caption-fetching event by fetching the value of the caption, you trigger infinite recursion.

Resources