3
*****************************************************************************
5
* See the file COPYING.modifiedLGPL.txt, included in this distribution, *
6
* for details about the copyright. *
8
* This program is distributed in the hope that it will be useful, *
9
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
10
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. *
12
*****************************************************************************
14
Authors: Werner Pamler, Alexander Klenin
17
- Add the ChartListbox to a form with a TChart
18
- Connect the chart to the listbox by setting the chart property of the
19
TChartlistbox. The ChartListbox will be populated with the series in the
20
chart and will automatically track changes of the series.
21
- Check/uncheck series in the ChartListbox to show/hide them in the chart.
22
As usual, checking is done by mouse clicks or by pressing the space bar.
23
- Play with the properties to modify the standard behavior, e.g. set
24
CheckStyle to cbsRadioButton in order to get a radiobutton behavior, i.e. to
25
allow only one visible series.
35
Classes, Controls, StdCtrls,
36
TAChartUtils, TACustomSeries, TALegend, TAGraph;
39
TChartListbox = class;
41
TChartListboxIndexEvent = procedure (
42
ASender: TObject; AIndex: Integer) of object;
44
TChartListboxAddSeriesEvent = procedure (
45
ASender: TChartListbox; ASeries: TCustomChartSeries;
46
AItems: TChartLegendItems; var ASkip: Boolean
49
TCheckBoxesStyle = (cbsCheckbox, cbsRadiobutton);
51
TChartListOption = (cloShowCheckboxes, cloShowIcons, cloRefreshOnSourceChange);
52
TChartListOptions = set of TChartListOption;
55
SHOW_ALL = [cloShowCheckboxes, cloShowIcons];
58
TChartListbox = class(TCustomListbox)
61
FCheckStyle: TCheckBoxesStyle;
62
FLegendItems: TChartLegendItems;
64
FOnAddSeries: TChartListboxAddSeriesEvent;
65
FOnCheckboxClick: TChartListboxIndexEvent;
66
FOnItemClick: TChartListboxIndexEvent;
67
FOnPopulate: TNotifyEvent;
68
FOnSeriesIconDblClick: TChartListboxIndexEvent;
69
FOptions: TChartListOptions;
70
FSeriesIconClicked: Integer;
71
function GetChecked(AIndex: Integer): Boolean;
72
function GetSeries(AIndex: Integer): TCustomChartSeries;
73
function GetSeriesCount: Integer;
74
procedure EnsureSingleChecked(AIndex: Integer = -1);
75
procedure SetChart(AValue: TChart);
76
procedure SetChecked(AIndex: Integer; AValue: Boolean);
77
procedure SetCheckStyle(AValue: TCheckBoxesStyle);
78
procedure SetOnAddSeries(AValue: TChartListboxAddSeriesEvent);
79
procedure SetOnPopulate(AValue: TNotifyEvent);
80
procedure SetOptions(AValue: TChartListOptions);
83
procedure DblClick; override;
85
AIndex: Integer; ARect: TRect; AState: TOwnerDrawState); override;
86
procedure KeyDown(var AKey: Word; AShift: TShiftState); override;
88
AButton: TMouseButton; AShift: TShiftState; AX, AY: Integer); override;
91
const AItemRect: TRect; out ACheckboxRect, ASeriesIconRect: TRect);
92
procedure ClickedCheckbox(AIndex: Integer); virtual;
93
procedure ClickedItem(AIndex: Integer); virtual;
94
procedure ClickedSeriesIcon(AIndex: Integer); virtual;
95
function CreateLegendItems: TChartLegendItems;
99
constructor Create(AOwner: TComponent); override;
100
destructor Destroy; override;
101
function FindSeriesIndex(ASeries: TCustomChartSeries): Integer;
102
procedure MeasureItem(AIndex: Integer; var AHeight: Integer); override;
103
procedure RemoveSeries(ASeries: TCustomChartSeries);
104
procedure SeriesChanged(ASender: TObject);
106
property Checked[AIndex: Integer]: Boolean read GetChecked write SetChecked;
107
property Series[AIndex: Integer]: TCustomChartSeries read GetSeries;
108
property SeriesCount: Integer read GetSeriesCount;
111
property Chart: TChart read FChart write SetChart;
112
property CheckStyle: TCheckBoxesStyle
113
read FCheckStyle write SetCheckStyle default cbsCheckbox;
114
property Options: TChartListOptions
115
read FOptions write SetOptions default SHOW_ALL;
117
property OnAddSeries: TChartListboxAddSeriesEvent
118
read FOnAddSeries write SetOnAddSeries;
119
property OnCheckboxClick: TChartListboxIndexEvent
120
read FOnCheckboxClick write FOnCheckboxClick;
121
property OnItemClick: TChartListboxIndexEvent
122
read FOnItemClick write FOnItemClick;
123
property OnPopulate: TNotifyEvent read FOnPopulate write SetOnPopulate;
124
property OnSeriesIconDblClick: TChartListboxIndexEvent
125
read FOnSeriesIconDblClick write FOnSeriesIconDblClick;
128
// property AllowGrayed;
131
property BorderSpacing;
132
property BorderStyle;
135
property Constraints;
139
property ExtendedSelect;
141
property IntegralHeight;
144
property MultiSelect;
145
property OnChangeBounds;
147
// property OnClickCheck;
148
property OnContextPopup;
156
// property OnItemClick;
160
property OnMouseDown;
161
property OnMouseEnter;
162
property OnMouseLeave;
163
property OnMouseMove;
165
property OnMouseWheel;
166
property OnMouseWheelDown;
167
property OnMouseWheelUp;
170
property OnStartDrag;
171
property OnUTF8KeyPress;
172
property ParentBidiMode;
174
property ParentShowHint;
191
Graphics, Math, LCLIntf, LCLType, SysUtils, Themes,
192
TACustomSource, TADrawerCanvas, TADrawUtils, TAEnumerators, TAGeometry;
196
RegisterComponents(CHART_COMPONENT_IDE_PAGE, [TChartListbox]);
199
constructor TChartListbox.Create(AOwner: TComponent);
201
inherited Create(AOwner);
202
Style := lbOwnerDrawFixed;
203
FListener := TListener.Create(@FChart, @SeriesChanged);
204
FOptions := SHOW_ALL;
207
destructor TChartListbox.Destroy;
209
FreeAndNil(FListener);
210
FreeAndNil(FLegendItems);
214
procedure TChartListbox.CalcRects(
215
const AItemRect: TRect; out ACheckboxRect, ASeriesIconRect: TRect);
216
{ based on the rect of a listbox item, calculates the locations of the
217
checkbox and of the series icon }
221
ACheckBoxRect := Rect(-1, -1, -1, -1);
222
ASeriesIconRect := Rect(-1, -1, -1, -1);
223
w := GetSystemMetrics(SM_CYMENUCHECK);
225
if cloShowCheckboxes in Options then begin
227
ACheckboxRect := Bounds(Left + 1, (Top + Bottom - w) div 2, w, w);
228
if cloShowIcons in Options then
229
x += ACheckboxRect.Right;
232
if cloShowIcons in Options then
235
if cloShowIcons in Options then
236
ASeriesIconRect := Rect(
237
x, AItemRect.Top + 2, x + FChart.Legend.SymbolWidth, AItemRect.Bottom - 2);
240
procedure TChartListbox.ClickedCheckbox(AIndex: Integer);
242
Checked[AIndex] := not Checked[AIndex];
243
if Assigned(OnCheckboxClick) then
244
OnCheckboxClick(Self, AIndex);
247
procedure TChartListbox.ClickedItem(AIndex: Integer);
249
if Assigned(OnItemClick) then
250
OnItemClick(Self, AIndex);
253
procedure TChartListbox.ClickedSeriesIcon(AIndex: Integer);
255
if Assigned(OnSeriesIconDblClick) then
256
OnSeriesIconDblClick(Self, AIndex);
259
function TChartListbox.CreateLegendItems: TChartLegendItems;
260
{ creates the a TLegendItems list and populates it with information for
261
all series contained in the Chart. In case of MultiLegend items, only
262
a single legend item is used }
265
s: TCustomChartSeries;
267
Result := TChartLegendItems.Create;
269
if FChart = nil then exit;
270
for s in CustomSeries(Chart) do begin
271
if Assigned(OnAddSeries) then begin
273
FOnAddSeries(Self, s, Result, skip);
274
if skip then continue;
276
s.GetSingleLegendItem(Result);
284
procedure TChartListbox.DblClick;
287
if FSeriesIconClicked <> -1 then
288
ClickedSeriesIcon(FSeriesIconClicked);
291
procedure TChartListbox.DrawItem(
292
AIndex: Integer; ARect: TRect; AState: TOwnerDrawState);
293
{ draws the listbox item }
295
UNTHEMED_FLAGS: array [TCheckboxesStyle, Boolean] of Integer = (
296
(DFCS_BUTTONCHECK, DFCS_BUTTONCHECK or DFCS_CHECKED),
297
(DFCS_BUTTONRADIO, DFCS_BUTTONRADIO or DFCS_CHECKED)
299
THEMED_FLAGS: array [TCheckboxesStyle, Boolean] of TThemedButton = (
300
(tbCheckBoxUncheckedNormal, tbCheckBoxCheckedNormal),
301
(tbRadioButtonUnCheckedNormal, tbRadioButtonCheckedNormal)
306
te: TThemedElementDetails;
310
if Assigned(OnDrawItem) then begin
311
OnDrawItem(Self, AIndex, ARect, AState);
315
(odPainted in AState) or (FChart = nil) or not InRange(AIndex, 0, Count - 1)
318
Canvas.FillRect(ARect);
319
CalcRects(ARect, rcb, ricon);
321
if cloShowCheckboxes in Options then begin
322
ch := Checked[AIndex];
323
if ThemeServices.ThemesEnabled then begin
324
te := ThemeServices.GetElementDetails(THEMED_FLAGS[FCheckStyle, ch]);
325
ThemeServices.DrawElement(Canvas.Handle, te, rcb);
329
Canvas.Handle, rcb, DFC_BUTTON, UNTHEMED_FLAGS[FCheckStyle, ch]);
335
Canvas.Brush.Style := bsClear;
336
if cloShowIcons in Options then begin
337
id := TCanvasDrawer.Create(Canvas);
338
id.Pen := Chart.Legend.SymbolFrame;
339
FLegendItems[AIndex].Draw(id, ricon);
342
Canvas.TextOut(x + 2, ARect.Top, FLegendItems.Items[AIndex].Text);
345
procedure TChartListbox.EnsureSingleChecked(AIndex: Integer);
348
ser: TCustomChartSeries;
350
if (FCheckStyle <> cbsRadioButton) or not (cloShowCheckboxes in Options) then
352
FListener.OnNotify := nil;
354
for i := 0 to FLegendItems.Count - 1 do begin
356
if ser = nil then continue;
357
if (AIndex < 0) and ser.Active then
360
ser.Active := AIndex = i;
363
FListener.OnNotify := @SeriesChanged;
367
function TChartListbox.FindSeriesIndex(ASeries: TCustomChartSeries): Integer;
368
{ searches the internal legend items list for the specified series }
370
for Result := 0 to FLegendItems.Count - 1 do
371
if GetSeries(Result) = ASeries then exit;
375
function TChartListbox.GetChecked(AIndex: Integer): Boolean;
376
{ report the checked status. This is determined by the visibility of the
377
series with the given index. }
379
ser: TBasicChartSeries;
381
ser := GetSeries(AIndex);
382
Result := (ser <> nil) and ser.Active;
385
function TChartListbox.GetSeries(AIndex: Integer): TCustomChartSeries;
386
{ extracts, for the given index, the series from the internal
389
legitem: TLegendItem;
391
legitem := FLegendItems[AIndex];
392
if (legitem <> nil) and (legitem.Owner is TCustomChartSeries) then
393
Result := TCustomChartSeries(legitem.Owner)
398
function TChartListbox.GetSeriesCount : Integer;
399
{ determines the number of series displayed in the listbox.
400
Note that this may be different from the Chart.SeriesCount if
401
RemoveSeries has been called }
403
Result := FLegendItems.Count;
406
procedure TChartListbox.KeyDown(var AKey: Word; AShift: TShiftState);
407
{ allows checking/unchecking of items by means of pressing the space bar }
410
(AKey = VK_SPACE) and (AShift = []) and
411
(cloShowCheckboxes in Options) and (ItemIndex >= 0)
413
ClickedCheckbox(ItemIndex);
417
inherited KeyDown(AKey, AShift);
420
// Item height is determined as maximum of:
421
// checkbox height, text height, ItemHeight property value.
422
procedure TChartListbox.MeasureItem(AIndex: Integer; var AHeight: Integer);
424
inherited MeasureItem(AIndex, AHeight);
425
AHeight := Max(CalculateStandardItemHeight, AHeight);
426
if cloShowCheckboxes in Options then
427
AHeight := Max(AHeight, GetSystemMetrics(SM_CYMENUCHECK) + 2);
430
procedure TChartListbox.MouseDown(
431
AButton: TMouseButton; AShift: TShiftState; AX, AY: Integer);
432
{ standard MouseDown handler: checks if the click occured on the checkbox,
433
on the series icon, or on the text.
434
The visibility state of the item's series is changed when clicking on the
435
checkbox, and an event OnCheckboxClick is generated.
436
An event OnSeriesIconClick is generated when double-clicking on the
437
series icon; the method stores the series list index here.
438
An event OnItemClick is generated when the click occured neither on the
439
checkbox nor the series icon.
446
FSeriesIconClicked := -1;
448
if AButton <> mbLeft then exit;
450
index := GetIndexAtXY(AX, AY);
451
if index < 0 then exit;
452
CalcRects(ItemRect(index), rcb, ricon);
453
if (cloShowCheckboxes in Options) and IsPointInRect(p, rcb) then
454
ClickedCheckbox(index)
455
else if (cloShowIcons in Options) and IsPointInRect(p, ricon) then
456
// Remember clicked index for the double click event.
457
FSeriesIconClicked := index
461
inherited MouseDown(AButton, AShift, AX, AY);
465
procedure TChartListbox.Populate;
466
{ populates the listbox with all series contained in the chart. Use the event
467
OnPopulate if you don't omit special series from the listbox (RemoveSeries) }
474
if FChart = nil then exit;
475
FreeAndNil(FLegendItems);
476
FLegendItems := CreateLegendItems;
477
Chart.Legend.SortItemsByOrder(FLegendItems);
478
for li in FLegendItems do
479
// The caption is owner-drawn, but add it anyway for user convenience.
480
Items.AddObject(li.Text, li);
481
if Assigned(OnPopulate) then
488
procedure TChartListbox.RemoveSeries(ASeries: TCustomChartSeries);
489
{ removes the series from the listbox, but keeps it in the chart }
493
index := FindSeriesIndex(ASeries);
494
if index = -1 then exit;
495
FLegendItems.Delete(index);
500
procedure TChartListbox.SeriesChanged(ASender: TObject);
501
{ Notification procedure of the listener. Responds to chart broadcasts
502
by populating the listbox with the chart's series }
505
(ASender is TCustomChartSource) and
506
not (cloRefreshOnSourceChange in Options)
510
{ in case of radiobutton mode, it is necessary to uncheck the other
511
series; there can be only one active series in this mode }
513
(ASender is TCustomChartSeries) and (ASender as TCustomChartSeries).Active
515
EnsureSingleChecked(FindSeriesIndex(ASender as TCustomChartSeries))
520
procedure TChartListbox.SetChart(AValue: TChart);
521
{ connects the ListBox to the chart }
523
if FChart = AValue then exit;
525
if FListener.IsListening then
526
FChart.Broadcaster.Unsubscribe(FListener);
528
if FChart <> nil then
529
FChart.Broadcaster.Subscribe(FListener);
533
procedure TChartListbox.SetChecked(AIndex: Integer; AValue: Boolean);
534
{ shows/hides the series with the specified index of its listbox item.
535
In case of radiobutton style, all other series are hidden if AValue=true }
537
ser: TCustomChartSeries;
539
ser := GetSeries(AIndex);
540
if (ser = nil) or (ser.Active = AValue) then exit;
541
// Do not listen to this change since we know what changed.
542
FListener.OnNotify := nil;
544
ser.Active := AValue;
546
FListener.OnNotify := @SeriesChanged;
549
EnsureSingleChecked(FindSeriesIndex(ser));
553
procedure TChartListbox.SetCheckStyle(AValue: TCheckBoxesStyle);
554
{ selects "checkbox" or "radiobutton" styles. In radiobutton mode, only
555
one series can be visible }
557
if FCheckStyle = AValue then exit;
558
FCheckStyle := AValue;
563
procedure TChartListbox.SetOnAddSeries(AValue: TChartListboxAddSeriesEvent);
565
if TMethod(FOnAddSeries) = TMethod(AValue) then exit;
566
FOnAddSeries := AValue;
570
procedure TChartListbox.SetOnPopulate(AValue: TNotifyEvent);
572
if TMethod(FOnPopulate) = TMethod(AValue) then exit;
573
FOnPopulate := AValue;
577
procedure TChartListbox.SetOptions(AValue: TChartListOptions);
579
if FOptions = AValue then exit;