【问题标题】:TEdit not redrawing correctly with Invalidate in Delphi 5TEdit 在 Delphi 5 中使用 Invalidate 无法正确重绘
【发布时间】:2014-05-03 21:04:07
【问题描述】:

在使用 Cirtix 时,Delphi 5 中的 TScrollBox 存在问题,在某些系统上,当用户通过单击滚动条末端顶部或底部的按钮进行滚动时,整个应用程序会冻结。我们最初在 QucikReports 预览中遇到了这个问题,并通过在 TScrollBox 中实现我们自己的滚动条来解决它。

我们现在有一个使用 TScrollBox 的定制工作,并且客户端报告了一个类似的问题,所以我正在以相同的方式解决它。我隐藏了 TScrollBox 滚动条并添加了我自己的。单击这些时,我会调用以下内容。

请注意,此测试代码当前未在 Citrix 中运行,我已在 XP 和 Window 7 上进行了测试。

我正在关闭控件的重绘,移动所有子控件,然后重新打开绘图并调用 Invalidate。我希望 invalidate 可以完全重绘控件,但这并没有发生。

procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
  I: Integer;
begin
  if (x = 0) and (y = 0) then
    Exit;

  // Stop the control from repaining while we're updating it
  try
    SendMessage(FScrollBox.Handle, WM_SETREDRAW, 0, 0);

    for I := 0 to FScrollBox.ControlCount - 1 do
    begin
      if (FScrollBox.Controls[I] = FVScrollBar) or (FScrollBox.Controls[I] = FHScrollBar) then
        Continue;

      FScrollBox.Controls[I].Left := FScrollBox.Controls[I].Left + x;
      FScrollBox.Controls[I].Top := FScrollBox.Controls[I].Top + y;
    end;

  finally
    // Turn on painting again
    SendMessage(FScrollBox.Handle, WM_SETREDRAW, 1, 0);
  end;
  // Redraw everything
  InvalidateEverything(FScrollBox);
end;

重绘控件的代码

procedure TScrollBoxScrollReplacement.InvalidateEverything(AControl: TControl);
var
  I: Integer;
begin
  AControl.Invalidate();

  if (AControl is TWinControl) then
    for I := 0 to TWinControl(AControl).ControlCount - 1 do
      InvalidateEverything(TWinControl(AControl).Controls[I]);
end;

我添加了 Invalidate、Refresh 和 Reapint 并循环遍历所有子控件以使其正常工作,但仍然没有运气。编辑框如下所示:

如果我将 Visible 设置为 false 并返回 true,那么它们将正确重绘,但显然会出现可怕的闪烁。如果我最小化最大化窗口,或者将其拖到屏幕上,它们也会正确重绘。

任何帮助将不胜感激。

编辑:关于答案的一些信息。

寻找解决方案的用户,我建议您尝试两者。大卫和塞尔塔克的。根据 Microsoft 的文档,David 看起来是正确的解决方案。但是,使用Delphi的滚动框,直接放在滚动框里的标签会闪烁,哪里是放在滚动框内的groupbox里的标签,却是完美的平滑。我认为这可能是所有不来自 TWinControl 的组件的问题。使用 David 的解决方案,滚动本身更流畅,但使用 WM_SETREDRAW 和 RedrawWindow 时滑动更少。我本来希望同时接受这两个答案,因为它们都有各自的优点和缺点。

编辑:下面整个班级的代码 要进行测试,只需在表单中添加一个带有一些控件的滚动框并调用

TScrollBoxScrollReplacement.Create(ScrollBox1);

.

unit ScrollBoxScrollReplacement;

interface

uses extctrls, stdctrls, SpScrollBox, forms, Controls, classes, Messages, Windows, Sysutils, Math;

type
  TScrollBoxScrollReplacement = class(TComponent)
  private
    FLastVScrollPos: Integer;
    FLastHScrollPos: Integer;
    FScrollBox: TScrollBox;
    FVScrollBar: TScrollBar;
    FHScrollBar: TScrollBar;
    FVScrollBarVisible: Boolean;
    FHScrollBarVisible: Boolean;
    FCornerPanel: TPanel;
    FMaxRight: Integer;
    FMaxBottom: Integer;
    FOriginalResizeEvent: TNotifyEvent;
    FOriginalCanResizeEvent: TCanResizeEvent;
    FInScroll: Boolean;
    function GetHScrollHeight: Integer;
    function GetVScrollWidth: Integer;
    procedure ReplaceScrollBars;
    function SetUpScrollBar(AControlScrollBar: TControlScrollBar; AKind: TScrollBarKind): TScrollBar;
    procedure ScrollBoxResize(Sender: TObject);
    procedure ScrollBarEnter(Sender: TObject);
    procedure PositionScrollBars;
    procedure Scroll(Sender: TObject; ScrollCode: TScrollCode; var ScrollPos: Integer);
    procedure ScrollControls(x, y: Integer);
    procedure CalculateControlExtremes();
    procedure ResetVScrollBarRange;
    procedure ResetHScrollBarRange;
    function IsReplacementControl(AControl: TControl): Boolean;
    property HScrollHeight: Integer read GetHScrollHeight;
    property VScrollWidth: Integer read GetVScrollWidth;
    procedure ScrollBoxCanResize(Sender: TObject; var NewWidth,
      NewHeight: Integer; var Resize: Boolean);
  public
    constructor Create(AScrollBox: TScrollBox); reintroduce; overload;
    destructor Destroy(); override;
    procedure ResetScrollBarRange();
    procedure BringReplacementControlsToFront();
  end;

implementation

{ TScrollBoxScrollReplacement }

constructor TScrollBoxScrollReplacement.Create(AScrollBox: TScrollBox);
begin
  // Set up the scrollbox as our owner so we're destroyed when the scrollbox is
  inherited Create(AScrollBox);

  FScrollBox := AScrollBox;
  ReplaceScrollBars();

  // We make a note of any existing resize and can resize events so we can call them to make sure we don't break anything
  FOriginalResizeEvent := FScrollBox.OnResize;
  FScrollBox.OnResize := ScrollBoxResize;

  FOriginalCanResizeEvent := FScrollBox.OnCanResize;
  FScrollBox.OnCanResize := ScrollBoxCanResize;
end;

// This is called (unintuitively) when controls are moved within the scrollbox. We can use this to reset our scrollbar ranges

procedure TScrollBoxScrollReplacement.ScrollBoxCanResize(Sender: TObject; var NewWidth,
  NewHeight: Integer; var Resize: Boolean);
begin
  if (not FInScroll) then
  begin
    ResetScrollBarRange();
    BringReplacementControlsToFront();
  end;

  if (Assigned(FOriginalCanResizeEvent)) then
    FOriginalCanResizeEvent(Sender, NewWidth, NewHeight, Resize);
end;


procedure TScrollBoxScrollReplacement.ScrollBoxResize(Sender: TObject);
begin
  if (Assigned(FOriginalResizeEvent)) then
    FOriginalResizeEvent(Sender);

  ResetScrollBarRange();
end;

// Hides the original scrollbars and adds in ours

procedure TScrollBoxScrollReplacement.ReplaceScrollBars();
begin
  FVScrollBar := SetUpScrollBar(FScrollBox.VertScrollBar, sbVertical);
  FVScrollBarVisible := FVScrollBar.Visible;
  FHScrollBar := SetUpScrollBar(FScrollBox.HorzScrollBar, sbHorizontal);
  FHScrollBarVisible := FHScrollBar.Visible;

  FCornerPanel := TPanel.Create(FScrollBox);
  FCornerPanel.Parent := FScrollBox;

  ResetScrollBarRange();
end;


procedure TScrollBoxScrollReplacement.PositionScrollBars();
begin
  // Align our scrollbars correctly
  FVScrollBar.Top := 0;
  FVScrollBar.Left := FScrollBox.ClientWidth - FVScrollBar.Width;
  FVScrollBar.Height := FScrollBox.ClientHeight - HScrollHeight;
  //  FVScrollBar.BringToFront();

  FHScrollBar.Left := 0;
  FHScrollBar.Top := FScrollBox.ClientHeight - FHScrollBar.Height;
  FHScrollBar.Width := FScrollBox.ClientWidth - VScrollWidth;
  //  FHScrollBar.BringToFront();

    // If both scrollbars are visible we'll put a panel in the corner so we can't see components through it
  if (FVScrollBar.Visible) and (FHScrollBar.Visible) then
  begin
    FCornerPanel.Left := FHScrollBar.Width;
    FCornerPanel.Top := FVScrollBar.Height;
    FCornerPanel.Width := FVScrollBar.Width;
    FCornerPanel.Height := FHScrollBar.Height;
    FCornerPanel.Visible := True;
    //    FCornerPanel.BringToFront();
  end
  else
    FCornerPanel.Visible := False;
end;


procedure TScrollBoxScrollReplacement.ResetScrollBarRange();
begin
  CalculateControlExtremes();

  ResetVScrollBarRange();
  ResetHScrollBarRange();

  PositionScrollBars();
end;

procedure TScrollBoxScrollReplacement.ResetVScrollBarRange();
var
  ScrollMax: Integer;
  ScrollAmount: Integer;
begin
  // If all the controls fit to the right of the screen, but there are controls off the left then we'll scroll right.
  ScrollMax := FMaxBottom - FScrollBox.ClientHeight + FHScrollBar.Height;
  if (ScrollMax < 0) and (FLastVScrollPos > 0) then
  begin
    ScrollAmount := Min(Abs(ScrollMax), FLastVScrollPos);
    ScrollControls(0, ScrollAmount);
    FLastVScrollPos := FLastVScrollPos - ScrollAmount;
    CalculateControlExtremes();
  end;

  FVScrollBar.Max := Max(FMaxBottom - FScrollBox.ClientHeight + FHScrollBar.Height + FLastVScrollPos, 0);
  FVScrollBar.Visible := (FVScrollBar.Max > 0) and FVScrollBarVisible;
end;


procedure TScrollBoxScrollReplacement.ResetHScrollBarRange();
var
  ScrollMax: Integer;
  ScrollAmount: Integer;
begin
  // If all the controls fit to the bottom of the screen, but there are controls off the top then we'll scroll up.
  ScrollMax := FMaxRight - FScrollBox.ClientWidth + FVScrollBar.Width;
  if (ScrollMax < 0) and (FLastHScrollPos > 0) then
  begin
    ScrollAmount := Min(Abs(ScrollMax), FLastHScrollPos);
    ScrollControls(ScrollAmount, 0);
    FLastHScrollPos := FLastHScrollPos - ScrollAmount;
    CalculateControlExtremes();
  end;

  FHScrollBar.Max := Max(FMaxRight - FScrollBox.ClientWidth + FVScrollBar.Width + FLastHScrollPos, 0);
  FHScrollBar.Visible := (FHScrollBar.Max > 0) and FHScrollBarVisible;
end;


function TScrollBoxScrollReplacement.SetUpScrollBar(AControlScrollBar: TControlScrollBar; AKind: TScrollBarKind): TScrollBar;
begin
  Result := TScrollBar.Create(FScrollBox);
  Result.Visible := AControlScrollBar.Visible;
  Result.Parent := FScrollBox;
  Result.Kind := AKind;
  Result.Ctl3D := False;
  Result.Max := AControlScrollBar.Range;
  Result.OnEnter := ScrollBarEnter;
  Result.OnScroll := Scroll;
  Result.SmallChange := 5;
  Result.LargeChange := 20;

  AControlScrollBar.Visible := False;
end;

destructor TScrollBoxScrollReplacement.Destroy;
begin
  inherited;
end;

procedure TScrollBoxScrollReplacement.ScrollBarEnter(Sender: TObject);
begin
  // We just call this here to make sure our ranges are set correctly - a backup in case things go wrong
  ResetScrollBarRange();
end;

procedure TScrollBoxScrollReplacement.Scroll(Sender: TObject;
  ScrollCode: TScrollCode; var ScrollPos: Integer);
var
  Change: Integer;
begin
  ResetScrollBarRange();

  if (Sender = FVScrollBar) then
  begin
    Change := FLastVScrollPos - ScrollPos;
    ScrollControls(0, Change);
    FLastVScrollPos := ScrollPos;
  end
  else if (Sender = FHScrollBar) then
  begin
    Change := FLastHScrollPos - ScrollPos;
    ScrollControls(Change, 0);
    FLastHScrollPos := ScrollPos;
  end;
end;

// Moves all the controls in the scrollbox except for the scrollbars we've added

{procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
  I: Integer;
begin
  if (x = 0) and (y = 0) then
    Exit;

  // Stop the control from repaining while we're updating it
  SendMessage(FScrollBox.Handle, WM_SETREDRAW, 0, 0);
  FInScroll := True;
  try
    for I := 0 to FScrollBox.ControlCount - 1 do
    begin
      if IsReplacementControl(FScrollBox.Controls[I]) then
        Continue;

      FScrollBox.Controls[I].Left := FScrollBox.Controls[I].Left + x;
      FScrollBox.Controls[I].Top := FScrollBox.Controls[I].Top + y;
    end;

  finally
    // Turn on painting again
    FInScroll := False;
    SendMessage(FScrollBox.Handle, WM_SETREDRAW, 1, 0);
  end;

  // Redraw everything
  RedrawWindow(FSCrollBox.Handle, nil, 0, RDW_ERASE or RDW_INVALIDATE or RDW_ALLCHILDREN);
end;  }


procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
  I: Integer;
  Control: TControl;
  WinControl: TWinControl;
  hWinPosInfo: HDWP;
begin
  if (x = 0) and (y = 0) then
    Exit;

  hWinPosInfo := BeginDeferWindowPos(0);
  Win32Check(hWinPosInfo<>0);
  try
    for I := 0 to FScrollBox.ControlCount - 1 do
    begin
      Control := FScrollBox.Controls[I];
      if (Control = FVScrollBar) or (Control = FHScrollBar) then
        Continue;
      if Control is TWinControl then
      begin
        WinControl := FScrollBox.Controls[I] as TWinControl;
        hWinPosInfo := DeferWindowPos(
          hWinPosInfo,
          WinControl.Handle,
          0,
          WinControl.Left + x,
          WinControl.Top + y,
          WinControl.Width,
          WinControl.Height,
          SWP_NOZORDER or SWP_NOOWNERZORDER or SWP_NOACTIVATE
        );
        Win32Check(hWinPosInfo<>0);
      end
      else
        Control.SetBounds(Control.Left + x, Control.Top + y, Control.Width, Control.Height);
    end;
  finally
    EndDeferWindowPos(hWinPosInfo);
  end;
end;



// works out where our right most and bottom most controls are so we can set the scrollbars correctly

procedure TScrollBoxScrollReplacement.CalculateControlExtremes();
var
  I: Integer;
  Right: Integer;
  Bottom: Integer;
begin
  FMaxRight := 0;
  FMaxBottom := 0;
  for I := 0 to FScrollBox.ControlCount - 1 do
  begin
    if IsReplacementControl(FScrollBox.Controls[I]) then
      Continue;

    Right := FScrollBox.Controls[I].Left + FScrollBox.Controls[I].Width;
    Bottom := FScrollBox.Controls[I].Top + FScrollBox.Controls[I].Height;

    FMaxRight := Max(FMaxRight, Right);
    FMaxBottom := Max(FMaxBottom, Bottom);
  end;
end;

function TScrollBoxScrollReplacement.GetHScrollHeight: Integer;
begin
  if (FHScrollBar.Visible) then
    Result := FHScrollBar.Height
  else
    Result := 0;
end;

function TScrollBoxScrollReplacement.GetVScrollWidth: Integer;
begin
  if (FVScrollBar.Visible) then
    Result := FVScrollBar.Width
  else
    Result := 0;
end;

// Returns true if the passed control is one of the controls we've added

function TScrollBoxScrollReplacement.IsReplacementControl(
  AControl: TControl): Boolean;
begin
  Result := (AControl = FVScrollBar) or (AControl = FHScrollBar) or (AControl = FCornerPanel);
end;

procedure TScrollBoxScrollReplacement.BringReplacementControlsToFront;
begin
  FVScrollBar.BringToFront();
  FHScrollBar.BringToFront();
  FCornerPanel.BringToFront();
end;

end.

【问题讨论】:

  • 随机调用所有的 Invalidate、Repaint 和 Refresh 是毫无意义的。后两者相同,都调用Invalidate。据推测,原始代码仅称为 Invalidate。迭代孩子似乎毫无意义,因为这是自动发生的。 TWinControl 的演员阵容相当令人担忧。请不要将事物投射到它们不是的样子。
  • 是的,这是我一直在修改的代码。我会假设在 ScrollBox 上对 Invalidate 进行简单调用会起作用 - 但它没有。我把那个代码留在那里,因为它显示了我尝试过的内容。从屏幕截图中可以看出 - 这是一个测试应用,距离发布还有很长的路要走。
  • 基本上你只需要FScrollBox.Invalidate 并且可以废弃InvalidateEverything。你的生产代码中真的有这个功能吗?使用 for 循环和虚假演员表?这肯定会导致问题。
  • @DavidHeffernan 如前所述,这是测试代码。我只是在调用 FScrollBox.Invalidate。那没有用。正如您从屏幕截图中看到的那样,所有东西都有一个 TWinControl。仅调用 FScrollBox.Invalidate 不会正确重绘面板。虽然孩子们循环修复了这个问题,但编辑框没有正确绘制。这不是发布代码 - 这是尝试解决问题的代码,我稍后会整理。如果您能帮助解决问题,将不胜感激。
  • @DavidHeffernan Refresh 调用 Repaint,但他们不调用 Invalidate。立即重绘绘画,而 Invalidate 通过消息工作,因此仅在应用程序空闲或调用 Application.ProcessMessages 时才处理。对于这种情况,Invalide 应该是唯一必要的,如果您想重新绘制某些内容以响应单击或滚动等用户操作,它是最佳解决方案。此外,这种重绘会传播给父母,因此您不必为控件树中的每个父母/孩子递归地调用它。

标签: delphi delphi-5


【解决方案1】:

你可以替换你的

FScrollBox.Invalidate();

RedrawWindow(FSCrollBox.Handle, nil, 0,
    RDW_ERASE or RDW_INVALIDATE or RDW_ALLCHILDREN);

使所有控件失效并正确更新。 RDW_ERASE 用于擦除控件的先前位置,RDW_ALLCHILDREN 用于处理内部的窗口控件。由于RDW_INVALIDATE,标签等非win控件应该已经重新绘制。

虽然这种方法可能有助于避免您观察到的闪烁,但它也可能会导致拇指跟踪时滚动的平滑度有所下降。这是因为滚动位置可能需要比处理绘制周期更频繁地更新。为了避免这种情况,您可以立即更新控制位置,而不是使其失效:

RedrawWindow(FSCrollBox.Handle, nil, 0,
    RDW_ERASE or RDW_INVALIDATE or RDW_UPDATENOW or RDW_ALLCHILDREN);

【讨论】:

  • 完美——正是我一直在寻找的那种东西。我该去阅读有关 RedrawWindow 的所有内容了。
  • @Sertac 这比 DeferWindowPos 好吗?在我看来,WM_SETREDRAW 在平滑滚动方面的表现要差得多。我知道提问者似乎认为WM_SETREDRAW 是唯一的解决方案,但我从未真正相信它的实用性。
  • @David - 我真的不知道。我既没有尝试过 DeferWindowPs,也没有看到该框在远程会话中执行。
  • @SertacAkyuz 我不是在谈论远程会话。我说的是本地会话。
  • @David - 现在尝试与 DeferWindowPos 进行比较,我没有发现任何区别,一些控件在这两种方法中偶尔会出现轻微闪烁。
【解决方案2】:

我发现,一旦我删除了两条 WM_SETREDRAW 消息,您的代码就开始工作了。那是你的根本问题。您将需要删除 WM_SETREDRAW 消息。

这无疑意味着您仍然需要解决闪烁问题,但这是一个不同的问题。我的快速实验表明DeferWindowPos 可以解决这个问题。例如:

procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
  I: Integer;
  Control: TControl;
  WinControl: TWinControl;
  hWinPosInfo: HDWP;
begin
  if (x = 0) and (y = 0) then
    Exit;

  hWinPosInfo := BeginDeferWindowPos(0);
  Win32Check(hWinPosInfo<>0);
  try
    for I := 0 to FScrollBox.ControlCount - 1 do
    begin
      Control := FScrollBox.Controls[I];
      if (Control = FVScrollBar) or (Control = FHScrollBar) then
        Continue;
      if Control is TWinControl then
      begin
        WinControl := FScrollBox.Controls[I] as TWinControl;
        hWinPosInfo := DeferWindowPos(
          hWinPosInfo,
          WinControl.Handle,
          0,
          WinControl.Left + x,
          WinControl.Top + y,
          WinControl.Width,
          WinControl.Height,
          SWP_NOZORDER or SWP_NOOWNERZORDER or SWP_NOACTIVATE
        );
        Win32Check(hWinPosInfo<>0);
      end
      else
        Control.SetBounds(Control.Left + x, Control.Top + y, Control.Width, Control.Height);
    end;
  finally
    EndDeferWindowPos(hWinPosInfo);
  end;
end;

您的非窗口控件仍会闪烁,但您可以将它们设为窗口,或者确实将滚动框的全部内容放在窗口控件中。哎呀,只要你这样做,就足以解决问题!

就其价值而言,我的试验表明DeferWindowPosWM_SETREDRAWRedrawWindow 提供更流畅的滚动。但是这些测试并不详尽,您可能会在您的应用程序中发现不同的结果。


关于您的代码的一些旁白:

您对try/finally 的使用不正确。模式必须是:

BeginSomething;
try
  Foo;
finally
  EndSomething;
end;

你对SendMessage的调用弄错了。

您在InvalidateEverything 中使用了错误的演员表。您不能盲目地将TControl 转换为TWinControl。也就是说,该功能没有好处。您可以完全删除它。它试图做的事情可以通过调用父控件的Invalidate 来执行。

【讨论】:

  • 我很清楚,问题是它也开始闪烁 - 尝试拖动滚动条。
  • 可能是这样,但恐怕这是您所提问题的答案。这就是您的控件不重新绘制的原因。如果要解决闪烁,那就另当别论了。
  • @WillCalderwood 如果用户处于远程会话中,您肯定没有对滚动框进行实时跟踪吗?
  • @WillCalderwood 只是一个关于远程桌面设计的旧文章的链接blogs.msdn.com/b/oldnewthing/archive/2006/01/03/508694.aspx
  • @WillCalderwood 我根本看不到窗口控件的闪烁。非窗口控件会闪烁,因为您不能将它们与DeferWindowPos 一起使用。所以,我在想TLabel。有两种处理方法:将非窗口控件放在窗口容器中。可能并不总是有帮助。或替换为窗口控件。使用TStaticText 而不是TLabel。我倾向于后者。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-10-17
  • 1970-01-01
  • 1970-01-01
  • 2016-12-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多