There is something that I didn't understand with TListBox and TListBoxItem in Delphi 10.2 Tokyo.
Some values (TListBoxItem) are load to my ListBox, when the first letter change I add a TListBoxGroupHeader.
procedure TForm1.Button1Click(Sender: TObject);
var
lbItem: TListBoxItem;
Letter: string;
ListBoxGroupHeader: TListBoxGroupHeader;
i: integer;
ListValue: TStringList;
begin
Letter := '';
ListValue := TStringList.Create;
try
ListValue.Add('Germany');
ListValue.Add('Georgie');
ListValue.Add('France');
ListValue.Add('Venezuela');
ListValue.Add('Poland');
ListValue.Add('Russia');
ListValue.Add('Sweden');
ListValue.Add('Denmark');
ListBox1.BeginUpdate;
for i := 0 to ListValue.Count - 1 do
begin
if Letter <> Copy(ListValue[i], 0, 1).ToUpper then
begin
ListBoxGroupHeader := TListBoxGroupHeader.Create(ListBox1);
ListBoxGroupHeader.Text := Copy(ListValue[i], 0, 1).ToUpper;
ListBox1.AddObject(ListBoxGroupHeader);
end;
lbItem := TListBoxItem.Create(ListBox1);
lbItem.Text := ListValue[i];
lbItem.Tag := i;
ListBox1.AddObject(lbItem);
Letter := Copy(ListValue[i], 0, 1).ToUpper;
end;
finally
ListBox1.EndUpdate;
FreeAndNil(ListValue);
end;
end;
I use a TEdit to search in this ListBox. That's here that I have a problem. If ListBoxItem contain the content of the Edit I set Visible to True, else I set it to False.
procedure TForm1.Edit1ChangeTracking(Sender: TObject);
var
i : integer;
ListBoxItem: TListBoxItem;
begin
ListBox1.BeginUpdate;
try
for i := 0 to ListBox1.Items.Count - 1 do
begin
if ListBox1.ListItems[i] is TListBoxItem then
begin
ListBoxItem := TListBoxItem(ListBox1.ListItems[i]);
if Edit1.Text.Trim = '' then
begin
ListBoxItem.Visible := True
end
else
begin
if ListBox1.ListItems[i] is TListBoxGroupHeader then
ListBoxItem.Visible := False
else
ListBoxItem.Visible := ListBoxItem.Text.ToLower.Contains(Edit1.Text.Trim.ToLower);
end;
end;
end;
finally
ListBox1.EndUpdate;
end;
end;
The first GroupHeader (letter G) is always visible ! and it's look like there is a ListBoxItem behind the GroupHeader.. When I use a checkpoint Visible is set to false .. so I didn't understand..
If I write the letter "V" I only see the GroupHeader with letter "G".
I have evene try to change the text value if it's a GroupHeader.
if ListBox1.ListItems[i] is TListBoxGroupHeader then
ListBoxItem.Text := '>>' + ListBoxItem.Text + '<<'
Thats change text but not for the first GroupHeader (letter G) ...
Don't know if I use it bad, or if it's a bug ??
I could have reproduce what you've described and it has something to do with hiding header whilst keeping item under that header visible. In such case application shows header rather than the item. I haven't checked what's wrong inside but it seems it is not what you want. IMHO you want to keep visible items that match to a search text with their respective header and hide only headers with no items under.
If that is so, try this:
procedure FilterItems(const Text: string; ListBox: TListBox);
var
I: Integer; { ← loop variable }
Hide: Boolean; { ← flag indicating if we want to hide the last header we passed }
Item: TListBoxItem; { ← currently iterated item }
Head: TListBoxGroupHeader; { ← last header item we passed during iteration }
begin
Head := nil;
Hide := True;
ListBox.BeginUpdate;
try
{ if search text is empty, show all items }
if Text.IsEmpty then
for I := 0 to ListBox.Content.ControlsCount - 1 do
ListBox.ListItems[I].Visible := True
else
{ otherwise compare text in non header items }
begin
for I := 0 to ListBox.Content.ControlsCount - 1 do
begin
Item := ListBox.ListItems[I];
{ if the iterated item is header }
if Item is TListBoxGroupHeader then
begin
{ set the previous header visibility by at least one visible item }
if Assigned(Head) then
Head.Visible := not Hide;
{ assume hiding this header and store its reference }
Hide := True;
Head := TListBoxGroupHeader(Item);
end
else
{ if the iterated item is a regular item }
if Item is TListBoxItem then
begin
{ set the item visibility by matching text; if the item remains visible, it
means we don't want to hide the header, so set our flag variable as well }
if Item.Text.ToLower.Contains(Text) then
begin
Hide := False;
Item.Visible := True;
end
else
Item.Visible := False;
end;
end;
{ the iteration finished, so now setup visibility of the last header we passed }
if Assigned(Head) then
Head.Visible := not Hide;
end;
finally
ListBox.EndUpdate;
end;
end;
procedure TForm1.Edit1ChangeTracking(Sender: TObject);
begin
FilterItems(Edit1.Text.Trim.ToLower, ListBox1);
end;
Related
with TMainMenu we have AutoLineReduction property to hide doubled separators when menu item is hidden, how to do the same with ActionMainMenuBar and ActionManager?
I didn't find an internal method for this, but we can do it manually.
We have to add the OnPopup method to the ActionMainMenuBar:
procedure TFormMain.MenuBarPopup(Sender: TObject; Item: TCustomActionControl);
begin
// Make all separators visible
for var I := 0 to Item.ActionClient.Items.Count - 1 do begin
var Itm := Item.ActionClient.Items[I];
if (Itm.Caption = '-') then
Itm.Visible := True;
end;
// Hide doubled separators
for var I := 0 to Item.ActionClient.Items.Count - 1 do begin
var Itm := Item.ActionClient.Items[I];
if (Itm.Caption = '-') then begin // Search next separator
var bFound := False;
for var J := I + 1 to Item.ActionClient.Items.Count - 1 do begin
var Itm2 := Item.ActionClient.Items[J];
if Itm2.Visible then begin
bFound := (Itm2.Caption <> '-');
Break;
end;
end;
Itm.Visible := bFound;
end;
end;
end;
Is it very strange that this component does not contain such a property...
I have a number of files in a directory, where each file has two lines, line 1 is the string that I want to put into my ListBox, and line 2 is the background color that I want that ListBox item to have (represented as an 8-digit hex value).
The contents of each file looks like this:
string
14603481
This is my code so far:
for i := 0 to PathList.Count - 1 do
begin
FileLines := TStringList.Create;
try
FileLines.LoadFromFile(PathList.Strings[i]);
s := FileLines[0]; { Loads string to add to ListBox1 }
RGBColor := FileLines[1];
finally
FileLines.Free;
end;
ListBox1.Items.AddObject(s, TObject(RGBColor)); { This code doesn't work, but hopefully you get what I'm }
end; { trying to do }
All other examples that do anything similar to this declare the color in the DrawItem procedure, but I need to set the color from within this for loop, since each entry will have a unique color.
How do I set the color of each item uniquely from within this loop?
The VCL's TListBox does not natively support any kind of per-item coloring. The TListBox.Font and TListBox.Color properties apply to all items equally.
To do what you are asking for, you will have to set the TListBox.Style property to lbOwnerDrawFixed and then use the TListBox.OnDrawItem event to custom-draw the items manually however you want, eg:
var
...
s: string;
RGBColor: Integer;
begin
...
for i := 0 to PathList.Count - 1 do
begin
FileLines := TStringList.Create;
try
FileLines.LoadFromFile(PathList[i]);
s := FileLines[0];
RGBColor := StrToInt(FileLines[1]);
finally
FileLines.Free;
end;
ListBox1.Items.AddObject(s, TObject(RGBColor));
end;
...
end;
...
procedure TMyForm.ListBox1DrawItem(Control: TWinControl;
Index: Integer; const Rect: TRect; State: TOwnerDrawState);
var
LB: TListBox;
begin
LB := TListBox(Control);
if odSelected in State then
begin
LB.Canvas.Brush.Color := clHighlight;
LB.Canvas.Font.Color := clHighlightText;
end else
begin
LB.Canvas.Brush.Color := TColor(Integer(LB.Items.Objects[Index]));
LB.Canvas.Font.Color := LB.Font.Color;
end;
LB.Canvas.FillRect(Rect);
LB.Canvas.TextRect(Rect, Rect.Left + 2, Rect.Top + 2, LB.Items[Index]);
if (odFocused in State) and not (odNoFocusRect in State) then
LB.Canvas.DrawFocusRect(Rect);
end;
Problem
My TListBox is getting blank when its last TListBoxItem is programatically checked. To illustrate it better, hereby what I mean by getting blank:
Context
I'm generating a list from a TJSONArray. Each item looks like {"event_code","event_name"}.
Then, I compare if the event_code is written on a second TJSONArray : json_response_available_events. If it does, the ListBoxItem will be checked.
Code
procedure TFormHome.TimerGetEventsTimer(Sender: TObject);
var
K : Integer;
Z : Integer;
ListCount : Integer;
AvailableList_Count: Integer;
lb_item: TListBoxItem;
event_code_first_array: string;
event_code : string;
event_name : string;
begin
// Disable this timer for now
TimerGetEvents.Enabled := false;
// Get List of Notifications
json_response_events := DM_Auth0.ServerMethods1Client.GetEventsCodeAndDescription(communication_token);
json_response_available_events := DM_Auth0.ServerMethods1Client.GetAllowedNotificationsList(communication_token, genset_id);
ListCount := json_response_events.Count -1;
AvailableList_Count := json_response_available_events.Count - 1;
for K := 0 to (ListCount) do
begin
// Get complete Event Code and Name
event_name := json_response_events.Items[K].toString;
// Get Event Code
event_code_first_array := StringReplace(event_name.Split([':'])[0], '"', '', [rfReplaceAll]);
// Get Event Name
event_name := StringReplace(event_name.Split([':'])[1], '"', '', [rfReplaceAll]);
// Create ListBoxItem
lb_item := TListBoxItem.Create(self);
lb_item.Parent := lb_notifications;
lb_item.Text := event_name;
lb_item.StyleLookup := 'listboxitemleftdetail';
// Check if this Item code is available
for Z := 0 to (AvailableList_Count) do
begin
if json_response_available_events.Items[Z] <> nil then
begin
// Get Event Code
event_code := json_response_available_events.Items[Z].toString;
// Format
event_code := StringReplace(event_code, '"', '', [rfReplaceAll]);
if event_code_first_array.Contains(event_code) then
begin
if K <= ListCount then
begin
lb_item.IsChecked := true;
lb_item.IsSelected := false;
end;
end;
end;
end;
end;
end;
Analysis
If we set to < only, it displays the list correctly but the last item will remain unchecked.
if K < ListCount then
begin
lb_item.IsChecked := true;
lb_item.IsSelected := false;
end;
I can even change it's properties when its = like
if K = ListCount then
begin
lb_item.Text := 'Deadpool for President';
end;
and lb_item.isChecked := false works fine, but when setting lb_item.isChecked := true it gets all weirdly blank.
Why is it happening? And if there's a better way to do what I'm doing, the help will be appreciated.
I have a function to update a cxGrid made with help from answers to Loop through records on a cxgrid and update a field/column
But it is sometimes acting a bit strange. If I open the form with the cxGrid and click the columnheader without doing anything else, the records are updateted OK. But if the 'selectorbar' is moved away from the top, the record marked is not updated.
I am sure it is a property that needs to be changed, but which one.
The variable fSelected is set to False at FormShow and is ther so that the user can unselect records as well.
procedure TfrmContactsSelect.colContactSelectedHeaderClick(Sender: TObject);
var
i: Integer;
Index: Integer;
BookMark : TBookMark;
Contact: variant;
begin
if fMulti = True then
begin
Screen.Cursor := crHourGlass;
fSelected := not fSelected;
BookMark := qryContacts.GetBookmark;
qryContacts.DisableControls;
try
for i := 0 to grdContactsView1.DataController.FilteredRecordCount - 1 do
begin
Index := grdContactsView1.DataController.FilteredRecordIndex[i];
Contact := grdContactsView1.DataController.Values[Index, 4];
if grdContactsView1.DataController.LocateByKey(Contact) then
begin
qryContacts.Edit;
qryContacts.FieldByName('fldcontact_selected').AsBoolean := fSelected;
qryContacts.Post;
end;
end;
finally
qryContacts.EnableControls;
qryContacts.GotoBookmark(BookMark);
qryContacts.FreeBookmark(BookMark);
end;
Screen.Cursor := crDefault;
end;
end;
Delphi XE7, DevExpress 14.2.2, UniDAC 5.5.12 for DB access
Comment:
I have ended up with the following solution based on the answer and input from MartynA
procedure TfrmContactsSelect.colContactSelectedHeaderClick(Sender: TObject);
var
i: Integer;
Index: Integer;
MarkedRecord: variant;
CurrentRecord: variant;
begin
if fMulti = True then
begin
Screen.Cursor := crHourGlass;
fSelected := not fSelected;
Index := grdContactsView1.DataController.FocusedRecordIndex;
MarkedRecord := grdContactsView1.DataController.Values[Index, colContactGuid.ID];
try
for i := 0 to grdContactsView1.DataController.FilteredRecordCount - 1 do
begin
Index := grdContactsView1.DataController.FilteredRecordIndex[i];
CurrentRecord := grdContactsView1.DataController.Values[Index, colContactGuid.ID];
if grdContactsView1.DataController.LocateByKey(CurrentRecord) then
begin
grdContactsView1.DataController.Edit;
grdContactsView1.DataController.SetEditValue(colContactSelected.ID, fSelected, evsText);
grdContactsView1.DataController.Post;
end;
end;
finally
grdContactsView1.DataController.LocateByKey(MarkedRecord);
end;
Screen.Cursor := crDefault;
end;
end;
I can reproduce your problem using the sample project I posted in my answer to your other q.
Try this: Add a TMemo to your form, and inside the 'if grdContactsView1.DataController.LocateByKey(Contact) then' block, write the value of a row-unique datafield and the Selected datafield value to the memo.
Then, what I get when some row other than the top row is selected is that one row is listed twice in the memo, with Selected both false and true, and one of the rows in the filter isn't listed at all, which I think accounts for the behaviour you're seeing. If I then comment out the .Edit .. .Post lines, it correctly lists all the rows in the filter.
So evidently doing the Selected field changes inside a block which iterated the FilteredRecordIndex property of the DBTableView is what's causing the problem.
Personally, I find that it goes a bit against the grain to modify dataset rows in code via a DB-aware control (because you usually end up fighting the DB-awareness of the control), but in this case, it's straightforward to do the processing via the DBTableView of the cxGrid.
procedure TForm1.ProcessFilteredRecords;
var
PrevV,
V : Variant;
i,
Index: Integer;
S : String;
begin
// First, pick up a reference to the current record
// so that we can return to it afterwards
Index := cxGrid1DBTableView1.DataController.FocusedRecordIndex;
PrevV := cxGrid1DBTableView1.DataController.Values[Index, 0];
try
for i := 0 to cxGrid1DBTableView1.DataController.FilteredRecordCount - 1 do begin
Index := cxGrid1DBTableView1.DataController.FilteredRecordIndex[i];
V := cxGrid1DBTableView1.DataController.Values[Index, 0];
if cxGrid1DBTableView1.DataController.LocateByKey(V) then begin
cxGrid1DBTableView1.DataController.Edit;
// 2 is the index of my Selected column in the grid
if cxGrid1DBTableView1.DataController.SetEditValue(2, True, evsText) then
Caption := 'OK'
else
Caption := 'Failed';
cxGrid1DBTableView1.DataController.Post;
end;
end;
finally
if cxGrid1DBTableView1.DataController.LocateByKey(PrevV) then
Caption := 'OK'
else
Caption := 'Failed';
end;
end;
Another way to avoid the problem is to change the Selected states in two steps:
Iterate the FilteredRecordIndex to build a list of rows to change - in your case this would be a list of guids
Then, iterate the list of rows and update their Selected states.
Code:
procedure TForm1.ProcessFilteredRecords;
var
V : Variant;
i,
Index: Integer;
BM : TBookMark;
S : String;
TL : TStringList;
begin
Memo1.Lines.Clear;
TL := TStringList.Create;
try
for i := 0 to cxGrid1DBTableView1.DataController.FilteredRecordCount - 1 do begin
Index := cxGrid1DBTableView1.DataController.FilteredRecordIndex[i];
V := cxGrid1DBTableView1.DataController.Values[Index, 0];
if cxGrid1DBTableView1.DataController.LocateByKey(V) then begin
if CDS1.FieldByName('Selected').AsBoolean then
S := 'True'
else
S := 'False';
S := CDS1.FieldByName('Name').AsString + ' ' + S;
Memo1.Lines.Add(S);
TL.Add(CDS1.FieldByName('Guid').AsString);
end;
end;
try
BM := CDS1.GetBookMark;
CDS1.DisableControls;
for i := 0 to TL.Count - 1 do begin
if CDS1.Locate('guid', TL[i], []) then begin
CDS1.Edit;
CDS1.FieldByName('Selected').AsBoolean := True;
CDS1.Post;
end
end;
finally
CDS1.EnableControls;
CDS1.GotoBookmark(BM);
CDS1.FreeBookmark(BM);
end;
finally
TL.Free;
end;
end;
Like you, I was expecting that changing a property or two of the cxGrid might avoid the problem without any code, but I haven't been able to find anything which does.
I have grid panel 16 x 4 like this one:
Sometimes i want to hide some rows and to move bottom rows up. When I set component visible property to false the layout is not updated:
Nevertheless the row size type is set to auto:
Why the component don't set row height to zero when there is nothing to display?
Why the component don't set row height to zero when there is nothing to display ?
Because the row is considered as empty only if there are no components in all columns in that row, not if they're visible or not. So the same returns the IsRowEmpty method. To workaround this, you'd need to be notified by the cell component about its visibility change. When this notification is generated, you can check the row just like the IsRowEmpty method does, except you'll check if the controls are visible, not if they're assigned. Based on the result of such method, you can then set the size of the Value to 0 to hide the row.
With a help of interposed class, the method for checking if all controls in a row or column are visible, you might write something like this. Those methods return True, when all existing controls in a certain row or column are visible, False otherwise:
uses
ExtCtrls, Consts;
type
TGridPanel = class(ExtCtrls.TGridPanel)
public
function IsColContentVisible(ACol: Integer): Boolean;
function IsRowContentVisible(ARow: Integer): Boolean;
end;
implementation
function TGridPanel.IsColContentVisible(ACol: Integer): Boolean;
var
I: Integer;
Control: TControl;
begin
Result := False;
if (ACol > -1) and (ACol < ColumnCollection.Count) then
begin
for I := 0 to ColumnCollection.Count -1 do
begin
Control := ControlCollection.Controls[I, ACol];
if Assigned(Control) and not Control.Visible then
Exit;
end;
Result := True;
end
else
raise EGridPanelException.CreateFmt(sInvalidColumnIndex, [ACol]);
end;
function TGridPanel.IsRowContentVisible(ARow: Integer): Boolean;
var
I: Integer;
Control: TControl;
begin
Result := False;
if (ARow > -1) and (ARow < RowCollection.Count) then
begin
for I := 0 to ColumnCollection.Count -1 do
begin
Control := ControlCollection.Controls[I, ARow];
if Assigned(Control) and not Control.Visible then
Exit;
end;
Result := True;
end
else
raise EGridPanelException.CreateFmt(sInvalidRowIndex, [ARow]);
end;
And the usage shown for the first row:
procedure TForm1.Button1Click(Sender: TObject);
begin
// after you update visibility of controls in the first row...
// if any of the controls in the first row is not visible, change the
// row's height to 0, what makes it hidden, otherwise set certain height
if not GridPanel1.IsRowContentVisible(0) then
GridPanel1.RowCollection[0].Value := 0
else
GridPanel1.RowCollection[0].Value := 50;
end;
I've got a hacky solution ... keeping the Autosizing
Procedure ShowHideControlFromGrid(C:TControl);
begin
if C.Parent = nil then
begin
c.Parent := TWinControl(c.Tag)
end
else
begin
c.Tag := NativeInt(C.Parent);
c.Parent := nil;
end;
end;
procedure TForm4.Button1Click(Sender: TObject);
begin // e.g. Call
ShowHideControlFromGrid(Edit5);
ShowHideControlFromGrid(Edit6);
ShowHideControlFromGrid(Edit7);
ShowHideControlFromGrid(Label1);
end;