Unit SmbGridFull; { This was copied from PointsTopListGrid.pas (rev 1.1). Initially this is the same idea but with different data. This is intended to display all fields that we track, as used in debugging. The final version will have a lot fewer fields, and different fields for each window. At some point we want to add some special features, like the ability to slow or stop the sorting, and the ability to show some fields graphically and/or in color. } interface Uses Controls, Grids, Classes, Types, Messages, Graphics; Type TBitmap = Graphics.TBitmap; // If a string is missing or invalid, it should be set to ''. If a double // is missing or invalid it should be set to NaN. // For simplicity we'll only have one of these. It wastes a little space, // but I suspect that we'll change our minds a lot about the exact fields on // each window. TSmbTopListEntry = Record Symbol : String; PriceSpike : Double; // Recent volatility change. VolRate : Double; // Our normal relative volume. TodaysRange : Double; // As a % of ATR OpeningRange : Double; // TodaysRange frozen 15 minutes after the market opens. Strength : Double; // Up from the open as a % of ATR. UpTrend : Double; // Linear regression slope. NearHigh, NearLow : Double; // Distance from high/low as % of ATR. Both are normally positive. NewHigh, NewLow : Double; // 1.0 means very recent. 0.0 means very old. // The rest of these are aimed at JoeFavaloro Last : Double; Change : Double; Volume : Integer; Num : Integer; End; TSmbTopListEntries = Array Of TSmbTopListEntry; TSmbTopListFullGrid = Class(TDrawGrid) Private FEntries : TSmbTopListEntries; FPending : TSmbTopListEntries; FPendingValid : Boolean; FMissingSymbolCount : Integer; FMissingSymbolDescription : String; FIgnoreSortTimer, FFreezeSort : Boolean; FLastSortTime : TDateTime; FTextColor : TColor; HT1, HT2, LT1, LT2 : TBitmap; FColumnData : Array Of Integer; FAllowLinking : Boolean; FPlayedSoundList : TStringList; Procedure SetFreezeSort(Value : Boolean); Function CanSortNow : Boolean; Procedure CopyEntriesFromPending; Procedure UpdateColumnHeaders; Procedure SetColWidth(ACol : LongInt; Const SizeHint : String); Procedure WMNCCalcSize(var msg: TMessage); message WM_NCCALCSIZE; Private FSelectedSymbol : String; Procedure StringGrid1SelectCell(Sender: TObject; ACol, ARow: Integer; Var CanSelect: Boolean); Procedure FontChanged(Sender : TObject); Protected Procedure DrawCell(ACol, ARow: Longint; ARect: TRect; AState: TGridDrawState); Override; //Procedure MouseUp(Button: TMouseButton; // Shift: TShiftState; // X, Y: Integer); Override; Public // I make a copy of this so you can change the original. // The stuff you want to show up on top is entry 0 of the array. Procedure SetEntries(Entries : TSmbTopListEntries); // Presumably you just set visible = true, and align = alClient. // We set the Parent to be the Owner. That is required to make the // initialization go smoothly. Constructor Create(AOwner: TWinControl); Reintroduce; Destructor Destroy; Override; // Use Color to change the background color, and TextColor to change the // foreground color. We do not automatically redraw when these are // changed. We could, but for simplicity I'm assuming that these will // only be set on creation. // These still work to change the default colors, but you probably // want to leave them alone. Several columns have the colors driven // by the data. Property TextColor : TColor Read FTextColor Write FTextColor; // Presumably this is read from the config file. You can call it at // any time, but you should call it at least once before displaying // the window. Calling this with the empty string will set the default // column layout, which shows all columns. That's aimed at debugging, // not real use. Not calling this at all will show nothing interesting. // (We might show something, just because it's hard to display an empty // table.) Do not call this unless we have a valid parent! Procedure SetColumns(ColumnsString : String); // Some windows resort each time we get new data from the server. (The // server includes some pauses, and the data doesn't change that // quickly, so we don't need more pauses.) Other windows will not // resort more than once every 30 seconds, to make them more readable. Property IgnoreSortTimer : Boolean Read FIgnoreSortTimer Write FIgnoreSortTimer; // The user can force us to stop sorting, regardless of the data from // the server and the timer. This comes from the GUI and we expect it // to change at any time. Property FreezeSort : Boolean Read FFreezeSort Write SetFreezeSort; // For historical reasons the main window owns the timer object. Procedure DoTimer; // We Export these so the menu can show some additional status. Property SortIsPending : Boolean Read FPendingValid; Property MissingSymbolCount : Integer Read FMissingSymbolCount; Property MissingSymbolDescription : String Read FMissingSymbolDescription; Procedure SortNow; End; implementation Uses ExternalLinkingUnit, ColorTools, MainWindow, Math, SysUtils, Windows; Const ColSymbol = 0; ColPriceSpike = 1; ColVolRate = 2; ColTodaysRange = 3; ColOpeningRange = 4; ColStrength = 5; ColUpTrend = 6; ColNearHigh = 7; ColNewHigh = 8; ColNearLow = 9; ColNewLow = 10; ColMaxSMB = 10; ColLast = 11; ColChange = 12; ColVolume = 13; ColNum = 14; MinPriceSpikeForHighlight = 7.0; { SMB specifically asked for scroll bars when we first started. However, the app was taking too much space, so they changed their mind and asked us to remove the scroll bars. } Procedure TSmbTopListFullGrid.WMNCCalcSize(var msg: TMessage); Var Style : Integer; Begin // Disable the horizontal scroll bar to make it look more like what they // already had. // Code inspired by: http://www.delphi3000.com/articles/article_3254.asp style := getWindowLong( handle, GWL_STYLE ); If (Style And (WS_HSCROLL Or WS_VSCROLL)) <> 0 Then SetWindowLong(Handle, GWL_STYLE, Style And Not (WS_HSCROLL Or WS_VSCROLL)); Inherited End; Procedure DrawPieClock(Canvas : TCanvas; R : TRect; Full : Double; NewHigh : Boolean); Function ForegroundColor : TColor; Begin { ForegroundColor } If NewHigh Then Result := $00ff00 // Bright green Else Result := $0066ff // Orange End; { ForegroundColor } Procedure MakeSquare; Var Extra : Integer; Begin { MakeSquare } Extra := (R.Bottom - R.Top) - (R.Right - R.Left); If Extra > 0 Then Begin // Taller than it is wide. Inc(R.Top, Extra Div 2); Dec(R.Bottom, Succ(Extra) Div 2) End Else If Extra < 0 Then Begin Extra := -Extra; Inc(R.Left, Extra Div 2); Dec(R.Right, Succ(Extra) Div 2) // Wider than it is tall. End; Extra := (R.Bottom - R.Top) - (R.Right - R.Left); Assert(Extra = 0) End; { MakeSquare } Var CenterX, CenterY : Integer; Theta : Double; Begin { DrawPieClock } If (Not IsNan(Full)) And (Full > 0) Then Begin MakeSquare; If R.Bottom - R.Top > 6 Then Begin Canvas.Pen.Style := psClear; Canvas.Brush.Color := ForegroundColor; If Full >= 1.0 Then Canvas.Ellipse(R) Else Begin CenterX := (R.Left + R.Right) Div 2; CenterY := (R.Top + R.Bottom) Div 2; // One ray is at 12:00. // As full goes from 1.0 to 0.0, the other ray starts at 12:00 // and moves clockwise back to 12:00. Theta := (Full + 0.25) * -2 * Pi; Canvas.Pie(R.Left, R.Top, R.Right, R.Bottom, CenterX, CenterY - 20, CenterX + Round(Cos(Theta)*50), CenterY + Round(Sin(Theta)*50)) End End End End; { DrawPieClock } Procedure DrawSmoothPieClock(Canvas : TCanvas; Var B1, B2 : TBitmap; R : TRect; Full : Double; NewHigh : Boolean; Background : TColor); Const Quality = 3; Var InternalR : TRect; Begin { DrawSmoothPieClock } If Not Assigned(B1) Then Begin B1 := TBitmap.Create; B1.PixelFormat := pf24bit End; B1.Width := (R.Right - R.Left) * Quality; B1.Height := (R.Bottom - R.Top) * Quality; If Not Assigned(B2) Then Begin B2 := TBitmap.Create; B2.PixelFormat := pf24bit End; B1.Canvas.Brush.Color := Background; InternalR.Top := 0; InternalR.Left := 0; InternalR.Bottom := B1.Height; InternalR.Right := B1.Width; B1.Canvas.FillRect(InternalR); DrawPieClock(B1.Canvas, InternalR, Full, NewHigh); Reduce(B1, B2, Quality, Quality); Canvas.Draw(R.Left, R.Top, B2) End; { DrawSmoothPieClock } { This was copied in large part from AlertGrids.Pas in the ActiveX control. There are several variations of this out there, but this one seemed like the simplest. } Procedure TSmbTopListFullGrid.DrawCell(ACol, ARow: Longint; ARect: TRect; AState: TGridDrawState); Var ClientRect : TRect; Function ClientWidth : Integer; Begin Result := ClientRect.Right - ClientRect.Left End; Function ClientHeight : Integer; Begin Result := ClientRect.Bottom - ClientRect.Top End; Procedure TextRightJustify(TextString : String); Var TextHeight, TextWidth : Integer; Begin TextHeight := Canvas.TextHeight(TextString); TextWidth := Canvas.TextWidth(TextString); Canvas.Brush.Style := bsClear; Canvas.TextRect(ClientRect, ClientRect.Left + Max(ClientWidth - TextWidth, 0), ClientRect.Top + Max(ClientHeight - TextHeight, 0) Div 2, TextString) End; Procedure TextLeftJustify(TextString : String); Var TextHeight : Integer; Begin TextHeight := Canvas.TextHeight(TextString); Canvas.Brush.Style := bsClear; Canvas.TextRect(ClientRect, ClientRect.Left, ClientRect.Top + Max(ClientHeight - TextHeight, 0) Div 2, TextString) End; Procedure TextCenter(TextString : String); Var TextHeight, TextWidth : Integer; Begin TextHeight := Canvas.TextHeight(TextString); TextWidth := Canvas.TextWidth(TextString); Canvas.TextRect(ClientRect, ClientRect.Left + Max(ClientWidth - TextWidth, 0) Div 2, ClientRect.Top + Max(ClientHeight - TextHeight, 0) Div 2, TextString); End; Function TooWide(TextString : String) : Boolean; Begin Result := Canvas.TextWidth(TextString) > ClientWidth End; Function MakeIntegerFit(I : Integer) : String; Procedure Adjust(Adjusted : Double; Symbol : Char); Begin Result := Format('%.2n%s', [Adjusted, Symbol]); If TooWide(Result) Then Begin Result := Format('%.1n%s', [Adjusted, Symbol]); If TooWide(Result) Then Begin Result := Format('%.0n%s', [Adjusted, Symbol]); If TooWide(Result) Then Result := '...' End End End; Begin Result := Format('%.0n', [I+0.0]); If TooWide(Result) Then If (I < 1000) Then Result := '...' Else If (I < 1000000) Then Adjust(I / 1.0e+3, 'K') Else Adjust(I / 1.0e+6, 'M') End; Function MakeStringFit(S : String) : String; Var Count, SpaceRemaining : Integer; Begin If TooWide(S) Then Begin Count := 1; SpaceRemaining := ClientWidth - Canvas.TextWidth('...'); While Count <= Length(S) Do Begin SpaceRemaining := SpaceRemaining - Canvas.TextWidth(S[Count]); If SpaceRemaining < 0 Then Break; Inc(Count) End; Result := Copy(S, 0, Pred(Count)) + '...'; End Else Result := S; End; Function MakePriceFit(P : Double) : String; Begin Result := Format('%f', [P]); If TooWide(Result) Then Result := '...' End; Function MakePercentFit(P : Double) : String; Begin // Several of the fields are a % of ATR. We always display 1 place // after the decimal. And we don't display the % sign. That's // different from our other TIQs, but that's how the web displays most % // values, so it's not that unreasonable. Some things have digits after // the decimal in Adam's picture, but he said 1 is correct. Result := Format('%.1f', [P]); If TooWide(Result) Then Result := '...' End; Procedure RemoveFromPlayList(S : String); Var I : Integer; Begin // Remove a symbol from the played list so it is eligible to play // the sound again. For I := 0 To FPlayedSoundList.Count-1 Do Begin if S = FPlayedSoundList[I] Then Begin FPlayedSoundList.Delete(I); Break End End End; Function AddToPlayList(S : String) : Boolean; Var I : Integer; Begin // Add a symbol to the played list but only if it is not already in the // played list. Result := True; For I := 0 To FPlayedSoundList.Count-1 Do Begin if S = FPlayedSoundList[I] Then Begin Result := False; Break End End; if Result Then FPlayedSoundList.Add(S) End; Var DataIndex, DataTypeIndex : Integer; EntryValid : Boolean; ShowHeader : Boolean; Name : String; FgColor, BgColor : TColor; Begin { TSmbTopListFullGrid.DrawCell } DataIndex := Pred(ARow); EntryValid := (DataIndex >= 0) And (DataIndex < Length(FEntries)); DataTypeIndex := FColumnData[ACol]; ShowHeader := (gdFixed In AState) Or (EntryValid And (FEntries[DataIndex].Symbol = '-')); Canvas.Font.Style := []; // Remove symbol from sound list since it is now below the alert threshold If EntryValid And (DataTypeIndex = ColPriceSpike) And (FEntries[DataIndex].PriceSpike < MinPriceSpikeForHighlight) Then RemoveFromPlayList(FEntries[DataIndex].Symbol); If ShowHeader Then Begin If FreezeSort Then BgColor := $f8fe20 // Pastel blue. Another suggestion was a duller yellow: #d8d846 //BgColor := $20fef8 // Yellow -- Brad thought the pink was too close to the other pink. //BgColor := $8a8ae6 // Dark Pink. Else If FMissingSymbolCount > 0 Then BgColor := $c0c0ff // Light Pink. Else BgColor := $d8e9ec; // Fixed color. Grey. FgColor := clBlack End Else If DataTypeIndex In [ColSymbol, ColVolRate] Then Begin // The symbol column gets the color from the vol rate. In the old // software this color was applied to other colors. In the old // software foreground color changed, too. I think it will be more // readable if it's fixed. FgColor := clWhite; With FEntries[DataIndex] Do If (Not EntryValid) Or IsNan(VolRate) Then BgColor := clBlack Else If VolRate <= 0.5 Then BgColor := $ff0000 // Bright Blue Else If VolRate >= 3.5 Then BgColor := $0000ff // Brigh Red Else If VolRate < 1.0 Then // 1.0 is black, 0.5 is blue, continuously fade between them. BGColor := Round((1.0 - VolRate)*2*255) * $10000 Else // 1.0 is black, 3.5 is red, continuously fade between them. BGColor := Round((VolRate - 1.0)/2.5*255) End Else If EntryValid And (DataTypeIndex = ColPriceSpike) And (FEntries[DataIndex].PriceSpike >= MinPriceSpikeForHighlight) Then Begin FGColor := clBlack; BgColor := clYellow; // Play sound if symbol is added to the play sound list. if AddToPlayList(FEntries[DataIndex].Symbol) Then MainWindow.MainForm.PlayPriceSpikeSound End Else Begin BgColor := Color; FgColor := TextColor End; Canvas.Brush.Color := BgColor; Canvas.Font.Color := FgColor; Canvas.FillRect(ARect); ClientRect := ARect; If ShowHeader And Ctl3D Then Begin // This seems to always fill in with the gray / fixed color. :( // We have code elsewhere which will make my own 3d border in any // color I like. DrawEdge(Canvas.Handle, ARect, BDR_RAISEDINNER, BF_BOTTOM Or BF_RIGHT Or BF_TOP Or BF_LEFT); Inc(ClientRect.Left); Inc(ClientRect.Top); Dec(ClientRect.Right); Dec(ClientRect.Bottom) End; If ClientWidth > 14 Then Begin Inc(ClientRect.Left, 3); Dec(ClientRect.Right, 3) End; If ShowHeader Then Begin Case DataTypeIndex Of ColSymbol : Name := 'Symbol'; ColPriceSpike : Name := 'PrSpk'; ColVolRate : Name := 'IPR'; ColTodaysRange : Name := 'TRang'; ColOpeningRange : Name := 'ORang'; ColStrength : Name := 'Strnth'; ColUpTrend : Name := 'Up Tr'; ColNearHigh : Name := 'Nr Hi'; ColNewHigh : Name := 'H'; ColNearLow : Name := 'Nr Lo'; ColNewLow : Name := 'L'; ColLast : Name := 'Last'; ColChange : Name := '%Chg'; ColVolume : Name := 'Volume'; ColNum : Name := '#'; Else Name := ''; End; TextCenter(MakeStringFit(Name)) { This is a column or row header. When we start sorting the items, we should consider using the triangles from TSortableHeaderCellRenderer.DrawTriangle in MarketViewBase.Pas to show which row or column is the sort key, and the direction of the sort operation. } End Else If EntryValid Then With FEntries[DataIndex] Do Case DataTypeIndex Of ColSymbol : Begin TextLeftJustify(MakeStringFit(Symbol)); If (gdSelected In AState) Then Begin Canvas.Brush.Color := fgColor; Canvas.FrameRect(ARect) End End; ColPriceSpike : If Not IsNaN(PriceSpike) Then TextRightJustify(MakePercentFit(PriceSpike)); ColVolRate : If Not IsNaN(VolRate) Then TextRightJustify(MakePercentFit(VolRate)); ColTodaysRange : If Not IsNaN(TodaysRange) Then TextRightJustify(MakePercentFit(TodaysRange)); ColOpeningRange : If Not IsNaN(OpeningRange) Then TextRightJustify(MakePercentFit(OpeningRange)); ColStrength : If Not IsNaN(Strength) Then TextRightJustify(MakePercentFit(Strength)); ColUpTrend : If Not IsNaN(UpTrend) Then TextRightJustify(MakePercentFit(UpTrend)); ColNearHigh : If Not IsNaN(NearHigh) Then TextRightJustify(MakePercentFit(NearHigh)); ColNewHigh : DrawSmoothPieClock(Canvas, HT1, HT2, ClientRect, NewHigh, True, BgColor); ColNearLow : If Not IsNaN(NearLow) Then TextRightJustify(MakePercentFit(NearLow)); ColNewLow : DrawSmoothPieClock(Canvas, LT1, LT2, ClientRect, NewLow, False, BgColor); ColLast : If Not IsNaN(Last) Then TextRightJustify(MakePriceFit(Last)); ColChange : If Not IsNaN(Change) Then TextRightJustify(MakePercentFit(Change)); ColVolume : If Volume > 0 Then TextRightJustify(MakeIntegerFit(Volume)); ColNum : If Num > 0 Then TextRightJustify(MakeIntegerFit(Num)); End End; { TSmbTopListFullGrid.DrawCell } Procedure TSmbTopListFullGrid.SetEntries(Entries : TSmbTopListEntries); Function SymbolsMatch : Boolean; Var { SymbolsMatch } I : Integer; Begin If Length(Entries) <> Length(FEntries) Then Result := False Else Begin Result := True; For I := Low(Entries) To High(Entries) Do If Entries[I].Symbol <> FEntries[I].Symbol Then Begin Result := False; Break End End End; { SymbolsMatch } Procedure UpdateValues; Var I, Found : Integer; CurrentPositions : TStringList; Begin { UpdateValues } FMissingSymbolCount := 0; FMissingSymbolDescription := ''; CurrentPositions := TStringList.Create; Try CurrentPositions.CaseSensitive := False; For I := Low(FEntries) To High(FEntries) Do CurrentPositions.AddObject(FEntries[I].Symbol, TObject(I)); CurrentPositions.Sorted := True; For I := Low(FPending) To High(FPending) Do Begin If CurrentPositions.Find(FPending[I].Symbol, Found) Then FEntries[Integer(CurrentPositions.Objects[Found])] := FPending[I] Else Begin Inc(FMissingSymbolCount); Case FMissingSymbolCount Of 1 : FMissingSymbolDescription := FPending[I].Symbol; 2..5 : FMissingSymbolDescription := FMissingSymbolDescription + ', ' + FPending[I].Symbol; End End End; If FMissingSymbolCount > 5 Then FMissingSymbolDescription := FMissingSymbolDescription + ' and ' + IntToStr(FMissingSymbolCount-5) + ' more'; Invalidate Finally CurrentPositions.Free End End; { UpdateValues } Begin { TSmbTopListFullGrid.SetEntries } SetLength(Entries, Length(Entries)); FPending := Entries; FPendingValid := True; If SymbolsMatch Then // No sorting is required. Maybe nothing changed. Maybe it was a // change to a value but not the order. This is quick and easy. No // need to optimize for the case where nothing at all changed. The // server filters those cases out for us. CopyEntriesFromPending Else If CanSortNow Then SortNow Else UpdateValues End; { TSmbTopListFullGrid.SetEntries } Procedure TSmbTopListFullGrid.DoTimer; Begin If CanSortNow Then SortNow End; Procedure TSmbTopListFullGrid.SortNow; Begin If FPendingValid Then Begin CopyEntriesFromPending; FLastSortTime := Now End End; Procedure TSmbTopListFullGrid.SetFreezeSort(Value : Boolean); Begin If FFreezeSort <> Value Then Begin FFreezeSort := Value; // We color code the window to say when it's frozen. That should // update instantly even if none of the data changes. Invalidate; // If you turn off freezing, you immediately see the last update, // regardless of the last time we updated. FLastSortTime := 0; DoTimer End // Ideally IgnoreSortTimer would had a set method very similar to this. // I didn't bother because I know that property is only set when we first // start. End; Function TSmbTopListFullGrid.CanSortNow : Boolean; Const NormalPause = 1.0 / 24.0 / 60.0 / 2.0; // Thirty seconds. Begin If Not FPendingValid Then Result := False Else If FreezeSort Then Result := False Else If IgnoreSortTimer Then Result := True Else Result := Now > FLastSortTime + NormalPause End; Procedure TSmbTopListFullGrid.CopyEntriesFromPending; Var S : TGridRect; SelectedIndex : Integer; Begin Assert(FPendingValid); FPendingValid := False; FMissingSymbolCount := 0; FMissingSymbolDescription := ''; FEntries := FPending; FAllowLinking := False; // The next row might change FSlectedSymbol because of a callback to our // code. That's part of the reason we disable the callback. RowCount := Succ(Max(1, Length(FEntries))); FAllowLinking := True; Invalidate; S := Selection; SelectedIndex := Pred(S.Top); If SelectedIndex > High(FEntries) Then Begin // The old section is below what's currently in the table. FSelectedSymbol := ''; TopRow := FixedRows End Else If FEntries[SelectedIndex].Symbol <> FSelectedSymbol Then Begin // The row that was selected has changed. We take the simplest path // and we unselect everything. S.Top := MaxInt; S.Bottom := MaxInt; Selection := S; FSelectedSymbol := ''; TopRow := FixedRows End // Else ... The selected row did not change, so keep the selection in // place. This case is important. Sometimes we receive this call when // nothing has changed! We certainly don't want to clear the selection // in that case. End; Procedure TSmbTopListFullGrid.SetColWidth(ACol : LongInt; Const SizeHint : String); Begin If Ctl3D Then ColWidths[ACol] := 16 + Canvas.TextWidth(SizeHint) Else ColWidths[ACol] := 12 + Canvas.TextWidth(SizeHint) End; { Procedure TSmbTopListFullGrid.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); Var DataIndex, Col, Row : Integer; Begin Inherited; If Button = mbLeft Then Begin MouseToCell(X, Y, Col, Row); DataIndex := Pred(Row); If (DataIndex >= 0) And (DataIndex < Length(FEntries)) Then ExternalLinkingWindow.SendSymbol(FEntries[DataIndex].Symbol) End End; } Var DebugErrorCount : Integer = 0; Procedure TSmbTopListFullGrid.StringGrid1SelectCell(Sender: TObject; ACol, ARow: Integer; Var CanSelect: Boolean); Var Index : Integer; NewSelection : String; Begin Try If Not FAllowLinking Then Exit; Index := ARow - FixedRows; If (Index < Low(FEntries)) Or (Index > High(FEntries)) Then NewSelection := '' Else NewSelection := FEntries[Index].Symbol; If (NewSelection <> FSelectedSymbol) And (NewSelection <> '') And (NewSelection <> '-') Then Begin FSelectedSymbol := NewSelection; ExternalLinkingWindow.SendSymbol(NewSelection) End Except // I got a strange exception once when I clicked on an grid. This was // the first time I'd clicked on the grid since the proram started. I // can only guess it cames from this function. I could never reproduce // the error. PDS. Inc(DebugErrorCount) End End; Destructor TSmbTopListFullGrid.Destroy; Begin HT1.Free; HT2.Free; LT1.Free; LT2.Free; FPlayedSoundList.Free; Inherited End; Constructor TSmbTopListFullGrid.Create(AOwner: TWinControl); Begin Assert(Assigned(AOwner)); // We could watch for when the owner is changed, but that's a pain! Inherited Create(AOwner); DoubleBuffered := True; Color := clBlack; TextColor := clWhite; RowCount := 2; FixedRows := 1; FixedCols := 0; Options := [goColSizing, goThumbTracking]; DefaultDrawing := False; Parent := AOwner; ParentFont := True; Canvas.Font := Font; DefaultRowHeight := Canvas.TextHeight('Xy()') + 4; UpdateColumnHeaders; // Put us into a good enough state that we don't crash! Options := Options - [goRangeSelect] + [goRowSelect]; OnSelectCell := StringGrid1SelectCell; Font.OnChange := FontChanged; FPlayedSoundList := TstringList.Create; End; // This makes the grid match FColumnData Procedure TSmbTopListFullGrid.UpdateColumnHeaders; Var I : Integer; Begin If Length(FColumnData) = 0 Then Begin // Create one column because the grid control does not like to be empty. SetLength(FColumnData, 1); FColumnData[0] := ColSymbol End; ColCount := Length(FColumnData); For I := Low(FColumnData) To High(FColumnData) Do Begin Case FColumnData[I] Of ColSymbol: SetColWidth(I, 'XXXXX'); ColVolRate, ColPriceSpike, ColTodaysRange, ColOpeningRange: SetColWidth(I, '99.9'); ColStrength, ColUpTrend: // Note that the old version use parenthesis rather than a negative sign. SetColWidth(I, '-99.9'); ColNearHigh, ColNearLow: // If it's negative I assume it will be small! SetColWidth(I, '99.9'); ColNewHigh, ColNewLow: ColWidths[I] := DefaultRowHeight; ColLast : SetColWidth(I, '999.9'); ColChange : SetColWidth(I, '-1.93'); ColVolume : SetColWidth(I, '100,000,000'); ColNum : SetColWidth(I, '100'); // Unknowns do not raise an error. End End; Invalidate End; Procedure TSmbTopListFullGrid.SetColumns(ColumnsString : String); Var I : Integer; ColumnsList : TStringList; Begin // Minimal error handling or support for old versions. This should // come from a file created by us. The rest of the code is pretty open // to an invalid column number. It will have the default width and it // will be blank. ColumnsList := TStringList.Create; Try ColumnsList.CommaText := Trim(ColumnsString); If ColumnsList.Count = 0 Then Begin // The default // All of the SMB fields. When development started, the server // sent all fields for all lists. Now most of these are empty. SetLength(FColumnData, Succ(ColMaxSMB)); For I := 0 To ColMaxSMB Do FColumnData[I] := I End Else Begin // Read from string SetLength(FColumnData, ColumnsList.Count); For I := 0 To Pred(ColumnsList.Count) Do FColumnData[I] := StrToIntDef(ColumnsList[I], 0) End Finally ColumnsList.Free End; UpdateColumnHeaders End; Procedure TSmbTopListFullGrid.FontChanged(Sender : TObject); Begin Canvas.Font := Font; DefaultRowHeight := Canvas.TextHeight('Xy()') + 4; UpdateColumnHeaders; Invalidate End; End.