(Delphi 6 with TChart, Win XP)
I'm getting erratic behavior trying to clear the points from a point series, and of course, this code used to work.
Basically, this part of my program generates 5 data points and plots them. When I try to clear them using OSC_Series.Clear I get a "List index out of bounds [0]" error.
I checked to make sure there was nothing odd about the values I was plotting. All is good there. Then I tried different things to try to isolate and work around the problem.
Here's some code.
type
TksGraph_DataFrm = class(TForm)
.
.
.
private
OSC_Series: TPointSeries
public
end;
procedure TksGraph_DataFrm.cat7AnalysisInitialize(var P:TTest_Project);
begin
// Do a bunch of stuff.
// Set up the analysis data points series.
OSC_Series:=TPointSeries.Create(self);
AnalysisChart.AddSeries(OSC_Series);
with OSC_Series do
begin
Title:='';
HorizAxis:=aBothHorizAxis;
VertAxis:=aBothVertAxis;
SeriesColor:=clRed;
Pointer.Brush.Color:=clYellow;
Pointer.HorizSize:=4;
Pointer.VertSize:=4;
Pointer.Style:=psRectangle;
Pointer.Visible:=true;
LinePen.Color:=clBlack;
LinePen.Width:=1;
Linepen.Visible:=true;
ShowInLegend:=false;
XValues.Order:=LoNone;
end;
end;
procedure TksGraph_DataFrm.cat7AnalysisRefresh(var P:TTest_Project);
var X,Y:single;
begin
X:= some value
Y:= some value
// Plot the result.
OSC_Series.AddXY(X,Y);
showmessage(
'Count = '+inttostr(OSC_Series.Count)+#13+
'X = '+FloatToStr(X)+#13+
'Y = '+FloatToStr(Y)+#13+
'Plot-X = '+FloatToStr(OSC_Series.XValue[OSC_Series.Count-1])+#13+
'Plot-Y = '+FloatToStr(OSC_Series.YValue[OSC_Series.Count-1]));
end;
Here is the routine I to use to reset the series. I'm including code that does and does not work.
procedure TksGraph_DataFrm.cat7AnalysisClear(var P:TTest_Project);
var i:integer;
begin
// This should work, but it gives me the list out of bounds error
// unless the count is 0.
OSC_Series.Clear;
// This does not work, for obvious reasons. I get a "list out of
// bounds [3] for this one.
for i:=0 to OSC_Series.Count - 1 do OSC_Series.Delete[0];
// It seems this should work, but again I get the out of bounds [0]
// error.
repeat
OSC_Series.Delete(0);
until OSC_Series.Count = 0;
// This works. Don't ask me why.
showmessage('A - '+inttostr(OSC_Series.Count));
OSC_Series.Clear;
showmessage('B - '+inttostr(OSC_Series.Count));
// This also works.
sleep(2000);
OSC_Series.Clear;
// This does not work.
sleep(1000);
OSC_Series.Clear;
end;
I'm stumped, obviously.
This smells like the code is working with an object (OSC_Series) which has been destroyed and the memory then re-used for something else. When you then use the stale reference to that memory you get unexpected and unpredictable results.
Where is OSC_Series free'd ?
I would check all such places and make sure that you do not attempt to use the OSC_Series reference after it has been free'd.
Note also that since the series is owned by the form it could be that the form itself is contriving to executing code in events after it has destroyed its owned components (including this series).
OK, dumb and not dumb.
I experimented with where I put the showmessage statement. I found I could only avoid the error if that statement came after the OSC_Series.Clear statement. I kept moving that statement back until it was after the call to the AnalysisRefresh routine, which is in a button's OnClick event handler. This means that none of the code in the refresh, enable, or update routines was causing this.
Stepping back a bit, if the AnalysisRefresh routine fails the user is shown a message. After that box is closed OSC_Series.Clear is called. If I close the box by pressing SPACE or ENTER on the keyboard... no error. If I use the mouse, error.
The chart behind the mouse has an OnMouseMove event where I display the mouse position on a status bar. I also display a hint if the mouse is near a plotted point. After clicking the message box with a mouse to close it the OnMouseMove event is called and by the time it gets to where it can display the hint, the plotted point is gone, and... error.
So, it seemed like an almost random error, but it wasn't. The trigger was just somewhere else entirely. That's you my try/except block wasn't catching the error. EurekaLog was catching it, but in a different procedure far, far away. (Deltics' answer was pretty close.)
Thanks for your help and suggestions.
Dang it if some days I can push hundreds of lines of code with no problems, then something like this pops up and it costs me near two days.
Related
I am using Delphi 10 Seattle Subscription Update 1 and TeeChart Standard v2015.15.150420 which came bundled with Delphi.
I drop a TDBChart component on a new VCL application's blank form. I then use the sample code as outlined in the "Adding a Function" tutorial found at http://www.teechart.net/docs/teechart/vclfmx/tutorials/UserGuide/Tutorials/tutorial7.htm#AddFunction in the form's OnCreate event. With this code everything works as it should and I get two bar series populated with sample values and one line series which represents the average of the two bar series.
The problem comes in if I don't want the average represented by a line series, but rather by, say, a bar series. If I change the TLineSeries to a TBarSeries and run the program, it causes an "access violation at 0x0066d665: read of address 0x00000198" on adding the first bar series as a datasource to the function series (tmpLineSeries), eg. tmpLineSeries.DataSources.Add( tmpBarSeries1 );.
Here's the problem code (see "AV occurs here" below). Note that the only code that changed from the working tutorial example was, as explained, tmpLineSeries that had been changed to a TBarSeries type instead of a TLineSeries type :
procedure TForm1.FormCreate(Sender: TObject);
var tmpBarSeries1,
tmpBarSeries2 : TBarSeries;
tmpLineSeries : TBarSeries;
begin
//Add 2 data Series
tmpBarSeries1:=TBarSeries.Create(Self);
tmpBarSeries2:=TBarSeries.Create(Self);
DBChart1.AddSeries(tmpBarSeries1);
DBChart1.AddSeries(tmpBarSeries2);
//Populate them with data (here random)
tmpBarSeries1.FillSampleValues(10);
tmpBarSeries2.FillSampleValues(10);
//Add a series to be used for an Average Function
tmpLineSeries:=TBarSeries.Create(Self);
DBChart1.AddSeries(tmpLineSeries);
//Define the Function Type for the new Series
tmpLineSeries.SetFunction(TAverageTeeFunction.Create(Self));
//Define the Datasource for the new Function Series
//Datasource accepts the Series titles of the other 2 Series
tmpLineSeries.DataSources.Clear;
tmpLineSeries.DataSources.Add( tmpBarSeries1 ); ////// AV occurs here!!!
tmpLineSeries.DataSources.Add( tmpBarSeries2 );
// *Note - When populating your input Series manually you will need to
// use the Checkdatasource method
// - See the section entitled 'Defining a Datasource'
//Change the Period of the Function so that it groups averages
//every 2 Points
tmpLineSeries.FunctionType.Period := 2;
end;
It seems to either be a bug in TeeChart or I am missing a configuration step necessary for BarSeries that is not necessary for LineSeries.
Can anyone see what I am doing wrong, or alternatively suggest a workaround for the bug? I don't think upgrading to the latest version of TeeChart is an option at this stage because, as I understand it, this can only be done by upgrading Delphi (I'm already at the latest update of Delphi), or alternatively purchasing the standalone version of TeeChart.
This looks like a bug to me. I have add it (bug #1484) to Steema Software's bugzilla. The only workaround I can think of is setting TTeeFunction.Period property to a value greater than zero so the offending code is not executed. For example:
//workaround
tmpLineSeries.FunctionType.Period:=1;
tmpLineSeries.DataSources.Add( tmpBarSeries1 ); ////// AV occurs here!!!
tmpLineSeries.DataSources.Add( tmpBarSeries2 );
UPDATE: This has been fixed for the next TeeChart release.
I have downloaded opensource delphi twain component (TDelphiTwain).
The interesting thing is, that when placed and saved on the form it creates bad dfm entry for itself.
object DelphiTwain: TDelphiTwain
OnSourceDisable = DelphiTwainSourceDisable
OnSourceSetupFileXfer = DelphiTwainSourceSetupFileXfer
TransferMode = ttmMemory
SourceCount = 0
Info.MajorVersion = 1
Info.MinorVersion = 0
Info.Language = tlDanish
Info.CountryCode = 1
Info.Groups = [tgControl, tgImage, tgAudio, MinorVersion]
Info.VersionInfo = 'Application name'
Info.Manufacturer = 'Application manufacturer'
Info.ProductFamily = 'App product family'
Info.ProductName = 'App product name'
LibraryLoaded = False
SourceManagerLoaded = False
Left = 520
Top = 136
end
The problem is with the line:
Info.Groups = [tgControl, tgImage, tgAudio, MinorVersion]
There are only three possible elements:
tgControl, tgImage and tgAudio
It adds MinorVersion everytime I Save the form.
When the app is run I get the error that there is invalid property for Info.Groups.
When i rmeove the bad part manually and without leaving dfm file the app starts ok.
I looked in the internet and there was one inquire regarding these strange issue, unfortunately it hasn't been resolved.
I think that there is some sort of memory corruption. In the post in teh internet, strange signs were displayed ...
Has anyone worked with that component or could give me some hint how this could be fixed?
The error seems to be in TTwainIdentity.GetGroups where result is not initialized. You can try to change the code by replacing
Include(Result, tgControl);
with
Result := [tgControl];
You have to recompile the package to make this change work inside the IDE.
I don't know the component, but I think the problem lies in the TTwainIdentity.GetGroups method. It starts like this:
begin
Include(Result, tgControl);
This means that it assumes that Result is initialized to an empty set. However, Result may contain garbage, and not necessarily an empty set. Change this method to look like this:
function TTwainIdentity.GetGroups(): TTwainGroups;
{Convert from Structure.SupportedGroups to TTwainGroups}
begin
Result := [tgControl];
if DG_IMAGE AND Structure.SupportedGroups <> 0 then
Include(Result, tgImage);
if DG_AUDIO AND Structure.SupportedGroups <> 0 then
Include(Result, tgAudio);
end;
Some result types will not throw a compiler warning about not being initialized, but that doesn't mean they are empty. Same goes, for instance, for strings.
See also: http://qc.embarcadero.com/wc/qcmain.aspx?d=894
But still, it is odd that this happens. Apparently, Delphi tries to find the name of the given item in the set and accidentally finds the name of another property. It seems to me that quite some checks in writing the dfm are missing if this happens. :)
I am getting Index was out of range. Must be non-negative and less than the size of the collection. Parameter name: index randomly emailed through to me on my website. I cannot reproduce this error either by force or general testing and it is somewhat confusing.
From what I can see in the stack trace, it happens randomly when opening a dataset and trying to get a value. The stack trace showed I got the error when calling this function:
function TDB.FGetLastInsertID: Integer;
const
selSQL = 'select scope_identity() as LastID';
var
selCursor: TDataSet;
begin
selCursor := Cursor(selSQL); //Returns a DataSet from a TADOQuery
try
Result := selCursor.FieldByName('LASTID').AsInteger;
finally
selCursor.Close;
selCursor.Free;
end;
end;
As I said, I cannot get this to reproduce the error and it seems to happen randomly on any query I run. I have tried closing the connection, setting it to inactive ect to reproduce the error but cannot.
Has anyone got any ideas?
EDIT: It seems its the close causing issues after examining the stack trace more closely:
Stack Trace: at
System.Collections.ArrayList.get_Item(Int32 index) at
Borland.Vcl.TList.Delete(Int32 Index) at Borland.Vcl.TList.Remove(Object Item) at
Borland.Vcl.TDBBufferList.FreeHGlobal(IntPtr Ptr) at
Borland.Vcl.TCustomADODataSet.FreeRecordBuffer(IntPtr& Buffer) at
Borland.Vcl.TDataSet.SetBufListSize(Int32 Value) at
Borland.Vcl.TDataSet.CloseCursor() at
Borland.Vcl.TDataSet.SetActive(Boolean Value) at Borland.Vcl.TDataSet.Close()
EDIT2: I have put a check in the code to make sure the selCursor.Active before selCursor.Close. The stack trace suggests there is nothing to close.
What happens if the Cursor returns an empty Dataset ?
You try to Access the selCursor.FieldByName ... without a record, I think.
Very odd and I still don't know why but it seems that randomly the Cursor is already closed. It seems that adding:
if (selCursor.Active) then
selCursor.Close;
has sorted the issue ...
Thanks for the answers and time spent trying to help.
selCursor := Cursor(selSQL); //Returns a DataSet from a TADOQuery
Maybe problem is here, FieldByName is in exception handler so you have error on next line. You have to check if Cursor() is ok.
I'm working on Word automation and to get rid of "Call was rejected by callee" / "the message filter indicated that the application is busy" errors I implemented an IMessageFilter. The messagefilter works like a charm when I automate Word directly like:
Word.Documents.Open(...)
Document.SaveAs(...)
But when I call TOleContainer.DoVerb(ovPrimary), I still get errors when Word is displaying a modal dialog. Why does the MessageFilter not work with TOleContainers DoVerb methode?
"Call was rejected by callee" is what you always get when Word is in interactive state, ie displaying a dialog. This is not restricted to Word. It also happens with Excel, for example when the user was editing a cell. And it does not have to be obvious in the user interface either. When you start editing a cell, move focus to another application and come back to Excel, the UI doesn't give you a clue but it is still in "interactive" mode and will reject automation calls with the "Call was rejected by callee" error.
So basically when you automate Word in conjunction with user interaction (and not just with Word in a background process), you should be prepared to get and handle these errors.
Edit
If you want to know whether Excel or Word is in interactive mode before calling any other COM method: just ask the COM-server whether it is "Ready":
Result := _GetActiveOleObject('Excel.Application');
try
aSharedInstance := not VarIsClear(Result);
if aSharedInstance then
Version := Result.Version; // If this produces an exception, then use a dedicated instance.
// In case checking the version does not produce an exception, but Excel still isn't
// ready, we'll check that as well.
// By the way, for some unclear reason, partial evaluation does not work on .Ready,
// so we'll do it like this:
if aSharedInstance and (StrToIntDef(StringBefore('.', Version), 0) >= EXCEL_VERSION_2002) then
aSharedInstance := Result.Ready;
except
aSharedInstance := False;
end;
if not aSharedInstance then
Result := CreateOleObject('Excel.Application');
Update
Apparently Word doesn't have a "Ready" property (whoever said Microsoft was consistent?). In that case you need to determine its readiness yourself by calling a simple (and fast) property before the actual call, and assuming that when that throws an exception, Word isn't ready. In the above example the Version is retrieved before the Ready property. If that throws an exception, we just assume that the application (Excel in this case) isn't ready and proceed accordingly.
Something along the lines of:
while Tries <= MaxTries do
try
Version := Word.Version;
Tries := MaxTries + 1; // Indicate success
Word.TheCallYouReallyWantToDo;
except
Inc(Tries);
sleep(0);
end;
Note Word.Version does not throw an exception when a dialog is open, so that is no use for figuring out whether Word is ready. :( You will have to experiment to find one that does.
IMessageFilter doesn't handle all exceptions, for example, at some points, office applications 'suspend' their object model, at which point it cannot be invoked and throws: 0x800AC472 (VBA_E_IGNORE)
In order to get around this, you have to put your call in a loop and wait for it to succeed:
while(true)
{
try
{
office_app.DoSomething();
break;
}
catch(COMException ce)
{
LOG(ce.Message);
}
}
// continue after successful call
See here for more details.
I have the following code called on WM_MOVE
procedure TForm15.WMMove(var Message: TMessage);
begin
if( (((TWMMove(Message).XPos + Self.Width >= Form14.Left) and (TWMMove(Message).XPos <= Form14.Left)) or
((TWMMove(Message).XPos <= Form14.Left + Form14.Width) and (TWMMove(Message).XPos >= Form14.Left))) and
(((TWMMove(Message).YPos + Self.Height >= Form14.Top) and (TWMMove(Message).YPos <= Form14.Top)) or
((TWMMove(Message).YPos <= Form14.Top + Form14.Height) and (TWMMove(Message).YPos >= Form14.Top))))
then begin
Self.DragKind := dkDock;
end else Self.DragKind := dkDrag;
end;
It's probably the ugliest if statement you've seen in your life,but that's not the question. :)
It's supposed to change DragKind if the current form(Self) is somewhere inside the mainForm(Form14).
However,When It sets DragKind to dkDock ,it doesn't make the currentForm(Self) dockable unless the user stops moving the form and start moving it again,so I'm trying to do the following:
If the result from the statement above is non zero and dkDock is set then Send the following messages to the form:
WM_EXITSIZEMOVE //stop moving the form
WM_ENTERSIZEMOVE //start movement again
However,I don't know how to do it:
SendMessage(Self.Handle,WM_EXITSIZEMOVE,?,?);
I tried using random parameters(1,1) ,but no effect.Maybe that's not the right way?
It looks like those two messages are notifications - directly sending them probably doesn't do anything. As Raymond Chen once said (horribly paraphrased), directly sending these messages and expecting action is like trying to refill your gas tank by moving the needle in the gauge.
WM_SIZING and WM_MOVING are messages that you can use to monitor a user changing a window's size and position, and block further changes.
From MSDN:
lParam
Pointer to a RECT structure
with the current position of the
window, in screen coordinates. To
change the position of the drag
rectangle, an application must change
the members of this structure.
So you can change the members to force the window to remain in a single position.
This is a notification message, it does not cause the form to stop moving.
And both parameter are ignored (unused).