Slow eventhandler in TPageControl.OnChange causes weird behaviour - delphi

When I add slow code to the OnChange event of TPageControl I run into problems.
If the code is fast and doesn't take a lot of time, things are fine.
However if the code takes a long time to return +/- 0.5 to 1 second, the PageControl starts to act weird.
If the user changes a page sometimes it doesn't do anything on the first click, and a second click on the page is required to actually make the change happen.
I've kind of sort of fixed this with code like this.
(I've simplified it a bit, just to show the idea)
type TDelayProc = procedure(Sender: TObject) of object;
TForm = class(TForm)
...
private
FDelayedSender: TObject;
FDelayedEvent: TDelayProc;
procedure SetDelayedEvent(Value: TDelayProc);
property FDelayedSender: TObject read FDelayedSender write FDelayedSender;
property FDelayedEvent: TDelayProc read FDelayedEvent write SetDelayedEvent;
...
procedure TForm1.SetDelayedEvent(Value: TDelayProc);
begin
Timer1.Active:= false;
FDelayedEvent:= Value;
if Assigned(Value) then Timer1.Active:= true
else DelayedSender:= nil;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
Timer1.Active:= false;
if Assigned(DelayedEvent) then DelayedEvent(DelayedSender);
end;
procedure TForm1.PageControl1Change(Sender: TObject);
begin
if PageControl1.ActivePage = TSPage1 then begin
DelayedSender:= Button1;
DelayedEvent:= Button1Click;
end; {if}
end;
As you can see this is a horrible hack.
The code I'm calling is in QuickReport to prepare a report and MySQL query and such, so I don't have much control over that.
I'm think there's some Win32 messaging that I'm messing up by not returning from TPageControl.OnChange fast enough, the delay is definitely shorter than 3 seconds though.
I've tried ProcessMessages, but that just made things worse and I don't want to use a separate thread for this.
How do I fix this so I can use the OnChange event handler like normal

I'm unclear about why you're using the TTimer stuff. If it were me, I think I'd just PostMessage a custom message to my form from the OnChange event, so the OnChange handler would return immediately. That would allow the PageControl message flow to behave normally. Then in the Message handler for that custom message I would (1) show/start a progress bar form running on a 2nd thread, (2) start the activity which is taking so much time, and (3) when the time consuming activity finishes, shut down the progress bar.
Here's some code for a threaded progress bar, that I modified from something Peter Below posted years ago. It's NOT pretty, but users don't care about that as much as they care about "nothing happening" on the screen.
unit AniMg;
{ Unit for displaying animated progress bar during a lengthy process.
* Painting of progress is done in a secondary thread, so it updates even during processing
which doesn't process Windows messages (and therefore doesn't update visible windows).
* Does NOT call Application.ProcessMessages...so it doesn't alter the order in which the
application processed messages.
USAGE:
//Delays display of the progress form. When this property <> 0, caller must pepper
//his code with .UpdateVisible calls, or the form will never be displayed.
AniMgr.DelayBeforeVisible := 3000;
//If DelayBeforeVisible time has elapsed, displays the progress form & starts thread.
AniMgr.UpdateVisible;
//Displays the progress form & starts painting it in a secondary thread.
//(If DelayBeforeVisible <> 0, sets the form's caption or caption-to-be.)
AniMgr.Push('Some caption');
//To change captions without closing/opening the progress bar form...
AniMgr.Push('Another caption');
//Close the form
AniMgr.PopAll;
NOTES:
* Do NOT call DisableTaskWindows in this unit!! It's tempting to do that when the progress
form is shown, to make it function modally. However, do so at your own risk! Having
DisableTaskWindows in effect resulted in an AV when we were called from certain routines
or component's code.
AUTHOR:
* Mark Wilsdorf, Flagship Technologies, Inc., www.goflagship.com.
* Thanks to Peter Below for his original code for painting the progress bar, and his many
years of providing stellar examples and explanations to the Delphi community.
DEVELOPMENT:
* Originally put FAniform.Show/Update on a TTimer delay, so the progress form wouldn't
display just for a brief instant during quick processes. However, we had to get rid of
Application.ProcessMessages calls (which caused problems in caller), so the TTimer wouldn't
fire. Can't make the 2ndary thread do the Show/Update job either, for the same reason:
Synchronize() won't work because the main thread is occupied in other code, and without
Application.ProcessMessages calls the Synchronize(Show/Update code) doesn't get called
until the lengthy main thread code processing finishes. The only solution appears to be:
have the 2ndary thread be fully responsible for creating and showing/updating the entire
progress window, entirely via Windows API calls.
}
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, RzLabel, ExtCtrls, RzPanel;
{$I DEFINES.PAS}
type
T_AniForm = class(TForm)
RzPanel2: TRzPanel;
RzLabel1: TRzLabel;
RzPanel1: TRzPanel;
public
r : TRect;
constructor Create(AOwner: TComponent); override;
end;
//Do NOT call DisableTaskWindows in this unit!!
//We may be called from rtnes or components which attempt to update the UI, resulting
//in an AV in certain circumstances. This was the result when used with the popular
//Developer's Express component, ExpressQuantumGrid.
TAniThread = class(TThread)
private
FWnd: HWND;
FPaintRect: TRect;
FbkColor, FfgColor: TColor;
FInterval: integer;
protected
procedure Execute; override;
public
constructor Create(paintsurface : TWinControl; {Control to paint on }
paintrect : TRect; { area for animation bar }
bkColor, barcolor : TColor; { colors to use }
interval : integer); { wait in msecs between paints}
end;
TAniMgr = class(TObject)
private
FStartTime: DWord; //=Cardinal. Same as GetTickCount
FDelayBeforeVisible: cardinal;
FRefCount: integer;
FAniThread : TAniThread;
FAniForm: T_AniForm;
// procedure SetDelayBeforeVisible(Value: cardinal);
procedure StopIt;
public
procedure Push(const NewCaption: string);
procedure UpdateVisible;
//procedure Pop; Don't need a Pop menthod until we Push/Pop captions...
procedure PopAll;
//
//Delay before form shows. Takes effect w/r/t to first Push() call.
property DelayBeforeVisible: cardinal read FDelayBeforeVisible write FDelayBeforeVisible;
end;
function AniMgr: TAniMgr; //function access
implementation
{$R *.dfm}
var
_AniMgr : TAniMgr = nil; //Created privately in Initialization section
//Do NOT DisableTaskWindows in this unit!!
//We're called from some rtnes which attempt to update the UI, resulting in an AV.
//DisabledWindows: pointer = nil;
function AniMgr: TAniMgr;
begin
if not Assigned(_AniMgr) then
_AniMgr := TAniMgr.Create;
Result := _AniMgr;
end;
//---------------------------------------------------------------------------------------------
// TAniMgr
//---------------------------------------------------------------------------------------------
procedure TAniMgr.UpdateVisible;
{ Checks our form's visibility & calls form.Update if appropriate.
* This rtne implements DelayBeforeVisible handling. }
begin
//Thd may be terminating...
if Assigned( FAniThread ) and FAniThread.Terminated then
exit;
if Assigned(FAniForm) and
( (DelayBeforeVisible = 0) or (GetTickCount - FStartTime > DelayBeforeVisible) ) then begin
if not Assigned(FAniThread) then
with FAniForm do begin
Show;
//Form.Update processes our paint msgs to paint the form. Do NOT call
//Application.ProcessMessages here!! It may disrupt caller's intended message flow.
Update;
//Start painting progress bar on the form
FAniThread := TAniThread.Create(RzPanel1, r, FAniForm.color, clActiveCaption, 100);
end
else
FAniForm.Update;
end;
end;
procedure TAniMgr.Push(const NewCaption: string);
{ We don't really Push a stack of captions (though we could)...for now that's not
important; we just manage the form and RefCount. }
begin
//Thd may be terminating...
if Assigned( FAniThread ) and FAniThread.Terminated then
exit;
FRefCount := FRefCount + 1;
if FAniForm = nil then begin
FAniForm := T_AniForm.Create(nil);
//If FAniForm was nil this is the first Push() of a series, so get
//a starting tick count for DelayBeforeShowing management
FStartTime := GetTickCount;
end;
FAniForm.RzLabel1.Caption := NewCaption;
UpdateVisible;
end;
procedure TAniMgr.StopIt;
begin
if Assigned( FAniThread ) then begin
if not FAniThread.Terminated then begin
FAniThread.Terminate;
FAniThread.WaitFor;
end;
end;
FreeAndNil(FAniThread);
FreeAndNil(FAniForm);
end;
//procedure TAniMgr.Pop;
//{ We don't really Pop a stack of captions...for now that's not important; we just
// decrement the RefCount. }
//begin
// if FRefCount > 0 then
// FRefCount := FRefCount - 1;
// if (FRefCount = 0) then
// StopIt;
//end;
procedure TAniMgr.PopAll;
begin
if FRefCount > 0 then try
StopIt;
finally
FRefCount := 0;
end;
end;
//---------------------------------------------------------------------------------------------
// T_AniForm
//---------------------------------------------------------------------------------------------
constructor T_AniForm.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
r := RzPanel1.ClientRect;
InflateRect(r, - RzPanel1.BevelWidth, - RzPanel1.BevelWidth);
end;
//---------------------------------------------------------------------------------------------
// TAniThread
//---------------------------------------------------------------------------------------------
constructor TAniThread.Create(paintsurface : TWinControl;
paintrect : TRect; bkColor, barcolor : TColor; interval : integer); //BeforePaint: integer);
begin
inherited Create(True); //Suspended
FWnd := paintsurface.Handle;
FPaintRect := paintrect;
FbkColor := bkColor;
FfgColor := barColor;
FInterval := interval;
FreeOnterminate := False; //So we can use WaitFor & know it's dead.
Resume;
end;
procedure TAniThread.Execute;
var
image : TBitmap;
DC : HDC;
left, right : integer;
increment : integer;
imagerect : TRect;
state : (incRight, incLeft, decLeft, decRight);
begin
Image := TBitmap.Create;
try
with Image do begin
Width := FPaintRect.Right - FPaintRect.Left;
Height := FPaintRect.Bottom - FPaintRect.Top;
imagerect := Rect(0, 0, Width, Height);
end; { with }
left := 0;
right := 0;
increment := imagerect.right div 50;
//WAS... increment := imagerect.right div 50;
state := Low(State);
while not Terminated do begin
with Image.Canvas do begin
Brush.Color := FbkColor;
FillRect(imagerect);
case state of
incRight: begin
Inc(right, increment);
if right > imagerect.right then
begin
right := imagerect.right;
Inc(state);
end; { if }
end; { case incRight }
incLeft: begin
Inc(left, increment);
if left >= right then
begin
left := right;
Inc(state);
end; { if }
end; { case incLeft }
decLeft: begin
Dec(left, increment);
if left <= 0 then
begin
left := 0;
Inc(state);
end; { if }
end; { case decLeft }
decRight: begin
Dec(right, increment);
if right <= 0 then
begin
right := 0;
state := incRight;
end; { if }
end; { case decLeft }
end; { case }
Brush.Color := FfgColor;
FillRect(Rect(left, imagerect.top, right, imagerect.bottom));
end; { with }
DC := GetDC(FWnd);
if DC <> 0 then try
BitBlt(DC,
FPaintRect.Left,
FPaintRect.Top,
imagerect.right,
imagerect.bottom,
Image.Canvas.handle,
0, 0,
SRCCOPY);
finally
ReleaseDC(FWnd, DC);
end;
Sleep(FInterval);
end; { while not Terminated}
finally
Image.Free;
end;
InvalidateRect(FWnd, nil, True);
end;
initialization
finalization
if Assigned(_AniMgr) then begin
_AniMgr.PopAll;
_AniMgr.Free;
end;
end.

The only explanation I have is that your long running handler is pumping the message queue. So long as you don't pump the queue you can take as long as you like handling an event. It might look messy since you are neglecting the queue but it will work normally.

I wish there was a BeforeChange event
that gave me the new page as a
parameter [...]
There almost is. Use the OnChanging event and the IndexOfTabAt function:
// Warning: Don't use, see below!
procedure TForm1.PageControl1Changing(Sender: TObject;
var AllowChange: Boolean);
var
pnt: TPoint;
NewTabIndex: integer;
begin
if not GetCursorPos(pnt) then Exit;
pnt := PageControl1.ScreenToClient(pnt);
NewTabIndex := PageControl1.IndexOfTabAt(pnt.X, pnt.Y);
if NewTabIndex <> -1 then
ShowMessageFmt('Next up: tab with index %d.', [NewTabIndex]);
end;
But: This only works if the user clicks a tab. It does not work if the user navigates the tab control using the keyboard. Therefore, this answer is useless (other than for educational purposes).

Related

VCL richedit, slow to change word colors

I have the following code in a delphi program (VCL based desktop application) to iterate through lines of text (sentences of between about 8-15 words) in a richedit, find instances of a user selected word, and then color that word 'red' should it appear on a line.
The problem: The color changing proceeds painfully slowly (several minutes elapse) if the procedure must work through more than a few thousand lines. I'm left sitting here while the cursor dances around. Here's the procedure that is the source of the delay:
procedure Color_Words(RE: TRichEdit; Word: String; Color: TColor);
var
i, startPos, CharPos2, nosChars: Integer;
begin
startPos := 0;
nosChars := 0;
charpos2:=0;
RE.lines.beginupdate;
for i := 0 to Pred(RE.Lines.Count) do
begin
nosChars := nosChars + Length(RE.Lines[i]);
CharPos2 := RE.FindText(word, startPos,nosChars,stmatchcase]);
startPos := CharPos2+1;
RE.SelStart := CharPos2;
RE.SelLength :=(Length(word));
RE.SelAttributes.Color := Color;
end;
RE.Lines.EndUpdate;
end;
Can someone come up with a procedure that is much, much quicker, or advise me how to solve matters? Also, if you could explain the slow processing in layman's terms that would be wonderful. (I am but a hobbyist).
First thing to do is change your code to use the version 4.1 of RichEdit control (introduced with Windows XP SP1), that alone might speed things up.
"RichEdit20W": Riched20.dll (Windows 98)
"RICHEDIT50W": Msftedit.dll (Windows XP SP1)
Windows continues to support the older versions of the RichEdit control, but Delphi stubbornly continues to use the old version, as you can see in Vcl.ComCtrls.pas:
procedure TCustomRichEdit.CreateParams(var Params: TCreateParams);
const
RichEditClassName = 'RICHEDIT20W';
begin
inherited CreateParams(Params);
CreateSubClass(Params, RichEditClassName); //<-- 'RICHEDIT20W'
//...
end;
Tell Delphi to use the Windows XP era RichEdit 4.1
There are a couple ways to fix this; the least intrusive is to create a new unit:
MicrosoftEdit.pas
unit MicrosoftEdit;
interface
uses
Vcl.ComCtrls, Winapi.RichEdit, Vcl.Controls, Winapi.Windows, System.Classes;
type
TRichEdit = class(Vcl.ComCtrls.TRichEdit)
protected
procedure CreateParams(var Params: TCreateParams); override;
end;
implementation
{ TMicrosoftEdit }
procedure TRichEdit.CreateParams(var Params: TCreateParams);
const
MSFTEDIT_CLASS = 'RICHEDIT50W'; //Richedit 4.1, Msftedit.dll
begin
LoadLibrary('msftedit.dll');
inherited CreateParams({var}Params);
CreateSubClass({var}Params, MSFTEDIT_CLASS); //"RICHEDIT50W"
end;
end.
And then include MicrosoftEdit.pas as the last unit in the interface section of your form's uses clause. And you can even be doubly sure that it works by re-declaring TRichEdit to be your new TRichEdit:
unit MyForm;
uses
Forms, RichEdit, MicrosoftEdit;
type
TRichEdit = MicrosoftEdit.TRichEdit; //use our own TRichEdit
TMyForm = class(TForm)
RichEdit1: TRichEdit;
private
protected
public
end;
//...
OnChange?
If you are making formatting changes to the text in a RichEdit:
procedure TMyForm.Button1Click(Sender: TObject);
begin
Color_Words(RichEdit1, 'Trump', clRed);
end;
and you have an OnChange handler attached to the RichEdit, it will fire the OnChange every time the formatting changes. You need to stop that:
procedure TMyForm.Button1Click(Sender: TObject);
var
oldOnChange: TNotifyEvent;
begin
oldOnChange := RichEdit1.OnChange;
RichEdit1.OnChange := nil;
try
Color_Words(RichEdit1, 'Trump', clRed);
finally
RichEdit1.OnChange := oldOnChange;
end;
end;
Undos
In addition, every coloring change you make will be recorded in the Undo list! As well as the RichEdit redrawing every time. Stop those:
procedure TMyForm.Button1Click(Sender: TObject);
var
oldOnChange: TNotifyEvent;
begin
oldOnChange := RichEdit1.OnChange;
RichEdit1.OnChange := nil;
try
RichEditSuspendAll(RichEdit1, True);
try
Color_Words(RichEdit1, 'Trump', clRed);
finally
RichEditSuspendAll(RichEdit1, False);
end;
finally
RichEdit1.OnChange := oldOnChange;
end;
end;
With a helper function:
procedure RichEditSuspendAll(ARichEdit: TRichEdit; bSuspend: Boolean);
var
doc: ITextDocument;
re: IUnknown;
begin
{
http://bcbjournal.org/articles/vol3/9910/Faster_rich_edit_syntax_highlighting.htm
int eventMask = ::SendMessage(RichEdit1->Handle, EM_SETEVENTMASK, 0, 0);
SendMessage(RichEdit1->Handle, WM_SETREDRAW, false, 0);
ParseAllText(RichEdit1);
SendMessage(RichEdit1->Handle, WM_SETREDRAW, true, 0);
InvalidateRect(RichEdit1->Handle, 0, true);
SendMessage(RichEdit1->Handle, EM_SETEVENTMASK, 0, eventMask);
}
{
http://support.microsoft.com/KB/199852
How To Suspend and Resume the Undo Functionality in Richedit 3.0
If it is necessary to Undo an action that is performed before a suspend, after resuming the Undo, then,
tomFalse must be replaced with "tomSuspend" and tomTrue must be replaced with "tomResume".
This method retains the contents of the Undo buffer even when Undo is suspended.
Applications can retrieve an ITextDocument pointer from a rich edit control.
To do this, send an EM_GETOLEINTERFACE message to retrieve an IRichEditOle
object from a rich edit control. Then, call the object's
IUnknown::QueryInterface method to retrieve an ITextDocument pointer.
}
if ARichEdit = nil then
raise Exception.Create('ARichEdit is nil');
if SendMessage(ARichEdit.Handle, EM_GETOLEINTERFACE, 0, LPARAM(#re)) = 0 then
raise Exception.Create('Could not get OleInterface from RichEdit');
doc := re as ITextDocument;
doc := RichEditGetTextDocument(ARichEdit);
if bSuspend then
begin
RichEdit.Perform(WM_SETREDRAW, 0, 0); //disable all painting of the control
doc.Undo(Integer(tomSuspend)); // Suspends Undo.
end
else
begin
doc.Undo(Integer(tomResume)); // Resumes Undo.
RichEdit.Perform(WM_SETREDRAW, 0, 0); //disable all painting of the control
end;
end;

Doing a continuous action while a TButton is held down

How would I be able to do a continuous action while a button is held down? For example, I have made a custom 'Numpad' for my application, which has a Delete button. As of right now, I have to click it separately, but I want it to keep deleting while it is held down.
procedure TFrame1.deleteClick(Sender: TObject);
var
MiString: string;
begin
global_gotten_mode := precheck.global_edit_mode;
precheck.Form2.input_field.SetFocus;
MiString := Copy(precheck.Form2.input_field.Text, 0, (length(precheck.Form2.input_field.Text) - 1));
precheck.Form2.input_field.Text := MiString;
Form2.input_field.SelStart := high(integer);
end;
Add a timer and activate it on the OnMouseDown event.
As long as the button is held down, the timer will kick in at a rate of your choice.
When the button is released, the OnMouseUp event disables the timer.
Something in this way:
procedure TFrame1.BtnMouseDown(Sender : TObject);
begin
global_gotten_mode := precheck.global_edit_mode;
precheck.Form2.input_field.SetFocus;
fMyBtnTimer.Interval := 500; // Initial repetition rate
fMyBtnTimer.Enabled := true;
end;
procedure TFrame1.BtnMouseUp(Sender : TObject);
begin
fMyBtnTimer.Enabled := false;
end;
procedure TFrame1.MyBtnTimerEvent(Sender : TObject);
var
MiString: string;
begin
fMyBtnTimer.Interval := 200; // Increase repetition rate
MiString := Copy( precheck.Form2.input_field.Text,
0,
length(precheck.Form2.input_field.Text) - 1);
precheck.Form2.input_field.Text := MiString;
Form2.input_field.SelStart := high(integer);
end;

WebBrowser component navigation through multiple pages does not work

Trying to navigate using WebBrowser component automatically through code it doesn't work. The navigation includes the login page and after that some other pages. The first page button login works fine. On second page the next button needed an application.processmessages before executing to make it work. On the next/third page I cannot make automatically the next button to work.
CODE:
//CLICK BUTTON
function clickForm1(WebBrowser: TWebBrowser; FieldName: string): Boolean;
var
i, j: Integer;
FormItem: Variant;
begin
Result := False;
//no form on document
if WebBrowser.OleObject.Document.all.tags('FORM').Length = 0 then
begin
Exit;
end;
//count forms on document
for I := 0 to WebBrowser.OleObject.Document.forms.Length - 1 do
begin
FormItem := WebBrowser.OleObject.Document.forms.Item(I);
for j := 0 to FormItem.Length - 1 do
begin
try
//when the fieldname is found, try to fill out
if FormItem.Item(j).Name = FieldName then
begin
FormItem.Item(j).click;
Result := True;
end;
except
Exit;
end;
end;
end;
end;
//SEARCH INSIDE THE MEMO
procedure TForm2.Button7Click(Sender: TObject);
var
i: Integer;
a: string;
begin
Memo1.Lines.Add('');
Memo1.Lines.Text := ' ' + Memo1.Lines.Text;
for i := 0 to Length(Memo1.Lines.Text) - Length(edit7.Text) do
begin
a := Copy(Memo1.Lines.Text, i, Length(edit7.Text));
if CheckBox1.Checked = True then //FIND CASE Sensitive
begin
if a = edit7.Text then
begin
find := True;
x := 2;
Memo1.Lines.Text := Copy(Memo1.Lines.Text, 2, Length(Memo1.Lines.Text) - 1);
Memo1.SetFocus;
Memo1.SelStart := i - 2;
Memo1.SelLength := Length(edit7.Text);
break;
end;
end
else
begin
if lowercase(a) = lowercase(edit7.Text) then
begin
Memo1.Lines.Text := Copy(Memo1.Lines.Text, 2, Length(Memo1.Lines.Text) - 1);
find := True;
x := 2;
Memo1.SetFocus;
Memo1.SelStart := i - 2;
Memo1.SelLength := Length(edit7.Text);
break;
end;
end;
end;
end;
//HTML TO MEMO
procedure TForm2.Button6Click(Sender: TObject);
var
iall : IHTMLElement;
begin
if Assigned(WebBrowser1.Document) then
begin
iall := (WebBrowser1.Document AS IHTMLDocument2).body;
while iall.parentElement <> nil do
begin
iall := iall.parentElement;
end;
memo1.Text := iall.outerHTML;
end;
end;
procedure TForm2.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; var URL: OleVariant);
var
Document: IHtmlDocument2;
CurWebrowser : IWebBrowser;
TopWebBrowser: IWebBrowser;
WindowName : string;
ovElements: OleVariant;
i: Integer;
begin
CurWebrowser := pDisp as IWebBrowser;
TopWebBrowser := (ASender as TWebBrowser).DefaultInterface;
if CurWebrowser=TopWebBrowser then
begin
button6.Click; // HTML TO MEMO
TRY
button7.Click; //SEARCH LOGIN FORM
if find=true then Begin
clickForm1(WebBrowser1, 'move'); //CLICK LOGIN BUTTON
End Else begin Null; End;
FINALLY find:=false; END;
TRY
button8.Click; //SEARCH HOME (AFTER LOGIN) FORM
if find1=true then Begin
Application.ProcessMessages;//NEEDED IN ORDER THE BUTTON TO BE PRESSED.
clickForm1(WebBrowser1, 'refresh'); //CLICK NEXT PAGE BUTTON
End;
FINALLY find1:=false;END;
TRY
button9.Click; //SEARCH WORKLIST FORM
if find2=true then Begin
clickForm1(WebBrowser1, 'next'); //CLICK NEW FORM BUTTON
End;
FINALLY find2:=false;END;
end;
end;
I'm not sure how much you know about working with Event Handlers in code.
Objects like Forms and WebBrowsers typically have one or more event properties that are used to define what happens when the event occurs. So, an event property is a property of an object that can hold the information necessary to invoke (call) a procedure (or function, but not usually) of the same object or another one. The procedure to call has to have the right "signature" for the type definition of the event. If it does then an "event handler" can be assigned to the event property in code, as I'll show below.
One can use event properties and event-handling code in Delphi in a simple way, without knowing any of this, just by going to the Events tab of the Object Inspector and double-clicking next to one of the event names. What that actually does is to create a new handler procedure and to assign it to the corresponding event property of the object (well, not quite, actually that assignment is done at run-time when the host form is loaded).
What I mean by "signature" is the routine type (procedure or function) and its list of parameters, and their types, in its definition.
So, for a WebBrowser, the signature of the OnDocumentComplete event is
procedure (Sender: TObject; const pDisp: IDispatch; var URL: OLEVariant);
The clever thing is that you can assign the OnDocumentComplete property to
any procedure of an object that has the exact same signature. The event type for the WB's OnDocumentComplete is defined in the import unit ShDocVw, btw
So, let's suppose you write three methods that contain the code you want to run
when the WB completes loading URLs A, B and C, respectively:
procedure TForm1.DocCompleteA(Sender: TObject; const
pDisp: IDispatch; var URL: OLEVariant);
begin
// Do your stuff for arrival at site/page A here
// Then update NavigationOK flag to reflect if you succeeded or failed
if NavigationOK then begin
WebBrowser1.OnDocumentComplete := DocCompleteB;
// Now navigate to site/page B
end
else
WebBrowser1.OnDocumentComplete := Nil;
end;
procedure TForm1.DocCompleteB(Sender: TObject; const
pDisp: IDispatch; var URL: OLEVariant);
begin
end;
procedure TForm1.DocCompleteC(Sender: TObject; const
pDisp: IDispatch; var URL: OLEVariant);
begin
end;
Then, you can assign the WB's OnDocumentComplete property to each of them in turn,
with something like the code at the end of DocCompleteA which updates the WB's OnDocumentComplete to the code needed for B, and so on, in turn. The NavigationOK variable is just a flag to indicate that our navigation stays "on course" as it progresses. If it gets set to false because something went wrong, we set the WB's OnDocumentComplete to Nil, so that it does nothing next time the event occurs.
Then, you can kick off the whole "tour" of sites with something like this:
procedure TForm1.NavigateSites;
begin
NavigationOK := True;
WebBrowser1.OnDocumentComplete := DocCompleteA;
WebBrowser1.Navigate(...); // Navigate to site A
end;
Of course, you don't have to do the updating of the WB's OnDocumentComplete property and navigation to the next URL in the current DocCompleteX. In fact, it's probably clearer if you do those if a higher level procedure like the NavigateSites one, and more easily maintainable, which can be important if you're navigating others' sites, which are apt to be changed without any prior warning.

How do I position a TOpenDialog

I have got a Delphi application which uses TOpenDialog to let the user select a file. By default, the open dialog is displayed centered on the current monitor which nowadays can be "miles" away from the application's window. I would like the dialog to be displayed centered on the TOpenDialog's owner control, failing that, I'd settle for the application's main window.
The following code kind of works, it is derived from TJvOpenDialog which gave me some hint on how to do it:
type
TMyOpenDialog = class(TJvOpenDialog)
private
procedure SetPosition;
protected
procedure DoFolderChange; override;
procedure WndProc(var Msg: TMessage); override;
end;
procedure TMyOpenDialog.SetPosition;
begin
var
Monitor: TMonitor;
ParentControl: TWinControl;
Res: LongBool;
begin
if (Assigned(Owner)) and (Owner is TWinControl) then
ParentControl := (Owner as TWinControl)
else if Application.MainForm <> nil then
ParentControl := Application.MainForm
else begin
// this code was already in TJvOpenDialog
Monitor := Screen.Monitors[0];
Res := SetWindowPos(ParentWnd, 0,
Monitor.Left + ((Monitor.Width - Width) div 2),
Monitor.Top + ((Monitor.Height - Height) div 3),
Width, Height,
SWP_NOACTIVATE or SWP_NOZORDER);
exit; // =>
end;
// this is new
Res := SetWindowPos(GetParent(Handle), 0,
ParentControl.Left + ((ParentControl.Width - Width) div 2),
ParentControl.Top + ((ParentControl.Height - Height) div 3),
Width, Height,
SWP_NOACTIVATE or SWP_NOZORDER);
end;
procedure TMyOpenDialog.DoFolderChange
begin
inherited DoFolderChange; // call inherited first, it sets the dialog style etc.
SetPosition;
end;
procedure TMyOpenDialog.WndProc(var Msg: TMessage);
begin
case Msg.Msg of
WM_ENTERIDLE: begin
// This has never been called in my tests, but since TJVOpenDialog
// does it I figured there may be some fringe case which requires
// SetPosition being called from here.
inherited; // call inherited first, it sets the dialog style etc.
SetPosition;
exit;
end;
end;
inherited;
end;
"kind of works" meaning that the first time the dialog is opened, it is displayed centered on the owner form. But, if I then close the dialog, move the window and open the dialog again, SetWindowPos doesn't seem to have any effect even though it does return true. The dialog gets opened at the same position as the first time.
This is with Delphi 2007 running on Windows XP, the target box is also running Windows XP.
The behaviour you describe I can reproduce only by passing a bogus value for the OwnerHwnd to the dialog's Execute method.
This window handle is then passed on to the underlying Windows common control and in fact you will have other problems with your dialogs if you do not set it to the handle of the active form when the dialog is shown.
For example when I call Execute and pass Application.Handle, the dialog always appears on the same window, in a rather bizarre location, irrespective of where my main form is.
When I call Execute and pass the handle to my main form, the dialog appears on top of the main form, slightly shifted to the right and down. This is true no matter which monitor the form is on.
I am using Delphi 2010 and I don't know whether or not you have the overloaded version of Execute available on your version of Delphi. Even if you don't have that available, you should still be able to create a derived class that will pass a more sensible value for OwnerHwnd.
Although I don't have conclusive 100% evidence that this is your problem, I think that this observation will lead you to a satisfactory resolution.
TJvOpenDialog is a descendant of TOpenDialog, hence you should run your placement call after the VCL centers the dialog. The VCL does it in response to a CDN_INITDONE notification. Responding to a WM_SHOWWINDOW message is too early, and in my tests the window procedure never receives a WM_ENTERIDLE message.
uses
commdlg;
[...]
procedure TJvOpenDialog.DoFolderChange;
begin
inherited DoFolderChange;
// SetPosition; // shouldn't be needing this, only place the dialog once
end;
procedure TJvOpenDialog.WndProc(var Msg: TMessage);
begin
case Msg.Msg of
WM_NOTIFY: begin
if POFNotify(Msg.LParam)^.hdr.code = CDN_INITDONE then begin
inherited; // VCL centers the dialog here
SetPosition; // we don't like it ;)
Exit;
end;
end;
inherited;
end;
or,
procedure TJvOpenDialog.WndProc(var Msg: TMessage);
begin
case Msg.Msg of
WM_NOTIFY: if POFNotify(Msg.LParam)^.hdr.code = CDN_INITDONE then
Exit;
end;
inherited;
end;
to have the dialog where the OS puts it, it actually makes sense.
I tried both examples without success ... but here is a symple solution:
type
TPThread = class(TThread)
private
Title : string;
XPos,YPos : integer;
protected
procedure Execute; override;
end;
TODialogPos = class(Dialogs.TOpenDialog)
private
Pt : TPThread;
public
function Execute(X,Y : integer):boolean; reintroduce;
end;
TSDialogPos = class(Dialogs.TSaveDialog)
private
Pt : TPThread;
public
function Execute(X,Y : integer):boolean; reintroduce;
end;
implementation
procedure TPThread.Execute;
var ODhandle : THandle; dlgRect : TRect;
begin
ODhandle:= FindWindow(nil, PChar(Title));
while (ODhandle = 0) do ODhandle:= FindWindow(nil, PChar(Title));
if ODhandle <> 0 then begin
GetWindowRect(ODhandle, dlgRect);
with dlgRect do begin
XPos:=XPos-(Right-Left) div 2;
YPos:=YPos-(Bottom-Top) div 2;
MoveWindow(ODhandle, XPos, YPos,Right-Left,Bottom-Top,True);
SetWindowPos(ODhandle, HWND_TOP, XPos, YPos, 0, 0, SWP_NOSIZE);
end
end;
DoTerminate;
end;
function TODialogPos.Execute(X,Y : integer):boolean;
begin
Pt:= TPThread.Create(False);
Pt.XPos := X;
Pt.YPos := Y;
if Self.Title <> '' then
Pt.Title := Self.Title
else begin
Self.Title := 'Open';
Pt.Title := Self.Title;
end;
Result:= inherited Execute;
Pt.Free;
end;
function TSDialogPos.Execute(X,Y : integer):boolean;
begin
Pt:= TPThread.Create(False);
Pt.XPos := X;
Pt.YPos := Y;
if Self.Title <> '' then
Pt.Title := Self.Title
else begin
Self.Title := 'Save';
Pt.Title := Self.Title;
end;
Result:= inherited Execute;
Pt.Free;
end;
...
Use it like (for example center Save Dilaog in Form1) the following code:
type
TForm1 = class(TForm)
...
...
dlgSave:=TSDialogPos.Create(self);
dlgSave.Filter := 'Symbol File (*.asy)|*.asy';
dlgSave.Options:=[ofHideReadOnly,ofExtensionDifferent,ofPathMustExist,
ofCreatePrompt,ofNoTestFileCreate,ofNoNetworkButton,
ofOldStyleDialog,ofEnableIncludeNotify,ofEnableSizing];
...
with dlgSave do begin
Title :='Copy : [ *.asy ] with Attributes';
InitialDir:= DirectoryList.Directory;
FileName:='*.asy';
end;
...
with Form1 do
if dlgSave.Execute(Left+Width div 2, Top+Height div 2) then begin
// your code
end;
...
dlgSave.Free
...

Close Delphi dialog after [x] seconds

Is it possible to get Delphi to close a ShowMessage or MessageDlg Dialog after a certain length of time?
I want to show a message to the user when the application is shut down, but do not want to stop the application from shutting down for more than 10 seconds or so.
Can I get the default dialog to close after a defined time, or will I need to write my own form?
Your application is actually still working while a modal dialog or system message box or similar is active (or while a menu is open), it's just that a secondary message loop is running which processes all messages - all messages sent or posted to it, and it will synthesize (and process) WM_TIMER and WM_PAINT messages when necessary as well.
So there's no need to create a thread or jump through any other hoops, you simply need to schedule the code that closes the message box to be run after those 10 seconds have elapsed. A simple way to do that is to call SetTimer() without a target HWND, but a callback function:
procedure CloseMessageBox(AWnd: HWND; AMsg: UINT; AIDEvent: UINT_PTR;
ATicks: DWORD); stdcall;
var
Wnd: HWND;
begin
KillTimer(AWnd, AIDEvent);
// active window of the calling thread should be the message box
Wnd := GetActiveWindow;
if IsWindow(Wnd) then
PostMessage(Wnd, WM_CLOSE, 0, 0);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
TimerId: UINT_PTR;
begin
TimerId := SetTimer(0, 0, 10 * 1000, #CloseMessageBox);
Application.MessageBox('Will auto-close after 10 seconds...', nil);
// prevent timer callback if user already closed the message box
KillTimer(0, TimerId);
end;
Error handling ommitted, but this should get you started.
You can try to do it with a standard Message dialog. Create the dialog with CreateMessageDialog procedure from Dialogs and after add the controls that you need.
In a form with a TButton define onClick with this:
procedure TForm1.Button1Click(Sender: TObject);
var
tim:TTimer;
begin
// create the message
AMsgDialog := CreateMessageDialog('This is a test message.',mtWarning, [mbYes, mbNo]) ;
lbl := TLabel.Create(AMsgDialog) ;
tim := TTimer.Create(AMsgDialog);
counter := 0;
// Define and adding components
with AMsgDialog do
try
Caption := 'Dialog Title' ;
Height := 169;
// Label
lbl.Parent := AMsgDialog;
lbl.Caption := 'Counting...';
lbl.Top := 121;
lbl.Left := 8;
// Timer
tim.Interval := 400;
tim.OnTimer := myOnTimer;
tim.Enabled := true;
// result of Dialog
if (ShowModal = ID_YES) then begin
Button1.Caption := 'Press YES';
end
else begin
Button1.Caption := 'Press NO';
end;
finally
Free;
end;
end;
An the OnTimer property like this:
procedure TForm1.MyOnTimer(Sender: TObject);
begin
inc(counter);
lbl.Caption := 'Counting: ' + IntToStr(counter);
if (counter >= 5) then begin
AMsgDialog.Close;
end;
end;
Define the variables and procedure:
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
AMsgDialog: TForm;
lbl:TLabel;
counter:integer;
procedure MyOnTimer(Sender: TObject);
end;
And test it.
The form close automatically when the timer final the CountDown. Similar this you can add other type of components.
Regards.
Try this:
function MessageBoxTimeOut(hWnd: HWND; lpText: PChar; lpCaption: PChar;
uType: UINT; wLanguageId: WORD; dwMilliseconds: DWORD): integer;
stdcall; external user32 name 'MessageBoxTimeoutA';
I've been using this for quite some time; it works a treat.
OK. You have 2 choices:
1 - You can create your own MessageDialog form. Then, you can use it and add a TTimer that will close the form when you want.
2 - You can keep using showmessage and create a thread that will use FindWindow (to find the messadialog window) and then close it.
I recommend you to use you own Form with a timer on it. Its cleaner and easier.
This works fine with windows 98 and newers...
I don't use the " MessageBoxTimeOut" because old windows 98, ME, doesn't have it...
this new function works like a "CHARM"..
//add this procedure
procedure DialogBoxAutoClose(const ACaption, APrompt: string; DuracaoEmSegundos: Integer);
var
Form: TForm;
Prompt: TLabel;
DialogUnits: TPoint;
ButtonTop, ButtonWidth, ButtonHeight: Integer;
nX, Lines: Integer;
function GetAveCharSize(Canvas: TCanvas): TPoint;
var
I: Integer;
Buffer: array[0..51] of Char;
begin
for I := 0 to 25 do Buffer[I] := Chr(I + Ord('A'));
for I := 0 to 25 do Buffer[I + 26] := Chr(I + Ord('a'));
GetTextExtentPoint(Canvas.Handle, Buffer, 52, TSize(Result));
Result.X := Result.X div 52;
end;
begin
Form := TForm.Create(Application);
Lines := 0;
For nX := 1 to Length(APrompt) do
if APrompt[nX]=#13 then Inc(Lines);
with Form do
try
Font.Name:='Arial'; //mcg
Font.Size:=10; //mcg
Font.Style:=[fsBold];
Canvas.Font := Font;
DialogUnits := GetAveCharSize(Canvas);
//BorderStyle := bsDialog;
BorderStyle := bsToolWindow;
FormStyle := fsStayOnTop;
BorderIcons := [];
Caption := ACaption;
ClientWidth := MulDiv(Screen.Width div 4, DialogUnits.X, 4);
ClientHeight := MulDiv(23 + (Lines*10), DialogUnits.Y, 8);
Position := poScreenCenter;
Prompt := TLabel.Create(Form);
with Prompt do
begin
Parent := Form;
AutoSize := True;
Left := MulDiv(8, DialogUnits.X, 4);
Top := MulDiv(8, DialogUnits.Y, 8);
Caption := APrompt;
end;
Form.Width:=Prompt.Width+Prompt.Left+50; //mcg fix
Show;
Application.ProcessMessages;
finally
Sleep(DuracaoEmSegundos*1000);
Form.Free;
end;
end;
////////////////////////////How Call It//////////////////
DialogBoxAutoClose('Alert'', "This message will be closed in 10 seconds',10);
/////////////////////////////////////////////////////////
MessageBox calls this function internally and pass 0xFFFFFFFF as timeout parameter, so the probability of it being removed is minimal (thanks to Maurizio for that)
I thought about using a separate thread, but it's probably going to get you into a lot of unnecessary code etc. Windows dialogs were simply not made for this thing.
You should do your own form. On the good side, you can have custom code/UI with a countdown like timed dialog boxes do.
No. ShowMessage and MessageDlg are both modal windows, which means that your application is basically suspended while they're displayed.
You can design your own replacement dialog that has a timer on it. In the FormShow event, enable the timer, and in the FormClose event disable it. In the OnTimer event, disable the timer and then close the form itself.
You can hook up the Screen.OnActiveFormChange event and use Screen.ActiveCustomForm if it is a interested form that you want to hook up the timer to close it
{code}
procedure abz.ActiveFormChange(Sender: TObject);
var
Timer: TTimer;
begin
if (Screen.ActiveCutomForm <> nil) and //valid form
(Screen.ActiveCutomForm.Tag = 0) and //not attached a timer yet
(Screen.ActiveCutomForm.ClassName = 'TMessageForm') //any interested form type check
then
begin
Timer := TTimer.Create(Screen.ActiveCutomForm); // let the form owned so it will be freed
Timer.Enabled := False;
Timer.Tag := Integer(Screen.ActiveCutomForm); // keep track to be used in timer event
.... setup any timer interval + event
Screen.ActiveCutomForm.Tag := Integer(Timer);
Timer.Enabled := True;
end;
end;
{code}
enjoy
Best way is to use a stayontop form and manage a counter to disappear using the alfpha blend property of the form, at the end of the count just close the form, but
the control will be passed to the active control needed before showing the form, this way, user will have a message which disappears automatically and wont prevent the usage of the next feature, very cool trick for me.
You can do this with WTSSendMessage.
You can find this in the JWA libraries, or call it yourself.

Resources