I am trying the TeeChart software before I buy and was directed from their website to here for support.
I am trying to get my head around using the TScrollPagerTool as it seems perfect for what I am trying to achieve but I am experiencing performance issues when resizing.
In the resize event for the chart it is advised to add the following line if you are using this scrolling tool:
ScrollPagerTool.Series := Chart1.Series[0];
I don't know why you have to reassign the series but the chart won't resize without it. Further more that line effectively adds the Series again, meaning I have twice the series now and next resize, 3 times the series and so on, therefore I get a performance problem with scrolling after a few resizes.
Can someone tell me if I am doing this right or of an alternate method here?
I can post more code if required.
First note that TScrollPagerTool uses a TSubChartTool with a clone of the main series in it. That's why the the SubChart is set when the Series is assigned, because it has no sense without a series.
In some TeeChart versions, the SubChart needs the main chart to be already drawn when you assign a series to the tool in order to calculate the space each chart should use (the ChartRects). That's why we assign the series at the OnResize event, but I see in TeeChart VCL you can do it all at creation time:
uses Series, TeeScrollPagerTool;
procedure TForm1.FormCreate(Sender: TObject);
begin
Chart1.AddSeries(TLineSeries).FillSampleValues;
(Chart1.Tools.Add(TScrollPagerTool) as TScrollPagerTool).Series:=Chart1[0];
end;
BUG:
I've seen there was actually a bug, as you said. Assigning the series in the OnResize event to make the chart and the subchart resize with the form, it made the source series to be cloned again and again:
uses Series, TeeScrollPagerTool;
var scrollPager1: TScrollPagerTool;
procedure TForm1.FormCreate(Sender: TObject);
begin
Chart1.Align:=alClient;
scrollPager1:=Chart1.Tools.Add(TScrollPagerTool) as TScrollPagerTool;
Chart1.AddSeries(TLineSeries).FillSampleValues;
scrollPager1.Series:=Chart1[0];
end;
procedure TForm1.Chart1Resize(Sender: TObject);
begin
scrollPager1.Series:=Chart1[0];
end;
I've made some changes to fix it:
In the next version, the Series property will remove all the series in the subchart and will clone the series assigned. So calling it several times won't end up in more and more clones. It will also call SetUpScrollPager that calculates the ChartRect for both the main chart and the subchart.
I've also made the SetUpScrollPager public so this will be the one to call at OnResize event to adjust the ChartRects without needing to add or remove series.
procedure TForm1.Chart1Resize(Sender: TObject);
begin
scrollPager1.SetUpScrollPager;
end;
WORKAROUND:
In the meanwhile, with the actual version you still could remove all the series in the SubChart and use the Series property to clone the source series, all at the OnResize event:
procedure TForm1.Chart1Resize(Sender: TObject);
begin
scrollPager1.SubChartTChart.RemoveAllSeries;
scrollPager1.Series:=Chart1[0];
end;
RELATED FEATURE:
How to have multiple series in the ScollPager? You can use the SubChartTChart property to access the subchart and add/remove any series to/from it. But you still need one series in the Series property. So it gets a bit tricky:
uses Series, TeeScrollPagerTool;
var scrollPager1: TScrollPagerTool;
procedure TForm1.FormCreate(Sender: TObject);
var i: Integer;
begin
Chart1.Align:=alClient;
scrollPager1:=Chart1.Tools.Add(TScrollPagerTool) as TScrollPagerTool;
for i:=0 to 4 do
begin
Chart1.AddSeries(TLineSeries).FillSampleValues;
if scrollPager1.Series=nil then
scrollPager1.Series:=Chart1[i]
else
scrollPager1.SubChartTChart.AddSeries(CloneChartSeries(Chart1[i]));
end;
end;
That's why I've also added a new AddSeries(const Value: TChartSeries) method. Use it to add several series to the ScrollPager:
for i:=0 to 4 do
begin
Chart1.AddSeries(TLineSeries).FillSampleValues;
scrollPager1.AddSeries(Chart1[i]);
end;
Related
I have a Delphi program with 3 successive nested (master-details) FireDAC datasets namely: SuperMaster, Master and Details dataset.
In the AfterScroll event of the Details dataset only, new records are inserted into the database.
My program logic needs the AfterScroll event of the Details dataset to fire only once: when any of the parent datasets (SuperMaster or Master) scrolled to another record. The AfterScroll event should not trigger more than once because this will cause my logic to insert wrong records.
The problem is that when scrolling occurs to any of the parent datasets (from the current record to another one, even to the adjacent one), i.e. moving a single record only, the Details dataset AfterScroll event fired more than once and sometimes more than twice!
I do not want to mess with the original FireDac classes by overriding its AfterScroll event to force it to do my logic only once, nor to add a Boolean flag to my logic so that it runs only once.
I tried to put all the tables into one view, but it costs me to change the program logic and make future updates more demanding.
I searched and read many articles and a book ("Delphi in Depth: FIREDAC" by Cary Jensen), but could not find a solution nor know why or how it occurs!
Why does this happen? I want to understand the basics please. Is there any elegant solution?
Short of changing the FireDAC source (which I think would be a thoroughly bad idea - beware of opening Pandora's box) I don't think your goal of avoiding the
use of a Boolean flag is achievable. It should however be trivial to achieve if you do use a Boolean flag.
I also wonder whether you observation that the Detail's AfterScroll event occurs more than once per detail dataset
is correct, if that is what you are saying. If it is, you should edit your q to include a minimal, reproducible example, preferably based on the code below.
TDataSet and its descendants, including all the FireDAC datasets operate to a very tightly designed
state machine which includes the handling of master-detail behaviour that's been tested by all the TDataSet
usage since Delphi was first released. Trying to mess with that would invite
disaster.
If you try the minimal VCL project below, I think you'll find it's not very hard to satisfy yourself that
the Detail's AfterScroll event behaves exactly as you would expect from the coding of TDataSet's and FireDAC's source.
Running the code you will find that the breakpoint in DetailAfterScroll trips 4 times before
the first breakpoint on MasterFirst. The next time the BP trips, observe the call stack via
View | Debug Windows | Call stack; if you look down the cal stack, you'll find that the call to Detail.AfterScroll
was ultimately called via the penultimate line of
procedure TFDMasterDataLink.DataEvent(Event: TDataEvent; Info: NativeInt);
An FDMasterDataLink is automatically created to handle the triggering of Detail data events based on
the operation of the Master. And that, really, is the end of the story. because however much you might
disagree with this behaviour , you can't really do anything about it short of using a Boolean flag
in your own code.
I think it would be wise to verify that DetailAfterScroll is only being called once per dataset that's
on the detail side of the Master. If it's happening more than once, it would be worth checking that
it isn't your own code (or linking together of DataSets) that's causing it.
As you'll see, nearly all of what the example does is defined in the OnCreate event
to avoid having to use the Object Inspector to set up the components, so that is should "just work".
Code:
type
TForm1 = class(TForm)
Master: TFDMemTable;
Detail: TFDMemTable;
DataSource1: TDataSource;
procedure FormCreate(Sender: TObject);
procedure DetailAfterScroll(DataSet: TDataSet);
public
DetailsScrolled : Integer;
end;
[...]
procedure TForm1.DetailAfterScroll(DataSet: TDataSet);
begin
// Place debugger breakpoint on the following line
Inc(DetailsScrolled);
end;
procedure TForm1.FormCreate(Sender: TObject);
var
AField : TField;
begin
AField := TIntegerField.Create(Self);
AField.FieldName := 'MasterID';
AField.DataSet := Master;
Master.CreateDataSet;
AField := TIntegerField.Create(Self);
AField.FieldName := 'DetailID';
AField.DataSet := Detail;
AField := TIntegerField.Create(Self);
AField.FieldName := 'MasterID';
AField.DataSet := Detail;
Detail.CreateDataSet;
DataSource1.DataSet := Master;
Detail.MasterSource := DataSource1;
Detail.MasterFields := 'MasterID';
Detail.IndexFieldNames := 'MasterID;DetailID';
Master.InsertRecord([1]);
Master.InsertRecord([2]);
Detail.InsertRecord([1, 1]);
Detail.InsertRecord([2, 1]);
Detail.InsertRecord([3, 2]);
// Place debugger breakpoint on EACH of the following three lines
Master.First;
Master.Next;
Master.First;
end;
[TeeChart 2015.14.150120]
I've a Gantt-Chart so the x axis is date based. I would need to mark time periods in the chart. So i.e. mark 2009 -2011 describing it with a certain description, 2011-2013 with another name ...
I've tried to use TColorBandTool but it had some drawbacks:
I wasn't able anymore to click the Chart-Entries (even if the tool was marked as "behind"
I could not show a description of the period.
So I've tried to use TColorLineTool which worked better (almost perfect) but:
the text panels for differnt period where shown at the same (vertical) position, so they overlapped some times.
When the last TextPanel was longer then the remaining part of the chart, it "dropped" out, the Chart was not redimensioned in order to show the Panel within the Diagram.
So, now I had another Idea: to use different series to build one line over the whole width of the chart, each series for one period to show. But I would have to show the description of these series in extra Legends (TExtraLegendTool) in oder to have space enough for the texts. But I couldn't get the TExtraLegendTool shown. I assume there is a bug in that version of TeeChart since also the demo put by the installer doesn't show that tool.
Now I'm rather at a loss how to go on. Anybody has an idea?
I've given a try at the first approach:
I've tried to use TColorBandTool but it had some drawbacks:
I wasn't able anymore to click the Chart-Entries (even if the tool was marked as "behind"
I could not show a description of the period.
And I think it works fine for me here. This is how it looks and below the code I used.
Note I disabled the TColorBandTool's AllowDrag, ResizeStart and ResizeEnd properties.
var gantt1: TGanttSeries;
greenBand, blueBand: TColorBandTool;
greenAnnot, blueAnnot: TAnnotationTool;
procedure TForm1.FormCreate(Sender: TObject);
begin
Chart1.Title.Visible:=false;
Chart1.View3D:=false;
Chart1.Legend.Alignment:=laBottom;
gantt1:=Chart1.AddSeries(TGanttSeries) as TGanttSeries;
gantt1.FillSampleValues(10);
greenBand:= Chart1.Tools.Add(TColorBandTool) as TColorBandTool;
with greenBand do
begin
Color:=clGreen;
Transparency:=50;
Axis:=Chart1.Axes.Bottom;
StartValue:=gantt1.StartValues[1];
EndValue:=gantt1.EndValues[2];
AllowDrag:=false;
ResizeStart:=false;
ResizeEnd:=false;
end;
blueBand:= Chart1.Tools.Add(TColorBandTool) as TColorBandTool;
with blueBand do
begin
Color:=clBlue;
Transparency:=50;
Axis:=Chart1.Axes.Bottom;
StartValue:=gantt1.StartValues[9];
EndValue:=gantt1.EndValues[8];
AllowDrag:=false;
ResizeStart:=false;
ResizeEnd:=false;
end;
Chart1.MarginTop:=10;
Chart1.Draw;
greenAnnot:=Chart1.Tools.Add(TAnnotationTool) as TAnnotationTool;
with greenAnnot do
begin
Shape.Transparent:=true;
Shape.Font.Color:=clGreen;
Text:='Green annotation';
Top:=Chart1.ChartRect.Top-15;
Left:=Chart1.Axes.Bottom.CalcPosValue(greenBand.StartValue+(greenBand.EndValue-greenBand.StartValue)/2) -
(Chart1.Canvas.TextWidth(Text) div 2);
end;
blueAnnot:=Chart1.Tools.Add(TAnnotationTool) as TAnnotationTool;
with blueAnnot do
begin
Shape.Transparent:=true;
Shape.Font.Color:=clBlue;
Text:='Blue annotation';
Top:=Chart1.ChartRect.Top-15;
Left:=Chart1.Axes.Bottom.CalcPosValue(blueBand.StartValue+(blueBand.EndValue-blueBand.StartValue)/2) -
(Chart1.Canvas.TextWidth(Text) div 2);
end;
end;
IDE: c++ Builder XE5 Update 2
TeeChart Build: 2014.11.140512
I am trying to bring certain series on a TChart component to the front(as you would typically do with a BringToFront() function).
I've done some reading and found the following options/suggestions:
A. Change the ZOrder property of the series.
B. Use TChart.ExchangeSeries()
Using TChart.ExchangeSeries() is not a proper way of changing the z-order of a series. Its primary function is to swap around two series in a TChart component's SeriesList(which then inherently changes the z-order of those series). If you require your series-ordering to be fixed(fixed ordering in SeriesList), then this will not work.
Changing the ZOrder properties of the series delivered better results. However, changing the ZOrder of the first series(Series[0]) apparently does nothing. Series[0] seem to like sitting at the back of the class.
The above might be the result of my implementation. In which case, some more details:
On my TChart component I have multiple series. The series-types can be changed dynamically. The types to which the series can be changed are limited to TLineSeries and TBarSeries.
I always want the TLineSeries at the front.
Any advice on how this can be done?
(Will we ever see that elusive TChartSeries.BringToFront() function?) :)
Any advice on how this can be done?
Looks like mixing series styles and changing ZOrder is not working very well. For example, using this code snippet:
uses Series;
procedure TForm1.FormCreate(Sender: TObject);
begin
Chart1.AddSeries(TLineSeries.Create(Self)).FillSampleValues(10);
Chart1.AddSeries(TLineSeries.Create(Self)).FillSampleValues(10);
Chart1.AddSeries(TBarSeries.Create(Self)).FillSampleValues(10);
Chart1.AddSeries(TBarSeries.Create(Self)).FillSampleValues(10);
Chart1.AddSeries(TBarSeries.Create(Self)).FillSampleValues(10);
end;
procedure TForm1.Button1Click(Sender: TObject);
var i: Integer;
begin
for i:=0 to Chart1.SeriesCount-1 do
begin
Chart1[i].Marks.Visible:=False;
if Chart1[i] is TLineSeries then
Chart1[i].ZOrder:=Chart1.SeriesCount - 1 - i
else
Chart1[i].ZOrder:=i;
end;
end;
Line series are brought to the front but bar series style remain there. I have also tried with bar series StackGroups: However, it doesn't make much of a difference. We'd need StackGroups for line series as well for this work.
(Will we ever see that elusive TChartSeries.BringToFront() function?)
:)
Why not? I have added your request to Steema Software's bugzilla platform: http://bugs.teechart.net/show_bug.cgi?id=853. Feel free to sign up and add yourself to the CC List to receive automatic issue updates.
I have a TPanel pnlMain, where several dynamic TPanels are created (and pnlMain is their Parent) according to user actions, data validations, etc. Every panel contains one colored grid full of strings. Apart from panels, there are some open source arrows components and a picture. Whole bunch of stuff.
Now I want user to be able to print this panel (I asked how to do it on this question), but before printing, user must be presented with a new form, containing copy of pnlMain. On this form user has to do some changes, add few components and then print his customized copy of pnlMain. After printing user will close this form and return to original form with original pnlMain. And – as you can guess – original pnlMain must remain intact.
So is there any clever way to copy whole TPanel and it’s contents? I know I can make it manually iterating through pnlMain.Controls list.
Code based as iterating on child controls, but not bad in anyway ;-)
procedure TForm1.btn1Click(Sender: TObject);
function CloneComponent(AAncestor: TComponent): TComponent;
var
XMemoryStream: TMemoryStream;
XTempName: string;
begin
Result:=nil;
if not Assigned(AAncestor) then
exit;
XMemoryStream:=TMemoryStream.Create;
try
XTempName:=AAncestor.Name;
AAncestor.Name:='clone_' + XTempName;
XMemoryStream.WriteComponent(AAncestor);
AAncestor.Name:=XTempName;
XMemoryStream.Position:=0;
Result:=TComponentClass(AAncestor.ClassType).Create(AAncestor.Owner);
if AAncestor is TControl then TControl(Result).Parent:=TControl(AAncestor).Parent;
XMemoryStream.ReadComponent(Result);
finally
XMemoryStream.Free;
end;
end;
var
aPanel: TPanel;
Ctrl, Ctrl_: TComponent;
i: integer;
begin
//handle the Control (here Panel1) itself first
TComponent(aPanel) := CloneComponent(pnl1);
with aPanel do
begin
Left := 400;
Top := 80;
end;
//now handle the childcontrols
for i:= 0 to pnl1.ControlCount-1 do begin
Ctrl := TComponent(pnl1.Controls[i]);
Ctrl_ := CloneComponent(Ctrl);
TControl(Ctrl_).Parent := aPanel;
TControl(Ctrl_).Left := TControl(Ctrl).Left;
TControl(Ctrl_).top := TControl(Ctrl).top;
end;
end;
code from Delphi3000 article
Too much code... ObjectBinaryToText and ObjectTextToBinary do the job nicely using streaming.
Delphi 7 have a code example, don't know 2009 (or 2006, never bothered to look) still have it.
See D5 help file for those functions (don't have d5 available here).
I'd do it by using RTTI to copy all the properties. You'd still have to iterate over all the controls, but when you need to set up the property values, RTTI can help automate the process. You can get an example towards the bottom of this article, where you'll find a link to some helper code, including a CopyObject routine.
As an extension of this question:
TForm.OnResize is sometimes fired before a form is first shown, but not always. For example, if BorderStyle is either bsDialog or bsNone, then OnResize will not fire. For all other BorderStyle values (and with all other properties at their defaults), OnResize does fire.
Are there other things that affect whether OnResize will fire before the form is shown? For example, other properties, or combinations of properties, that can affect this?
The OnResize event is a result of the ShowWindow API function sending a WM_SIZE message to the window. That bears repeating: the message is coming from Windows, not from Delphi. It's a Windows function (ShowWindow) that's (sometimes) sending the message that triggers the event -- so the VCL source code is not really helpful in this case.
Bonus points for definitive answers based on documented ShowWindow / WM_SIZE behavior, e.g. references to MSDN documentation or Petzold books.
Maybe it even depend on user's display settings or desktop theme or Windows version. If OnResize were giving me problems like this, I would build my program to always expect it and handle it in any situation, no matter what I think to be the cause.
I believe that OnResize will fire when an event dispatch a message
saying that form size (left, bottom, width, height) will be modified.
Since you already discovered which message fires that event, you need
now trace where the message is sent in the vcl.
Look at the vcl source code to see if you can spot those operations.
Edit: let's go low level. Forms in windows (grossly talking) have what
is called "window class" (it's not a class like we know it oop). All times the window class of the form is resized (and form is visible), the WM_SIZE is sent.
So it will not happen all the times the form is shown, but only the it's dimensions are changed compared with underlying window class.
As you have observed, many properties valuez change the dimensions of the form (even a few pixels).
This is a very superficial explanation, that's a ton of other details - but it's my understanding how things works "under the hood".
There's no substitute for testing. How about creating a form in code, setting the properties you're interested in and recording when the resize event is called.
If you'll excuse the ugliness of the code, here's a rough proof of concept that tests all combinations of BorderStyle and Position without explicitly coding for each one. You can add more properties and take it as far as you like. A tool like CodeSite would make the logging cleaner and easier, too.
Create an application with 2 forms. Make sure the second one isn't auto-created.
In the second form, add a property and add a little logging code to the form's Resize event:
private
FOnResizeFired: TNotifyEvent;
public
property OnResizeFired: TNotifyEvent read FOnResizeFired write FOnResizeFired;
end;
...
procedure TForm2.FormResize(Sender: TObject);
begin
if Assigned(FOnResizeFired) then
FOnResizeFired(self);
end;
In the main form, add TypInfo to the uses clause and drop a button and a memo on the form.
Add a simple procedure:
procedure TForm1.ResizeDetected(Sender: TObject);
begin
Memo1.Lines.Add(' *** Resize detected');
end;
Now add the following to the ButtonClick event:
procedure TForm1.Button1Click(Sender: TObject);
var
lBorderStyle: TFormBorderStyle;
lBorderStyleName: string;
lPosition: TPosition;
lPositionName: string;
lForm: TForm2;
begin
Memo1.Clear;
for lBorderStyle in [low(TFormBorderStyle) .. high(TFormBorderStyle)] do
begin
for lPosition in [low(TPosition) .. high(TPosition)] do
begin
lBorderStyleName := GetEnumName(TypeInfo(TFormBorderStyle), Integer(lBorderStyle));
lPositionName := GetEnumName(TypeInfo(TPosition), Integer(lPosition));
Memo1.Lines.Add(Format('Border: %s Position: %s', [lBorderStyleName, lPositionName]));
Memo1.Lines.Add(' Creating form');
lForm := TForm2.Create(self);
try
Memo1.Lines.Add(' Form Created');
lForm.OnResizeFired := ResizeDetected;
Memo1.Lines.Add(' Setting border style');
lForm.BorderStyle := lBorderStyle;
Memo1.Lines.Add(' Setting Position');
lForm.Position := lPosition;
Memo1.Lines.Add(' Showing form');
lForm.Show;
Memo1.Lines.Add(' Form Shown');
lForm.Close;
Memo1.Lines.Add(' Form Closed');
finally
FreeAndNil(lForm);
Memo1.Lines.Add(' Form Freed');
end;
end;
end;
end;
You'll notice that resize fires when some properties are set before the form is shown, and I see that in some combinations, resize seems to fire twice when the form is shown. Interesting.