List index out of bounds in TPageControl - delphi

I am stuck with a TPageControl that exhibits some strange behaviour..
The control has 3 pages but when I do
for I:=0 to PageControl.PageCount-1 do begin
PageControl.Pages[I].TabVisible := False;
PageControl.Pages[I].Visible := Ord(iColorScale.GenerationMode) = I;
end;
I get a 'List index out of bounds (3)' error when executing the first line of the first iteration of the loop equivalent to
PageControl.Pages[0].TabVisible := False;
Now, when I view the PageControl properties in the debugger, everything seems to be in order. The PageCount is expectedly 3, and I can see all the pages and their properties, including TabVisible of page 0, in the evaluator
I'm using Delphi XE on a windows 7 machine.. Does anyone have an idea what is going on? I'm at a loss.

tldr: set PageControl.HandleNeeded before setting TabVisible.
There is a good explanation here (by Greg Chapman): TabVisible on TabSheet and index error
For future SO reference (copy/paste):
If the PageControl's handle was destroyed (which can
happen if setting some property in the PageControl or any of its parent windows causes a call to RecreateWnd), the PageControl saves the visible tabs in a TStringList (FSaveTabs). Setting TabVisible results in a call to this routine:
procedure TTabSheet.SetTabShowing(Value: Boolean);
var
Index: Integer;
begin
if FTabShowing <> Value then
if Value then
begin
FTabShowing := True;
FPageControl.InsertTab(Self);
end else
begin
Index := TabIndex;
FTabShowing := False;
FPageControl.DeleteTab(Self, Index);
end;
end;
During the call to FPageControl.DeleteTab, the PageControl will recreate its handle if necessary. In doing so, it tries to reset the visible tabs using FSaveTabs. However, it can get confused because one of the tabs that it added to FSaveTabs is now invisible (TabSheet.FTabShowing = false). This causes the IndexError. So the fix is to make sure the handle is recreated before setting TabVisible.

Related

Argument Out of Range issues using FMX TListBox

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;

Shortcut triggers TAction on first created form instead of form with focus

I found (in Delphi 2010) that shortcuts always end up on first form (as owned by main form) that has that action, but not the currently focused form. My TMainFrm owns several TViewFrm. Each has a TActionManager with the same TActons.
I see some ways out, but wonder whats the best fix.. (and not a bad hack)
The forms are navigated using a tabset which calls their Hide() and Show(). I'd did not expect hidden forms to receive keypresses. Am i doing something wrong?
It seems that action shortcuts are always start at the main form, and using TCustomForm.IsShortCut() get distributed to owned forms. I see no logic there to respect hidden windows, should i override it and have it trigger the focused form first?
Disabling all TActions in TViewFrm.Hide() .. ?
Moving the TActionToolBar to TMainFrm but that is a pit of snakes and last resort.
I have found a workaround thats good enough for me; my main form now overrides TCustomForm.IsShortcut() and first checks visible windows from my list of editor tabs.
A list which i conveniently already have, so this might not work for everyone.
// Override TCustomForm and make it check the currently focused tab/window first.
function TFormMain.IsShortCut(var Message: TWMKey): Boolean;
function DispatchShortCut(const Owner: TComponent) : Boolean; // copied function unchanged
var
I: Integer;
Component: TComponent;
begin
Result := False;
{ Dispatch to all children }
for I := 0 to Owner.ComponentCount - 1 do
begin
Component := Owner.Components[I];
if Component is TCustomActionList then
begin
if TCustomActionList(Component).IsShortCut(Message) then
begin
Result := True;
Exit;
end
end
else
begin
Result := DispatchShortCut(Component);
if Result then
Break;
end
end;
end;
var
form : TForm;
begin
Result := False;
// Check my menu
Result := Result or (Menu <> nil) and (Menu.WindowHandle <> 0) and
Menu.IsShortCut(Message);
// Check currently focused form <------------------- (the fix)
for form in FEditorTabs do
if form.Visible then
begin
Result := DispatchShortCut(form);
if Result then Break;
end;
// ^ wont work using GetActiveWindow() because it always returns Self.
// Check all owned components/forms (the normal behaviour)
if not Result then
Result := inherited IsShortCut(Message);
end;
Another solution would be to change DispatchShortCut() to check for components being visible and/or enabled, but that might impact more than i'd like. I wonder whether the original code architects had a reason not to -- by design. Best would be have it called twice: first to give priority to visible+enabled components, and second call as fallback to normal behavior.

Iterating a panels controls results in an index out of bounds error. Why?

I am using Delphi 7 (Yes, I hear the guffaws). I have a tabbed notebook that I want certain controls to appear only in a sequence where the prior control is finished correctly. For each page in the notebook, I have a named sheet. And for the controls on that sheet, I use the tag property to determine whether they are visible at each step. Some steps result in one new control showing, some steps have as many as five controls popping into view. I thought to simply iterate through the controls on any tab sheet that's in view and turn off anything with a tag greater than the current step value. On the page in question, there appear to be 23 controls in all, some labels that are always in view, some edit fields that pop up into view and some arrow-shaped buttons for advancing when a newly popped up field gets changed. Seemed simple enough, except I kept generating Index out of range errors. The sequence would shut down with out a detailed error message for EurekaLog, not anything opened up that should have been. I finally 'resolved' the issue by plugging in a check for the NAME of the control I knew was last in the list and quitting the loop at that point. I also added the extra test for Kounter.tag <> zero to avoid leaving the Submit and Cancel buttons on in some routes. Ideas why the Kounter just kept on past 23?
procedure TFrmMain.VizToggleWTP;
var
kounter: Integer;
kontrol: TControl;
Kontrolz: Integer;
begin
Kontrolz := sheetPrintouts.ControlCount;
for Kounter := 1 to Kontrolz
do begin
// To avoid index error, check for the Cancel Button and exit at that point
if sheetPrintouts.Controls[kounter].Name = 'BtnCancelwtp'
then Break;
if (sheetPrintouts.Controls[Kounter]) is TNXEdit
then begin
kontrol := TNXEdit(sheetPrintouts.Controls[Kounter]);
kontrol.visible := (kontrol.Tag <= wtpStep);
end;
if (sheetPrintouts.Controls[Kounter]) is TJvShapedButton
then begin
kontrol := TJvShapedButton(sheetPrintouts.Controls[Kounter]);
kontrol.visible := ((kontrol.Tag <= wtpStep) and (kontrol.Tag <> 0));
end;
end;
end;
You need to replace
for Kounter := 1 to Kontrolz do
with
for Kounter := 0 to Kontrolz-1 do
since the Controls array is zero-based.
For instance, if there are three controls, they are indexed 0, 1, 2 and not 1, 2, 3.

Displaying Hints in a Delphi XE2 TActionMainMenubar

I am struggling greatly trying to get hints displayed in a TActionMainMenuBar.
Delphi XE2.
I am creating the menus at runtime. I add categories, subitems, clear the menu out, that all works great. Clicking on the menu item works properly (for now it just does a ShowMessage with the action item tag but that's fine).
This is the code that adds a new menu item :
function TActionF.NewAction(AParent: TActionClientItem; Caption: String; aTag : integer; ExecuteAction: TNotifyEvent):TActionClientItem;
var
newActionClient : TActionClientItem;
AnewAction : TAction;
begin
newActionClient := TActionClientItem(AParent.Items.insert(AParent.Items.Count));
newActionClient.Caption := Caption; //??
newActionClient.UsageCount := -1; // turn of menu priority stuff for now
AnewAction := TAction.Create(Self);
AnewAction.Tag := aTag;
AnewAction.ImageIndex := -1;
AnewAction.Caption := Caption;
AnewAction.Hint := Caption + 'Action Tag = ' + IntToStr(aTag);
AnewAction.OnHint := acnDoHint; // fixed, could be parameter, but onHint is never called !!??
AnewAction.OnExecute := ExecuteAction; // passed as parameter
newActionClient.Action := AnewAction;
Result := newActionClient;
end;
I am setting the Hint" of the action. I have also experimented with assigning the OnHint, but the OnHint is never called. I simply cannot get at that hint when browsing the menu.
I have ShowHint set True everywhere I can see a place to do it.
The problem is that I cannot get any menu hints displayed no matter what I try. If i could just get at it I could display it myself (if the program won't). The OnHint is never called.
I have posted the full source of my menu program (Delphi XE2) , a small example narrowed down as best I could, in my public DropBox if anyone wants to see the program.
https://dl.dropbox.com/u/58421925/Actions.zip
This does exactly what you want: www.delphi.about.com/od/vclusing/a/menuitemhints.htm
It handles the WM_MENUSELECT message and shows the hint in its own window ( TMenuItemHint = class(THintWindow) ).

Delphi - overriding hide behaviour of TForm.showModal

I am currently writing a windowing system for an existing Delphi application.
Currently, the program consists of a number of full-sized forms which are shown modally in the order they are required and none of which can be moved by the user. My aim is to allow all of these forms to be moveable. Previously forms were stacked on top of each other but since none could be moved the background forms were not visible to the user. My solution so far has been to hide the 'parent' form when opening a new child, and reshowing it when that child is closed.
Unfortunately since each child is called with showModal, the call the make the parent form visible does not come until after the modal process has completed and hence after the child form has been hidden so the user sees a split second flash where no form is visible.
Is there a way I can prevent the modal forms from being hidden automatically after their process has completed? This would allow me to manually hide them once the parent form is visible again. I have tried to schedule this in the FormHide event of each child form but this does not work as a child form is also hidden when opening one of its own children.
EDIT:
Here is what I have so far based of Remy's advice below
procedure openModalChild(child: TForm; parent: TForm);
var
WindowList: Pointer;
SaveFocusCount: Integer;
SaveCursor: TCursor;
SaveCount: Integer;
ActiveWindow: HWnd;
Result: integer;
begin
CancelDrag;
with child do begin
Application.ModalStarted;
try
ActiveWindow := GetActiveWindow;
WindowList := DisableTaskWindows(0);
//set the window to fullscreen if required
setScreenMode(child);
try
Show; //show the child form
try
SendMessage(Handle, CM_ACTIVATE, 0, 0);
ModalResult := 0;
repeat
Application.HandleMessage;
//if Forms.Application.FTerminate then ModalResult := mrCancel else
if ModalResult <> 0 then closeModal(child as TCustomForm);
until ModalResult <> 0;
Result := ModalResult;
SendMessage(Handle, CM_DEACTIVATE, 0, 0);
if GetActiveWindow <> Handle then ActiveWindow := 0;
finally
parent.Show;
Hide;
end;
finally
EnableTaskWindows(WindowList);
parent.Show; //reshow the parent form
if ActiveWindow <> 0 then SetActiveWindow(ActiveWindow);
end;
finally
Application.ModalFinished;
end;
end;
end;
This works well but the only problem is the active repeat loop never breaks, even after the child has been escaped and so the parent form is never reshown.
Is there any way I can resolve this?
ShowModal() explicitally calls Show() just before entering its modal processing loop, and explicitally calls Hide() immediately after exiting the loop. You cannot change that without altering the code in the VCL's Forms.pas source file.
If you need finer control over the windows, without editing VCL source code, then don't use ShowModal() at all. Use Show(), Hide(), DisableTaskWindows(), and EnableTaskWindows() yourself as needed. I would sugest you look at Forms.pas to see how they are used. Copy the implementation of ShowModal() into your own function, then you can customize it as needed.

Resources