Make a TDBEdit show the characters remaining - delphi

The problem I'm wanting to solve is to display to the user the remaining characters left in a field as they are typing into a TDBEdit.
Currently I'm doing something along the lines of
lCharRemaining.Caption := Field.Size - length(dbedit.text);
i.e. updating a label in the OnChange event for the TDBEdit, which works perfectly fine. However I'm wanting to do this for a number of TDBEdits and tried to write a custom component that would display the length remaining within the edit box on the right. It however interferes with editing. I was perhaps thinking that I could display a hint while someone was typing indicating the remaining space in the field - any suggestions?
Here is the code for my component (if someone can suggest improvements).
unit DBEditWithLenghtCountdown;
interface
uses
SysUtils, Classes, Controls, StdCtrls, Mask, DBCtrls, messages, Graphics;
type
TDBEditWithLenghtCountdown = class(TDBEdit)
private
{ Private declarations }
FCanvas: TCanvas;
procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
protected
{ Protected declarations }
property Canvas: TCanvas read FCanvas;
procedure WndProc(var Message: TMessage); override;
public
{ Public declarations }
function CharactersRemaining : integer;
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
{ Published declarations }
end;
procedure Register;
implementation
uses
db, Types;
procedure Register;
begin
RegisterComponents('Samples', [TDBEditWithLenghtCountdown]);
end;
{ TDBEditWithLenghtCountdown }
function TDBEditWithLenghtCountdown.CharactersRemaining: integer;
begin
result := -1;
if Assigned(Field)then
begin
result := Field.Size - Length(Text);
end;
end;
constructor TDBEditWithLenghtCountdown.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FCanvas := TControlCanvas.Create;
TControlCanvas(FCanvas).Control := Self;
end;
destructor TDBEditWithLenghtCountdown.Destroy;
begin
FCanvas.Free;
inherited;
end;
procedure TDBEditWithLenghtCountdown.WMPaint(var Message: TWMPaint);
var
R: TRect;
Remaining : string;
WidthOfText: Integer;
x: Integer;
begin
inherited;
if not focused then
exit;
Remaining := IntToStr(CharactersRemaining);
R := ClientRect;
Inc(R.Left, 1);
Inc(R.Top, 1);
Canvas.Brush.Assign(Self.Brush);
Canvas.Brush.Style := bsClear;
Canvas.Font.Assign(Self.Font);
Canvas.Font.Color := clRed;
WidthOfText := Canvas.TextWidth(Remaining);
x := R.right - WidthOfText - 4;
Canvas.TextOut(x,2, Remaining);
end;
procedure TDBEditWithLenghtCountdown.WndProc(var Message: TMessage);
begin
inherited WndProc(Message);
with Message do
case Msg of
CM_MOUSEENTER, CM_MOUSELEAVE, WM_LBUTTONUP, WM_LBUTTONDOWN,
WM_KEYDOWN, WM_KEYUP,
WM_SETFOCUS, WM_KILLFOCUS,
CM_FONTCHANGED, CM_TEXTCHANGED:
begin
Invalidate;
end;
end; // case
end;
end.

You can test how it would look like without any text interference by setting the edit margins to leave space for the tip text. A quick test:
type
TDBEditWithLenghtCountdown = class(TDBEdit)
..
protected
procedure CreateWnd; override;
property Canvas: TCanvas read FCanvas;
..
procedure TDBEditWithLenghtCountdown.CreateWnd;
var
MaxWidth, Margins: Integer;
begin
inherited;
MaxWidth := Canvas.TextWidth('WW');
Margins := Perform(EM_GETMARGINS, 0, 0);
Margins := MakeLong(HiWord(Margins), LoWord(Margins) + MaxWidth);
Perform(EM_SETMARGINS, EC_LEFTMARGIN or EC_RIGHTMARGIN, Margins);
end;
Beyond this is personal opinion but I find this a bit confusing. What I would do is probably publish a status panel field on the derived edit, and output some text to it if it is assigned when the text of the edit control changes.
edit: Here's a somewhat extended version that should take care of the issue mentioned in the comment (if navigate left with a long text, edit text overwrites tip text), and also sets margins only if the control has focus. (Not full code duplicated from the question, only modified bits.)
type
TDBEditWithLenghtCountdown = class(TDBEdit)
private
FCanvas: TCanvas;
FTipWidth: Integer;
FDefMargins: Integer;
procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
protected
..
procedure TDBEditWithLenghtCountdown.WMPaint(var Message: TWMPaint);
var
PaintStruct: TPaintStruct;
EndPaint: Boolean;
Rgn: HRGN;
R, TipR: TRect;
Remaining : string;
begin
if not Focused then
inherited
else begin
EndPaint := Message.Dc = 0;
if Message.DC = 0 then
Message.DC := BeginPaint(Handle, PaintStruct);
R := ClientRect;
TipR := R;
TipR.Left := TipR.Right - FTipWidth;
Remaining := IntToStr(CharactersRemaining);
Canvas.Handle := Message.DC;
SetBkColor(Canvas.Handle, ColorToRGB(Color));
Canvas.Font := Font;
Canvas.Font.Color := clRed;
Canvas.TextRect(TipR, Remaining, [tfSingleLine, tfCenter, tfVerticalCenter]);
R.Right := TipR.Left;
Rgn := CreateRectRgn(R.Left, R.Top, R.Right, R.Bottom);
SelectClipRgn(Canvas.Handle, Rgn);
DeleteObject(Rgn);
inherited;
if EndPaint then
windows.EndPaint(Handle, PaintStruct);
end;
end;
procedure TDBEditWithLenghtCountdown.WndProc(var Message: TMessage);
const
TipMargin = 3;
begin
inherited WndProc(Message);
with Message do
case Msg of
CM_MOUSEENTER, CM_MOUSELEAVE, WM_LBUTTONUP, WM_LBUTTONDOWN,
WM_KEYDOWN, WM_KEYUP,
CM_TEXTCHANGED: Invalidate;
WM_CREATE: FDefMargins := Perform(EM_GETMARGINS, 0, 0);
CM_FONTCHANGED:
begin
Canvas.Handle := 0;
Canvas.Font := Font;
FTipWidth := Canvas.TextWidth('67') + 2 * TipMargin;
end;
WM_SETFOCUS:
Perform(EM_SETMARGINS, EC_LEFTMARGIN or EC_RIGHTMARGIN,
MakeLong(HiWord(FDefMargins), LoWord(FDefMargins) + FTipWidth));
WM_KILLFOCUS:
Perform(EM_SETMARGINS, EC_LEFTMARGIN or EC_RIGHTMARGIN, FDefMargins);
end;
end;

Just as a base for you to start with, if do not want to derive every Edit-Component, here is a general approach for every Component derived from TCustomEdit.
Set the MaxLength of the Edit-Component to a Value > 0 and this Unit will paint you a thin red line below the text as a fill indicator.
The Unit has only to be present in your Project.
unit ControlInfoHandler;
interface
uses
Vcl.Forms;
implementation
uses
System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.StdCtrls;
type
TControlInfoHandler = class( TComponent )
private
FCurrent : TWinControl;
FCurrentLength : Integer;
protected
procedure ActiveControlChange( Sender : TObject );
procedure ApplicationIdle( Sender : TObject; var Done : Boolean );
procedure Notification( AComponent : TComponent; Operation : TOperation ); override;
end;
THackedEdit = class( TCustomEdit )
published
property MaxLength;
end;
var
LControlInfoHandler : TControlInfoHandler;
{ TControlInfoHandler }
procedure TControlInfoHandler.ActiveControlChange( Sender : TObject );
begin
FCurrent := Screen.ActiveControl;
FCurrentLength := 0;
if Assigned( FCurrent )
then
FCurrent.FreeNotification( Self );
end;
procedure TControlInfoHandler.ApplicationIdle( Sender : TObject; var Done : Boolean );
var
LEdit : THackedEdit;
LCanvas : TControlCanvas;
LWidth : Integer;
begin
if not Assigned( FCurrent ) or not ( FCurrent is TCustomEdit )
then
Exit;
LEdit := THackedEdit( FCurrent as TCustomEdit );
if ( LEdit.MaxLength > 0 )
then
begin
LCanvas := TControlCanvas.Create;
LCanvas.Control := LEdit;
LCanvas.Pen.Style := psSolid;
LCanvas.Pen.Width := 2;
LWidth := LEdit.Width - 6;
if FCurrentLength <> LEdit.GetTextLen
then
begin
LCanvas.Pen.Color := LEdit.Color;
LCanvas.MoveTo( 0, LEdit.Height - 4 );
LCanvas.LineTo( LWidth, LEdit.Height - 4 );
end;
LCanvas.Pen.Color := clRed;
LWidth := LWidth * LEdit.GetTextLen div LEdit.MaxLength;
LCanvas.MoveTo( 0, LEdit.Height - 4 );
LCanvas.LineTo( LWidth, LEdit.Height - 4 );
FCurrentLength := LEdit.GetTextLen;
end;
end;
procedure TControlInfoHandler.Notification( AComponent : TComponent; Operation : TOperation );
begin
inherited;
if ( FCurrent = AComponent ) and ( Operation = opRemove )
then
FCurrent := nil;
end;
initialization
LControlInfoHandler := TControlInfoHandler.Create( Application );
Screen.OnActiveControlChange := LControlInfoHandler.ActiveControlChange;
Application.OnIdle := LControlInfoHandler.ApplicationIdle;
end.

Related

Writing a custom property inspector - How to handle inplace editor focus when validating values?

Overview
I am trying to write my own simple property inspector but I am facing a difficult and rather confusing problem. First though let me say that my component is not meant to work with or handle component properties, instead it will allow adding custom values to it. The full source code of my component is further down the question and it should look something like this once you have installed it in a package and run it from a new empty project:
Problem (brief)
The issue is regarding the use of inplace editors and validating the property values. The idea is, if a property value is not valid then show a message to the user notifying them that the value cannot be accepted, then focus back to the row and inplace editor that was originally focused on.
We can actually use Delphi's very own Object Inspector to illustrate the behavior I am looking for, for example try writing a string in the Name property that cannot be accepted then click away from the Object Inspector. A message is shown and upon closing it, it will focus back to the Name row.
Source Code
The question becomes too vague without any code but due to the nature of the component I am trying to write it's also quite large. I have stripped it down as much as possible for the purpose of the question and example. I am sure there will be some comments asking me why I didn't do this or do that instead but it's important to know that I am no Delphi expert and often I make wrong decisions and choices but I am always willing to learn so all comments are welcomed, especially if it aids in finding my solution.
unit MyInspector;
interface
uses
Winapi.Windows,
Winapi.Messages,
System.Classes,
System.SysUtils,
Vcl.Controls,
Vcl.Dialogs,
Vcl.StdCtrls,
Vcl.Graphics,
Vcl.Forms;
type
TMyInspectorItems = class(TObject)
private
FPropertyNames: TStringList;
FPropertyValues: TStringList;
procedure AddItem(APropName, APropValue: string);
procedure Clear;
public
constructor Create;
destructor Destroy; override;
end;
TOnMouseMoveEvent = procedure(Sender: TObject; X, Y: Integer) of object;
TOnSelectRowEvent = procedure(Sender: TObject; PropName, PropValue: string; RowIndex: Integer) of object;
TMyCustomInspector = class(TGraphicControl)
private
FInspectorItems: TMyInspectorItems;
FOnMouseMove: TOnMouseMoveEvent;
FOnSelectRow: TOnSelectRowEvent;
FRowCount: Integer;
FNamesFont: TFont;
FValuesFont: TFont;
FSelectedRow: Integer;
procedure SetNamesFont(const AValue: TFont);
procedure SetValuesFont(const AValue: TFont);
procedure CalculateInspectorHeight;
function GetMousePosition: TPoint;
function MousePositionToRowIndex: Integer;
function RowIndexToMousePosition(ARowIndex: Integer): Integer;
function GetRowHeight: Integer;
function GetValueRowWidth: Integer;
function RowExists(ARowIndex: Integer): Boolean;
function IsRowSelected: Boolean;
protected
procedure Loaded; override;
procedure Paint; override;
procedure WMKeyDown(var Message: TMessage); message WM_KEYDOWN;
procedure WMMouseDown(var Message: TMessage); message WM_LBUTTONDOWN;
procedure WMMouseMove(var Message: TMessage); message WM_MOUSEMOVE;
procedure WMMouseUp(var Message: TMessage); message WM_LBUTTONUP;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function RowCount: Integer;
property Items: TMyInspectorItems read FInspectorItems write FInspectorItems;
property OnMouseMove: TOnMouseMoveEvent read FOnMouseMove write FOnMouseMove;
property OnSelectRow: TOnSelectRowEvent read FOnSelectRow write FOnSelectRow;
published
property Align;
end;
TMyPropertyInspector = class(TScrollBox)
private
FInspector: TMyCustomInspector;
FInplaceStringEditor: TEdit;
FSelectedRowName: string;
FLastSelectedRowName: string;
FLastSelectedRow: Integer;
function SetPropertyValue(RevertToPreviousValueOnFail: Boolean): Boolean;
procedure InplaceStringEditorEnter(Sender: TObject);
procedure InplaceStringEditorExit(Sender: TObject);
procedure InplaceStringEditorKeyPress(Sender: TObject; var Key: Char);
procedure SelectRow(Sender: TObject; PropName, PropValue: string; RowIndex: Integer);
function ValidateStringValue(Value: string): Boolean;
protected
procedure Loaded; override;
procedure WMSize(var Message: TMessage); message WM_SIZE;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure AddItem(APropName, APropValue: string);
function GetSelectedPropertyName: string;
function GetSelectedPropertyValue: string;
function RowCount: Integer;
end;
var
FCanSelect: Boolean;
implementation
{ TMyInspectorItems }
constructor TMyInspectorItems.Create;
begin
inherited Create;
FPropertyNames := TStringList.Create;
FPropertyValues := TStringList.Create;
end;
destructor TMyInspectorItems.Destroy;
begin
FPropertyNames.Free;
FPropertyValues.Free;
inherited Destroy;
end;
procedure TMyInspectorItems.AddItem(APropName, APropValue: string);
begin
FPropertyNames.Add(APropName);
FPropertyValues.Add(APropValue);
end;
procedure TMyInspectorItems.Clear;
begin
FPropertyNames.Clear;
FPropertyValues.Clear;
end;
{ TMyCustomInspector }
constructor TMyCustomInspector.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FInspectorItems := TMyInspectorItems.Create;
FNamesFont := TFont.Create;
FNamesFont.Color := clWindowText;
FNamesFont.Name := 'Segoe UI';
FNamesFont.Size := 9;
FNamesFont.Style := [];
FValuesFont := TFont.Create;
FValuesFont.Color := clNavy;
FValuesFont.Name := 'Segoe UI';
FValuesFont.Size := 9;
FValuesFont.Style := [];
end;
destructor TMyCustomInspector.Destroy;
begin
FInspectorItems.Free;
FNamesFont.Free;
FValuesFont.Free;
inherited Destroy;
end;
procedure TMyCustomInspector.Loaded;
begin
inherited Loaded;
end;
procedure TMyCustomInspector.Paint;
procedure DrawBackground;
begin
Canvas.Brush.Color := clWindow;
Canvas.Brush.Style := bsSolid;
Canvas.FillRect(Rect(0, 0, Self.Width, Self.Height));
end;
procedure DrawNamesBackground;
begin
Canvas.Brush.Color := clWindow;
Canvas.Brush.Style := bsSolid;
Canvas.FillRect(Rect(0, 0, Self.Width div 2, Self.Height));
end;
procedure DrawNamesSelection;
begin
if (FRowCount > -1) and (RowExists(MousePositionToRowIndex)) then
begin
Canvas.Brush.Color := $00E0E0E0;
Canvas.Brush.Style := bsSolid;
Canvas.FillRect(Rect(0, RowIndexToMousePosition(FSelectedRow),
Self.Width div 2, RowIndexToMousePosition(FSelectedRow) + GetRowHeight));
end;
end;
procedure DrawNamesText;
var
I: Integer;
Y: Integer;
begin
FRowCount := FInspectorItems.FPropertyNames.Count;
Canvas.Brush.Style := bsClear;
Canvas.Font.Color := FNamesFont.Color;
Canvas.Font.Name := FNamesFont.Name;
Canvas.Font.Size := FNamesFont.Size;
Y := 0;
for I := 0 to FInspectorItems.FPropertyNames.Count -1 do
begin
Canvas.TextOut(2, Y, FInspectorItems.FPropertyNames.Strings[I]);
Inc(Y, GetRowHeight);
end;
end;
procedure DrawValuesBackground;
begin
Canvas.Brush.Color := clWindow;
Canvas.Brush.Style := bsSolid;
Canvas.FillRect(Rect(Self.Width div 2, 0, Self.Width, Self.Height));
end;
procedure DrawValuesSelection;
begin
if (FRowCount > -1) and (RowExists(MousePositionToRowIndex)) then
begin
Canvas.DrawFocusRect(Rect(Self.Width div 2, RowIndexToMousePosition(FSelectedRow),
Self.Width, RowIndexToMousePosition(FSelectedRow) + GetRowHeight));
end;
end;
procedure DrawValues;
var
I, Y: Integer;
begin
FRowCount := FInspectorItems.FPropertyValues.Count;
Y := 0;
for I := 0 to FInspectorItems.FPropertyValues.Count -1 do
begin
Canvas.Brush.Style := bsClear;
Canvas.Font.Color := FValuesFont.Color;
Canvas.Font.Name := FValuesFont.Name;
Canvas.Font.Size := FValuesFont.Size;
Canvas.TextOut(Self.Width div 2 + 2, Y + 1, FInspectorItems.FPropertyValues.Strings[I]);
Inc(Y, GetRowHeight);
end;
end;
begin
DrawNamesBackground;
DrawNamesSelection;
DrawNamesText;
DrawValuesBackground;
DrawValuesSelection;
DrawValues;
end;
procedure TMyCustomInspector.WMKeyDown(var Message: TMessage);
begin
inherited;
case Message.WParam of
VK_DOWN:
begin
end;
end;
end;
procedure TMyCustomInspector.WMMouseDown(var Message: TMessage);
begin
inherited;
Parent.SetFocus;
FSelectedRow := MousePositionToRowIndex;
if FSelectedRow <> -1 then
begin
if Assigned(FOnSelectRow) then
begin
FOnSelectRow(Self, FInspectorItems.FPropertyNames.Strings[FSelectedRow],
FInspectorItems.FPropertyValues.Strings[FSelectedRow], FSelectedRow);
end;
end;
Invalidate;
end;
procedure TMyCustomInspector.WMMouseMove(var Message: TMessage);
begin
inherited;
if Assigned(FOnMouseMove) then
begin
FOnMouseMove(Self, GetMousePosition.X, GetMousePosition.Y);
end;
end;
procedure TMyCustomInspector.WMMouseUp(var Message: TMessage);
begin
inherited;
end;
procedure TMyCustomInspector.SetNamesFont(const AValue: TFont);
begin
FNamesFont.Assign(AValue);
Invalidate;
end;
procedure TMyCustomInspector.SetValuesFont(const AValue: TFont);
begin
FValuesFont.Assign(AValue);
Invalidate;
end;
procedure TMyCustomInspector.CalculateInspectorHeight;
var
I, Y: Integer;
begin
FRowCount := FInspectorItems.FPropertyNames.Count;
Y := GetRowHeight;
for I := 0 to FRowCount -1 do
begin
Inc(Y, GetRowHeight);
end;
if Self.Height <> Y then
Self.Height := Y;
end;
function TMyCustomInspector.GetMousePosition: TPoint;
var
Pt: TPoint;
begin
Pt := Mouse.CursorPos;
Pt := ScreenToClient(Pt);
Result := Pt;
end;
function TMyCustomInspector.MousePositionToRowIndex: Integer;
begin
Result := GetMousePosition.Y div GetRowHeight;
end;
function TMyCustomInspector.RowIndexToMousePosition(
ARowIndex: Integer): Integer;
begin
Result := ARowIndex * GetRowHeight;
end;
function TMyCustomInspector.GetRowHeight: Integer;
begin
Result := FNamesFont.Size * 2 + 1;
end;
function TMyCustomInspector.GetValueRowWidth: Integer;
begin
Result := Self.Width div 2;
end;
function TMyCustomInspector.RowCount: Integer;
begin
Result := FRowCount;
end;
function TMyCustomInspector.RowExists(ARowIndex: Integer): Boolean;
begin
Result := MousePositionToRowIndex < RowCount;
end;
function TMyCustomInspector.IsRowSelected: Boolean;
begin
Result := FSelectedRow <> -1;
end;
{ TMyPropertyInspector }
constructor TMyPropertyInspector.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Self.DoubleBuffered := True;
Self.Height := 150;
Self.HorzScrollBar.Visible := False;
Self.TabStop := True; // needed to receive focus
Self.Width := 250;
FInspector := TMyCustomInspector.Create(Self);
FInspector.Parent := Self;
FInspector.Align := alTop;
FInspector.Height := 0;
FInspector.OnSelectRow := SelectRow;
FInplaceStringEditor := TEdit.Create(Self);
FInplaceStringEditor.Parent := Self;
FInplaceStringEditor.BorderStyle := bsNone;
FInplaceStringEditor.Color := clWindow;
FInplaceStringEditor.Height := 0;
FInplaceStringEditor.Left := 0;
FInplaceStringEditor.Name := 'MyPropInspectorInplaceStringEditor';
FInplaceStringEditor.Top := 0;
FInplaceStringEditor.Visible := False;
FInplaceStringEditor.Width := 0;
FInplaceStringEditor.Font.Assign(FInspector.FValuesFont);
FInplaceStringEditor.OnEnter := InplaceStringEditorEnter;
FInplaceStringEditor.OnExit := InplaceStringEditorExit;
FInplaceStringEditor.OnKeyPress := InplaceStringEditorKeyPress;
FCanSelect := True;
end;
destructor TMyPropertyInspector.Destroy;
begin
FInspector.Free;
FInplaceStringEditor.Free;
inherited Destroy;
end;
procedure TMyPropertyInspector.Loaded;
begin
inherited Loaded;
end;
procedure TMyPropertyInspector.WMSize(var Message: TMessage);
begin
FInspector.Width := Self.Width;
Invalidate;
end;
procedure TMyPropertyInspector.AddItem(APropName, APropValue: string);
begin
FInspector.CalculateInspectorHeight;
FInspector.Items.AddItem(APropName, APropValue);
FInspector.Invalidate;
Self.Invalidate;
end;
function TMyPropertyInspector.GetSelectedPropertyName: string;
begin
Result := '';
if FInspector.FSelectedRow <> -1 then
begin
Result := FInspector.FInspectorItems.FPropertyNames.Strings[FInspector.FSelectedRow];
end;
end;
function TMyPropertyInspector.GetSelectedPropertyValue: string;
begin
Result := '';
if FInspector.FSelectedRow <> -1 then
begin
Result := FInspector.FInspectorItems.FPropertyValues.Strings[FInspector.FSelectedRow];
end;
end;
function TMyPropertyInspector.RowCount: Integer;
begin
Result := FInspector.RowCount;
end;
procedure TMyPropertyInspector.InplaceStringEditorEnter(Sender: TObject);
begin
FCanSelect := False;
FLastSelectedRow := FInplaceStringEditor.Tag;
end;
procedure TMyPropertyInspector.InplaceStringEditorExit(Sender: TObject);
begin
if SetPropertyValue(True) then
begin
FCanSelect := True;
end;
end;
procedure TMyPropertyInspector.InplaceStringEditorKeyPress(Sender: TObject;
var Key: Char);
begin
if Key = Chr(VK_RETURN) then
begin
Key := #0;
FInplaceStringEditor.SelectAll;
end;
end;
procedure TMyPropertyInspector.SelectRow(Sender: TObject; PropName, PropValue: string; RowIndex: Integer);
begin
FSelectedRowName := PropName;
FLastSelectedRowName := PropName;
FInplaceStringEditor.Height := FInspector.GetRowHeight - 2;
FInplaceStringEditor.Left := Self.Width div 2;
FInplaceStringEditor.Tag := RowIndex;
FInplaceStringEditor.Text := GetSelectedPropertyValue;
FInplaceStringEditor.Top := FInspector.RowIndexToMousePosition(FInspector.FSelectedRow) + 1 - Self.VertScrollBar.Position;
FInplaceStringEditor.Visible := True;
FInplaceStringEditor.Width := FInspector.GetValueRowWidth - 3;
FInplaceStringEditor.SetFocus;
FInplaceStringEditor.SelectAll;
end;
function TMyPropertyInspector.SetPropertyValue(
RevertToPreviousValueOnFail: Boolean): Boolean;
var
S: string;
begin
Result := False;
S := FInplaceStringEditor.Text;
if ValidateStringValue(S) then
begin
Result := True;
end
else
begin
ShowMessage('"' + S + '"' + 'is not a valid value.');
Result := False;
end;
end;
function TMyPropertyInspector.ValidateStringValue(Value: string): Boolean;
begin
// a quick and dirty way of testing for a valid string value, here we just
// look for strings that are not zero length.
Result := Length(Value) > 0;
end;
end.
Problem (detailed)
The confusion I have all comes down to who receives focus first and how to handle and respond to it correctly. Because I am custom drawing my rows I determine where the mouse is when clicking on the inspector control and then I draw the selected row to show this. When handling the inplace editors however, especially the OnEnter and OnExit event I have been facing all kinds of funky problems where in some cases I have been stuck in a cycle of the validate error message repeatedly showing for example (because focus is switching from my inspector to the inplace editor and back and forth).
To populate my inspector at runtime you can do the following:
procedure TForm1.Button1Click(Sender: TObject);
begin
MyPropertyInspector1.AddItem('A', 'Some Text');
MyPropertyInspector1.AddItem('B', 'Hello World');
MyPropertyInspector1.AddItem('C', 'Blah Blah');
MyPropertyInspector1.AddItem('D', 'The Sky is Blue');
MyPropertyInspector1.AddItem('E', 'Another String');
end;
A little something you may try:
Click on a row
Delete the contents from the inplace editor
Select another row
The validate error message box appears (don't close it yet)
With the message box still visible, move your mouse over another row
Now press Enter to close the message box
You will notice the selected row has now moved to where the mouse was
What I need is after the validate message box has shown and closed, I need to set the focus back to the row that was been validated in the first place. It gets confusing because it seems (or so I think) that the inplace editors OnExit is been called after the WMMouseDown(var Message: TMessage); code of my inspector.
To put it as simple as I can if the question remains unclear, the behavior of the Delphi Object Inspector is what I am trying to implement into my component. You enter a value into the inplace editors, if it fails the validation then display a messagebox and then focus back to the row that was last selected. The inplace editor validation should occur as soon as focus is switched away from the inplace editor.
I just can't seem to figure out what is been called first and what is blocking events been fired, it becomes confusing because the way I draw my selected row is determined by where the mouse was when clicking on the inspector control.
This is your flow of events:
TMyCustomInspector.WMMouseDown is called
Therein, Parent.SetFocus is called
The focus is removed from the Edit control and TMyPropertyInspector.InplaceStringEditorExit is called
The message dialog is shown by SetPropertyValue
FSelectedRow is being reset
TMyPropertyInspector.SelectRow is called (via TMyCustomInspector.FOnSelectRow) which resets the focus to the replaced Edit control.
What you need to is to prevent FSelectedRow being reset in case of validation did not succeed. All needed ingredients are already there, just add this one condition:
if FCanSelect then
FSelectedRow := MousePositionToRowIndex;
A few remarks:
Make FCanSelect a protected or private field of TMyCustomInspector,
You need to check for limits in TMyCustomInspector.MousePositionToRowIndex in order to return -1.
Your problem is very interesting. From what I gather, you want a way to reset the focus to the invalid row, when the evaluation is false. Where I see you do this is in your SetPropertyValue function. I believe if you do the following, you will be able to reset the focus after the user clicks "OK" in the message:
function TMyPropertyInspector.SetPropertyValue(
RevertToPreviousValueOnFail: Boolean): Boolean;
var
S: string;
begin
Result := False;
S := FInplaceStringEditor.Text;
if ValidateStringValue(S) then
begin
Result := True;
end
else
begin
if (MessageDlg('"' + S + '"' + 'is not a valid value.', mtError, [mbOK], 0)) = mrOK then
begin
SelectRow(nil, FSelectedRowName, FInplaceStringEditor.Text, FInplaceStringEditor.Tag);
end;
Result := False;
end;
end;
Changing the ShowMessage to MessageDlg will allow for an action to occur when the button is pressed. Then calling your SelectRow function with (what I believe are) global variables representing the information about the last row will set that focus to the bad cell.

How to make custom BitBtn?

How to make custom BitBtn with color property?
I have found one solution here, but it is a TButton not TBitBtn so I have edited the code as follows :
unit ColorBitBtn;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, StdCtrls, Buttons, ExtCtrls;
type
TColorBitBtn = class(TBitBtn)
private
ShowBackColor : Boolean;
FCanvas : TCanvas;
IsFocused : Boolean;
FBackColor : TColor;
FForeColor : TColor;
FHoverColor : TColor;
procedure SetBackColor(const Value: TColor);
procedure SetForeColor(const Value: TColor);
procedure SetHoverColor(const Value: TColor);
protected
procedure CreateParams(var Params: TCreateParams); override;
procedure WndProc(var Message : TMessage); override;
procedure SetBitBtnStyle(Value: Boolean);
procedure DrawBitBtn(Rect: TRect; State: UINT);
procedure CMEnabledChanged(var Message: TMessage); message CM_ENABLEDCHANGED;
procedure CMFontChanged(var Message: TMessage); message CM_FONTCHANGED;
procedure CNMeasureItem(var Message: TWMMeasureItem); message CN_MEASUREITEM;
procedure CNDrawItem(var Message: TWMDrawItem); message CN_DRAWITEM;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property BackColor : TColor read FBackColor write SetBackColor default clBtnFace;
property ForeColor : TColor read FForeColor write SetForeColor default clBtnText;
property HoverColor: TColor read FHoverColor write SetHoverColor default clBtnFace;
end;
procedure Register;
implementation
constructor TColorBitBtn.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ShowBackColor := True;
FCanvas := TCanvas.Create;
BackColor := clBtnFace;
ForeColor := clBtnText;
HoverColor := clBtnFace;
end;
destructor TColorBitBtn.Destroy;
begin
FreeAndNil(FCanvas);
inherited Destroy;
end;
procedure TColorBitBtn.WndProc(var Message : TMessage);
begin
if (Message.Msg = CM_MOUSELEAVE) then
begin
ShowBackColor := True;
Invalidate;
end;
if (Message.Msg = CM_MOUSEENTER) then
begin
ShowBackColor := False;
Invalidate;
end;
inherited;
end;
procedure TColorBitBtn.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
with Params do
Style := Style or BS_OWNERDRAW;
end;
procedure TColorBitBtn.SetBitBtnStyle(Value: Boolean);
begin
if Value <> IsFocused then
begin
IsFocused := Value;
Invalidate;
end;
end;
procedure TColorBitBtn.CNMeasureItem(var Message: TWMMeasureItem);
begin
with Message.MeasureItemStruct^ do
begin
itemWidth := Width;
itemHeight := Height;
end;
end;
procedure TColorBitBtn.CNDrawItem(var Message: TWMDrawItem);
var
SaveIndex: Integer;
begin
with Message.DrawItemStruct^ do
begin
SaveIndex := SaveDC(hDC);
FCanvas.Lock;
try
FCanvas.Handle := hDC;
FCanvas.Font := Font;
FCanvas.Brush := Brush;
DrawBitBtn(rcItem, itemState);
finally
FCanvas.Handle := 0;
FCanvas.Unlock;
RestoreDC(hDC, SaveIndex);
end;
end;
Message.Result := 1;
end;
procedure TColorBitBtn.CMEnabledChanged(var Message: TMessage);
begin
inherited;
Invalidate;
end;
procedure TColorBitBtn.CMFontChanged(var Message: TMessage);
begin
inherited;
Invalidate;
end;
procedure TColorBitBtn.SetBackColor(const Value: TColor);
begin
if FBackColor <> Value then
begin
FBackColor:= Value;
Invalidate;
end;
end;
procedure TColorBitBtn.SetForeColor(const Value: TColor);
begin
if FForeColor <> Value then
begin
FForeColor:= Value;
Invalidate;
end;
end;
procedure TColorBitBtn.SetHoverColor(const Value: TColor);
begin
if FHoverColor <> Value then
begin
FHoverColor:= Value;
Invalidate;
end;
end;
procedure TColorBitBtn.DrawBitBtn(Rect: TRect; State: UINT);
var Flags, OldMode: Longint;
IsDown, IsDefault, IsDisabled: Boolean;
OldColor: TColor;
OrgRect: TRect;
NewCaption : string;
begin
NewCaption := Caption;
OrgRect := Rect;
Flags := DFCS_BUTTONPUSH or DFCS_ADJUSTRECT;
IsDown := State and ODS_SELECTED <> 0;
IsDisabled := State and ODS_DISABLED <> 0;
IsDefault := State and ODS_FOCUS <> 0;
if IsDown then Flags := Flags or DFCS_PUSHED;
if IsDisabled then Flags := Flags or DFCS_INACTIVE;
if (IsFocused or IsDefault) then
begin
FCanvas.Pen.Color := clWindowFrame;
FCanvas.Pen.Width := 1;
FCanvas.Brush.Style := bsClear;
FCanvas.Rectangle(Rect.Left, Rect.Top, Rect.Right, Rect.Bottom);
InflateRect(Rect, - 1, - 1);
end;
if IsDown then
begin
FCanvas.Pen.Color := clBtnShadow;
FCanvas.Pen.Width := 1;
FCanvas.Brush.Color := clBtnFace;
FCanvas.Rectangle(Rect.Left, Rect.Top, Rect.Right, Rect.Bottom);
InflateRect(Rect, - 1, - 1);
end
else
begin
DrawFrameControl(FCanvas.Handle, Rect, DFC_BUTTON, Flags);
end;
if IsDown then OffsetRect(Rect, 1, 1);
OldColor := FCanvas.Brush.Color;
if ShowBackColor then
FCanvas.Brush.Color := BackColor
else
FCanvas.Brush.Color := HoverColor;
FCanvas.FillRect(Rect);
FCanvas.Brush.Color := OldColor;
OldMode := SetBkMode(FCanvas.Handle, TRANSPARENT);
FCanvas.Font.Color := ForeColor;
if IsDisabled then
DrawState(FCanvas.Handle, FCanvas.Brush.Handle, nil, Integer(NewCaption), 0,
((Rect.Right - Rect.Left) - FCanvas.TextWidth(NewCaption)) div 2,
((Rect.Bottom - Rect.Top) - FCanvas.TextHeight(NewCaption)) div 2,
0, 0, DST_TEXT or DSS_DISABLED)
else
begin
InflateRect(Rect, -4, -4);
DrawText(FCanvas.Handle, PChar(NewCaption), - 1, Rect, DT_WORDBREAK or DT_CENTER);
end;
SetBkMode(FCanvas.Handle, OldMode);
if (IsFocused and IsDefault) then
begin
Rect := OrgRect;
InflateRect(Rect, - 4, - 4);
FCanvas.Pen.Color := clWindowFrame;
FCanvas.Brush.Color := clBtnFace;
DrawFocusRect(FCanvas.Handle, Rect);
end;
end;
procedure Register;
begin
RegisterComponents('Standard', [TColorBitBtn]);
end;
initialization
RegisterClass(TColorBitBtn); // needed for persistence at runtime
end.
After doing the same. it compiles perfectly without any error. But the Font.Color does not get changed on any event like OnClick, OnMouseDown etc and another problem is not look like Button or BitBtn after enabling Theme Manifest like the following picture
Here the first is Standard Button, Standard BitBtn followed by Custom BitBtn created by the above code after adding Theme Manifest.

Performance issues re-sizing large amount of components on form resize

I feel my failure so far lies in search terms as information on this has to be pretty common. Basically I am looking for common solutions and best practices when performing resizes on several components while a form is resized.
I have a form with a component that is based upon TScrollBox. The ScrollBox contains rows which are added dynamically at run time. They are basically a subcomponent. Each one has an image on the left and a memo on the right. The height is set based upon the width and aspect ratio of the image. Upon the resize of the scroll box a loop sets the width of the rows triggering the rows own internal resize. The loop also sets the relative top position if the heights have changed.
Screen shot:
Around 16 rows performs fine. My goal is closer to 32 rows which is very choppy and can peg a core at 100% usage.
I have tried:
Added a check to prevent a new resize starting while the previous has yet to complete. It answered if it occured and it does sometimes.
I tried preventing it resizing more often than every 30 ms which would allow for 30 frame per second drawing. Mixed results.
Changed the rows base component from TPanel to TWinControl. Not sure if there is a performance penalty using the Panel but its an old habit.
With and without double buffering.
I would like to allow row resizing to occur during a resize as a preview to how large the image will be in the row. That eliminates one obvious solution that in some applications is an acceptable loss.
Right now the resize code internally for the row is completely dynamic and based upon the dimensions of each image. Next thing I plan to try is to basically specify the Aspect Ratio, Max Width/Height based on the largest image in the collection. This should reduce the amount of math per row. But it seems like the issues are more the resize event and the loop itself?
Full unit code for the components:
unit rPBSSVIEW;
interface
uses
Classes, Controls, Forms, ExtCtrls, StdCtrls, Graphics, SysUtils, rPBSSROW, Windows, Messages;
type
TPBSSView = class(TScrollBox)
private
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure ResizeRows(Sender: TObject);
procedure AddRow(FileName: String);
procedure FillRow(Row: Integer; ImageStream: TMemoryStream);
end;
var
PBSSrow: Array of TPBSSRow;
Resizingn: Boolean;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Standard', [TScrollBox]);
end;
procedure TPBSSView.AddRow(FileName: String);
begin
SetLength(PBSSrow,(Length(PBSSrow) + 1));
PBSSrow[Length(PBSSrow)-1] := TPBSSRow.create(self);
With PBSSrow[Length(PBSSrow)-1] do
begin
Left := 2;
if (Length(PBSSrow)-1) = 0 then Top := 2 else Top := ((PBSSRow[Length(PBSSRow) - 2].Top + PBSSRow[Length(PBSSRow) - 2].Height) + 2);
Width := (inherited ClientWidth - 4);
Visible := True;
Parent := Self;
PanelLeft.Caption := FileName;
end;
end;
procedure TPBSSView.FillRow(Row: Integer; ImageStream: TMemoryStream);
begin
PBSSRow[Row].LoadImageFromStream(ImageStream);
end;
procedure TPBSSView.ResizeRows(Sender: TObject);
var
I, X: Integer;
begin
if Resizingn then exit
else
begin
Resizingn := True;
HorzScrollBar.Visible := False;
X := (inherited ClientWidth - 4);
if Length(PBSSrow) > 0 then
for I := 0 to Length(PBSSrow) - 1 do
Begin
PBSSRow[I].Width := X; //Set Width
if not (I = 0) then //Move all next ones down.
begin
PBSSRow[I].Top := (PBSSRow[(I - 1)].Top + PBSSRow[(I - 1)].Height) + 2;
end;
Application.ProcessMessages;
End;
HorzScrollBar.Visible := True;
Resizingn := False;
end;
end;
constructor TPBSSView.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
OnResize := ResizeRows;
DoubleBuffered := True;
VertScrollBar.Tracking := True;
Resizingn := False;
end;
destructor TPBSSView.Destroy;
begin
inherited;
end;
end.
Row Code:
unit rPBSSROW;
interface
uses
Classes, Controls, Forms, ExtCtrls, StdCtrls, Graphics, pngimage, SysUtils;
type
TPBSSRow = class(TWinControl)
private
FImage: TImage;
FPanel: TPanel;
FMemo: TMemo;
FPanelLeft: TPanel;
FPanelRight: TPanel;
FImageWidth: Integer;
FImageHeight: Integer;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure MyPanelResize(Sender: TObject);
procedure LeftPanelResize(Sender: TObject);
published
procedure LoadImageFromStream(ImageStream: TMemoryStream);
property Image: TImage read FImage;
property Panel: TPanel read FPanel;
property PanelLeft: TPanel read FPanelLeft;
property PanelRight: TPanel read FPanelRight;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Standard', [TWinControl]);
end;
procedure TPBSSRow.MyPanelResize(Sender: TObject);
begin
if (Width - 466) <= FImageWidth then FPanelLeft.Width := (Width - 466)
else FPanelLeft.Width := FImageWidth;
FPanelRight.Width := (Width - FPanelLeft.Width);
end;
procedure TPBSSRow.LeftPanelResize(Sender: TObject);
var
AspectRatio: Extended;
begin
FPanelRight.Left := (FPanelLeft.Width);
//Enforce Info Minimum Height or set Height
if FImageHeight > 0 then AspectRatio := (FImageHeight/FImageWidth) else
AspectRatio := 0.4;
if (Round(AspectRatio * FPanelLeft.Width)) >= 212 then
begin
Height := (Round(AspectRatio * FPanelLeft.Width));
FPanelLeft.Height := Height;
FPanelRight.Height := Height;
end
else
begin
Height :=212;
FPanelLeft.Height := Height;
FPanelRight.Height := Height;
end;
if Fimage.Height >= FImageHeight then FImage.Stretch := False else Fimage.Stretch := True;
if Fimage.Width >= FImageWidth then FImage.Stretch := False else Fimage.Stretch := True;
end;
procedure TPBSSRow.LoadImageFromStream(ImageStream: TMemoryStream);
var
P: TPNGImage;
n: Integer;
begin
P := TPNGImage.Create;
ImageStream.Position := 0;
P.LoadFromStream(ImageStream);
FImage.Picture.Assign(P);
FImageWidth := P.Width;
FImageHeight := P.Height;
end;
constructor TPBSSRow.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
BevelInner := bvNone;
BevelOuter := bvNone;
BevelKind := bkNone;
Color := clWhite;
OnResize := MyPanelResize;
DoubleBuffered := True;
//Left Panel for Image
FPanelLeft := TPanel.Create(Self);
with FPanelLeft do
begin
SetSubComponent(true);
Align := alLeft;
Parent := Self;
//SetBounds(0,0,100,100);
ParentBackground := False;
Color := clBlack;
Font.Color := clLtGray;
Constraints.MinWidth := 300;
BevelInner := bvNone;
BevelOuter := bvNone;
BevelKind := bkNone;
BorderStyle := bsNone;
OnResize := LeftPanelResize;
end;
//Image for left panel
FImage := TImage.Create(Self);
FImage.SetSubComponent(true);
FImage.Align := alClient;
FImage.Parent := FPanelLeft;
FImage.Center := True;
FImage.Stretch := True;
FImage.Proportional := True;
//Right Panel for Info
FPanelRight := TPanel.Create(Self);
with FPanelRight do
begin
SetSubComponent(true);
Parent := Self;
Padding.SetBounds(2,5,5,2);
BevelInner := bvNone;
BevelOuter := bvNone;
BevelKind := bkNone;
BorderStyle := bsNone;
Color := clLtGray;
end;
//Create Memo in Right Panels
FMemo := TMemo.create(self);
with FMemo do
begin
SetSubComponent(true);
Parent := FPanelRight;
Align := alClient;
BevelOuter := bvNone;
BevelInner := bvNone;
BorderStyle := bsNone;
Color := clLtGray;
end;
end;
destructor TPBSSRow.Destroy;
begin
inherited;
end;
end.
A few tips:
TWinControl already ís a container, you do not need another panel inside it to add controls
You do not need an TImage component to view a graphic, that can also with TPaintBox, or as in my example control below, a TCustomControl,
Since all of your other panels are not recognizable (borders and bevels are disabled), loose them altogether and place the TMemo directly on your row control,
SetSubComponent is only for design time usage. You do not need it. Nor the Register procedures for that matter.
Put the global rows array inside your class definition, otherwise multiple TPBSSView controls will use the same array!
TWinControl already tracks all its child controls, so you won't need the array anyway, see my example below,
Make use of the Align property to save you from realigning manually,
If the memo control is just for showing text, then remove it and paint the text yourself.
Try this one for starters:
unit PBSSView;
interface
uses
Windows, Messages, Classes, Controls, SysUtils, Graphics, ExtCtrls, StdCtrls,
Forms, PngImage;
type
TPBSSRow = class(TCustomControl)
private
FGraphic: TPngImage;
FStrings: TStringList;
function ImageHeight: Integer; overload;
function ImageHeight(ControlWidth: Integer): Integer; overload;
function ImageWidth: Integer; overload;
function ImageWidth(ControlWidth: Integer): Integer; overload;
procedure WMEraseBkgnd(var Message: TWmEraseBkgnd); message WM_ERASEBKGND;
procedure WMWindowPosChanging(var Message: TWMWindowPosChanging);
message WM_WINDOWPOSCHANGING;
protected
procedure Paint; override;
procedure RequestAlign; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure LoadImageFromStream(Stream: TMemoryStream);
property Strings: TStringList read FStrings;
end;
TPBSSView = class(TScrollBox)
private
function GetRow(Index: Integer): TPBSSRow;
procedure WMEnterSizeMove(var Message: TMessage); message WM_ENTERSIZEMOVE;
procedure WMEraseBkgnd(var Message: TWmEraseBkgnd); message WM_ERASEBKGND;
procedure WMExitSizeMove(var Message: TMessage); message WM_EXITSIZEMOVE;
protected
procedure PaintWindow(DC: HDC); override;
public
constructor Create(AOwner: TComponent); override;
procedure AddRow(const FileName: TFileName);
procedure FillRow(Index: Integer; ImageStream: TMemoryStream);
property Rows[Index: Integer]: TPBSSRow read GetRow;
end;
implementation
{ TPBSSRow }
constructor TPBSSRow.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Width := 300;
Height := 50;
FStrings := TStringList.Create;
end;
destructor TPBSSRow.Destroy;
begin
FStrings.Free;
FGraphic.Free;
inherited Destroy;
end;
function TPBSSRow.ImageHeight: Integer;
begin
Result := ImageHeight(Width);
end;
function TPBSSRow.ImageHeight(ControlWidth: Integer): Integer;
begin
if (FGraphic <> nil) and not FGraphic.Empty then
Result := Round(ImageWidth(ControlWidth) * FGraphic.Height / FGraphic.Width)
else
Result := Height;
end;
function TPBSSRow.ImageWidth: Integer;
begin
Result := ImageWidth(Width);
end;
function TPBSSRow.ImageWidth(ControlWidth: Integer): Integer;
begin
Result := ControlWidth div 2;
end;
procedure TPBSSRow.LoadImageFromStream(Stream: TMemoryStream);
begin
FGraphic.Free;
FGraphic := TPngImage.Create;
Stream.Position := 0;
FGraphic.LoadFromStream(Stream);
Height := ImageHeight + Padding.Bottom;
end;
procedure TPBSSRow.Paint;
var
R: TRect;
begin
Canvas.StretchDraw(Rect(0, 0, ImageWidth, ImageHeight), FGraphic);
SetRect(R, ImageWidth, 0, Width, ImageHeight);
Canvas.FillRect(R);
Inc(R.Left, 10);
DrawText(Canvas.Handle, FStrings.Text, -1, R, DT_EDITCONTROL or
DT_END_ELLIPSIS or DT_NOFULLWIDTHCHARBREAK or DT_NOPREFIX or DT_WORDBREAK);
Canvas.FillRect(Rect(0, ImageHeight, Width, Height));
end;
procedure TPBSSRow.RequestAlign;
begin
{eat inherited}
end;
procedure TPBSSRow.WMEraseBkgnd(var Message: TWmEraseBkgnd);
begin
Message.Result := 1;
end;
procedure TPBSSRow.WMWindowPosChanging(var Message: TWMWindowPosChanging);
begin
inherited;
if (FGraphic <> nil) and not FGraphic.Empty then
Message.WindowPos.cy := ImageHeight(Message.WindowPos.cx) + Padding.Bottom;
end;
{ TPBSSView }
procedure TPBSSView.AddRow(const FileName: TFileName);
var
Row: TPBSSRow;
begin
Row := TPBSSRow.Create(Self);
Row.Align := alTop;
Row.Padding.Bottom := 2;
Row.Parent := Self;
end;
constructor TPBSSView.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
VertScrollBar.Tracking := True;
end;
procedure TPBSSView.FillRow(Index: Integer; ImageStream: TMemoryStream);
begin
Rows[Index].LoadImageFromStream(ImageStream);
end;
function TPBSSView.GetRow(Index: Integer): TPBSSRow;
begin
Result := TPBSSRow(Controls[Index]);
end;
procedure TPBSSView.PaintWindow(DC: HDC);
begin
{eat inherited}
end;
procedure TPBSSView.WMEnterSizeMove(var Message: TMessage);
begin
if not AlignDisabled then
DisableAlign;
inherited;
end;
procedure TPBSSView.WMEraseBkgnd(var Message: TWmEraseBkgnd);
var
DC: HDC;
begin
DC := GetDC(Handle);
try
FillRect(DC, Rect(0, VertScrollBar.Range, Width, Height), Brush.Handle);
finally
ReleaseDC(Handle, DC);
end;
Message.Result := 1;
end;
procedure TPBSSView.WMExitSizeMove(var Message: TMessage);
begin
inherited;
if AlignDisabled then
EnableAlign;
end;
end.
If this still performs badly, then there are multiple other enhancements possible.
Update:
Flickering eliminated by overriding/intercepting WM_ERASEBKGND (and intercepting PaintWindow for versions < XE2),
Better performance by making use of DisableAlign and EnableAlign.
I don't know if this will make a significant difference, but instead setting PBSSRow[I].Width and PBSSRow[I].Top separately, make one call to PBSSRow[I].SetBounds instead. This will save you one Resize event for that SubComponent.

Delphi: Changing the Button Color using a Class Helper

I need to change the visual style of my delphi form controls inorder to show them from a .Net environment. To do this, I need to change the colors of delphi controls to blue ($00FCF5EE). I have used "TButton" controls widely which doesn't have a "Color" property.So, instead of changing the buttons to speed buttons, I have tried a different approach by introducing a parent form and inheriting all the other forms from this parent form. In the parent form, I have a class helper to change the color of buttons. Here is the code: (I am using Delphi 2007)
TButtonHelper=class helper for TButton
private
procedure doChangeColor;
public
procedure DrawChangeColor;
end;
TParentForm = class(TForm)
public
procedure AfterConstruction; override;
end;
And in the implementation section, I have
procedure TButtonHelper.doChangeColor;
var
SaveIndex: Integer;
FCanvas:TCanvas;
rect:TRect;
begin
if csDestroying in ComponentState then exit;
FCanvas:=TCanvas.Create;
SaveIndex := SaveDC(Self.Handle);
FCanvas.Lock;
try
FCanvas.Handle := Handle;
FCanvas.Font := Font;
FCanvas.Brush := self.Brush;
FCanvas.Brush.Color:=$00FCF5EE;
FCanvas.FillRect(BoundsRect);//Omitting the code to draw the text
finally
FCanvas.Handle := 0;
FCanvas.Unlock;
RestoreDC(handle, SaveIndex);
FCanvas.Free;
end;
end;
procedure TButtonHelper.DrawChangeColor;
begin
doChangeColor;
self.Repaint;
end;
procedure TParentForm.AfterConstruction;
var
i : Integer;
wc: TControl;
begin
inherited;
for i := 0 to self.ControlCount - 1 do begin
wc:=Controls[i];
if wc is TButton then
TButton(wc).DrawChangeColor;
end;
end;
But this doesn't work. Although the doChangeColor method is executed, it is not changing the color of the button.Please let me know what I am missing here.
Thanking you all,
Pradeep
here's a class that adds color properties to the standard TButton:
unit u_class_colorbutton;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, StdCtrls, Buttons, ExtCtrls;
type
TColorButton = class(TButton)
private
ShowBackColor : Boolean;
FCanvas : TCanvas;
IsFocused : Boolean;
FBackColor : TColor;
FForeColor : TColor;
FHoverColor : TColor;
procedure SetBackColor(const Value: TColor);
procedure SetForeColor(const Value: TColor);
procedure SetHoverColor(const Value: TColor);
protected
procedure CreateParams(var Params: TCreateParams); override;
procedure WndProc(var Message : TMessage); override;
procedure SetButtonStyle(Value: Boolean); override;
procedure DrawButton(Rect: TRect; State: UINT);
procedure CMEnabledChanged(var Message: TMessage); message CM_ENABLEDCHANGED;
procedure CMFontChanged(var Message: TMessage); message CM_FONTCHANGED;
procedure CNMeasureItem(var Message: TWMMeasureItem); message CN_MEASUREITEM;
procedure CNDrawItem(var Message: TWMDrawItem); message CN_DRAWITEM;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property BackColor : TColor read FBackColor write SetBackColor default clBtnFace;
property ForeColor : TColor read FForeColor write SetForeColor default clBtnText;
property HoverColor: TColor read FHoverColor write SetHoverColor default clBtnFace;
end;
procedure Register;
implementation
constructor TColorButton.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ShowBackColor := True;
FCanvas := TCanvas.Create;
BackColor := clBtnFace;
ForeColor := clBtnText;
HoverColor := clBtnFace;
end;
destructor TColorButton.Destroy;
begin
FreeAndNil(FCanvas);
inherited Destroy;
end;
procedure TColorButton.WndProc(var Message : TMessage);
begin
if (Message.Msg = CM_MOUSELEAVE) then
begin
ShowBackColor := True;
Invalidate;
end;
if (Message.Msg = CM_MOUSEENTER) then
begin
ShowBackColor := False;
Invalidate;
end;
inherited;
end;
procedure TColorButton.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
with Params do
Style := Style or BS_OWNERDRAW;
end;
procedure TColorButton.SetButtonStyle(Value: Boolean);
begin
if Value <> IsFocused then
begin
IsFocused := Value;
Invalidate;
end;
end;
procedure TColorButton.CNMeasureItem(var Message: TWMMeasureItem);
begin
with Message.MeasureItemStruct^ do
begin
itemWidth := Width;
itemHeight := Height;
end;
end;
procedure TColorButton.CNDrawItem(var Message: TWMDrawItem);
var
SaveIndex: Integer;
begin
with Message.DrawItemStruct^ do
begin
SaveIndex := SaveDC(hDC);
FCanvas.Lock;
try
FCanvas.Handle := hDC;
FCanvas.Font := Font;
FCanvas.Brush := Brush;
DrawButton(rcItem, itemState);
finally
FCanvas.Handle := 0;
FCanvas.Unlock;
RestoreDC(hDC, SaveIndex);
end;
end;
Message.Result := 1;
end;
procedure TColorButton.CMEnabledChanged(var Message: TMessage);
begin
inherited;
Invalidate;
end;
procedure TColorButton.CMFontChanged(var Message: TMessage);
begin
inherited;
Invalidate;
end;
procedure TColorButton.SetBackColor(const Value: TColor);
begin
if FBackColor <> Value then
begin
FBackColor:= Value;
Invalidate;
end;
end;
procedure TColorButton.SetForeColor(const Value: TColor);
begin
if FForeColor <> Value then
begin
FForeColor:= Value;
Invalidate;
end;
end;
procedure TColorButton.SetHoverColor(const Value: TColor);
begin
if FHoverColor <> Value then
begin
FHoverColor:= Value;
Invalidate;
end;
end;
procedure TColorButton.DrawButton(Rect: TRect; State: UINT);
var Flags, OldMode: Longint;
IsDown, IsDefault, IsDisabled: Boolean;
OldColor: TColor;
OrgRect: TRect;
NewCaption : string;
begin
NewCaption := Caption;
OrgRect := Rect;
Flags := DFCS_BUTTONPUSH or DFCS_ADJUSTRECT;
IsDown := State and ODS_SELECTED <> 0;
IsDisabled := State and ODS_DISABLED <> 0;
IsDefault := State and ODS_FOCUS <> 0;
if IsDown then Flags := Flags or DFCS_PUSHED;
if IsDisabled then Flags := Flags or DFCS_INACTIVE;
if (IsFocused or IsDefault) then
begin
FCanvas.Pen.Color := clWindowFrame;
FCanvas.Pen.Width := 1;
FCanvas.Brush.Style := bsClear;
FCanvas.Rectangle(Rect.Left, Rect.Top, Rect.Right, Rect.Bottom);
InflateRect(Rect, - 1, - 1);
end;
if IsDown then
begin
FCanvas.Pen.Color := clBtnShadow;
FCanvas.Pen.Width := 1;
FCanvas.Brush.Color := clBtnFace;
FCanvas.Rectangle(Rect.Left, Rect.Top, Rect.Right, Rect.Bottom);
InflateRect(Rect, - 1, - 1);
end
else
begin
DrawFrameControl(FCanvas.Handle, Rect, DFC_BUTTON, Flags);
end;
if IsDown then OffsetRect(Rect, 1, 1);
OldColor := FCanvas.Brush.Color;
if ShowBackColor then
FCanvas.Brush.Color := BackColor
else
FCanvas.Brush.Color := HoverColor;
FCanvas.FillRect(Rect);
FCanvas.Brush.Color := OldColor;
OldMode := SetBkMode(FCanvas.Handle, TRANSPARENT);
FCanvas.Font.Color := ForeColor;
if IsDisabled then
DrawState(FCanvas.Handle, FCanvas.Brush.Handle, nil, Integer(NewCaption), 0,
((Rect.Right - Rect.Left) - FCanvas.TextWidth(NewCaption)) div 2,
((Rect.Bottom - Rect.Top) - FCanvas.TextHeight(NewCaption)) div 2,
0, 0, DST_TEXT or DSS_DISABLED)
else
begin
InflateRect(Rect, -4, -4);
DrawText(FCanvas.Handle, PChar(NewCaption), - 1, Rect, DT_WORDBREAK or DT_CENTER);
end;
SetBkMode(FCanvas.Handle, OldMode);
if (IsFocused and IsDefault) then
begin
Rect := OrgRect;
InflateRect(Rect, - 4, - 4);
FCanvas.Pen.Color := clWindowFrame;
FCanvas.Brush.Color := clBtnFace;
DrawFocusRect(FCanvas.Handle, Rect);
end;
end;
procedure Register;
begin
RegisterComponents('Standard', [TColorButton]);
end;
initialization
RegisterClass(TColorButton); // needed for persistence at runtime
end.
You can hack it into your application easily:
find/replace all TButton references to TColorButton
inside your .pas and .dfm files.
You can set separate colors for background, font and hovering.
If you want add styling application wide, maybe it is better to create a GUI with a library that has native support like DevExpress, TMS, ...
Personally, I like DevExpress the most but that's a matter of personal taste.

Best way to do non-flickering, segmented graphics updates in Delphi?

I thought I could just throw this out there and just ask: I have seen Delphi controls that are flawless in terms of graphical effects. Meaning: no flickering, sectioned updates (only redraw the section of a control that is marked as dirty) and smooth scrolling.
I have coded a lot of graphical controls over the years, so I know about double buffering, dibs, bitblts and all the "common" stuff (I always use dibs to draw everything if possible, but there is an overhead). Also know about InvalidateRect and checking TCanvas.ClipRect for the actual rect that needs to be updated. Despite all these typical solutions, I find it very difficult to create the same quality components as say - Developer Express or Razed Components. If the graphics is smooth you can bet the scrollbars (native) flicker, and if the scrollbars and frame is smooth you can swear the background flickers during scrolling.
Is there a standard setup of code to handle this? A sort of best practises that ensures smooth redraws of the entire control -- including the non-client area of a control?
For instance, here is a "bare bone" control which take height for segmented updates (only redraw what is needed). If you create it on a form, try moving a window over it, and watch it replace the parts with colors (see paint method).
Does anyone have a similar base class that can handle non client area redraws without flickering?
type
TMyControl = Class(TCustomControl)
private
(* TWinControl: Erase background prior to client-area paint *)
procedure WMEraseBkgnd(var Message: TWmEraseBkgnd);message WM_ERASEBKGND;
Protected
(* TCustomControl: Overrides client-area paint mechanism *)
Procedure Paint;Override;
(* TWinControl: Adjust Win32 parameters for CreateWindow *)
procedure CreateParams(var Params: TCreateParams);override;
public
Constructor Create(AOwner:TComponent);override;
End;
{ TMyControl }
Constructor TMyControl.Create(AOwner:TComponent);
Begin
inherited Create(Aowner);
ControlStyle:=ControlStyle - [csOpaque];
end;
procedure TMyControl.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
(* When a window has this style set, any areas that its
child windows occupy are excluded from the update region. *)
params.ExStyle:=params.ExStyle + WS_CLIPCHILDREN;
(* Exclude VREDRAW & HREDRAW *)
with Params.WindowClass do
Begin
(* When a window class has either of these two styles set,
the window contents will be completely redrawn every time it is
resized either vertically or horizontally (or both) *)
style:=style - CS_VREDRAW;
style:=style - CS_HREDRAW;
end;
end;
procedure TMyControl.Paint;
(* Inline proc: check if a rectangle is "empty" *)
function isEmptyRect(const aRect:TRect):Boolean;
Begin
result:=(arect.Right=aRect.Left) and (aRect.Bottom=aRect.Top);
end;
(* Inline proc: Compare two rectangles *)
function isSameRect(const aFirstRect:TRect;const aSecondRect:TRect):Boolean;
Begin
result:=sysutils.CompareMem(#aFirstRect,#aSecondRect,SizeOf(TRect))
end;
(* Inline proc: This fills the background completely *)
Procedure FullRepaint;
var
mRect:TRect;
Begin
mRect:=getClientRect;
AdjustClientRect(mRect);
Canvas.Brush.Color:=clWhite;
Canvas.Brush.Style:=bsSolid;
Canvas.FillRect(mRect);
end;
begin
(* A full redraw is only issed if:
1. the cliprect is empty
2. the cliprect = clientrect *)
if isEmptyRect(Canvas.ClipRect)
or isSameRect(Canvas.ClipRect,Clientrect) then
FullRepaint else
Begin
(* Randomize a color *)
Randomize;
Canvas.Brush.Color:=RGB(random(255),random(255),random(255));
(* fill "dirty rectangle" *)
Canvas.Brush.Style:=bsSolid;
Canvas.FillRect(canvas.ClipRect);
end;
end;
procedure TMyControl.WMEraseBkgnd(var Message: TWmEraseBkgnd);
begin
message.Result:=-1;
end;
Updated
I just wanted to add that what did the trick was a combination of:
ExcludeClipRect() when drawing the non-clientarea, so you dont overlap with the graphics in the clientarea
Catching the WMNCCalcSize message rather than just using the bordersize for measurements. I also had to take height for the edge sizes:
XEdge := GetSystemMetrics(SM_CXEDGE);
YEdge := GetSystemMetrics(SM_CYEDGE);
Calling RedrawWindow() with the following flags whenever you have scrollbars that have moved or a resize:
mRect:=ClientRect;
mFlags:=rdw_Invalidate
or RDW_NOERASE
or RDW_FRAME
or RDW_INTERNALPAINT
or RDW_NOCHILDREN;
RedrawWindow(windowhandle,#mRect,0,mFlags);
When updating the background during the Paint() method, avoid drawing over possible child objects, like this (see the RDW_NOCHILDREN mentioned above):
for x := 1 to ControlCount do
begin
mCtrl:=Controls[x-1];
if mCtrl.Visible then
Begin
mRect:=mCtrl.BoundsRect;
ExcludeClipRect(Canvas.Handle,
mRect.Left,mRect.Top,
mRect.Right,mRect.Bottom);
end;
end;
Thanks for the help guys!
Double buffering and fancy drawing tactics are only half the story. The other half, some would argue the more critical half, is to limit how much of your control is invalidated.
In your comments, you mention that you use RedrawWindow(handle, #R, 0, rdw_Invalidate or rdw_Frame). What are you setting the R rectangle to? If you set it to your client area rect, then you are redrawing the entire client area of your control. When scrolling, only a small portion of your control needs to be redrawn - the slice at the "trailing edge" of the scroll direction. Windows will bitblit the rest of the client area screen to screen to move the existing pixels over in the scroll direction.
Also check whether you have set your window flags to require full redraw on scroll. I don't recall the flag names offhand, but you want them turned off so that scroll actions only invalidate a slice of your client area. I believe this is the Windows default.
Even with hardware accelerated graphics, less work is faster than more work. Get your invalidate rects down to the absolute minimum and reduce the number of pixels you're pushing across the system bus.
For instance, here is a "bare bone" control which take height for segmented updates (only redraw what is needed). If you create it on a form, try moving a window over it, and watch it replace the parts with colors (see paint method).
Does anyone have a similar base class that can handle non client area redraws without flickering?
Well, your TMyControl does not have a non client area (yet). So I added BorderWidth := 10; and now it has. ;)
In general, the non client area's of default Windows windows are automatically painted without flickering, including scrollbars, titles, etc... (at least, I have not witnessed otherwise).
If you want to paint your own border, you have to handle WM_NCPAINT. See this code:
unit Unit2;
interface
uses
Classes, Controls, Messages, Windows, SysUtils, Graphics;
type
TMyControl = class(TCustomControl)
private
procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
procedure WMNCPaint(var Message: TWMNCPaint); message WM_NCPAINT;
protected
procedure Paint; override;
procedure CreateParams(var Params: TCreateParams); override;
public
constructor Create(AOwner:TComponent);override;
end;
implementation
{ TMyControl }
constructor TMyControl.Create(AOwner:TComponent);
Begin
Randomize;
inherited Create(Aowner);
ControlStyle:=ControlStyle - [csOpaque];
BorderWidth := 10;
Anchors := [akLeft, akTop, akBottom, akRight];
end;
procedure TMyControl.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
Params.ExStyle := Params.ExStyle or WS_CLIPCHILDREN;
with Params.WindowClass do
style := style and not (CS_HREDRAW or CS_VREDRAW);
end;
procedure TMyControl.Paint;
begin
Canvas.Brush.Color := RGB(Random(255), Random(255), Random(255));
Canvas.FillRect(Canvas.ClipRect);
end;
procedure TMyControl.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
Message.Result := 1;
end;
procedure TMyControl.WMNCPaint(var Message: TWMNCPaint);
var
DC: HDC;
R: TRect;
begin
Message.Result := 0;
if BorderWidth > 0 then
begin
DC := GetWindowDC(Handle);
try
R := ClientRect;
OffsetRect(R, BorderWidth, BorderWidth);
ExcludeClipRect(DC, R.Left, R.Top, R.Right, R.Bottom);
SetRect(R, 0, 0, Width, Height);
Brush.Color := clYellow;
FillRect(DC, R, Brush.Handle);
finally
ReleaseDC(Handle, DC);
end;
end;
end;
end.
A few remarks:
Override CreateParams instead of declaring it virtual. Mind the compiler warning (though I think/hope this is a little mistake).
You don't have to check for isEmptyRect nor isSameRect. If ClipRect is empty, then there is nothing to draw. This is also the reason why never to call Paint directly, but always through Invalidate or equivalent.
AdjustClientRect is not needed. It is called internally when needed for its purpose.
And as a bonus, this is exactly how I draw a chessbord component:
type
TCustomChessBoard = class(TCustomControl)
private
FBorder: TChessBoardBorder;
FOrientation: TBoardOrientation;
FSquareSize: TSquareSize;
procedure BorderChanged;
procedure RepaintBorder;
procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
procedure WMNCPaint(var Message: TWMNCPaint); message WM_NCPAINT;
protected
procedure CreateParams(var Params: TCreateParams); override;
function GetClientRect: TRect; override;
procedure Paint; override;
procedure Resize; override;
public
constructor Create(AOwner: TComponent); override;
procedure Repaint; override;
end;
const
ColCount = 8;
RowCount = ColCount;
procedure TCustomChessBoard.BorderChanged;
begin
RepaintBorder;
end;
constructor TCustomChessBoard.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlStyle := [csOpaque];
end;
procedure TCustomChessBoard.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
with Params.WindowClass do
style := style and not (CS_HREDRAW or CS_VREDRAW);
end;
function TCustomChessBoard.GetClientRect: TRect;
begin
Result := Rect(0, 0, FSquareSize * ColCount, FSquareSize * RowCount);
end;
procedure TCustomChessBoard.Paint;
procedure DrawSquare(Col, Row: Integer);
var
R: TRect;
begin
R := Bounds(Col * FSquareSize, Row * FSquareSize, FSquareSize, FSquareSize);
Canvas.Brush.Color := Random(clWhite);
Canvas.FillRect(R);
end;
var
iCol: Integer;
iRow: Integer;
begin
with Canvas.ClipRect do
for iCol := (Left div FSquareSize) to (Right div FSquareSize) do
for iRow := (Top div FSquareSize) to (Bottom div FSquareSize) do
DrawSquare(iCol, iRow);
end;
procedure TCustomChessBoard.Repaint;
begin
inherited Repaint;
RepaintBorder;
end;
procedure TCustomChessBoard.RepaintBorder;
begin
if Visible and HandleAllocated then
Perform(WM_NCPAINT, 0, 0);
end;
procedure TCustomChessBoard.Resize;
begin
Repaint;
inherited Resize;
end;
procedure TCustomChessBoard.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
Message.Result := 1;
end;
procedure TCustomChessBoard.WMNCPaint(var Message: TWMNCPaint);
var
DC: HDC;
R: TRect;
R2: TRect;
SaveFont: HFONT;
procedure DoCoords(ShiftX, ShiftY: Integer; Alpha, Backwards: Boolean);
const
Format = DT_CENTER or DT_NOCLIP or DT_SINGLELINE or DT_VCENTER;
CoordChars: array[Boolean, Boolean] of Char = (('1', '8'), ('A', 'H'));
var
i: Integer;
C: Char;
begin
C := CoordChars[Alpha, Backwards];
for i := 0 to ColCount - 1 do
begin
DrawText(DC, PChar(String(C)), 1, R, Format);
DrawText(DC, PChar(String(C)), 1, R2, Format);
if Backwards then
Dec(C)
else
Inc(C);
OffsetRect(R, ShiftX, ShiftY);
OffsetRect(R2, ShiftX, ShiftY);
end;
end;
procedure DoBackground(Thickness: Integer; AColor: TColor;
DoPicture: Boolean);
begin
ExcludeClipRect(DC, R.Left, R.Top, R.Right, R.Bottom);
InflateRect(R, Thickness, Thickness);
if DoPicture then
with FBorder.Picture.Bitmap do
BitBlt(DC, R.Left, R.Top, R.Right - R.Left, R.Bottom - R.Top,
Canvas.Handle, R.Left, R.Top, SRCCOPY)
else
begin
Brush.Color := AColor;
FillRect(DC, R, Brush.Handle);
end;
end;
begin
Message.Result := 0;
if BorderWidth > 0 then
with FBorder do
begin
DC := GetWindowDC(Handle);
try
{ BackGround }
R := Rect(0, 0, Self.Width, Height);
InflateRect(R, -Width, -Width);
DoBackground(InnerWidth, InnerColor, False);
DoBackground(MiddleWidth, MiddleColor, True);
DoBackground(OuterWidth, OuterColor, False);
{ Coords }
if CanShowCoords then
begin
ExtSelectClipRgn(DC, 0, RGN_COPY);
SetBkMode(DC, TRANSPARENT);
SetTextColor(DC, ColorToRGB(Font.Color));
SaveFont := SelectObject(DC, Font.Handle);
try
{ Left and right side }
R := Bounds(OuterWidth, Width, MiddleWidth, FSquareSize);
R2 := Bounds(Self.Width - OuterWidth - MiddleWidth, Width,
MiddleWidth, FSquareSize);
DoCoords(0, FSquareSize, FOrientation in [boRotate090, boRotate270],
FOrientation in [boNormal, boRotate090]);
{ Top and bottom side }
R := Bounds(Width, OuterWidth, FSquareSize, MiddleWidth);
R2 := Bounds(Width, Height - OuterWidth - MiddleWidth, FSquareSize,
MiddleWidth);
DoCoords(FSquareSize, 0, FOrientation in [boNormal, boRotate180],
FOrientation in [boRotate090, boRotate180]);
finally
SelectObject(DC, SaveFont);
end;
end;
finally
ReleaseDC(Handle, DC);
end;
end;
end;
That is quite an open question. Much tips and answers have already been given. I would like to add two:
Include csOpaque in ControlStyle if you paint ClientRect fully,
Exclude CS_HREDRAW and CS_VREDRAW from Params.WindowClass.Style in CreateParams.
Since you are especially interested in drawing on TScrollingWinControl, I spend the last couple of hours on reducing the code of a planning component of mine, to get only the necessary painting and scrolling code. It is just an example and by no means fully functional or meant as holy, but it might provide some inspiration:
unit Unit2;
interface
uses
Classes, Controls, Windows, Messages, ComCtrls, Forms, Grids, Math, CommCtrl,
SysUtils, StdCtrls, Graphics, Contnrs;
type
TAwPlanGrid = class;
TContainer = class(TWinControl)
private
procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
protected
procedure CreateParams(var Params: TCreateParams); override;
procedure PaintWindow(DC: HDC); override;
public
constructor Create(AOwner: TComponent); override;
end;
TScrollEvent = procedure(Sender: TControlScrollBar) of object;
TScroller = class(TScrollingWinControl)
private
FOnScroll: TScrollEvent;
procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
protected
procedure CreateParams(var Params: TCreateParams); override;
function DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
MousePos: TPoint): Boolean; override;
procedure DoScroll(AScrollBar: TControlScrollBar);
property OnScroll: TScrollEvent read FOnScroll write FOnScroll;
public
constructor Create(AOwner: TComponent); override;
end;
TColumn = class(TCustomControl)
private
procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
procedure CMControlChange(var Message: TCMControlChange);
message CM_CONTROLCHANGE;
protected
procedure Paint; override;
public
constructor Create(AOwner: TComponent); override;
end;
TTimeLineHeader = class(TCustomHeaderControl)
protected
procedure SectionResize(Section: THeaderSection); override;
public
constructor Create(AOwner: TComponent); override;
end;
TTimeLineGrid = class(TStringGrid)
private
FOnRowHeightsChanged: TNotifyEvent;
FRowHeightsUpdating: Boolean;
protected
procedure Paint; override;
procedure RowHeightsChanged; override;
property OnRowHeightsChanged: TNotifyEvent read FOnRowHeightsChanged
write FOnRowHeightsChanged;
public
constructor Create(AOwner: TComponent); override;
function CanFocus: Boolean; override;
end;
TTimeLine = class(TContainer)
private
FHeader: TTimeLineHeader;
protected
TimeLineGrid: TTimeLineGrid;
public
constructor Create(AOwner: TComponent); override;
end;
THighwayHeader = class(TCustomHeaderControl)
private
FSectionWidth: Integer;
procedure SetSectionWidth(Value: Integer);
protected
function CreateSection: THeaderSection; override;
procedure SectionResize(Section: THeaderSection); override;
property SectionWidth: Integer read FSectionWidth write SetSectionWidth;
public
procedure AddSection(const AText: String);
constructor Create(AOwner: TComponent); override;
end;
THighwayScroller = class(TScroller)
private
procedure WMHScroll(var Message: TWMScroll); message WM_HSCROLL;
procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
procedure WMVScroll(var Message: TWMScroll); message WM_VSCROLL;
protected
procedure PaintWindow(DC: HDC); override;
procedure Resize; override;
public
constructor Create(AOwner: TComponent); override;
end;
THighwayColumn = class(TColumn)
end;
THighwayColumns = class(TObject)
private
FHeight: Integer;
FItems: TList;
FParent: TWinControl;
FWidth: Integer;
function Add: THighwayColumn;
function GetItem(Index: Integer): THighwayColumn;
procedure SetHeight(Value: Integer);
procedure SetWidth(Value: Integer);
protected
property Height: Integer read FHeight write SetHeight;
property Items[Index: Integer]: THighwayColumn read GetItem; default;
property Parent: TWinControl read FParent write FParent;
property Width: Integer read FWidth write SetWidth;
public
constructor Create;
destructor Destroy; override;
end;
THighway = class(TContainer)
private
procedure HeaderSectionResized(HeaderControl: TCustomHeaderControl;
Section: THeaderSection);
protected
Columns: THighwayColumns;
Header: THighwayHeader;
Scroller: THighwayScroller;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
end;
TParkingHeader = class(TCustomHeaderControl)
protected
procedure SectionResize(Section: THeaderSection); override;
procedure SetParent(AParent: TWinControl); override;
public
constructor Create(AOwner: TComponent); override;
end;
TParkingScroller = class(TScroller)
public
constructor Create(AOwner: TComponent); override;
end;
TParkingColumn = class(TColumn)
private
FItemHeight: Integer;
procedure SetItemHeight(Value: Integer);
protected
function CanAutoSize(var NewWidth, NewHeight: Integer): Boolean; override;
public
constructor Create(AOwner: TComponent); override;
property ItemHeight: Integer read FItemHeight write SetItemHeight;
end;
TParking = class(TContainer)
protected
Column: TParkingColumn;
Header: TParkingHeader;
Scroller: TParkingScroller;
procedure PaintWindow(DC: HDC); override;
procedure Resize; override;
public
constructor Create(AOwner: TComponent); override;
end;
TPlanItem = class(TGraphicControl)
protected
procedure Paint; override;
public
constructor Create(AOwner: TComponent); override;
end;
TPlanItems = class(TList)
public
procedure DayHeightChanged(OldDayHeight, NewDayHeight: Integer);
end;
TAwPlanGrid = class(TContainer)
private
FDayHeight: Integer;
FHighway: THighway;
FParking: TParking;
FPlanItems: TPlanItems;
FTimeLine: TTimeLine;
function GetColWidth: Integer;
procedure HighwayScrolled(Sender: TControlScrollBar);
procedure SetColWidth(Value: Integer);
procedure SetDayHeight(Value: Integer);
procedure TimeLineRowHeightsChanged(Sender: TObject);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure MouseWheelHandler(var Message: TMessage); override;
procedure Test;
property ColWidth: Integer read GetColWidth;
property DayHeight: Integer read FDayHeight;
end;
function GradientFill(DC: HDC; Vertex: PTriVertex; NumVertex: ULONG;
Mesh: Pointer; NumMesh, Mode: ULONG): BOOL; stdcall; overload;
external msimg32 name 'GradientFill';
implementation
function Round2(Value, Rounder: Integer): Integer;
begin
if Rounder = 0 then Result := Value
else Result := (Value div Rounder) * Rounder;
end;
// Layout:
//
// - PlanGrid
// - TimeLine - Highway - Parking
// - TimeLineHeader - HighwayHeader - ParkingHeader
// - TimeLineGrid - HighwayScroller - ParkingScroller
// - HighwayColumns - ParkingColumn
// - PlanItems - PlanItems
const
DaysPerWeek = 5;
MaxParkingWidth = 300;
MinColWidth = 50;
MinDayHeight = 40;
MinParkingWidth = 60;
DefTimeLineWidth = 85;
DividerColor = $0099A8AC;
DefColWidth = 100;
DefDayHeight = 48;
DefWeekCount = 20;
{ TContainer }
constructor TContainer.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlStyle := [csOpaque];
end;
procedure TContainer.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
with Params.WindowClass do
Style := Style and not (CS_HREDRAW or CS_VREDRAW);
end;
procedure TContainer.PaintWindow(DC: HDC);
begin
{ Eat inherited }
end;
procedure TContainer.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
Message.Result := 1;
end;
{ TScroller }
constructor TScroller.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlStyle := [csOpaque];
HorzScrollBar.Tracking := True;
VertScrollBar.Tracking := True;
end;
procedure TScroller.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
with Params.WindowClass do
Style := Style and not (CS_HREDRAW or CS_VREDRAW);
end;
function TScroller.DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
MousePos: TPoint): Boolean;
var
Delta: Integer;
begin
with VertScrollBar do
begin
Delta := Increment;
if WheelDelta > 0 then
Delta := -Delta;
if ssCtrl in Shift then
Delta := DaysPerWeek * Delta;
Position := Min(Round2(Range - ClientHeight, Increment), Position + Delta);
end;
DoScroll(VertScrollBar);
Result := True;
end;
procedure TScroller.DoScroll(AScrollBar: TControlScrollBar);
begin
if Assigned(FOnScroll) then
FOnScroll(AScrollBar);
end;
procedure TScroller.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
Message.Result := 1;
end;
{ TColumn }
procedure TColumn.CMControlChange(var Message: TCMControlChange);
begin
inherited;
if Message.Inserting then
Message.Control.Width := Width;
end;
constructor TColumn.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlStyle := [csOpaque];
end;
procedure TColumn.Paint;
type
PTriVertex = ^TTriVertex;
TTriVertex = packed record
X: DWORD;
Y: DWORD;
Red: WORD;
Green: WORD;
Blue: WORD;
Alpha: WORD;
end;
var
Vertex: array[0..1] of TTriVertex;
GRect: TGradientRect;
begin
Vertex[0].X := 0;
Vertex[0].Y := Canvas.ClipRect.Top;
Vertex[0].Red := $DD00;
Vertex[0].Green := $DD00;
Vertex[0].Blue := $DD00;
Vertex[0].Alpha := 0;
Vertex[1].X := Width;
Vertex[1].Y := Canvas.ClipRect.Bottom;
Vertex[1].Red := $FF00;
Vertex[1].Green := $FF00;
Vertex[1].Blue := $FF00;
Vertex[1].Alpha := 0;
GRect.UpperLeft := 0;
GRect.LowerRight := 1;
GradientFill(Canvas.Handle, #Vertex, 2, #GRect, 1, GRADIENT_FILL_RECT_H);
end;
procedure TColumn.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
Message.Result := 1;
end;
{ TTimeLineHeader }
constructor TTimeLineHeader.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlStyle := [csOpaque];
DoubleBuffered := True;
Sections.Add;
Sections[0].MinWidth := 40;
Sections[0].Width := DefTimeLineWidth;
Sections[0].MaxWidth := DefTimeLineWidth;
Sections[0].Text := '2011';
end;
procedure TTimeLineHeader.SectionResize(Section: THeaderSection);
begin
if HasParent then
Parent.Width := Section.Width;
inherited SectionResize(Section);
end;
{ TTimeLineGrid }
function TTimeLineGrid.CanFocus: Boolean;
begin
Result := False;
end;
constructor TTimeLineGrid.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Align := alCustom;
Anchors := [akTop, akRight, akBottom];
BorderStyle := bsNone;
ColCount := 2;
ColWidths[0] := 85;
ControlStyle := [csOpaque];
FixedCols := 1;
FixedRows := 0;
GridLineWidth := 0;
Options := [goFixedHorzLine, goRowSizing];
ScrollBars := ssNone;
TabStop := False;
Cells[0, 4] := 'Drag day height';
end;
procedure TTimeLineGrid.Paint;
begin
inherited Paint;
with Canvas do
if ClipRect.Right >= Width - 1 then
begin
Pen.Color := DividerColor;
MoveTo(Width - 1, ClipRect.Top);
LineTo(Width - 1, ClipRect.Bottom);
end;
end;
procedure TTimeLineGrid.RowHeightsChanged;
begin
inherited RowHeightsChanged;
if Assigned(FOnRowHeightsChanged) and (not FRowHeightsUpdating) then
try
FRowHeightsUpdating := True;
FOnRowHeightsChanged(Self);
finally
FRowHeightsUpdating := False;
end;
end;
{ TTimeLine }
constructor TTimeLine.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Align := alLeft;
Width := DefTimeLineWidth;
Height := 100;
FHeader := TTimeLineHeader.Create(Self);
FHeader.Parent := Self;
TimeLineGrid := TTimeLineGrid.Create(Self);
TimeLineGrid.RowCount := DefWeekCount * DaysPerWeek;
TimeLineGrid.SetBounds(0, FHeader.Height, Width, Height - FHeader.Height);
TimeLineGrid.Parent := Self;
end;
{ THighwayHeader }
procedure THighwayHeader.AddSection(const AText: String);
begin
with THeaderSection(Sections.Add) do
Text := AText;
end;
constructor THighwayHeader.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Align := alCustom;
Anchors := [akLeft, akTop, akRight];
ControlStyle := [csOpaque];
DoubleBuffered := True;
FullDrag := False;
end;
function THighwayHeader.CreateSection: THeaderSection;
begin
Result := THeaderSection.Create(Sections);
Result.MinWidth := MinColWidth;
Result.Width := FSectionWidth;
end;
procedure THighwayHeader.SectionResize(Section: THeaderSection);
begin
SectionWidth := Section.Width;
inherited SectionResize(Section);
end;
procedure THighwayHeader.SetSectionWidth(Value: Integer);
var
i: Integer;
begin
if FSectionWidth <> Value then
begin
FSectionWidth := Value;
for i := 0 to Sections.Count - 1 do
Sections[i].Width := FSectionWidth;
end;
end;
{ THighwayScroller }
constructor THighwayScroller.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Align := alCustom;
Anchors := [akLeft, akTop, akRight, akBottom];
ControlStyle := [csOpaque];
end;
procedure THighwayScroller.PaintWindow(DC: HDC);
begin
if ControlCount > 0 then
ExcludeClipRect(DC, 0, 0, ControlCount * Controls[0].Width,
Controls[0].Height);
FillRect(DC, ClientRect, Brush.Handle);
end;
procedure THighwayScroller.Resize;
begin
with VertScrollBar do
Position := Round2(Position, Increment);
DoScroll(HorzScrollBar);
DoScroll(VertScrollBar);
inherited Resize;
end;
procedure THighwayScroller.WMHScroll(var Message: TWMScroll);
begin
inherited;
DoScroll(HorzScrollBar);
end;
procedure THighwayScroller.WMPaint(var Message: TWMPaint);
begin
ControlState := ControlState + [csCustomPaint];
inherited;
ControlState := ControlState - [csCustomPaint];
end;
procedure THighwayScroller.WMVScroll(var Message: TWMScroll);
var
NewPos: Integer;
begin
NewPos := Round2(Message.Pos, VertScrollBar.Increment);
Message.Pos := NewPos;
inherited;
with VertScrollBar do
if Position <> NewPos then
Position := Round2(Position, Increment);
DoScroll(VertScrollBar);
end;
{ THighwayColumns }
function THighwayColumns.Add: THighwayColumn;
var
Index: Integer;
begin
Result := THighwayColumn.Create(nil);
Index := FItems.Add(Result);
Result.SetBounds(Index * FWidth, 0, FWidth, FHeight);
Result.Parent := FParent;
end;
constructor THighwayColumns.Create;
begin
FItems := TObjectList.Create(True);
end;
destructor THighwayColumns.Destroy;
begin
FItems.Free;
inherited Destroy;
end;
function THighwayColumns.GetItem(Index: Integer): THighwayColumn;
begin
Result := FItems[Index];
end;
procedure THighwayColumns.SetHeight(Value: Integer);
var
i: Integer;
begin
if FHeight <> Value then
begin
FHeight := Value;
for i := 0 to FItems.Count - 1 do
Items[i].Height := FHeight;
end;
end;
procedure THighwayColumns.SetWidth(Value: Integer);
var
i: Integer;
begin
if FWidth <> Value then
begin
FWidth := Max(MinColWidth, Value);
for i := 0 to FItems.Count - 1 do
with Items[i] do
SetBounds(Left + (FWidth - Width) * i, 0, FWidth, FHeight);
end;
end;
{ THighway }
constructor THighway.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Align := alClient;
Width := 100;
Height := 100;
Header := THighwayHeader.Create(Self);
Header.SetBounds(0, 0, Width, Header.Height);
Header.OnSectionResize := HeaderSectionResized;
Header.Parent := Self;
Scroller := THighwayScroller.Create(Self);
Scroller.SetBounds(0, Header.Height, Width, Height - Header.Height);
Scroller.Parent := Self;
Columns := THighwayColumns.Create;
Columns.Parent := Scroller;
end;
destructor THighway.Destroy;
begin
Columns.Free;
inherited Destroy;
end;
procedure THighway.HeaderSectionResized(HeaderControl: TCustomHeaderControl;
Section: THeaderSection);
begin
Columns.Width := Section.Width;
Scroller.HorzScrollBar.Increment := Columns.Width;
Header.Left := -Scroller.HorzScrollBar.Position;
end;
{ TParkingHeader }
const
BlindWidth = 2000;
constructor TParkingHeader.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Align := alCustom;
Anchors := [akLeft, akTop, akRight];
ControlStyle := [csOpaque];
DoubleBuffered := True;
Sections.Add;
Sections[0].Width := BlindWidth;
Sections.Add;
Sections[1].AutoSize := True;
Sections[1].Text := 'Parked';
end;
procedure TParkingHeader.SectionResize(Section: THeaderSection);
begin
if (Section.Index = 0) and HasParent then
begin
Parent.Width := Max(MinParkingWidth,
Min(Parent.Width - Section.Width + BlindWidth, MaxParkingWidth));
Section.Width := BlindWidth;
Sections[1].Width := Parent.Width - 2;
end;
inherited SectionResize(Section);
end;
procedure TParkingHeader.SetParent(AParent: TWinControl);
begin
inherited SetParent(AParent);
if HasParent then
begin
SetBounds(-BlindWidth + 2, 0, BlindWidth + Parent.Width, Height);
Sections[1].Width := Parent.Width - 2;
end;
end;
{ TParkingScroller }
constructor TParkingScroller.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Align := alCustom;
Anchors := [akLeft, akTop, akRight, akBottom];
ControlStyle := [csOpaque];
HorzScrollBar.Visible := False;
VertScrollBar.Increment := DefDayHeight;
end;
{ TParkingColumn }
function TParkingColumn.CanAutoSize(var NewWidth, NewHeight: Integer): Boolean;
begin
if HasParent then
NewHeight := Max(Parent.Height, ControlCount * FItemHeight);
Result := True;
end;
constructor TParkingColumn.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Align := alTop;
AutoSize := True;
FItemHeight := DefDayHeight;
end;
procedure TParkingColumn.SetItemHeight(Value: Integer);
var
i: Integer;
begin
if FItemHeight <> Value then
begin
FItemHeight := Value;
for i := 0 to ControlCount - 1 do
Controls[i].Height := FItemHeight;
TScroller(Parent).VertScrollBar.Increment := FItemHeight;
end;
end;
{ TParking }
constructor TParking.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Align := alRight;
Width := 120;
Height := 100;
Header := TParkingHeader.Create(Self);
Header.Parent := Self;
Scroller := TParkingScroller.Create(Self);
Scroller.SetBounds(1, Header.Height, Width, Height - Header.Height);
Scroller.Parent := Self;
Column := TParkingColumn.Create(Self);
Column.Parent := Scroller;
end;
procedure TParking.PaintWindow(DC: HDC);
var
R: TRect;
begin
Brush.Color := DividerColor;
SetRect(R, 0, Header.Height, 1, Height);
FillRect(DC, R, Brush.Handle);
end;
procedure TParking.Resize;
begin
Column.AdjustSize;
inherited Resize;
end;
{ TPlanItem }
constructor TPlanItem.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Anchors := [akLeft, akTop, akRight];
ControlStyle := [csOpaque];
Color := Random(clWhite);
end;
procedure TPlanItem.Paint;
begin
Canvas.Brush.Color := Color;
Canvas.FillRect(Canvas.ClipRect);
end;
{ TPlanItems }
procedure TPlanItems.DayHeightChanged(OldDayHeight, NewDayHeight: Integer);
var
i: Integer;
begin
for i := 0 to Count - 1 do
with TPlanItem(Items[i]) do
if not (Parent is TParkingColumn) then
begin
Top := Trunc(Top * (NewDayHeight / OldDayHeight));
Height := Trunc(Height * (NewDayHeight / OldDayHeight));
end;
end;
{ TAwPlanGrid }
constructor TAwPlanGrid.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlStyle := [csOpaque];
TabStop := True;
Width := 400;
Height := 200;
FTimeLine := TTimeLine.Create(Self);
FTimeLine.TimeLineGrid.OnRowHeightsChanged := TimeLineRowHeightsChanged;
FTimeLine.Parent := Self;
FParking := TParking.Create(Self);
FParking.Parent := Self;
FHighway := THighway.Create(Self);
FHighway.Scroller.OnScroll := HighwayScrolled;
FHighway.Parent := Self;
FPlanItems := TPlanItems.Create;
SetColWidth(DefColWidth);
SetDayHeight(DefDayHeight);
FHighway.Columns.Height := DefWeekCount * DaysPerWeek * FDayHeight;
end;
destructor TAwPlanGrid.Destroy;
begin
FPlanItems.Free;
inherited Destroy;
end;
function TAwPlanGrid.GetColWidth: Integer;
begin
Result := FHighway.Columns.Width;
end;
procedure TAwPlanGrid.HighwayScrolled(Sender: TControlScrollBar);
begin
if Sender.Kind = sbVertical then
FTimeLine.TimeLineGrid.TopRow := Sender.Position div FDayHeight
else
begin
FHighway.Header.Left := -Sender.Position;
FHighway.Header.Width := FHighway.Width + Sender.Position;
end;
end;
procedure TAwPlanGrid.MouseWheelHandler(var Message: TMessage);
var
X: Integer;
begin
with Message do
begin
X := ScreenToClient(SmallPointToPoint(TCMMouseWheel(Message).Pos)).X;
if X >= FParking.Left then
Result := FParking.Scroller.Perform(CM_MOUSEWHEEL, WParam, LParam)
else
Result := FHighway.Scroller.Perform(CM_MOUSEWHEEL, WParam, LParam);
end;
if Message.Result = 0 then
inherited MouseWheelHandler(Message);
end;
procedure TAwPlanGrid.SetColWidth(Value: Integer);
begin
if ColWidth <> Value then
begin
FHighway.Columns.Width := Value;
FHighway.Header.SectionWidth := ColWidth;
FHighway.Scroller.HorzScrollBar.Increment := ColWidth;
end;
end;
procedure TAwPlanGrid.SetDayHeight(Value: Integer);
var
OldDayHeight: Integer;
begin
if FDayHeight <> Value then
begin
OldDayHeight := FDayHeight;
FDayHeight := Max(MinDayHeight, Round2(Value, 4));
FTimeLine.TimeLineGrid.DefaultRowHeight := FDayHeight;
FHighway.Columns.Height := DefWeekCount * DaysPerWeek * FDayHeight;
FHighway.Scroller.VertScrollBar.Increment := FDayHeight;
FPlanItems.DayHeightChanged(OldDayHeight, FDayHeight);
end;
end;
procedure TAwPlanGrid.Test;
var
i: Integer;
PlanItem: TPlanItem;
begin
Randomize;
Anchors := [akLeft, akTop, akBottom, akRight];
for i := 0 to 3 do
FHighway.Columns.Add;
FHighway.Header.AddSection('Drag col width');
FHighway.Header.AddSection('Column 2');
FHighway.Header.AddSection('Column 3');
FHighway.Header.AddSection('Column 4');
for i := 0 to 9 do
begin
PlanItem := TPlanItem.Create(Self);
PlanItem.Parent := FParking.Column;
PlanItem.Top := i * DefDayHeight;
PlanItem.Height := DefDayHeight;
FPlanItems.Add(PlanItem);
end;
for i := 0 to 3 do
begin
PlanItem := TPlanItem.Create(Self);
PlanItem.Parent := FHighway.Columns[i];
PlanItem.Top := (i + 3) * DefDayHeight;
PlanItem.Height := DefDayHeight;
FPlanItems.Add(PlanItem);
end;
SetFocus;
end;
procedure TAwPlanGrid.TimeLineRowHeightsChanged(Sender: TObject);
var
iRow: Integer;
begin
with FTimeLine.TimeLineGrid do
for iRow := 0 to RowCount - 1 do
if RowHeights[iRow] <> DefaultRowHeight then
begin
SetDayHeight(RowHeights[iRow]);
Break;
end;
end;
end.
Testing code:
with TAwPlanGrid.Create(Self) do
begin
SetBounds(10, 100, 600, 400);
Parent := Self;
Test;
end;
My 2 cts.
i have seen the argument, and try to employ it in practice, that you should never draw over the same pixels more than once.
If you're drawing a red square on a white background then you paint everything white except where the red square will go, then you "fill in" the red square:
There's no flicker, and you're doing fewer drawing operations.
That is an extreme example of only invalidate what you have to, as dthorp mentions. If you're scrolling a control, use ScrollWindow to have the graphics subsystem move what's already there, and then just fill in the missing bit at the bottom.
There are going to be times where you have to paint the same pixels multiple times; ClearType text is the best example. ClearType rendering requires access to the pixels underneath - which means you're going to have to fill an area with white, then draw your text over it.
But even that can usually be mitigated by measuring the rects of the text you're going to render, fill clWhite everywhere else, then have DrawText fill in the empty areas - using a white HBRUSH background:
But that trick cannot work when drawing text on a gradient, or arbitrary existing content - so there will be flicker. In that case you have to double buffer in some way. (Although don't double buffer if the user is in a remote session - flickering is better than slow drawing).
Bonus Chatter: Now that i've explained why you shouldn't double buffer content when the user is running though Remote Desktop (i.e. Terminal Services), you now know what this Internet Explorer advanced option means, what it does, and why it is off by default:

Resources