CnPackTip#5
elphi 中的 Idle
Written by SkyJacker
http://www.cnpack.org
CnPack IV QQ Group: 130970
Thx: Passion
2007-3-16
在学习 ActionList 时, 了解了 Delphi 中的 Idle 。
得出了以下几点认识:
1、Action 是由 Applicaton 内部处理的。
2、Delphi 不是通过处理 WM_IDLE 消息进入 Idle 处理的。
3、Application 本身的意外处理函数也可能产生意外。
4、也许世上本无 WM_IDLE。
TApplication.Run; 内部处理了 Idle 过程和Action。
处理的顺序是:
1、首先处理Hint
2、用户也可以自定义Idle,如果用户定义了,处理用户定义的。
3、处理 action Idle
因此,如果在用户的 Idle 中这样写,会禁止 Action 的 Update。
procedure TFrmTest.MyIdle(Sender: TObject; var Done: Boolean);
begin
Done := false; // 禁止 Action 的 Update
end;
相关 Vcl 如下:
跟随 TApplication 的运行过程, 查看 action 的 Update 如何被调用.
1、
procedure TApplication.Run;
begin
FRunning := True;
try
AddExitProc(DoneApplication);
if FMainForm <> nil then
begin
case CmdShow of
SW_SHOWMINNOACTIVE: FMainForm.FWindowState := wsMinimized;
SW_SHOWMAXIMIZED: MainForm.WindowState := wsMaximized;
end;
if FShowMainForm then
if FMainForm.FWindowState = wsMinimized then
Minimize else
FMainForm.Visible := True;
repeat // 每个 Application 都是在不断的循环着
try
HandleMessage; // 进入消息循环处理
except
HandleException(Self); // 意外处理,顺便看一下
end;
until Terminated;
end;
finally
FRunning := False;
end;
end;
// Application 默认的意外处理,看看也挺有意思
procedure TApplication.HandleException(Sender: TObject);
begin
if GetCapture <> 0 then SendMessage(GetCapture, WM_CANCELMODE, 0, 0);
// 返回捕获当前鼠标的窗口,
// The WM_CANCELMODE message is sent to the focus window when a dialog box or message box is displayed;
// this enables the focus window to cancel modes, such as mouse capture.
if ExceptObject is Exception then
begin
if not (ExceptObject is EAbort) then // 疑问一:如果是 EAbort, 那么怎么办? 交给了操作系统?
if Assigned(FOnException) then // 处理用户自定义的 OnException
FOnException(Sender, Exception(ExceptObject))
else
ShowException(Exception(ExceptObject)); // 显示意外信息 E.Message
end else
SysUtils.ShowException(ExceptObject, ExceptAddr);
end;
疑问二:
procedure TApplication.ShowException(E: Exception);
var
Msg: string;
begin
Msg := E.Message;
if (Msg <> '') and (AnsiLastChar(Msg) > '.') then Msg := Msg + '.';
// 在消息的末尾加个 '.' ,但是太不严格了吧。因为在 '.' 的前面还有其他常见的 ascii 字符。
// 这一句到底要实现什么功能呢?谁能告诉我。
// 直接将这一句注释掉,应该更好。或者干脆写 msg = '未知 E.Message';
// 为什么注释掉更好,继续往下看就会更明白了。
MessageBox(PChar(Msg), PChar(GetTitle), MB_OK + MB_ICONSTOP);
end;
AnsiLastChar 返回的是指向字符串最后一个字符的指针。
那么 (AnsiLastChar(Msg) > '.') 可以这样写吗?一个指针和一个字符的比较。
测试发现: 如果 一个 PChar 与 一个字符比较, Delphi 会自动将 PChar 转为 PChar^。
有意思吧。
再来 AnsiLastChar 源码
function AnsiLastChar(const S: string): PChar;
var
LastByte: Integer;
begin
LastByte := Length(S);
if LastByte <> 0 then
begin
while ByteType(S, LastByte) = mbTrailByte do Dec(LastByte);
Result := @S[LastByte];
end
else
Result := nil; // 如果 S = '' ,则返回 nil
end;
注意了,如果使用 AnsiLastChar 要注意空指针的问题:
比如:
Msg := '';
if (AnsiLastChar(Msg) >= '.') then
将会出现访问非法内存 $00000000 的错误。
再返回来看
Msg := E.Message;
if (Msg <> '') and (AnsiLastChar(Msg) > '.') then Msg := Msg + '.';
也就是说, Delphi 并不能保证 E.Message 不为空。因此,加了 (Msg <> '') 的条件。
问题又来了,既然不能保证 E.Message 不为空,又因为 if A and B 这种条件会因为编译条件的不同而产生不同的结果。
因此,将编译选项改为完全计算,在Compiler Options 对话框中选择Complete Boolean Evaluation 选项,
那么,"Read of Address 00000000" 应该是意外处理中的意外了。
2、
procedure TApplication.HandleMessage;
var
Msg: TMsg;
begin
if not ProcessMessage(Msg) then // 如果没有获得任何消息,进入自定义过程 Idle
Idle(Msg);
end;
3、
procedure TApplication.Idle(const Msg: TMsg);
var
Control: TControl;
Done: Boolean;
begin
Control := DoMouseIdle;
if FShowHint and (FMouseControl = nil) then
CancelHint;
Application.Hint := GetLongHint(GetHint(Control));
Done := True;
try
if Assigned(FOnIdle) then FOnIdle(Self, Done); // 用户自定义 Idle
if Done then DoActionIdle; // 如果 Done = true , 处理 action Idle
except
HandleException(Self);
end;
if (GetCurrentThreadID = MainThreadID) and CheckSynchronize then
Done := False;
if Done then WaitMessage;
end;
4、
action相关
procedure TApplication.DoActionIdle;
var
I: Integer;
begin
for I := 0 to Screen.CustomFormCount - 1 do
with Screen.CustomForms[I] do
if HandleAllocated and IsWindowVisible(Handle) and
IsWindowEnabled(Handle) then
UpdateActions;
// 满足3个条件,才会处理 action:
//可视对象已经被创建、可视的、使能的
end;
procedure TCustomForm.UpdateActions;
var
I: Integer;
procedure TraverseClients(Container: TWinControl);
var
I: Integer;
Control: TControl;
begin
if Container.Showing then
for I := 0 to Container.ControlCount - 1 do
begin
Control := Container.Controls[I];
if (csActionClient in Control.ControlStyle) and Control.Visible then
Control.InitiateAction;
if Control is TWinControl then
TraverseClients(TWinControl(Control));
end;
end;
begin
if (csDesigning in ComponentState) or not Showing then Exit; // 设计态或者非显示则退出
{ Update form }
InitiateAction; // 执行 action 的 Update
{ Update main menu's top-most items }
if Menu <> nil then
for I := 0 to Menu.Items.Count - 1 do // 更新顶级菜单
with Menu.Items[I] do
if Visible then InitiateAction;
{ Update any controls }
TraverseClients(Self); // 执行控件关联的 action Update
end;
InitiateAction 调用:
procedure TControl.InitiateAction;
begin
if ActionLink <> nil then ActionLink.Update;
end;
function TBasicAction.Update: Boolean;
begin
if Assigned(FOnUpdate) then
begin
FOnUpdate(Self);
Result := True;
end
else Result := False;
end;
5、
消息处理
function TApplication.ProcessMessage(var Msg: TMsg): Boolean;
var
Handled: Boolean;
begin
Result := False;
if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then
begin
Result := True;
if Msg.Message <> WM_QUIT then
begin
Handled := False;
if Assigned(FOnMessage) then FOnMessage(Msg, Handled);
if not IsHintMsg(Msg) and not Handled and not IsMDIMsg(Msg) and
not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
end
else
FTerminate := True;
end;
end;
如果 PeekMessage 不到消息,就按 Delphi 的idle。
如果给 Application 不断发 WM_IDLE,除非用户在 Application 自定义处理 WM_IDLE 消息,
否则Delphi Appliction 不会处理。
也就是说 Delphi IDLE 就是无消息时候的处理,没有什么特别的。
如果有一点消息,就不会触发 idle,比如鼠标点击不放,idle 即 action update 也不会执行了。
在 Delphi Help 中解释空闲的定义是“应用程序不执行任何代码时为空闲”,
其实改为无应用程序要处理的消息时,应该更容易理解。
本来想测试一次 Applcaction 接收 Windows在消息队列空闲时发出的WM_IDLE 消息,
不过在 Messages.pas 居然没有 WM_IDLE 的定义. 谁能告诉我 WM_IDLE 的序号是多少?
关于 WM_IDLE 的一些资料:
[1] http://www.microsoft.com/msj/0797/c0797.aspx 它是用 MFC 说明,其中有一句:
"The WM_IDLE message will not be processed for modal dialogs; that is, when a DoModal is executing. There is, however, a MFC "private" message that is somewhat documented. It is the the WM_KICKIDLE message. You must add an entry to the message map for the dialog class header. "
如果 Delphi 使用 WM_IDLE 是不是也要考虑 ShowModal 的问题?
[2] http://www.tutorials-ne.com/ui/Drag-Drop-62308/
WM_IDLE
If you do a search on the web for the WM_IDLE windows message there will be
many hits. According to many sites, the WM_IDLE message is generated by the
operating system itself when the application's message queue is empty.
However, I couldn't find WM_IDLE anywhere within MSDN. Is this message indeed
defined by Windows? And if so, would it be possible for me to hook it?
WM_IDLE 在哪里?
[3] http://www.cs.sjsu.edu/faculty/beeson/courses/cs130/LectureNotes/2-HowWindowsWorks/2-HowWindowsWorks.html
This loop executes until there are no more messages in the message queue, and then it terminates.
Premature termination is prevented by WM_IDLE messages
that the operating system generates when nothing else is happening.
什么叫 "operating system generates when nothing else is happening" ?
也许世上本无 WM_IDLE。 可能都是自己定义并发出来的。
http://www.pudn.com/view/downloads64/sourcecode/windows/directx/224349/birdeye_view.dpr__.htm
while (TRUE) do
begin
if PeekMessage(uMsg, 0, 0, 0, PM_NOREMOVE) then
begin
if not GetMessage(uMsg, 0, 0, 0) then
break;
TranslateMessage(uMsg);
DispatchMessage(uMsg);
end
else
begin
PostMessage(hWindow, WM_IDLE, 0, 0);
WaitMessage;
end;
end;