How to create multiline legend in teechart? - legend

Does anybody know if there a way to write multiline legend of a chart?
I've tried to add TeeLineSeparator or #13, and it doesn't work?
Thanks very much

I'm afraid not in the current legend. The alternatives are to use the CustomLegend tool TeeChart Pro provides or to directly draw your shapes and strings in the OnAfterDraw event using custom drawing techniques. Ie:
uses Series, TeCanvas;
procedure TForm1.FormCreate(Sender: TObject);
var i: Integer;
begin
Chart1.Legend.Visible:=false;
Chart1.MarginRight:=22;
for i:=0 to 4 do
with Chart1.AddSeries(TBarSeries) as TBarSeries do
begin
Title:='Long title in Series number ' + IntToStr(i);
FillSampleValues;
Marks.Visible:=false;
MultiBar:=mbStacked;
end;
end;
procedure TForm1.Chart1AfterDraw(Sender: TObject);
var tmpHeight, i: Integer;
begin
tmpHeight:=Chart1.SeriesCount*33;
with Chart1.Canvas do
begin
Rectangle(Chart1.Width-130, 50, Chart1.Width-10, 50+tmpHeight);
for i:=0 to Chart1.SeriesCount-1 do
begin
Brush.Color:=Chart1[i].Color;
Rectangle(Chart1.Width-120, 65+i*30, Chart1.Width-120+15, 65+i*30+15);
TextOut(Chart1.Width-100, 60+i*30, WrapText(Chart1[i].Title, #13#10, ['.',' ',#9,'-'], 15));
end;
end;
end;

Related

Adressing multiple, similar variables/objects in Delphi

I am writing a program that uses many shapes and I need to create a procedure to turn them all white. The shapes in question are named SectorBorder1 to SectorBorder20.
Is there a way to adress the shapes like this or similarly?
SectorBorder[X].brush.color := ClWhite;
Inc(X);
...where X is the number (obviously), instead of having to do:
SectorBorder1.brush.color := ClWhite;
SectorBorder2.brush.color := ClWhite;
...
SectorBorder20.brush.color := ClWhite;
So basically being able to differentiate names through a variable. This is the only way I could think of describing it. (Sorry, could someone also maybe include a better discription?) Any advice would be greatly apreciated.
Use an array
private
SectorBorders: array[1..20] of TShape;
procedure TMyForm.FormCreate(Sender: TObject):
begin
SectorBorders[1] := SectorBorder1;
..
SectorBorders[20] := SectorBorder20;
end;
procedure TMyForm.SetAllToWhite;
var
X: Integer;
begin
for X := Low(SectorBorders) to High(SectorBorders) do
SectorBorders[X].Brush.Color := clWhite;
end;
As an alternative to an array you can use a List. If you use the TList from System.Generics.Containers you can easily pass through all elements of the list.
You use it like this:
interface
uses System.Generics.Collections; // you need this to get the TList, you also need your other refereces of course
...
protected
pShapes: TList<TShape>;
procedure TMyForm.FormCreate(Sender: TObject):
var
nLoop: Integer;
begin
Self.pShapes:=TList<TShape>.Create;
nLoop:=0;
while(nLoop<Self.ComponentCount) do
begin
if(Self.Components[nLoop] is TShape) then
pList.Add(TShape(Self.Components[nLoop]));
Inc(nLoop);
end;
end;
procedure TMyForm.SetAllToWhite;
var
pShape: TShape;
begin
for pShape in Self.pShapes do
pShape.Brush.Color := clWhite;
end;
Choosing whether you want to use an array or a TList will be partially down to preference, but also down to what else you may want to do with the collection of TShapes and how they are managed within the object.
You can use the FindComponent method of the form.
shape:= FindComponent('SectorBorder' + IntToStr(i)) as TShape;
if shape <> nil then
pShape.Brush.Color := clWhite;
See http://docwiki.embarcadero.com/CodeExamples/Sydney/en/FindComponent_(Delphi)

Is possible to set filter option in SMDBGrid to CaseInsensitive?

I have SMDBGrid component with show filter bar option set to true, but filter just working in case-sensitive mode
1.Try with lower case
2.Try with upper case
I have tried to insert the code in SMDBgrid.pas like this
procedure TSMDBGrid.ApplyFilter;
var
TempCol: Integer;
begin
if (eoFilterAutoApply in ExOptions) then
begin
TempCol := LeftCol;
BeginUpdate;
try
if DataLink.Active then
begin
DataLink.DataSet.CheckBrowseMode;
DataLink.DataSet.Filtered := False;
DataLink.DataSet.OnFilterRecord := nil;
DataLink.DataSet.OnFilterRecord := OnFilterRecord;
DataLink.DataSet.FilterOptions := [foCaseInsensitive]; <-- this the inserted code
DataLink.DataSet.Filtered := not FilterIsEmpty();//True;
end;
finally
LeftCol := TempCol;
EndUpdate;
end;
end;
if Assigned(OnFilterChanged) then
OnFilterChanged(Self);
end;
But no luck, Is posible filter the record ignoring the case?
PS:
I use Delphi 2009
You may use the OnAccentStringConvert event to transform the value for filter in column before compare:
begin
Result := UpperCase(S)
end;
Looks like I cope with this problem too. Trying to find any solution for Delphi XE 10.3 community edition and wrote to author of SMDBGrid and he found workaround.
Please use SQL ADOQuery as follows.
SELECT UPPER(field) FROM your_table
then use event OnAccentStringConvert and uppercase S String as follows:
function TYourFormName.DBGrridNameAccentStringConvert(Sender: TObject; const S: string): string;
begin
Result := UpperCase(S)
end;
This works very ugly, but at least works. Or you may just create filter by yourself for every table.

delphi - Save multiple records at once

Dont know how to formulate this exactly so bear with me please... I am saving text from a memo to a database with date selected in the PlannerCalendar1. Since I can select multiple dates in the PlannerCalendar1, how can I post the value of the memo to all dates selected in the PlannerCalendar1?So when I click 'save' the contents of the memo gets saved to all selected dates.Database is SQLite. The table also has an ID field which is autoinc (primary).PlannerCalendar is from the set of TMS components.
procedure TForm1.cxButton1Click(Sender: TObject);
var i:integer;
begin
with UniQuery1 do
begin
UniQuery1.SQL.Text:='INSERT INTO LOG (DATE,PERSON,DONE,TIME) VALUES (:a1,:a2,:a3,:a4)';
UniQuery1.PARAMS.ParamByName('A1').VALUE := PlannerCalendar1.Date;
UniQuery1.PARAMS.ParamByName('A2').VALUE := cxmemo1.Lines.text ;
UniQuery1.PARAMS.ParamByName('A3').VALUE := (0);
UniQuery1.PARAMS.ParamByName('A4').Value := AdvOfficeStatusBar1.Panels[0].Text;
UniQuery1.ExecSQL;
cxmemo1.Clear;
UniTable1.Refresh;
Tried this at the end but it wont work :
with plannercalendar1.Dates do
begin
for i := 0 to -1 do
begin
UniQuery1.PARAMS.ParamByName('A1').VALUE :=plannercalendar1.dates.Add + i ;
UniQuery1.ExecSQL;
end;
I have no idea what a PlannerCalendar is, but presumably there's some way to get at the list of dates that are selected. You want to do something like this:
UniQuery1.SQL.Text:='INSERT INTO LOG (DATE,PERSON,DONE,TIME) VALUES (:a1,:a2,:a3,:a4)';
UniQuery1.PARAMS.ParamByName('A2').VALUE := cxmemo1.Lines.text ;
UniQuery1.PARAMS.ParamByName('A3').VALUE := (0);
UniQuery1.PARAMS.ParamByName('A4').Value := AdvOfficeStatusBar1.Panels[0].Text;
for i := 0 to PlannerCalendar1.NumberOfDatesSelected-1 do begin
UniQuery1.PARAMS.ParamByName('A1').VALUE := PlannerCalendar1.SelectedDate[i];
UniQuery1.ExecSQL;
end;
Of course, NumberOfDatesSelected and SelectedDate are wild guesses. You'll need to find out what they're really called.
You need to use the Planner's SelectionToAbsTime method :-
Var
lStart, lEnd : TDateTime;
Begin
Planner1.SelectionToAbsTime(lStart, lEnd);
For I := Trunc(lStart) To Trunc(lEnd) Do
SaveMemosForDate(I);
End;

Variable structure for database results

A lot of times when we query the database, we just need one column with varchar.
So I've made a nice function for querying the database and putting the results in a stringlist:
function Getdatatostringlist(sqlcomponent, sqlquery: string): TStringlist;
What I'm looking for now is basically the same function but for results with multiple columns where you don't know in advance what type the data is, be it varchar, int, datetime.
What kind of datastructure would be good to use here.
The reason I want this is that I try not to work on open datasets. I like much more to fetch all results into a temporary structure, close the dataset and work on the results.
After Kobiks reply about using in Memory datasets I came up with the following, it's fast put together to test the concept:
procedure TForm1.Button2Click(Sender: TObject);
var
MyDataSet : TAdoDataSet;
begin
MyDataSet := GetDataToDataSet('SELECT naam FROM user WHERE userid = 1', ADOConnection1);
try
Form1.Caption := MyDataSet.FieldByName('naam').AsString;
finally
MyDataSet.free;
end;
end;
function TForm1.GetDataToDataSet(sSql: string; AdoConnection: TADOConnection): TAdoDataSet;
begin
Result := TAdoDataSet.Create(nil);
Result.LockType := ltBatchOptimistic;
Result.Connection := AdoConnection;
Result.CommandText := sSql;
Result.Open;
Result.Connection := nil;
end;
I think this is something to build on.
You should use any disconnected in-memory TDataSet descendant, such as TClientDataSet.
Do not attempt to re-invent the wheel by storing a record-set in some new "Variant" structure. A TClientDataSet already contains all features you need to manipulate a "temporary" data structure.
Here is how you create a TClientDataSet structure:
cds.FieldDefs.Add('id', ftInteger);
cds.FieldDefs.Add('name', ftString, 100);
// ...
// create it
cds.CreateDataSet;
// add some data records
cds.AppendRecord([1, 'Foo']);
cds.AppendRecord([2, 'Bar']);
Many TDataSets has an ability to be used as an in-memory (client) datasets depending on the provider and LockType, for example a TADODataSet with LockType=ltBatchOptimistic could fetch results-set from the server, and then remain disconnected.
For exchanging Data with Excel this structure is usefull, might be useful for other purposes.
Function GetDatasetasDynArray(Ads: TDataset; WithHeader: Boolean = true): Variant;
// 20130118 by Thomas Wassermann
var
i, x, y: Integer;
Fields: Array of Integer;
begin
x := 0;
y := Ads.RecordCount;
if WithHeader then
inc(y);
SetLength(Fields, Ads.FieldCount);
for i := 0 to Ads.FieldCount - 1 do
if Ads.Fields[i].Visible then
begin
Fields[x] := i;
inc(x);
end;
SetLength(Fields, x);
Result := VarArrayCreate([0, y - 1 , 0, length(Fields) - 1], VarVariant);
y := 0;
if WithHeader then
begin
for i := Low(Fields) to High(Fields) do
begin
Result[y, i] := Ads.Fields[Fields[i]].DisplayLabel;
end;
inc(y);
end;
try
Ads.DisableControls;
Ads.First;
while not Ads.EOF do
begin
for i := Low(Fields) to High(Fields) do
begin
Result[y, i] := Ads.Fields[Fields[i]].Value;
end;
Ads.Next;
inc(y);
end;
finally
Ads.EnableControls;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
DynArray:Variant;
begin
DynArray := GetDatasetasDynArray(Adodataset1,true);
//DynArray[0,x] Header or First row
//DynArray[1,x] First row or SecondRow
Excel.Range.Value := DynArray;
end;
Why don't you like working with open datasets? They usually do not block the server. Copying the data from the dataset to whatever you want is extra overhead which is most likely not necessary.
A dataset provides exactly the functionality you want: A matrix with variable columns and rows.
EDIT: However, if you have iterate through the dataset often, you should consider creating a class holding the relevant information and then copy the data into a generic list, dictionary, tree or whatever you need as fast lookup structure.
Of course you could think of building something smart which can be as flexible as a dataset but: The more general things get, the poorer the performance (usually).

dynamically create popup menu tree from sql server table in Delphi

I have a table like this:
id parent_id name
1 1 Root
2 1 Car
3 1 Plane
4 2 BMW
5 4 CLK
How can I dynamically create popup menu with all subitems in Delphi?
This is how it should look like:
Assuming root element has NULL as Parent_ID you can issue the request
Select ID, Parent_ID, Name from all_my_menus
order by Parent_ID nulls first, ID
where Menu_ID = :MenuIDParameter
1 <NULL> Root
8 <NULL> another root
2 1 Car
4 1 Plane
3 2 BMW
5 4 CLK
You would also cache in-memory created menu items: var MI_by_id: TDictionary<integer, TMenuItem>;
The traversing through the results would look like
var MI: TMenuItem;
MI_by_id: TDictionary<integer, TMenuItem>;
begin
MI_by_id := TDictionary<integer, TMenuItem>.Create;
try
While not Query.EOF do begin
MI := TMenuItem.Create(Self);
MI.Caption := Query.Fields[2].AsString;
MI.Tag := Query.Fields[0].AsInteger; // ID, would be helpful for OnClick event
MI.OnClick := ...some click handler
if Query.Fields[1].IsNull {no parent}
then MainMenu.Items.Add(MI)
else MI_by_id.Items[Query.Fields[1].AsInteger].Add(MI);
MI_by_id.Add(MI.Tag, MI); //save shortcut to potential parent for future searching
Query.Next;
end;
finally
MI_by_id.Free;
end;
end;
Actually, since we made sort upon Parent_ID on the query, all the children for given parent make single continuous list, so could be better to remove populated parents from the dictionary after we populated last child (i.e. after parent_ID got new value) and caching previously found parent otherwise in another local variable (instead of making yet another search through the dictionary).
However reasonable size for human-targeted menu should be much less to worth this. But you have to understand this approach most probably scales as O(n*n) thus would start loose speed very fast as the table grows.
http://docwiki.embarcadero.com/Libraries/XE3/en/Vcl.Menus.TMenuItem.Add
http://docwiki.embarcadero.com/CodeExamples/XE2/en/Generics.Collections.TDictionary_(Delphi)
Note: this also requires that for every non-root element ID > ParentID (put CHECK CONSTRAINT on the table)
1 <NULL> Root
8 <NULL> another root
7 1 Plane
3 4 BMW
4 7 CLK
5 8 Car
This would lead to BMW tied to create before its parent CLK created.
Violation for that conditions can be overcome by few means:
recursive load: select <items> where Parent_id is null, then for each of the added menu items do select <items> where Parent_id = :current_memuitem_id and so on that. This is like VirtualTreeView would work
ask SQL server to sort and flatten the tree - this is usually called self-recursive SQL selection and is server-dependant.
introduce one more collection variable - menu items w/o parent. After each new item added to the menu this collection should be searched if there are pending children to extract from it and move into the newly created parent.
Too many solutions for such a simple problem. Too bad you got ordered ID's because without ordered ID's things would have been more fun. Here's my own solution. On an empty form drop a button, a TClientDataSet and a TPopupMenu. Make the form's PopupMenu = PopupMenu1 so you can see the result. Add this to Button1.OnClick:
Note: I'm intentionally using TClientDataSet and not a real Query. This question is not about the query and this solution works with whatever TDataSet descendant you throw at it. Just make sure the result set is ordered on id, or else you could see the child nodes before the parents. Also note, half the code is used to fill up the ClientDataSet with the sample data in the question!
procedure TForm16.Button1Click(Sender: TObject);
var Prev: TDictionary<Integer, TMenuItem>; // We will use this to keep track of previously generated nodes so we do not need to search for them
CurrentItem, ParentItem: TMenuItem;
begin
if not ClientDataSet1.Active then
begin
// Prepare the ClientDataSet1 structure
ClientDataSet1.FieldDefs.Add('id', ftInteger);
ClientDataSet1.FieldDefs.Add('parent_id', ftInteger);
ClientDataSet1.FieldDefs.Add('name', ftString, 100);
ClientDataSet1.CreateDataSet;
// Fill the dataset
ClientDataSet1.AppendRecord([1, 1, 'Root']);
ClientDataSet1.AppendRecord([2, 1, 'Car']);
ClientDataSet1.AppendRecord([3, 1, 'Plane']);
ClientDataSet1.AppendRecord([4, 2, 'BMW']);
ClientDataSet1.AppendRecord([5, 4, 'CLK']);
end;
// Clear the existing menu
PopupMenu1.Items.Clear;
// Prepare the loop
Prev := TDictionary<Integer, TMenuItem>.Create;
try
ClientDataSet1.First; // Not required for a true SQL Query, only required here for re-entry
while not ClientDataSet1.Eof do
begin
CurrentItem := TMenuItem.Create(Self);
CurrentItem.Caption := ClientDataSet1['name'];
if (not ClientDataSet1.FieldByName('parent_id').IsNull) and Prev.TryGetValue(ClientDataSet1['parent_id'], ParentItem) then
ParentItem.Add(CurrentItem)
else
PopupMenu1.Items.Add(CurrentItem);
// Put the current Item in the dictionary for future reference
Prev.Add(ClientDataSet1['id'], CurrentItem);
ClientDataSet1.Next;
end;
finally Prev.Free;
end;
end;
Try this
procedure TForm1.MyPopup(Sender: TObject);
begin
with Sender as TMenuItem do ShowMessage(Caption);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
MyItem,MySubItem1: TMenuItem;
begin
Inc(Num);
MyItem:=TMenuItem.Create(Self);
MySubItem1:=TMenuItem.Create(Self);
MyItem.Caption:='Hello'+IntToStr(Num);
MySubItem1.Caption:='Good Bye'+IntToStr(Num);
MainMenu1.Items.Add(MyItem);
MainMenu1.Items[0].Insert(num-1,MySubItem1);
MyItem.OnClick:=MyPopUp;
MySubItem1.OnClick:=MyPopUp;
end;
Taken from http://www.greatis.com/delphicb/tips/lib/components-addmenuitem.html
This solution requires parent_id of root to be 0, tested with
Select 1 as ID, 0 as Parent_ID, 'Root' as Name
union
Select 2, 1, ' Car'
union
Select 3 , 1, 'Plane'
union
Select 4, 2, 'BMW'
union
Select 5, 4, 'CLK'
should by optimized, have just a lack of time ...
Function GetMenu(pop:TPopupmenu;ID:Integer):TMenuItem;
var
i:Integer;
Function CheckItem(mi:TMenuItem):TMenuItem;
var
i:Integer;
begin
Result := nil;
if mi.Name = 'DYN_' + INtToStr(ID) then Result := mi
else for i := 0 to mi.Count-1 do
if not Assigned(Result) then Result := CheckItem(mi[i]);
end;
begin
Result := nil;
for i := 0 to pop.Items.Count-1 do
begin
if not Assigned(Result) then Result := CheckItem(pop.Items[i]);
if Assigned(Result) then Break;
end;
end;
Function InsertMenuItem(pop:TPopupMenu;mi:TMenuItem;ID:Integer;Const caption:String):TMenuItem;
begin
Result := TMenuItem.Create(pop);
Result.Caption := caption;
Result.Name := 'DYN_' + INtToStr(ID) ;
if not Assigned(mi) then pop.Items.Add(Result) else mi.Add(Result);
end;
Function AddMenuItem(pop:TPopupmenu;ID:Integer;Ads:TDataset):TMenuItem;
begin
Ads.Locate('ID',ID,[]);
Result := GetMenu(pop,id);
if (not Assigned(Result)) then
begin
if (Ads.FieldByName('parent_ID').AsInteger<>0) then
begin
result := AddMenuItem(pop,Ads.FieldByName('parent_ID').AsInteger,Ads);
Ads.Locate('ID',ID,[]);
end;
Result := InsertMenuItem(pop,Result,ID,Ads.FieldByName('Name').AsString);
end;
Ads.Locate('ID',ID,[]);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
while not ADS.Eof do
begin
AddMenuItem(Popupmenu1,ads.FieldByName('ID').AsInteger,Ads);
Ads.Next
end;
end;
Interesting conundrum ...another late night thought, a practical answer for re-use :)
Make a derived component:
type
TCascadeMenuItem = class(TMenuItem)
private
Id: Integer;
public
function AddItem(const ToId, WithId: Integer; AName: string): Boolean;
end;
with code
function TCascadeMenuItem.AddItem(const ToId, WithId: Integer; AName: string): Boolean;
var
i: Integer;
cmi: TCascadeMenuItem;
begin
if ToId = Id then
begin
cmi := TCascadeMenuItem.Create(Owner);
cmi.Caption := AName;
cmi.Id := WithId;
Add(cmi);
Result := True;
end
else begin
i := 0;
Result := False;
while (i < Count) and (not Result) do
begin
Result := TCascadeMenuItem(Items[i]).AddItem(ToId,WithId, ANAme);
inc(i);
end;
end;
end;
Main form, Assumes your data:
procedure TForm4.Button2Click(Sender: TObject);
var
mi: TCascadeMenuItem;
i: Integer;
Added: Boolean;
begin
cds1.First;
while not cds1.Eof do
begin
i := 0;
Added := False;
while (i < pup.Items.Count) and (not Added) do
begin
Added := TCascadeMenuItem(pup.Items[i]).AddItem(cds1Parent_Id.AsInteger, cds1id.AsInteger, cds1name.AsString);
inc(i);
end;
if not Added then
begin // new root
mi := TCasCadeMenuItem.Create(Self);
mi.Caption := cds1name.AsString;
mi.id := cds1Parent_Id.AsInteger;
pup.Items.Add(mi);
end;
cds1.Next;
end;
end;
You could derive a TCascasePopupMenu and put it on the palette :)