I've been looking at how to put a progress bar in a TListView in Delphi, and I've got some code that works, BUT I want to add it to a SubItem and cannot figure out how.
object Form1: TForm1
Left = 221
Top = 113
Caption = 'Form1'
ClientHeight = 203
ClientWidth = 482
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'MS Sans Serif'
Font.Style = []
OldCreateOrder = False
OnCreate = FormCreate
DesignSize = (
482
203)
PixelsPerInch = 96
TextHeight = 13
object ListView1: TListView
Left = 16
Top = 16
Width = 449
Height = 177
Anchors = [akLeft, akTop, akRight, akBottom]
Columns = <>
FullDrag = True
TabOrder = 0
OnCustomDrawItem = ListView1CustomDrawItem
end
end
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ComCtrls, StdCtrls, CommCtrl;
type
TForm1 = class(TForm)
ListView1: TListView;
procedure FormCreate(Sender: TObject);
procedure ListView1CustomDrawItem(Sender: TCustomListView;
Item: TListItem; State: TCustomDrawState; var DefaultDraw: Boolean);
private
{ Private declarations }
procedure WMNotify(var Message: TWMNotify); message WM_NOTIFY;
procedure AdjustProgressBar(item: TListItem; r: TRect);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
var
i: Byte;
r: TRect;
pb: TProgressBar;
begin
Listview1.Columns.Add.Width := 100;
Listview1.Columns.Add.Width := 200;
Listview1.ViewStyle := vsReport;
Randomize;
for i:=0 to 40 do
begin
Listview1.Items.Add.Caption := 'Texte ' + IntToStr(i);
r := Listview1.Items[i].DisplayRect(drBounds);
pb := TProgressBar.Create(Self);
pb.Parent := Listview1;
pb.Position := Random(pb.Max);
Listview1.Items[i].Data := pb;
AdjustProgressBar(Listview1.Items[i], r);
end;end;
procedure TForm1.WMNotify(var Message: TWMNotify);
var
i: Integer;
r: TRect;
begin
case Message.NMHdr.code of
HDN_ITEMCHANGED, HDN_ITEMCHANGING:
begin
for i:=0 to Listview1.Items.Count-1 do
begin
r := Listview1.Items[i].DisplayRect(drBounds);
AdjustProgressBar(Listview1.Items[i], r);
end;
ListView1.Repaint;
end;end;
inherited;
end;
procedure TForm1.ListView1CustomDrawItem(Sender: TCustomListView;
Item: TListItem; State: TCustomDrawState; var DefaultDraw: Boolean);
var
r: TRect;
pb: TProgressBar;
begin
r := Item.DisplayRect(drBounds);
if r.Top>=Listview1.BoundsRect.Top then
AdjustProgressBar(Item, r);
end;
procedure TForm1.AdjustProgressBar(item: TListItem; r: TRect);
var
pb: TProgressBar;
begin
r.Left := r.Left + Listview1.columns[0].Width;
r.Right := r.Left + Listview1.columns[1].Width;
pb := item.Data;
pb.BoundsRect := r;
end;
end.
The code I want it to work with is:
...
with listview1.Items.Add do
begin
Caption := IntToStr(listview1.Items.Count);
SubItems.Add('blah');
SubItems.Add('blah');
SubItems.Add('blah');
{Add SubItem Progress Bar here Position 4 out of 10}
end;
The code you've shown doesn't really add a progress bar "to" a subitem. Rather, it takes a standalone progress bar and moves it to cover the space of the first two columns. That's what your AdjustProgressBar function does. It receives the bounding rectangle of the list item, which I think corresponds to the total width of all the columns. Then, it shifts the left side of the rectangle by the width of the first column, and it shifts the right side of the rectangle by the width of the second column.
You can adjust the coordinates of the progress bar however you want. For example, to make it cover the third column, shift the left side by the widths of the first two columns, and then set the right side to the left coordinate plus the third column's width.
But for that to work, you still need for the list item to have a subitem. You're just putting a progress bar on top of it, and you already have code to do that. You can't add an object as a subitem; a subitem is always text. The text can be blank, although for the benefit of screen readers that know how to read list views, it would be nice if you updated the text with the progress bar's value.
I'd take a look at the OnDrawItem and completely redraw the control myself.
Check this post.
Related
I want to create a slide effect: one bitmap is painted from right to left on a form's canvas. For this I use BitBlt.
I call this function in a Timer (20ms):
var ViewPort: TRect;
ViewPort.Left := 0;
ViewPort.Top := 0;
ViewPort.Width := 1400;
ViewPort.Height := 900;
x: integer := spnStep.Value; //SpinBox.Value = 10
procedure TfrmTester.Slide(BMP: TBitmap; ViewPort: TRect);
begin
Inc(x, spnStep.Value);
if x >= ViewPort.Width then
begin
x:= ViewPort.Width;
Timer.Enabled:= FALSE;
end;
BitBlt(frmTester.Canvas.Handle,
ViewPort.Width-x, 0, // X, Y
x, ViewPort.Height, // cX, cY
BMP.Canvas.Handle, 0, 0, SRCCOPY);
end;
However, the image does not progress smoothly. It has some kind of flicker, but not the kind of flicker that we know in the VCL. It is difficult to describe it. It is like the image moves two pixels forward and then one pixel backward.
How to make the image move smoothly?
Could the actually be caused by the refresh rate of the monitor?
Update: I don't know why, but it is caused by the timer.
If I call Slide() in a 'for' loop then the animation is smooth.
I know that the timer has an accuracy of ~15ms, but I still don't get it why it makes the image to shimmer.
If I add a sleed(1) inside the loop the shimmer effect appears again, and it is even worse. It really looks like the image is drawn twice.
First, you should only paint on the form in the form's OnPaint handler. I don't know if you do that or not, but you should do so.
Second, you cannot really rely on the temporal distance between successive WM_TIMER messages being very precise or even constant. So it is better to check the actual time each time you paint. For instance, you may use the formula Position = Original Position + Velocity × Time known from school physics.
Also, to avoid flickering, you should probably handle WM_ERASEBKGND.
Putting these together,
unit AnimatedRabbit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, Dialogs, ExtCtrls;
type
TMainForm = class(TForm)
Timer1: TTimer;
procedure FormCreate(Sender: TObject);
procedure FormPaint(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
private
FRabbit: TBitmap;
FStartTime: TDateTime;
const
Speed = -100;
function GetRabbitLeft: Double;
protected
procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
public
end;
var
MainForm: TMainForm;
implementation
uses
DateUtils;
{$R *.dfm}
procedure TMainForm.FormCreate(Sender: TObject);
begin
FRabbit := TBitmap.Create;
FRabbit.LoadFromFile('K:\rabbit.bmp');
FStartTime := Now;
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
FreeAndNil(FRabbit);
end;
procedure TMainForm.FormPaint(Sender: TObject);
var
x: Integer;
begin
x := Round(GetRabbitLeft);
BitBlt(
Canvas.Handle,
x,
0,
FRabbit.Width,
FRabbit.Height,
FRabbit.Canvas.Handle,
0,
0,
SRCCOPY
);
Canvas.Brush.Color := Color;
if x > 0 then
Canvas.FillRect(Rect(0, 0, x - 1, ClientHeight));
if x + FRabbit.Width < ClientWidth then
Canvas.FillRect(Rect(x + FRabbit.Width, 0, ClientWidth, ClientHeight));
end;
function TMainForm.GetRabbitLeft: Double;
begin
Result := ClientWidth + Speed * SecondSpan(Now, FStartTime);
end;
procedure TMainForm.Timer1Timer(Sender: TObject);
begin
Invalidate;
if GetRabbitLeft + FRabbit.Width < 0 then
Timer1.Enabled := False;
end;
procedure TMainForm.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
Message.Result := 1;
end;
end.
I think this is as good as you can make it using GDI (a graphics API from the 1980s). I bet it will look better in Direct2D (or OpenGL, if you prefer that).
Update
After further investigation, I suspect that the usual timer isn't good enough. The problem is two-fold: (1) The best FPS obtainable by a normal timer is too low. (2) The fact that the duration between two consecutive WM_TIMER messages isn't constant causes visual issues.
If I instead use a high-resolution multimedia timer, ignoring the fact that they are deprecated, I get a nicer result:
unit AnimatedRabbit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, Dialogs, ExtCtrls;
const
WM_DOINVALIDATE = WM_USER + 1;
type
TMainForm = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormPaint(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FRabbit: TBitmap;
FStartTime: TDateTime;
FMMEvent: Cardinal;
const
Speed = -100;
function GetRabbitLeft: Double;
protected
procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
procedure WMDoInvalidate(var Message: TMessage); message WM_DOINVALIDATE;
public
end;
var
MainForm: TMainForm;
implementation
uses
DateUtils, MMSystem, Math;
{$R *.dfm}
procedure RepaintFunc(wTimerID: UINT; msg: UINT; dwUser: NativeUINT;
dw1, dw2: NativeUINT); stdcall;
begin
PostMessage(MainForm.Handle, WM_DOINVALIDATE, 0, 0);
end;
procedure TMainForm.FormCreate(Sender: TObject);
const
TargetResolution = 1;
var
tc: TTimeCaps;
res: Cardinal;
begin
FRabbit := TBitmap.Create;
FRabbit.LoadFromFile('K:\rabbit.bmp');
FStartTime := Now;
if timeGetDevCaps(#tc, SizeOf(tc)) <> TIMERR_NOERROR then
Exit;
res := EnsureRange(TargetResolution, tc.wPeriodMin, tc.wPeriodMax);
if timeBeginPeriod(res) <> TIMERR_NOERROR then
Exit;
FMMEvent := timeSetEvent(10, res, RepaintFunc, 0, TIME_PERIODIC);
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
timeKillEvent(FMMEvent);
FreeAndNil(FRabbit);
end;
procedure TMainForm.FormPaint(Sender: TObject);
var
x: Integer;
begin
x := Round(GetRabbitLeft);
BitBlt(
Canvas.Handle,
x,
0,
FRabbit.Width,
FRabbit.Height,
FRabbit.Canvas.Handle,
0,
0,
SRCCOPY
);
Canvas.Brush.Color := Color;
if x > 0 then
Canvas.FillRect(Rect(0, 0, x - 1, ClientHeight));
if x + FRabbit.Width < ClientWidth then
Canvas.FillRect(Rect(x + FRabbit.Width, 0, ClientWidth, ClientHeight));
end;
function TMainForm.GetRabbitLeft: Double;
begin
Result := ClientWidth + Speed * SecondSpan(Now, FStartTime);
end;
procedure TMainForm.WMDoInvalidate(var Message: TMessage);
begin
Invalidate;
end;
procedure TMainForm.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
Message.Result := 1;
end;
end.
Update 2
And here is the non-deprecated version:
unit AnimatedRabbit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, Dialogs, ExtCtrls;
const
WM_DOINVALIDATE = WM_USER + 1;
type
TMainForm = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormPaint(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FRabbit: TBitmap;
FStartTime: TDateTime;
FTimer: THandle;
const
Speed = -100;
function GetRabbitLeft: Double;
protected
procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
procedure WMDoInvalidate(var Message: TMessage); message WM_DOINVALIDATE;
public
end;
var
MainForm: TMainForm;
implementation
uses
DateUtils, Math;
{$R *.dfm}
procedure RepaintFunc(Context: Pointer; Success: Boolean); stdcall;
begin
PostMessage(MainForm.Handle, WM_DOINVALIDATE, 0, 0);
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
FRabbit := TBitmap.Create;
FRabbit.LoadFromFile('K:\rabbit.bmp');
FStartTime := Now;
if not CreateTimerQueueTimer(FTimer, 0, RepaintFunc, nil, 0, 10, 0) then
RaiseLastOSError;
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
DeleteTimerQueueTimer(0, FTimer, INVALID_HANDLE_VALUE);
FreeAndNil(FRabbit);
end;
procedure TMainForm.FormPaint(Sender: TObject);
var
x: Integer;
begin
x := Round(GetRabbitLeft);
BitBlt(
Canvas.Handle,
x,
0,
FRabbit.Width,
FRabbit.Height,
FRabbit.Canvas.Handle,
0,
0,
SRCCOPY
);
Canvas.Brush.Color := Color;
if x > 0 then
Canvas.FillRect(Rect(0, 0, x - 1, ClientHeight));
if x + FRabbit.Width < ClientWidth then
Canvas.FillRect(Rect(x + FRabbit.Width, 0, ClientWidth, ClientHeight));
end;
function TMainForm.GetRabbitLeft: Double;
begin
Result := ClientWidth + Speed * SecondSpan(Now, FStartTime);
end;
procedure TMainForm.WMDoInvalidate(var Message: TMessage);
begin
Invalidate;
end;
procedure TMainForm.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
Message.Result := 1;
end;
end.
Also, I previously said that the precise result depends on CPU, GPU, OS, and monitor. But it also depends on the eye and brain. The thing that makes this animation require such a high-quality timer is the fact that the motion is a simple translation with constant velocity, and the eye + brain can easily spot any imperfection. If we had animated a bouncing ball or SHM, an old-school timer would have been enough.
You should not be drawing on the Form's Canvas from outside of its OnPaint event at all. All of the drawing should be in the OnPaint event only. Have your timer save the desired information into variables that the Form can access, and then Invalidate() the Form, and let its OnPaint event draw the image using the latest saved information.
Alternatively, simply display your BMP inside a TImage control, and then have the timer set that control's Left/Top/Width/Height properties as needed. Let the TImage handle the drawing of the image for you.
You can use AnimateWindow
Here's the DFM. Just add client aligned TPicture inside the TPanel
object Form30: TForm30
Left = 0
Top = 0
Caption = 'Form30'
ClientHeight = 337
ClientWidth = 389
Color = clBtnFace
DoubleBuffered = True
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object Panel1: TPanel
Left = 0
Top = 0
Width = 389
Height = 289
Align = alTop
BevelOuter = bvNone
Color = clRed
FullRepaint = False
ParentBackground = False
ShowCaption = False
TabOrder = 0
Visible = False
end
object Button1: TButton
Left = 136
Top = 304
Width = 75
Height = 25
Caption = 'Button1'
TabOrder = 1
OnClick = Button1Click
end
end
And the Button1.OnClick handler:
procedure TForm30.Button1Click(Sender: TObject);
begin
AnimateWindow(Panel1.Handle, 1000, AW_SLIDE or AW_HOR_POSITIVE or AW_ACTIVATE);
end;
(Delphi DX 10.3)
I have a big blank (white) canvas (on a Tpanel's descendant) where I draw some text (using Textout(), let's suppose text is always black) and graphics (lines, rectangles, nothing of so complex).
After drawing, I need to change the color of the white background of some specific areas, from white to another colour.
The effect I want to reach is much like an excel sheet with coloured cells. In the attached example, all the columns are created blank (white) like "value" and "difference", then the yellow (price) and the red (result) columns have been coloured.
If I could fill in the areas before writing the text, I would use SetBkMode(TRANSPARENT) and get an optimal result. Unfortunately I need to fill in the areas after writing the text and graphics.
The first solution I thought of was pixel-by-pixel replacement (using the Pixels[] function), but it's dramatically slow and graphically unsatisfactory.
So my question is: how can I color the background of an area containing already written text?
Here a Minimal Reproducibile Example.
Button 1 execute pixels substitution, very slow and with unsatisfactory graphical result.
Button 2 fills areas after writing text, using SetBkMode(TRANSPARENT). Perfect result, but I can't do it.
program BK_mode;
uses
Vcl.Forms,
main in 'main.pas' {Form1};
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
////////////////////////////////
unit main;
interface
uses Windows, Forms, SysUtils, Vcl.StdCtrls, UiTypes, System.Classes, Vcl.Controls, Vcl.ExtCtrls, Graphics, Dialogs;
type
TForm1 = class(TForm)
btn_01: TButton;
btn_02: TButton;
procedure btn_02Click(Sender: TObject);
procedure btn_01Click(Sender: TObject);
private
procedure write_text(canvas: TCanvas;x, y : integer;i_fontsize : smallint;const str_text: String;style : TFontStyles;bo_transparent : boolean);
procedure switch_color(canvas : TCanvas;color_source, color_target : TColor);
procedure prepare_example(bo_transparent : boolean);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
const
BASE_COLOR = clWhite;
ALTERNATIVE_COLOR = clRed;
procedure TForm1.write_text(canvas: TCanvas;x, y : integer;i_fontsize : smallint;const str_text: String;style : TFontStyles;bo_transparent : boolean);
begin
var lo_old_BK_color : TColor := canvas.Brush.Color;
var lo_old_BK_mode := GetBKMode(canvas.Handle);
if bo_transparent then SetBKMode(canvas.Handle, TRANSPARENT)
else begin
SetBKMode(canvas.Handle, OPAQUE);
canvas.Brush.Color := BASE_COLOR
end;
canvas.Font.Color := clBlack;canvas.Font.Size := i_fontsize;canvas.Font.Style := style;
TextOut(canvas.Handle, x, y, PChar(str_text), Length(str_text));
if NOT bo_transparent then canvas.Brush.Color := lo_old_BK_color;
SetBKMode(canvas.Handle, lo_old_BK_mode)
end;
procedure TForm1.switch_color(canvas : TCanvas;color_source, color_target : TColor);
begin
for var x := 0 to clientWidth-1 do
for var y := 0 to clientHeight - 1 do
if (canvas.Pixels[x, y] = color_source) then canvas.Pixels[x, y] := color_target
end;
procedure TForm1.prepare_example(bo_transparent : boolean);
begin
if bo_transparent then Color := ALTERNATIVE_COLOR else Color := BASE_COLOR;
invalidate;
application.MessageBox('Click me', 'Test');
canvas.Rectangle(10, 10, 200, 100);
canvas.MoveTo(10, 110);canvas.LineTo(200, 140);
canvas.MoveTo(10, 140);canvas.LineTo(200, 110);
write_text(canvas, 30, 30, 14, 'This is a text!', [], bo_transparent);
write_text(canvas, 30, 60, 11, 'This is another text!', [fsBold, fsItalic], bo_transparent)
end;
procedure TForm1.btn_01Click(Sender: TObject);
begin
prepare_example(FALSE);
switch_color(canvas, BASE_COLOR, ALTERNATIVE_COLOR)
end;
procedure TForm1.btn_02Click(Sender: TObject);
begin
prepare_example(TRUE)
end;
end.
/////////////////////////////////////////////////////////
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 283
ClientWidth = 208
Color = clWhite
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
DesignSize = (
208
283)
PixelsPerInch = 96
TextHeight = 13
object btn_01: TButton
Left = 17
Top = 161
Width = 178
Height = 51
Anchors = [akLeft, akRight, akBottom]
Caption = '1. write text and fill area'
Font.Charset = ANSI_CHARSET
Font.Color = clWindowText
Font.Height = -16
Font.Name = 'Arial'
Font.Style = [fsBold]
ParentFont = False
TabOrder = 0
WordWrap = True
OnClick = btn_01Click
end
object btn_02: TButton
Left = 17
Top = 220
Width = 178
Height = 51
Anchors = [akLeft, akRight, akBottom]
Caption = '2. fill area then write text'
Font.Charset = ANSI_CHARSET
Font.Color = clWindowText
Font.Height = -16
Font.Name = 'Arial'
Font.Style = [fsBold]
ParentFont = False
TabOrder = 1
WordWrap = True
OnClick = btn_02Click
end
end
What you are asking for is simply not doable this way. You need to redraw the entire Canvas, drawing your backgrounds BEFORE drawing lines+text on top. Once you have drawn, you need a repaint to do everything over. DO NOT draw on a Canvas from outside of a Paint event.
Your example draws on a TForm.Canvas, so use the TForm.OnPaint event. For a TPanel descendant, override the virtual Paint() method instead. Either way, keep some variables with the desired settings, use those variable while drawing, and call Invalidate() after updating the variables and you want to trigger a repaint.
For example:
unit main;
interface
uses Windows, Forms, SysUtils, Vcl.StdCtrls, UiTypes, System.Classes, Vcl.Controls, Vcl.ExtCtrls, Graphics, Dialogs;
type
TForm1 = class(TForm)
btn_01: TButton;
btn_02: TButton;
procedure btn_02Click(Sender: TObject);
procedure btn_01Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormPaint(Sender: TObject);
private
FDrawTransparent : Boolean;
FDrawColor : TColor;
procedure write_text(ACanvas: TCanvas; x, y : integer; i_fontsize : smallint; const str_text: String; style : TFontStyles; bo_transparent : boolean);
procedure prepare_example(bo_transparent : boolean; color_target : TColor);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
const
BASE_COLOR = clWhite;
ALTERNATIVE_COLOR = clRed;
procedure TForm1.FormCreate(Sender: TObject);
begin
FDrawTransparent := False;
FDrawColor := BASE_COLOR;
end;
procedure TForm1.FormPaint(Sender: TObject);
begin
Canvas.Brush.Color := Self.Color;
Canvas.Rectangle(10, 10, 200, 100);
Canvas.MoveTo(10, 110);
Canvas.LineTo(200, 140);
Canvas.MoveTo(10, 140);
Canvas.LineTo(200, 110);
write_text(Canvas, 30, 30, 14, 'This is a text!', [], FDrawTransparent);
write_text(Canvas, 30, 60, 11, 'This is another text!', [fsBold, fsItalic], FDrawTransparent);
end;
procedure TForm1.write_text(ACanvas: TCanvas; x, y : integer; i_fontsize : smallint; const str_text: String;style : TFontStyles; bo_transparent : boolean);
begin
var lo_old_BK_color := ACanvas.Brush.Color;
var lo_old_BK_mode := GetBKMode(ACanvas.Handle);
if bo_transparent then
SetBKMode(ACanvas.Handle, TRANSPARENT)
else begin
SetBKMode(ACanvas.Handle, OPAQUE);
ACanvas.Brush.Color := FDrawColor;
end;
ACanvas.Font.Color := clBlack;
ACanvas.Font.Size := i_fontsize;
ACanvas.Font.Style := style;
TextOut(ACanvas.Handle, x, y, PChar(str_text), Length(str_text));
if NOT bo_transparent then ACanvas.Brush.Color := lo_old_BK_color;
SetBKMode(ACanvas.Handle, lo_old_BK_mode);
end;
procedure TForm1.prepare_example(bo_transparent : boolean; color_target: TColor);
begin
FDrawTransparent := bo_transparent;
FDrawColor := color_target;
if bo_transparent then Color := ALTERNATIVE_COLOR else Color := BASE_COLOR;
Invalidate;
Application.MessageBox('Click me', 'Test');
end;
procedure TForm1.btn_01Click(Sender: TObject);
begin
prepare_example(False, ALTERNATIVE_COLOR);
end;
procedure TForm1.btn_02Click(Sender: TObject);
begin
prepare_example(True, BASE_COLOR);
end;
end.
In many website, I can see text box with hint in gray text. The hint will tell the user what should be inputted and whenever user tries to input anything, the hint will disappear.
Below is a sample of it:
I just wonder how to implement such a feature in Windows desktop application. I am using Delphi XE3 and it is possible to implement such a feature with TTextBox? Or is there such a VCL component available?
Here is an implementation that works for Windows XP (and also Windows 7 and 8.1, haven't tested it on Windows 10). Beware: I mostly use it in Windows XP and 8.1 and with Delphi 2007 and XE2. There may be bugs that I haven't seen yet.
It's also probably not the most elegant solution but it works and is easy to understand.
Prerequisites: Delphi Custom Containers Pack
Just save the following to u_dzCueEdit.dfm and u_dzCueEdit.pas, create a runtime package with it, create a corresponding design time package with a Register procedure and install it.
dfm file:
object dzCueEdit: TdzCueEdit
Left = 0
Top = 0
Width = 258
Height = 21
TabOrder = 0
OnResize = BoxResize
object ed_Cue: TEdit
Left = 1
Top = 1
Width = 256
Height = 19
Align = alClient
TabOrder = 0
OnChange = ed_CueChange
OnClick = ed_CueClick
OnEnter = ed_CueEnter
OnExit = ed_CueExit
end
object p_Cue: TPanel
Left = 64
Top = 0
Width = 242
Height = 21
BevelOuter = bvNone
Color = clMoneyGreen
ParentBackground = False
TabOrder = 1
OnClick = p_CueClick
OnEnter = p_CueEnter
object l_Cue: TLabel
AlignWithMargins = True
Left = 88
Top = 0
Width = 93
Height = 13
Margins.Left = 1
Margins.Top = 1
Margins.Right = 1
Margins.Bottom = 1
Caption = 'Cue text goes here'
Font.Charset = DEFAULT_CHARSET
Font.Color = clGray
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
ParentFont = False
OnClick = l_CueClick
end
end
end
pas file:
unit c_dzCueEdit;
interface
uses
Windows,
Messages,
SysUtils,
Classes,
Graphics,
Controls,
Forms,
Dialogs,
ExtCtrls,
StdCtrls,
ccBoxes;
type
TdzCueEdit = class(TBox)
p_Cue: TPanel;
l_Cue: TLabel;
procedure ed_CueClick(Sender: TObject);
procedure l_CueClick(Sender: TObject);
published
ed_Cue: TEdit;
procedure p_CueEnter(Sender: TObject);
procedure p_CueClick(Sender: TObject);
procedure ed_CueChange(Sender: TObject);
procedure ed_CueEnter(Sender: TObject);
procedure ed_CueExit(Sender: TObject);
procedure BoxResize(Sender: TObject);
private
procedure CheckCueBanner;
function GetCue: string;
function GetText: string;
procedure SetCue(const _Value: string);
procedure SetText(const _Value: string);
protected
public
constructor Create(_Owner: TComponent); override;
published
property Text: string read GetText write SetText;
property Cue: string read GetCue write SetCue;
end;
implementation
{$R *.DFM}
{ TdzCueEdit }
constructor TdzCueEdit.Create(_Owner: TComponent);
begin
inherited;
BevelOuter := bvNone;
l_Cue.Align := alClient;
p_Cue.Color := ed_Cue.Color;
end;
procedure TdzCueEdit.BoxResize(Sender: TObject);
var
Rect: TRect;
begin
Rect := ed_Cue.ClientRect;
// p_Cue.SetBounds(Rect.Left, Rect.Top, Rect.Right - Rect.Left, Rect.Bottom - Rect.Top);
p_Cue.SetBounds(Rect.Left + 4, Rect.Top + 3, Rect.Right - Rect.Left - 2, Rect.Bottom - Rect.Top - 2);
end;
procedure TdzCueEdit.CheckCueBanner;
begin
if ed_Cue.Text <> '' then
p_Cue.Visible := False
else begin
// if ed_Cue.Focused then
// p_Cue.Visible := False
// else
p_Cue.Visible := True;
end;
end;
procedure TdzCueEdit.ed_CueChange(Sender: TObject);
begin
CheckCueBanner;
end;
procedure TdzCueEdit.ed_CueClick(Sender: TObject);
begin
CheckCueBanner;
end;
procedure TdzCueEdit.ed_CueEnter(Sender: TObject);
begin
CheckCueBanner;
end;
procedure TdzCueEdit.ed_CueExit(Sender: TObject);
begin
CheckCueBanner;
end;
function TdzCueEdit.GetCue: string;
begin
Result := l_Cue.Caption;
end;
procedure TdzCueEdit.SetCue(const _Value: string);
begin
l_Cue.Caption := _Value;
end;
function TdzCueEdit.GetText: string;
begin
Result := ed_Cue.Text;
end;
procedure TdzCueEdit.l_CueClick(Sender: TObject);
begin
ed_Cue.SetFocus;
CheckCueBanner;
end;
procedure TdzCueEdit.SetText(const _Value: string);
begin
ed_Cue.Text := _Value;
end;
procedure TdzCueEdit.p_CueClick(Sender: TObject);
begin
ed_Cue.SetFocus;
CheckCueBanner;
end;
procedure TdzCueEdit.p_CueEnter(Sender: TObject);
begin
ed_Cue.SetFocus;
CheckCueBanner;
end;
end.
Alright I got something really tricky here...
I would like to DRAW/USE Headers to a ChildNode.
I think the idea is reasonable because it would look amazing to have headers in subnodes so the childnodes can be specified in a table.
Is there a feature that VST has or is it not possible at all?
Thanks for your help.
1. Is there a way to use VirtualTreeView for a master / detail grid view ?
No, there is no such feature available at this time and IMHO won't be, since that would involve a very big intervention to an existing code.
2. How to create fully functional header for a child node detail grid view ?
Considering few ways, how to simulate header look and behavior for child nodes I've found useful to use nested tree views for a detail grid view. This brings you the separateness for your detail data and allows you to minimize the whole simulation to positioning of the nested tree view into a child node's rectangle.
2.1. Startup project
In the following project I'm trying to show how complicated could be implement such an easy task like the positioning of a control inside of a child node could be (without involving the original VirtualTree code). Take it just as a startup project, not as a final solution.
2.2. Known issues & limitations:
this project was written and tested to use only one child per root node, so don't be surprised with a behavior when you exceed this limit, because this was not designed nor even tested for
when a double click column resize of a main tree animates the column resize, the nested tree views are overdrawn with lines when the canvas is being scrolled by the ScrollDC function
to keep the VirtualTree code without changing I've overrided the method for scroll bars updating. It is used to update nested tree views bounds whenever the scrollbars needs to be updated
current OnExpanded implementation fires the event before the range and scroll positions are fixed, what makes the code more complicated and with a big weakness - the bounds of a detail tree view are updated after the tree is shown, what can be sometimes visible
2.3. Project code
It was written and tested in Delphi 2009 with respect to use in Delphi 7. For commented version of a next code follow this link:
Unit1.pas
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, VirtualTrees;
type
TVTScrollBarsUpdateEvent = procedure(Sender: TBaseVirtualTree; DoRepaint: Boolean) of object;
TVirtualStringTree = class(VirtualTrees.TVirtualStringTree)
private
FOnUpdateScrollBars: TVTScrollBarsUpdateEvent;
public
procedure UpdateScrollBars(DoRepaint: Boolean); override;
published
property OnUpdateScrollBars: TVTScrollBarsUpdateEvent read FOnUpdateScrollBars write FOnUpdateScrollBars;
end;
type
PNodeSubTree = ^TNodeSubTree;
TNodeSubTree = class
FChildTree: TVirtualStringTree;
end;
type
TForm1 = class(TForm)
Button1: TButton;
VirtualStringTree1: TVirtualStringTree;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure VirtualStringTree1AfterAutoFitColumns(Sender: TVTHeader);
procedure VirtualStringTree1BeforeDrawTreeLine(Sender: TBaseVirtualTree;
Node: PVirtualNode; Level: Integer; var PosX: Integer);
procedure VirtualStringTree1Collapsed(Sender: TBaseVirtualTree;
Node: PVirtualNode);
procedure VirtualStringTree1ColumnResize(Sender: TVTHeader;
Column: TColumnIndex);
procedure VirtualStringTree1Expanded(Sender: TBaseVirtualTree;
Node: PVirtualNode);
procedure VirtualStringTree1FocusChanging(Sender: TBaseVirtualTree; OldNode,
NewNode: PVirtualNode; OldColumn, NewColumn: TColumnIndex;
var Allowed: Boolean);
procedure VirtualStringTree1FreeNode(Sender: TBaseVirtualTree;
Node: PVirtualNode);
procedure VirtualStringTree1MeasureItem(Sender: TBaseVirtualTree;
TargetCanvas: TCanvas; Node: PVirtualNode; var NodeHeight: Integer);
private
procedure InvalidateSubTrees(Tree: TBaseVirtualTree);
procedure ResizeSubTrees(Tree: TBaseVirtualTree);
procedure UpdateSubTreeBounds(Tree: TBaseVirtualTree; Node: PVirtualNode);
procedure OnUpdateScrollBars(Sender: TBaseVirtualTree; DoRepaint: Boolean);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
{ TVirtualStringTree }
procedure TVirtualStringTree.UpdateScrollBars(DoRepaint: Boolean);
begin
inherited;
if HandleAllocated and Assigned(FOnUpdateScrollBars) then
FOnUpdateScrollBars(Self, DoRepaint);
end;
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
begin
ReportMemoryLeaksOnShutdown := True;
VirtualStringTree1.NodeDataSize := SizeOf(TNodeSubTree);
VirtualStringTree1.OnUpdateScrollBars := OnUpdateScrollBars;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Data: PNodeSubTree;
Node: PVirtualNode;
begin
Node := VirtualStringTree1.AddChild(nil);
Node := VirtualStringTree1.AddChild(Node);
VirtualStringTree1.InitNode(Node);
Data := VirtualStringTree1.GetNodeData(Node);
Data^ := TNodeSubTree.Create;
Data^.FChildTree := TVirtualStringTree.Create(nil);
with Data.FChildTree do
begin
Visible := False;
Parent := VirtualStringTree1;
Height := 150;
DefaultNodeHeight := 21;
Header.AutoSizeIndex := 0;
Header.Font.Charset := DEFAULT_CHARSET;
Header.Font.Color := clWindowText;
Header.Font.Height := -11;
Header.Font.Name := 'Tahoma';
Header.Font.Style := [];
Header.Height := 21;
Header.Options := [hoColumnResize, hoDrag, hoShowSortGlyphs, hoVisible];
TabStop := False;
with Header.Columns.Add do
begin
Width := 100;
Text := 'Header item 1';
end;
with Header.Columns.Add do
begin
Width := 100;
Text := 'Header item 2';
end;
end;
end;
procedure TForm1.VirtualStringTree1AfterAutoFitColumns(Sender: TVTHeader);
begin
InvalidateSubTrees(Sender.Treeview);
end;
procedure TForm1.VirtualStringTree1BeforeDrawTreeLine(Sender: TBaseVirtualTree;
Node: PVirtualNode; Level: Integer; var PosX: Integer);
begin
if Level = 1 then
PosX := 0;
end;
procedure TForm1.VirtualStringTree1Collapsed(Sender: TBaseVirtualTree;
Node: PVirtualNode);
var
Data: PNodeSubTree;
begin
Data := VirtualStringTree1.GetNodeData(Node.FirstChild);
if Assigned(Data^) and Assigned(Data^.FChildTree) then
Data^.FChildTree.Visible := False;
end;
procedure TForm1.VirtualStringTree1ColumnResize(Sender: TVTHeader;
Column: TColumnIndex);
begin
ResizeSubTrees(Sender.Treeview);
end;
procedure TForm1.VirtualStringTree1Expanded(Sender: TBaseVirtualTree;
Node: PVirtualNode);
var
Data: PNodeSubTree;
begin
Data := VirtualStringTree1.GetNodeData(Node.FirstChild);
if Assigned(Data^) and Assigned(Data^.FChildTree) then
Data^.FChildTree.Visible := True;
end;
procedure TForm1.VirtualStringTree1FocusChanging(Sender: TBaseVirtualTree;
OldNode, NewNode: PVirtualNode; OldColumn, NewColumn: TColumnIndex;
var Allowed: Boolean);
begin
if Sender.GetNodeLevel(NewNode) = 1 then
begin
Allowed := False;
if Sender.AbsoluteIndex(OldNode) > Sender.AbsoluteIndex(NewNode) then
Sender.FocusedNode := Sender.GetPreviousSibling(OldNode)
else
if OldNode <> Sender.GetLastChild(nil) then
Sender.FocusedNode := Sender.GetNextSibling(OldNode)
else
Sender.FocusedNode := OldNode;
end;
end;
procedure TForm1.VirtualStringTree1FreeNode(Sender: TBaseVirtualTree;
Node: PVirtualNode);
var
Data: PNodeSubTree;
begin
Data := VirtualStringTree1.GetNodeData(Node);
if Assigned(Data^) then
begin
if Assigned(Data^.FChildTree) then
Data^.FChildTree.Free;
Data^.Free;
end;
end;
procedure TForm1.VirtualStringTree1MeasureItem(Sender: TBaseVirtualTree;
TargetCanvas: TCanvas; Node: PVirtualNode; var NodeHeight: Integer);
var
Data: PNodeSubTree;
begin
if VirtualStringTree1.GetNodeLevel(Node) = 1 then
begin
Data := VirtualStringTree1.GetNodeData(Node);
if Assigned(Data^) and Assigned(Data^.FChildTree) then
NodeHeight := Data^.FChildTree.Height + 8;
end;
end;
procedure TForm1.InvalidateSubTrees(Tree: TBaseVirtualTree);
var
Data: PNodeSubTree;
Node: PVirtualNode;
begin
Node := Tree.GetFirst;
while Assigned(Node) do
begin
if Tree.HasChildren[Node] then
begin
Data := Tree.GetNodeData(Node.FirstChild);
if Assigned(Data^) and Assigned(Data^.FChildTree) then
begin
Data^.FChildTree.Header.Invalidate(nil);
Data^.FChildTree.Invalidate;
end;
end;
Node := Tree.GetNextSibling(Node);
end;
end;
procedure TForm1.ResizeSubTrees(Tree: TBaseVirtualTree);
var
Node: PVirtualNode;
begin
Node := Tree.GetFirst;
while Assigned(Node) do
begin
if Tree.HasChildren[Node] then
UpdateSubTreeBounds(Tree, Node.FirstChild);
Node := Tree.GetNextSibling(Node);
end;
end;
procedure TForm1.UpdateSubTreeBounds(Tree: TBaseVirtualTree; Node: PVirtualNode);
var
R: TRect;
Data: PNodeSubTree;
begin
if Assigned(Node) then
begin
Data := Tree.GetNodeData(Node);
if Assigned(Data^) and Assigned(Data^.FChildTree) and
Data^.FChildTree.Visible then
begin
R := Tree.GetDisplayRect(Node, -1, False, True);
R.Left := R.Left + (Tree as TVirtualStringTree).Indent;
R.Top := R.Top + 4;
R.Right := R.Right - 8;
R.Bottom := R.Bottom - 4;
Data^.FChildTree.BoundsRect := R;
end;
end;
end;
procedure TForm1.OnUpdateScrollBars(Sender: TBaseVirtualTree; DoRepaint: Boolean);
begin
ResizeSubTrees(Sender);
end;
end.
Unit1.dfm
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 282
ClientWidth = 468
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
OnCreate = FormCreate
DesignSize = (
468
282)
PixelsPerInch = 96
TextHeight = 13
object VirtualStringTree1: TVirtualStringTree
Left = 8
Top = 8
Width = 371
Height = 266
Anchors = [akLeft, akTop, akRight, akBottom]
Header.AutoSizeIndex = 0
Header.Font.Charset = DEFAULT_CHARSET
Header.Font.Color = clWindowText
Header.Font.Height = -11
Header.Font.Name = 'Tahoma'
Header.Font.Style = []
Header.Height = 21
Header.Options = [hoColumnResize, hoDblClickResize, hoDrag, hoShowSortGlyphs, hoVisible]
TabOrder = 0
TreeOptions.MiscOptions = [toVariableNodeHeight]
OnAfterAutoFitColumns = VirtualStringTree1AfterAutoFitColumns
OnBeforeDrawTreeLine = VirtualStringTree1BeforeDrawTreeLine
OnCollapsed = VirtualStringTree1Collapsed
OnColumnResize = VirtualStringTree1ColumnResize
OnExpanded = VirtualStringTree1Expanded
OnFocusChanging = VirtualStringTree1FocusChanging
OnFreeNode = VirtualStringTree1FreeNode
OnMeasureItem = VirtualStringTree1MeasureItem
ExplicitWidth = 581
ExplicitHeight = 326
Columns = <
item
Position = 0
Width = 75
WideText = 'Column 1'
end
item
Position = 1
Width = 75
WideText = 'Column 2'
end
item
Position = 2
Width = 75
WideText = 'Column 3'
end>
end
object Button1: TButton
Left = 385
Top = 8
Width = 75
Height = 25
Anchors = [akTop, akRight]
Caption = 'Button1'
TabOrder = 1
OnClick = Button1Click
ExplicitLeft = 595
end
end
2.4. Screenshot
I'm in the process of reproducing Project Page Options IDE add-in¹. Particularly, this add-in replaces default behavior² of Open action in the Project Manager with its own behavior - to open a HTML page in the same internal browser which is used to display a Welcome Page. So, i want to do the same, but currently i failed to reach this menu.
I tried IOTAProjectManager interface, which facilitates an adding Project Manager's menu items³, but i learned what its notifiers are isolated from each other, so most probably this API is useless for my purpose.
Also, i tried to hook into application-wide action processing. It gave me absolutely no results, probably action list(s) are not used there at all.
I guess, disposition above leave me no choice but to resort to a hacks, which makes hackish solutions really welcome here. So, any idea please?
¹ For more info about that see this Q.
² There are 3 relevant items: Open, Show Markup, Show Designer. Open defaults to Show Designer without an add-in.
³ In the fact, this API allows adding items on-the-fly, and it probably makes things even more complicated.
Context menus illustrated:
As TOndrej mentioned in comment below, behavior of Open menu item changed only for HTML document configured as "Project Page" in the corresponding dialog.
I think the original Project Page extension does it by installing an IDE Notifier (see TProjectPageNotifier below). I don't think it has anything to do with the Project Manager. It simply listens to notifications about files which are being opened in the IDE and if it's the project page it will open it in the embedded browser instead of the default HTML designer. Here's my attempt to reproduce this functionality for Delphi 2007.
1) package:
package projpageide;
{$R *.res}
// ... some compiler options snipped for brevity
{$DESCRIPTION '_Project Page Options'}
{$LIBSUFFIX '100'}
{$DESIGNONLY}
{$IMPLICITBUILD ON}
requires
rtl,
designide;
contains
Projectpagecmds in 'Projectpagecmds.pas',
ProjectPageOptionsDlg in 'ProjectPageOptionsDlg.pas';
end.
2) data module with an action and a menu item to add to 'Project' menu:
unit ProjectPageCmds;
interface
uses
Windows,SysUtils, Classes, ActnList, Menus, Controls, Forms, Dialogs;
type
TProjectPageCmds = class(TDataModule)
ActionList1: TActionList;
PopupMenu1: TPopupMenu;
ProjectWelcomeOptions: TAction;
ProjectWelcomeOptionsItem: TMenuItem;
procedure ProjectWelcomeOptionsExecute(Sender: TObject);
procedure ProjectWelcomeOptionsUpdate(Sender: TObject);
private
public
end;
implementation
{$R *.dfm}
uses
XMLIntf, Variants, ToolsApi,
ProjectPageOptionsDlg;
type
IURLModule = interface(IOTAModuleData)
['{9D215B02-6073-45DC-B007-1A2DBCE2D693}']
function GetURL: string;
procedure SetURL(const URL: string);
property URL: string read GetURL write SetURL;
end;
TOpenNewURLModule = procedure(const URL: string; EditorForm: TCustomForm);
TProjectPageNotifier = class(TNotifierObject, IOTAIDENotifier)
procedure FileNotification(NotifyCode: TOTAFileNotification; const FileName: string; var Cancel: Boolean);
procedure BeforeCompile(const Project: IOTAProject; var Cancel: Boolean); overload;
procedure AfterCompile(Succeeded: Boolean); overload;
end;
const
sWelcomePageFile = 'WelcomePageFile';
sWelcomePageFolder = 'WelcomePageFolder';
var
DataModule: TProjectPageCmds = nil;
NotifierIndex: Integer = -1;
function FindURLModule: IURLModule;
var
I: Integer;
begin
Result := nil;
with BorlandIDEServices as IOTAModuleServices do
for I := 0 to ModuleCount - 1 do
if Supports(Modules[I], IURLModule, Result) then
Break;
end;
procedure OpenURL(const URL: string; UseExistingView: Boolean = True);
{$IFDEF VER220} // Delphi XE
const
SStartPageIDE = 'startpageide150.bpl';
SOpenNewURLModule = '#Urlmodule#OpenNewURLModule$qqrx20System#UnicodeStringp22Editorform#TEditWindow';
{$ENDIF}
{$IFDEF VER185} // Delphi 2007
const
SStartPageIDE = 'startpageide100.bpl';
SOpenNewURLModule = '#Urlmodule#OpenNewURLModule$qqrx17System#AnsiStringp22Editorform#TEditWindow';
{$ENDIF}
var
Module: IURLModule;
EditWindow: INTAEditWindow;
Lib: HMODULE;
OpenNewURLModule: TOpenNewURLModule;
begin
EditWindow := nil;
Module := nil;
if UseExistingView then
Module := FindURLModule;
if Assigned(Module) then
begin
Module.URL := URL;
(Module as IOTAModule).Show;
end
else
begin
{$IFDEF VER220}
EditWindow := (BorlandIDEServices as INTAEditorServices).TopEditWindow;
{$ENDIF}
{$IFDEF VER185}
if Assigned((BorlandIDEServices as IOTAEditorServices).TopView) then
EditWindow := (BorlandIDEServices as IOTAEditorServices).TopView.GetEditWindow;
{$ENDIF}
if not Assigned(EditWindow) or not Assigned(EditWindow.Form) then
Exit;
Lib := GetModuleHandle(SStartPageIDE);
if Lib = 0 then
Exit;
OpenNewURLModule := GetProcAddress(Lib, SOpenNewURLModule);
if #OpenNewURLModule <> nil then
OpenNewURLModule(URL, EditWindow.Form);
end;
end;
function ReadOption(const Project: IOTAProject; const SectionName, AttrName: WideString): WideString;
var
Node: IXMLNode;
begin
Result := '';
Node := (BorlandIDEServices as IOTAProjectFileStorage).GetProjectStorageNode(Project, SectionName, False);
if Assigned(Node) and (Node.HasAttribute(AttrName)) then
Result := Node.Attributes[AttrName];
end;
procedure WriteOption(const Project: IOTAProject; const SectionName, AttrName, Value: WideString);
var
Node: IXMLNode;
begin
Node := (BorlandIDEServices as IOTAProjectFileStorage).GetProjectStorageNode(Project, SectionName, False);
if not Assigned(Node) then
Node := (BorlandIDEServices as IOTAProjectFileStorage).AddNewSection(Project, SectionName, False);
Node.Attributes[AttrName] := Value;
Project.MarkModified;
end;
function GetCurrentProjectPageFileName: string;
var
Project: IOTAProject;
begin
Result := '';
Project := (BorlandIDEServices as IOTAModuleServices).GetActiveProject;
if Assigned(Project) then
Result := ReadOption(Project, sWelcomePageFile, 'Path');
end;
procedure TProjectPageCmds.ProjectWelcomeOptionsExecute(Sender: TObject);
var
Project: IOTAProject;
Dlg: TDlgProjectPageOptions;
I: Integer;
ModuleInfo: IOTAModuleInfo;
begin
Project := (BorlandIDEServices as IOTAModuleServices).GetActiveProject;
if not Assigned(Project) then
Exit;
Dlg := TDlgProjectPageOptions.Create(nil);
try
for I := 0 to Project.GetModuleCount - 1 do
begin
ModuleInfo := Project.GetModule(I);
if ModuleInfo.CustomId = 'HTMLTool' then
Dlg.cmbWelcomePage.Items.Add(ExtractRelativePath(ExtractFilePath(Project.FileName), ModuleInfo.FileName));
end;
Dlg.cmbWelcomePage.Text := ReadOption(Project, sWelcomePageFile, 'Path');
Dlg.edWelcomeFolder.Text := ReadOption(Project, sWelcomePageFolder, 'Path');
if Dlg.ShowModal = mrOK then
begin
WriteOption(Project, sWelcomePageFile, 'Path', Dlg.cmbWelcomePage.Text);
WriteOption(Project, sWelcomePageFolder, 'Path', Dlg.edWelcomeFolder.Text);
end;
finally
Dlg.Free;
end;
end;
procedure TProjectPageCmds.ProjectWelcomeOptionsUpdate(Sender: TObject);
var
Project: IOTAProject;
begin
Project := (BorlandIDEServices as IOTAModuleServices).GetActiveProject;
with (Sender as TAction) do
begin
Enabled := Assigned(Project);
Visible := Enabled;
end;
end;
procedure TProjectPageNotifier.FileNotification(NotifyCode: TOTAFileNotification; const FileName: string;
var Cancel: Boolean);
var
Project: IOTAProject;
begin
if (NotifyCode = ofnFileOpening) then
begin
Project := (BorlandIDEServices as IOTAModuleServices).GetActiveProject;
if not Assigned(Project) then
Exit;
if SameText(ReadOption(Project, sWelcomePageFile, 'Path'), ExtractRelativePath(ExtractFilePath(Project.FileName), FileName)) then
begin
Cancel := True;
OpenURL(FileName);
end;
end;
end;
procedure TProjectPageNotifier.AfterCompile(Succeeded: Boolean);
begin
// do nothing
end;
procedure TProjectPageNotifier.BeforeCompile(const Project: IOTAProject; var Cancel: Boolean);
begin
// do nothing
end;
procedure Initialize;
var
NTAServices: INTAServices;
Services: IOTAServices;
begin
if not BorlandIDEServices.GetService(INTAServices, NTAServices) or not BorlandIDEServices.GetService(IOTAServices, Services) then
Exit;
DataModule := TProjectPageCmds.Create(nil);
try
NTAServices.AddActionMenu('ProjectDependenciesItem', DataModule.ProjectWelcomeOptions, DataModule.ProjectWelcomeOptionsItem);
NotifierIndex := Services.AddNotifier(TProjectPageNotifier.Create);
except
FreeAndNil(DataModule);
raise;
end;
end;
procedure Finalize;
begin
if NotifierIndex <> -1 then
(BorlandIDEServices as IOTAServices).RemoveNotifier(NotifierIndex);
FreeAndNil(DataModule);
end;
initialization
Initialize;
finalization
Finalize;
end.
3) the data module's dfm:
object ProjectPageCmds: TProjectPageCmds
OldCreateOrder = False
Left = 218
Top = 81
Height = 150
Width = 215
object ActionList1: TActionList
Left = 32
Top = 8
object ProjectWelcomeOptions: TAction
Category = 'Project'
Caption = 'Pro&ject Page Options...'
HelpContext = 3146
OnExecute = ProjectWelcomeOptionsExecute
OnUpdate = ProjectWelcomeOptionsUpdate
end
end
object PopupMenu1: TPopupMenu
Left = 96
Top = 8
object ProjectWelcomeOptionsItem: TMenuItem
Action = ProjectWelcomeOptions
end
end
end
4) project page options dialog:
unit ProjectPageOptionsDlg;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;
type
TDlgProjectPageOptions = class(TForm)
bpCancel: TButton;
bpHelp: TButton;
bpOK: TButton;
cmbWelcomePage: TComboBox;
edWelcomeFolder: TEdit;
Label1: TLabel;
Label2: TLabel;
procedure bpOKClick(Sender: TObject);
procedure bpHelpClick(Sender: TObject);
private
procedure Validate;
public
end;
implementation
{$R *.dfm}
uses
ShLwApi, ToolsApi;
resourcestring
sProjectPageDoesNotExist = 'Project page ''%s'' does not exist';
sProjectFolderDoesNotExist = 'Project folder ''%s'' does not exist';
function CanonicalizePath(const S: string): string;
var
P: array[0..MAX_PATH] of Char;
begin
Win32Check(PathCanonicalize(P, PChar(S)));
Result := P;
end;
procedure TDlgProjectPageOptions.Validate;
var
Project: IOTAProject;
WelcomePagePath, WelcomeFolderPath: string;
begin
Project := (BorlandIDEServices as IOTAModuleServices).GetActiveProject;
if not Assigned(Project) then
Exit;
if cmbWelcomePage.Text <> '' then
begin
WelcomePagePath := CanonicalizePath(ExtractFilePath(Project.FileName) + cmbWelcomePage.Text);
if not FileExists(WelcomePagePath) then
begin
ModalResult := mrNone;
raise Exception.CreateFmt(sProjectPageDoesNotExist, [WelcomePagePath]);
end;
end;
if edWelcomeFolder.Text <> '' then
begin
WelcomeFolderPath := CanonicalizePath(ExtractFilePath(Project.FileName) + edWelcomeFolder.Text);
if not FileExists(WelcomeFolderPath) then
begin
ModalResult := mrNone;
raise Exception.CreateFmt(sProjectFolderDoesNotExist, [WelcomeFolderPath]);
end;
end;
ModalResult := mrOK;
end;
procedure TDlgProjectPageOptions.bpHelpClick(Sender: TObject);
begin
Application.HelpContext(Self.HelpContext);
end;
procedure TDlgProjectPageOptions.bpOKClick(Sender: TObject);
begin
Validate;
end;
end.
5) the dialog's dfm:
object DlgProjectPageOptions: TDlgProjectPageOptions
Left = 295
Top = 168
HelpContext = 3146
BorderIcons = [biSystemMenu]
BorderStyle = bsDialog
Caption = 'Project Page Options'
ClientHeight = 156
ClientWidth = 304
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
Position = poScreenCenter
DesignSize = (
304
156)
PixelsPerInch = 96
TextHeight = 13
object Label1: TLabel
Left = 8
Top = 6
Width = 65
Height = 13
Caption = '&Project page:'
FocusControl = cmbWelcomePage
end
object Label2: TLabel
Left = 8
Top = 62
Width = 80
Height = 13
Caption = '&Resource folder:'
FocusControl = edWelcomeFolder
end
object edWelcomeFolder: TEdit
Left = 8
Top = 81
Width = 288
Height = 21
Anchors = [akLeft, akTop, akRight]
TabOrder = 1
end
object bpOK: TButton
Left = 59
Top = 123
Width = 75
Height = 25
Anchors = [akRight, akBottom]
Caption = 'OK'
Default = True
ModalResult = 1
TabOrder = 2
OnClick = bpOKClick
end
object bpCancel: TButton
Left = 140
Top = 123
Width = 75
Height = 25
Anchors = [akRight, akBottom]
Cancel = True
Caption = 'Cancel'
ModalResult = 2
TabOrder = 3
end
object bpHelp: TButton
Left = 221
Top = 123
Width = 75
Height = 25
Anchors = [akRight, akBottom]
Caption = 'Help'
TabOrder = 4
OnClick = bpHelpClick
end
object cmbWelcomePage: TComboBox
Left = 8
Top = 25
Width = 288
Height = 21
Anchors = [akLeft, akTop, akRight]
ItemHeight = 13
TabOrder = 0
Text = 'cmbWelcomePage'
end
end
However, I don't know what effect the "Resource Folder" has. The option can be stored in and read from the .dproj file, it's also checked that it exists but I don't know how the original extension uses the folder path. If you find out please let me know, I'll include it in the code.
Also, part of this code is copied from my answer to another question of yours, which I compiled (and briefly tested) in Delphi 2007 and Delphi XE. This code was only compiled and briefly tested in Delphi 2007.
Hope this helps as a starting point, at least.