在Delphi中如何查找消息发送的位置?

3
我使用的程序中有一个第三方树形包(LMD Innovative的ElXTree)。我将其作为网格来使用。每当我选择单元格时,该行会获得焦点并突出显示,这是我想要的效果。
当我点击网格中的单元格并调用提供的 Inplace 编辑器时,该行会获得焦点。由于单元格处于编辑模式,因此仅有的突出显示是单元格而非整个行,这也是我想要的效果。
但当我正在对一个单元格进行 Inplace 编辑时,如果我点击另一个单元格调用 Inplace 编辑器,则先前单元格所在的行会被赋予焦点和突出显示,然后该行焦点立即被设为失去且不再突出显示,而新单元格所在的行则会被赋予焦点和突出显示。然后,新行立即变为失去焦点且不再突出显示,除了正在进行 Inplace 编辑的单元格。这会导致令人烦恼的双重闪烁,我想摆脱它。
我拥有这个程序包的源代码,我一直在调试它。我相信,如果我能找到是哪个部分引起了双重聚焦,我就能够弄清楚如何进行简单的修改,以防止它。
当我设置断点时,我发现自己在 Forms 单元中的 TApplication.Run 的消息处理循环中。该循环正在处理许多消息,其中包括设置焦点的两条消息。通过逐行跟踪程序代码,我可以一直跟踪到 Classes 单元中的 StdWndProc,消息在那里被分派。我拥有关于消息(Handle、参数等)的所有信息。
我没有并且不知道消息从哪里开始发送。调用堆栈中没有 ElXTree 单元来提示我。其中一个例程必须在当前调用堆栈之外独立发送了消息。
如果我能找出是哪个例程发送了该消息(即谁发送了它),那么我就可以解决这个问题。
有没有办法找出消息从哪里开始发送的呢?或者说,是否有其他方法可以处理我遇到的这个双重聚焦问题?
供参考,我使用的是 Delphi 2009。
进一步信息:
ElXTree 有几十个自己的 Windows 消息可以使用。在我的情况下,与此相关的两个是:
procedure WMSetFocus(var Msg: TWMSetFocus); message WM_SETFOCUS;
procedure WMKillFocus(var Msg: TWMKillFocus); message WM_KILLFOCUS;

procedure TElXTreeView.WMSetFocus(var Msg: TWMSetFocus);  { private }
begin
  inherited;
  FHasFocus := True;
  if (FOwner.HideSelection or (FOwner.HideSelectColor <> FOwner.FocusedSelectColor) or (FOwner.HideSelectTextColor <> FOwner.FocusedSelectTextColor)) and
     (FOwner.Items.Count > 0) then
    Invalidate;
  with FOwner do
    if Flat or FUseCustomScrollBars or IsThemed then
      UpdateFrame;
end;  { WMSetFocus }

procedure TElXTreeView.WMKillFocus(var Msg: TWMKillFocus);  { private }

begin
  FMouseSel := False;
  FPressed := False;
  FHasFocus := False;
  inherited;
  FHintItemEx := nil;
  DoHideLineHint;

  if HandleAllocated then
  begin
    with FOwner do
      if Flat or FUseCustomScrollBars or IsThemed then
      begin
        UpdateFrame;
        DrawFlatBorder(False, False);
        if FUseCustomScrollBars then
        begin
          HScrollBar.HideHint;
          VScrollBar.HideHint;
        end;
      end;
    if (FOwner.HideSelection or (FOwner.HideSelectColor <> FOwner.FocusedSelectColor) or (FOwner.HideSelectTextColor <> FOwner.FocusedSelectTextColor)) and
       (FOwner.Items.Count > 0) then
      Invalidate;
  end;
end;  { WMKillFocus }

当我在WMSetFocus例程中设置断点时,我得到以下调用堆栈: 调用堆栈 调用堆栈中仅有的另一个ElXTree例程在第四行。
procedure TElXTreeView.WndProc(var Message: TMessage);
var P1: TPoint;
    Item: TElXTreeItem;
    HCol: Integer;
    IP: TSTXItemPart;
begin
  if (FHintItem <> nil) and (FOwner.FHideHintOnMove) then
  begin
    if ((Message.Msg >= WM_MOUSEMOVE) and (Message.Msg <= WM_MOUSELAST)) or (Message.Msg = WM_NCMOUSEMOVE) then
    begin
      GetCursorPos(P1);
      P1 := ScreenToClient(P1);
      Item := GetItemAt(P1.X, P1.Y, IP, HCol);
      if Item <> FHintItem then
         DoHideLineHint;
      inherited;
      Exit;
    end
    else
    if
      ((Message.Msg >= WM_KEYFIRST) and (Message.Msg <= WM_KEYLAST)) or
      ((Message.Msg = CM_ACTIVATE) or (Message.Msg = CM_DEACTIVATE)) or
      (Message.Msg = CM_APPKEYDOWN) or (Message.Msg = CM_APPSYSCOMMAND) or
      (Message.Msg = WM_COMMAND) or
      ((Message.Msg > WM_MOUSEMOVE) and (Message.Msg <= WM_MOUSELAST))
      or (Message.Msg = WM_NCMOUSEMOVE) then
      DoHideLineHint;
  end;
  if (FHintItem <> nil) and ((Message.Msg = CM_ACTIVATE) or (Message.Msg = CM_DEACTIVATE))
    or (Message.Msg = WM_NCMOUSEMOVE) then
    DoHideLineHint;
  inherited;
end;

当我在这个例程中设置一个断点时,它似乎只通过“inherited”行并调用系统函数,最终到达StdWndProc来处理消息(正如我在原始问题中所描述的)。
追踪这个问题的准确性在于,我必须在调试代码的同时,在程序中进行鼠标点击并将鼠标指针保持在视觉控件上。如果在调试过程中移动或使用鼠标时出现错误,可能会导致额外的鼠标事件影响处理。 这使得调试变得非常困难。
但是我可以仔细地跟踪StdWndProc,并查看被发送的聚焦行的事件。 我似乎无法找到发出消息的问题所在。
那么,为什么我不知道是什么发出了消息? 嗯,我假设这是从PostMessage或SendMessage命令中发出的,正如David所说。 当我寻找ElXTree中所有这些调用的位置时,我只找到了这10个:
Result := SendMessage(hWnd, SBM_SetScrollInfo, Integer(Redraw), Integer(@ScrollInfo));

SendMessage(hWnd, SBM_GetScrollInfo, 0, Integer(@ScrollInfo));

SendMessage(FHScrollBar.Handle, Message.Msg, Message.wParam, Message.lParam);
SendMessage(FVScrollBar.Handle, Message.Msg, Message.wParam, Message.lParam);

case Key of
  VK_LEFT: begin
    PostMessage(FOwner.Handle, WM_HSCROLL, SB_LINELEFT, 0);
    Exit;
  end;
  VK_RIGHT: begin
    PostMessage(FOwner.Handle, WM_HSCROLL, SB_LINERIGHT, 0);
    Exit;
  end;
end;

FScrollbarsInitialized := True;
if UseCustomScrollbars then
  PostMessage(Handle, WM_UPDATESBFRAME, 0, 0);
end;

procedure TCustomElXTree.WMSysColorChange(var Msg: TWMSysColorChange);
begin
  inherited;

  PostMessage(FVScrollBar.Handle, Msg.Msg,  TMessage(Msg).WParam,  TMessage(Msg).LParam);
  PostMessage(FHScrollBar.Handle, Msg.Msg,  TMessage(Msg).WParam,  TMessage(Msg).LParam);
  PostMessage(FHeader.Handle, Msg.Msg,  TMessage(Msg).WParam,  TMessage(Msg).LParam);

end; { WMSysColorChange }

前7个涉及滚动条。接下来的3个与ColorChange有关。
我已经查看了所有其他LMD组件例程,以寻找消息发出的方式,但是那里没有任何有希望的东西。
所以我仍然卡住了,需要提示或线索,以找到请求将行聚焦的消息的发送者。
解决方法:
好吧,一旦我意识到Windows正在启动鼠标事件,我就能做些事情来停止大部分闪烁。不过这是一个真正的hack。如果有人知道更好的方法,我会很乐意听取建议。
在TElXTreeView.WndProc中,我用以下语句替换了继承语句:
  if (Message.Msg = WM_SETFOCUS) or (Message.Msg = WM_KILLFOCUS) then begin
      FOwner.Items.BeginUpdate;
      inherited;
      FOwner.Items.EndUpdate;
  end
  else
    inherited;

这样做可以防止在被调用的程序中多次发生聚焦。

它的功能除了一个例外:当我点击可编辑条目时,它仍然会先突出显示该条目,然后再进入编辑模式。这是因为突出显示发生在MouseDown事件上,而进入编辑模式发生在MouseUp事件上。我可能能够找到一种方法解决这个问题,但最初的尝试都没有成功。虽然如此,这还不如双重闪烁那么糟糕,如果必须这样我也能接受。

感谢你们帮助激活我的思维。采纳的答案归功于David他给了我关键线索。


也许我说得太早了。我发现其他一些控件(例如包含网格的页面)在控件之间进行分页时不会更新。我尝试在EndUpdate之后添加Refresh命令。一旦我这样做了,又出现了双重闪烁。这是一个真正混乱的问题。

我可能能够找到分页的解决方法,但我希望那个控件的开发人员能够回应我并提供更好的修复方法。

编程中这样的事情绝非乐趣所在。:-(


如果您有源代码,请在消息标识符(WM_Whatever)上执行grep,为执行/发送它的例程添加日志记录;为OnFocus*事件添加事件处理程序并在那里添加日志记录。这样应该可以帮助您缩小范围。 - Marjan Venema
需要在调试或运行时执行吗? - PA.
@Marjan 执行/发送不要通过TApplication.Run。 - David Heffernan
@David: 完全正确,Perform和SendMessage将在堆栈中可见。这里的“performing/sending”并不是指方法引用,而更多是指“触发”。 - Marjan Venema
@Ken:谢谢。在@David的回答下面看到“WM_SETFOCUS和WM_KILLFOCUS是由Windows发送的”后,我意识到了这一点。我的错误假设是LMD例程正在捕获鼠标点击并将这些事件转发。现在我正在TElXTreeView.WndProc中工作,并尝试找到所需的解决方法。 - lkessler
显示剩余4条评论
1个回答

3
这些信息是发布到消息队列而不是同步发送的。这很清楚,因为您将它们追溯到TApplication.Run,这是泵送您的主线程消息队列的例程。这就是为什么您在堆栈上看不到调用站点的原因。它们是通过对PostMessage的调用生成的,可能是在第三方组件中或更可能是由Windows生成。
我不了解这些组件,所以我怀疑我不能帮助解决您的问题。我认为您应该联系组件供应商,他们应该知道该怎么做。

谢谢David。我确实明白它们为什么不在堆栈上,但我正在寻找追踪它们被调用的方式。如果我能找到这个信息,解决方案应该很简单。我现在已经联系了供应商,并正在等待回复,但看起来这是由ELDOS开发的相当古老的代码,LMD从中购买了ElPack公司。我落后了几个版本,但他们的更改日志没有指示任何修复此问题的内容。 - lkessler
你能否像我建议的那样通过搜索PostMessage调用来找到这些调用吗? - David Heffernan
1
@lkessler WM_SETFOCUS和WM_KILLFOCUS是Windows响应键盘焦点变化而发送的消息。这种焦点变化可能是由用户操作或调用Windows.SetFocus引起的。 - David Heffernan
@David:这可能是关键。我一直在寻找发出“Focus”消息的例程,但实际上它可能是Windows本身。看来我需要在TElXTreeView.WndProc过程中拦截这两个事件,并确定何时需要覆盖这些消息的完成。谢谢。我会尝试一下。 - lkessler
@lkessler 你有向组件供应商询问如何做这件事吗? - David Heffernan
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接