I'm using a TListView in Firemonkey. On startup, I create 3 list view headers and keep references to them for future use (specifically inserting items below each header).
FItemHeader:= LV.Items.Add;
FItemHeader.Purpose:= TListItemPurpose.Header;
FItemHeader.Text:= 'Items';
FChargeHeader:= LV.Items.Add;
FChargeHeader.Purpose:= TListItemPurpose.Header;
FChargeHeader.Text:= 'Charges';
FPaymentHeader:= LV.Items.Add;
FPaymentHeader.Purpose:= TListItemPurpose.Header;
FPaymentHeader.Text:= 'Payments';
Then, I've added a few items below the first (Item) header, which works fine.
SomeItem:= LV.Items.Insert(FChargeHeader.Index);
Then later, I wish to insert an item below one of the other headers...
SomeItem:= LV.Items.Insert(FPaymentHeader.Index);
This is supposed to add the item just above the Payments Header (last item in the Charges section). However, it instead gets added further up the list (at index 2).
While debugging this issue, it came as a surprise that the 3 headers' indexes have not been updated after adding items. The following code demonstrated that:
S:= 'Items: '+IntToStr(FItemHeader.Index)+sLineBreak+
'Charges: '+IntToStr(FChargeHeader.Index)+sLineBreak+
'Payments: '+IntToStr(FPaymentHeader.Index);
ShowMessage(S);
The indexes were 0, 1, and 2 even though they're supposed to be larger (after adding some items).
Further debugging led me to add this code:
for X := 0 to LV.Items.Count-1 do begin
S:= S + LV.Items[X].Text+' - '+IntToStr(LV.Items[X].Index)+sLineBreak;
end;
ShowMessage(S);
Which reports all the correct indexes. Further, if I call this second piece of code and then the first piece, the indexes are fine..
for X := 0 to LV.Items.Count-1 do begin
S:= S + LV.Items[X].Text+' - '+IntToStr(LV.Items[X].Index)+sLineBreak;
end;
ShowMessage(S);
S:= 'Items: '+IntToStr(FItemHeader.Index)+sLineBreak+
'Charges: '+IntToStr(FChargeHeader.Index)+sLineBreak+
'Payments: '+IntToStr(FPaymentHeader.Index);
ShowMessage(S);
So after a bit more digging, it turns out a simple call to...
LV.Items[1].Index;
...forced that particular item to be re-indexed.
While technically this works, it's an extremely sloppy work-around. What else can I do to ensure this list view gets re-indexed prior to inserting an item?
NOTE: When I did a similar approach using a TListBox I did not have this issue. It's surely a bug in the TListView control. I'm not asking how to fix the bug, but for a better work-around.
NOTE: This work-around only appeared to work once or twice - further attempts and even this work-around doesn't force a re-index.
UPDATE
It seems that this is the only magical work-around, which is still very sloppy:
procedure ReindexListView(AListView: TListView);
var
X: Integer;
begin
for X := 0 to AListView.Items.Count-1 do
AListView.Items[X].Index;
end;
This is bug in Delphi XE8. Works fine in Delphi XE7 Update 1.
Read more about bug:
Delphi XE8 bug in TList<T>, need workaround
Really looks like a bug. Tested with Delphi XE8 under Win32 and Win64.
Method .AddItem has the same effect like .Insert
How it looks:
How it should be (and how it looks if you are using LV.Items[1].Index). NOTE: The same effect if prior adding item under "Charges" you going to select any item in TListView
While the real fix is in Embarcadero's ball court, this seems to be the only work-around:
procedure ReindexListView(AListView: TListView);
var
X: Integer;
begin
for X := 0 to AListView.Items.Count-1 do
AListView.Items[X].Index;
end;
Behind the scenes, there is a known bug in the generic TList implementation, specifically a mis-hap with the Insert function. I haven't confirmed whether this is the case or not, but this work-around does the trick at least.
Related
Having recently received a 'Tumbleweed' badge for my last question, I am not sure whether I should be asking any more questions, but here goes.
I am populating a TComboBox with items from a sqlite table and this works fine. In my previous version of Delphi I was able to use ComboBox1.Sorted := True; to sort the items, but this seems to have disappeared in Delphi 10.2. I can sort the items in the table by applying a query and then populate the TComboBox from the sorted table. However, for curiosities sake I would like to find out how one now sorts items in a TComboBox. I have found some references to TComboBox(Sort:Compare) but have not succeeded in getting this to work to as of yet.
Can somebody please shed some light on this - many thanks
In Firemonkey you can populate a TComboBox instance either simply with the Items property of type TStrings or you add TListBoxItem instances with the form designer. But internally always TListBoxItem for the elements is used.
To use the TComboBox.Sort you need to provide an anonymous compare-function.
This is a simple example usage of TComboBox.Sort
cbxItems.Sort(
function (pLeft, pRight: TFMXObject): Integer
var
lLeft, lRight: TListBoxItem;
begin
lLeft := TListBoxItem(pLeft);
lRight := TListBoxItem(pRight);
Result := String.Compare(lLeft.Text, lRight.Text);
end
);
I was trying out some things and wanted to make a delphi IDE extension.
My basic idea was expanding the ToDo list feature that is currently in the IDE.
Step one was adding a toolbutton to the IDE which would open a form showing the todo items.
But I noticed some weird things that I hopefully caused myself since that would mean it can be easily fixed.
I am adding my toolbutton to the CustomToolbar, which is the one with the blue questionmark (see screenshot later)
The thing that happens: I install my package and the button is added with the correct image, right next to the existing button.
Now I close the modal form with the installed packages and then the blue questionmark changes.
Don't mind the icon I used, I will use a different one eventually but ok.
So basicly the existing item changes to my own icon but disabled for some reason. And I can't figure out why this happens.
As suggested in the guide I found online I used a TDatamodule to implement my code.
My code:
procedure TDatamoduleToDoList.Initialize;
var
LResource, LhInst: Cardinal;
begin
LhInst := FindClassHInstance(Self.ClassType);
if LhInst > 0 then
begin
LResource := FindResource(LhInst, 'icon', RT_Bitmap);
if LResource > 0 then
begin
FBMP := Vcl.Graphics.TBitmap.Create;
FBMP.LoadFromResourceName(LhInst, 'icon');
end
else
DoRaise('Resource not found');
end
else
DoRaise('HInstance Couldn''t be found');
FToDoAction := TTodoAction.Create(Self);
FToDoAction.Category := actionCat;
FToDoAction.ImageIndex := FIntaServices.ImageList.Add(FBMP, nil);
FToDoAction.Name := 'my_very_own_action_man';
end;
procedure TDatamoduleToDoList.DataModuleCreate(Sender: TObject);
begin
//Create extension
if Supports(BorlandIDEServices, INTAServices, FIntaServices) then
begin
Initialize;
if FToDoAction <> nil then
FCustBut := TSpeedButton(FIntaServices.AddToolButton(sCustomToolBar, 'CstmToDoList', FToDoAction))
else
DoRaise('Initialize failed');
end
else
DoRaise('Something went wrong');
end;
DoRaise is my own procedure that simply destroys all of my objects and raises an exception, did this to prevent mem leaks in the ide.
But, I think, I don't do anything weird but yet this problem occurs.
So I'm hoping someone here might have done something simular and sees the error in my code.
Thanks in advance.
P.s. if you need any more info or see the rest of the unit let me know and ill put the entire unit on github or something like that.
Edit:
Thanks to #Uwe Raabe I managed to solve this problem.
The problem was found in the comments of INTAServices.AddImages
AddImages takes all the images from the given image list and adds them
to the
main application imagelist. It also creates an internal mapping array from the
original image indices to the new indices in the main imagelist. This
mapping is used by AddActionMenu to remap the ImageIndex property of the
action object to the new ImageIndex. This should be the first method
called when adding actions and menu items to the main application window.
The return value is the first index in the main application image list of
the first image in the source list. Call this function with an nil
image list to clear the internal mapping array. Unlike the AddImages function from
the ancestor interface, this version takes an Ident that allows the same base index
to be re-used. This is useful when the IDE implements demand-loading of
personalities so that the images will only get registered once and the same image
indices can be used.
The solution eventually was adding my image to a local imagelist which was added to the imagelist of IntaServices
Code:
procedure TDatamoduleToDoList.DataModuleCreate(Sender: TObject);
begin
//Create extension
if Supports(BorlandIDEServices, INTAServices, FIntaServices) then
begin
Initialize;
if FToDoAction <> nil then
begin
FCustBut := TSpeedButton(FIntaServices.AddToolButton(sCustomToolBar, 'CstmToDoList', FToDoAction));
FToDoAction.ImageIndex := FIntaServices.AddImages(FImages);//This is the fix
end
else
DoRaise('Initialize failed');
end
else
DoRaise('Something went wrong');
end;
You are not supposed to fiddle around with the INTAServices.ImageList directly. Instead use either INTAServices.AddMasked or INTAServices.AddImages (in case you have a local imagelist in your datamodule).
You can safely use the INTAServices.ImageList to be connected to your controls, but you should neither Add nor Delete the images in it directly.
I'm using a TListBox in Firemonkey, and I'm facing a strange issue when it comes to dynamically showing/hiding items. This includes both Delphi XE7 and XE8. The setup, there is a TPopupBox at the top of the form, where user chooses one of the items listed. Depending on which was chosen, the TListBox should show only certain TListBoxItems, and hide the rest. Part of this consists of resizing each list item height to 0 when not visible (otherwise it would leave an ugly gap between the items).
The problem is that very randomly and spontaneously (no pattern), selecting an item in this TPopupBox (calling OnChange which modifies visibility), produces an EArgumentOutOfRangeException at an unknown point. The code breaks in System.Generics.Collections.TListHelper.SetItemN() on the first line calling CheckItemRangeInline(AIndex); Within there, it's simply:
procedure TListHelper.CheckItemRangeInline(AIndex: Integer);
begin
if (AIndex < 0) or (AIndex >= FCount) then
raise EArgumentOutOfRangeException.CreateRes(#SArgumentOutOfRange);
end;
The exception continues to be raised over and over and over again with no end (starts with 4 in a row). When I use the debugger to step in, I can never manage to get it to happen.
There are a couple common procedures used here which control item visibility:
//lstTrans = TListBox
//Iterates through all items and hides everything
procedure TfrmMain.HideTransItems;
var
X: Integer;
begin
for X := 0 to lstTrans.Count-1 do begin
lstTrans.ListItems[X].Visible:= False;
end;
end;
//Sets height of visible items to 42, invisible items to 0
procedure TfrmMain.ResetTransHeights;
var
X: Integer;
LI: TListBoxItem;
begin
for X := 0 to lstTrans.Count-1 do begin
LI:= lstTrans.ListItems[X];
if LI.Visible then
LI.Height:= 42
else
LI.Height:= 0;
end;
end;
Then, when choosing something in the TPopupBox:
//cboTrans = TPopupBox
procedure TfrmMain.cboTransChange(Sender: TObject);
procedure E(AItem: TListBoxItem);
begin
AItem.Visible:= True;
end;
begin
HideTransItems; //Make all list items invisible
case cboTrans.ItemIndex of
0: begin
E(lbSomeListBoxItem);
E(lbSomeOtherItem);
//More calls to "E"
end;
1: begin
E(lbSomeListBoxItem2);
//More calls to "E"
end;
//More indexes
end;
ResetTransHeights; //Adjust visible list item heights to be seen
end;
(The full procedure is just a lot of the exact same types of calls, too much to post here)
Nowhere am I adding or removing items - only changing visibility
There are no events triggered which might be causing some faulty loop
The TPopupBox is located outside of the TListBox
Each TListBoxItem has one or two controls (yet it doesn't matter which ones are being shown/hidden)
Selecting an item in this TPopupBox may work one time, yet fail the next
Sometimes it occurs the first time I show/hide these items, sometimes it takes 20-30 tries
Never able to reproduce while stepping through in Debug
Why would I be receiving this exception, and how do I fix it?
Why would I be receiving this exception, and how do I fix it?
You know why you are receiving it. You are accessing an array with an index that lies outside the valid range.
The question is where that index is. If you cannot readily reproduce then you need to debug to gather diagnostics. On Windows you'd use a tool like madExcept to gather information. Most useful would be the call stack that led to the error.
If you don't have madExcept or a similar tool at hand use trace logging. Instrument your code so that it logs information that allows you to determine which access of the list is out of bounds. You'll likely end up iterating around this as you narrow down the search.
Finally, once you identify which code leads to the error, usually the problem becomes apparent.
I had the same issue when I was animating the height of a TListBoxItem.
The issue only occurred when I was changing the Height of a Selected item. I implemented Jerry Dodge's solution of setting the height to 0.01 instead of 0 which fixed the issue.
Delphi Berlin Code
{Delphi Berlin}
ItemIndex := 0;
Item := ListBox.ItemByIndex(ItemIndex);
Height := Item.Height;
FloatAnimation := TFloatAnimation.Create(nil);
FloatAnimation.Parent := Item;
FloatAnimation.PropertyName := 'height'
FloatAnimation.StartValue := Height;
FloatAnimation.StopValue := 0.01; {Setting to 0 causes "Argument out of range" if the item is selected}
FloatAnimation.Start;
(Someone edit the title if you can understand and define my problem better.)
The problem which I am having is with style formatting of a RichEdit "reverting" back to the default "nothing" aka [] and then back to whatever I set it to, bold or italic for example.
The thing that is at fault - I assume, since I have no idea how it is breaking things - is a procedure (REMainLinesCheck) that checks for amount of lines in the RichEdit and deletes the first one until a certain point is reached (to show a maximum of 14 lines at once) like so:
while REMain.Lines.Count > 14 do
REMain.Lines.Delete(0);
I have 6 occurrences of the above procedure in other procedures that add lines to the RichEdit, but none of them change RichEdit.SelAttributes.Style but one, which was adding only one Bold line like so:
REMain.SelAttributes.Style := [fsBold];
REMain.Lines.Add('something');
REMainLinesCheck;
So I have removed all occurrences except that one and started poking around, it didn't take long to see that it was working in fact fine, regular and bold lines where being added normally and excess lines where being deleted - no problems. But as soon as I reintroduced REMainLinesCheck procedure into another procedure (for clarity purposes, lets call it Proc3Lines, because that's what it does: adds 3 lines and then calls the check for excess lines), every line that follows this Proc3Lines that should be Bold is not... From what I have experienced here it seems that REMainLinesCheck does something in Proc3Lines, since without it everything is fine.
Obviously it's not a circle of procedures that call each other, but the other parts of the code have nothing to do with this RichEdit, not to mention that I don't change RichEdit.SelAttributes.Style anywhere for REMain except that one place that I have shown, there is another RichEdit in the same unit that I do change its line's style like that, but that cannot possibly be related in any way... could it? (No it does not, I just checked.)
Basically: what the hell Delphi? It cannot get any simpler than this and I am still managing to fail, can someone explain and/or fix this? Ask questions, I'll elaborate as much as I can if something is not clear.
To apply a format to a new added line, use the following:
procedure TForm1.Button1Click(Sender: TObject);
var
LineIndex: Integer;
begin
LineIndex := RichEdit1.Lines.Add('Something');
RichEdit1.SelStart := RichEdit1.Perform(EM_LINEINDEX, LineIndex, 0);
RichEdit1.SelLength := RichEdit1.Perform(EM_LINELENGTH, RichEdit1.SelStart, 0);
RichEdit1.SelAttributes.Style := [fsBold];
end;
This has worked for me:
procedure TformStart.Proc;
var
endtxtpos: integer;
begin
endtxtpos := Length(REMain.Text);
REMain.Lines.Add('something');
REMain.SelStart := endtxtpos-(REMain.Lines.Count-1);
REMain.SelLength := Length('something');
REMain.SelAttributes.Style := [fsBold];
end;
But since I don't know any better, please criticize and suggest how I can do it better.
I am using a VirtualStringTree control as a list view and using the sort features. However when I double click the VirtualStringTree header the sort direction symbol hides until I click the header again.
Can that behaviour be disabled?
Things that I have tried but do not work:
I have searched the properties and cannot find a related setting
I have linked the double click header event to the click header event
My environment is Delphi 2007 Pro, Windows 7 Pro 64bit.
I had the same issue with double-click and hiding of sorting triangle and instead I just wanted a simple toggle up/down with nothing else. This issue is present unfortunately in latest VirtualTreeView (4.8.7) as well.
Here is a bit of code that fixes the issue - put something like this in your OnHeaderClick event (not OnHeaderDblClick !).
The relevant line is if HitInfo.Column = NoColumn then Exit; which fixes the double-click problem. You may or may not use the rest of code for your own purposes but it may be useful to someone else. The rest of explanation is in the code comments.
You don't need to define OnHeaderDblClick event - it may be empty if not needed so you may want to remove that from your code.
UPDATE
Also read comments from TLama as it seems that with version 5.0.0. this fix may not operate as intended. With the current version it does though.
{**
A column header of a VirtualStringTree was clicked: Toggle the sort direction
}
procedure TMainForm.vstHeaderClick(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo);
begin
// Don't call sorting procedure on right click
// Some list-headers have a contextmenu which should popup then.
if HitInfo.Button = mbRight then Exit;
// Beginning with VT's r181, this proc is also called when doubleclicking-to-autofit
// Seems buggy in VT as this suddenly calls it with Column=-1 in those cases.
// See also issue #1150
if HitInfo.Column = NoColumn then Exit;
if Sender.SortColumn <> HitInfo.Column then Sender.SortColumn := HitInfo.Column
else if Sender.SortDirection = sdAscending then Sender.SortDirection := sdDescending
else Sender.SortDirection := sdAscending;
Sender.Treeview.SortTree( HitInfo.Column, Sender.SortDirection );
end;