65.9K
CodeProject 正在变化。 阅读更多。
Home

掌握用户界面中的滚动条:全面的实现指南

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2023 年 8 月 25 日

CPOL

9分钟阅读

viewsIcon

7507

downloadIcon

206

在 Windows 窗体中实现滚动条,因为 AutoScroll 不符合我的需求

引言

滚动条在使用户能够导航超出窗口可见边界的内容方面起着至关重要的作用。

虽然它们的目的很明显,但当需要无缝且自动调整滚动条时,复杂性就出现了——尤其是在用户与窗口交互时,例如在其中键入时。

关于我的方法,我开始测试并使其正确工作,首先是带滚动条的移动窗口。然后,我测试了窗口的大小调整。在这个阶段,挑战很大,因为我不理解滚动条是如何工作的。所以我测试和思考滚动条一段时间,然后解决方案出现了。既然现在工作正常,我测试了如何让滚动条自动移动,只要用户在键盘上打字。这样,窗口在其书写过程中就会前进。

理解滚动条

滚动条的主要目的是允许用户在文档大小超过其窗口大小时移动文档的视点。

存在两种滚动条:水平和垂直。

滚动条首先是一条条。你可以滚动这条条。当你滚动这条条时,你的文档的视点会同时移动。文档的顶部(或左侧)是滚动条开始的地方。文档的底部(或右侧)对应于滚动条的末尾。所以你不能去超出文档的顶部或底部。

自定义滚动条实现的需求

滚动条(水平和垂直)是常见的用户界面。在 Windows 窗体中,您可以通过将任何窗口上的 AutoScroll 布尔值设置为 true 来激活两个滚动条。当您的内容大于窗口时,滚动条可见,然后您可以滚动。但是,您只能自己移动滚动条。假设现在,您需要它自动移动,您无法访问属性或函数来修改默认行为。因此,您需要自己实现,以使文档在您键入文档时按照您隐式定义的视点移动。

滚动条的结构

在垂直(或水平)滚动条的顶部(或左侧)和底部(或右侧),有一个按钮,上面通常画着一个小箭头。在这两个极限之间,有一个可以沿着轨道细分的拇指或条。

按钮以小步幅移动滚动条,就像在文档中移动一样。因此,您可以非常缓慢地移动视点。拇指可以更快地移动文档视图。您也可以点击轨道进行大步移动。这些属性由位移量的百分比表示。例如,LargeChange 的默认值为 20SmallChange10

在设计窗口时,您可以将水平或垂直滚动条从工具箱拖到窗口,通常将垂直滚动条放在窗口的右边缘,水平滚动条放在下边缘。

当您使用滚动条时,您会设置一个值、一个最小值和一个最大值来指向文档。最简单的操作就是将最小值设置为零,最大值设置为您的文档大小。但通常最小值和最大值分别为零和 100。所以这是百分比,而不是最大长度。但如果您将最大值设置为 100,则该值必须在这些限制之内。滚动条的 Value 属性只是滚动条中的一个位置。现在,当您移动滚动条时,您只需要在自定义滚动条时设置文档的视点。如果您将 AutoScroll 布尔值设置为 true,程序会自动为您处理所有这些事情。

协调滚动条和内容

当您自定义滚动条时,您可以将最小值和最大值设置为任何您想要的数字。但该值不能超过这些限制。如果超过,程序将抛出错误异常。

为了使滚动条自动移动,您需要为滚动条设置一个值,该值不应超过最小值和最大值。此值对应于文档坐标的位置。

现在,如果您移动滚动条,该值应相应地随滚动条改变,您应该移动文档的视点。为此,您只需将文档的 LeftTop 属性设置为负值。这只是窗口剪裁,不会显示文档的左上角。这实际上是协调滚动条和内容的简单方法。

Using the Code

我编写了一个特定的类 UsingScrollbars。该类封装了两个滚动条的布局过程

  • 滚动条的初始化
  • 为每个滚动条添加了一个委托函数,用于捕获 Value 并移动文档
  • 您可以调用的一个特定函数,用于自动定位视点
  • 您可以调用的两个特定函数,用于在文档大小调整和窗口大小调整时

使用滚动条,您有五个参数

  • 一个 Value,表示滚动条位置
  • 一个 LargeChange,表示大步
  • 内部控件(文档)的 width(或 height
  • 面板内容(窗口)的 width(或 height
  • 内部控件(文档)的 Left(或 Top)角

这五个参数可以处理滚动条的所有功能。

我还编写了一个名为 UsingScrollbarsConverter 的特定类。此类提供了一些函数

  • ConvertFromValue:将 Value 转换为文档左上角的坐标
  • ConvertToValue:将文档左上角转换为 Value
  • VerifyIfNotViewed:如果位置未被查看,则返回 truefalse

此类简化了转换逻辑,因为您每次都必须在代码中调用这些函数。因此,此类提供了一个只需在需要时调用的函数。

有两个事件处理程序。一个用于水平滚动条,另一个用于垂直滚动条。这些事件处理程序分别使用委托函数 HorizontalScrollBarMethodVerticalScrollBarMethod 进行初始化。然后,当您移动滚动条时,会调用这些事件处理程序,您的目标是移动文档的左上角以更改窗口中文档的视点。

private void HorizontalScrollBarMethod(object? sender, ScrollEventArgs e)
{
    Debug.WriteLine("Horizontal new value : " + e.NewValue);
    int convertedValue = -UsingScrollBarsConverter.ConvertFromValue
                         (_panel.Width - _vScrollBar.Width,
                          _innerControl.Width, e.NewValue, _hScrollBar.LargeChange);
    Debug.WriteLine("Horizontal converted value : " + convertedValue);
    _innerControl.Left = convertedValue;
}

private void VerticalScrollBarMethod(object? sender, ScrollEventArgs e)
{
    Debug.WriteLine("Vertical new value : " + e.NewValue);
    int convertedValue = -UsingScrollBarsConverter.ConvertFromValue
                         (_panel.Height - _hScrollBar.Height,
                          _innerControl.Height, e.NewValue, _vScrollBar.LargeChange);
    Debug.WriteLine("Vertical converted value : " + convertedValue);
    _innerControl.Top = convertedValue;
}

注意对函数 UsingScrollBarsConverter.ConvertFromValue 使用的负号运算符。

我将最小值设置为 0,最大值设置为 100。所以,当我得到一个 Value 时,我需要将其转换为文档的视点。转换方法只是比例除法。

public static int ConvertFromValue(int panelContent, int innerControl, 
                                   int newValue, int LargeChange)
{
    // multiply
    float multiply = newValue * (innerControl - panelContent) / 
                                (float)(100 - LargeChange);
    return (int)multiply;
}

通常,我们只需通过计算滚动条的旧值和新值之间的差值来设置左上角。这里,我们不这样做,而是计算位置而不超出文档的右下角。如果函数没有精确计算文档的左上角,您应该进一步深入文档。

由于最大值为 100LargeChange20,您可以查看高达 80% 的文档。这意味着 20% 应相当于您的容器(面板)的大小。下面,图片显示了一个垂直滚动条,并解释了 LargeChange 值确切的含义。因此,在 ConvertFromValue 函数中,我们除以 100 - LargeChange 而不是 100

LargeChange 设置为一个值,该值使 Value 得到 80 左右的数字,而不是过多或过少。这些值最初等于

_vScrollBar.LargeChange = 20;
_hScrollBar.LargeChange = 20;
_vScrollBar.SmallChange = 10;
_vScrollBar.SmallChange = 10;

但是,为了在窗口大小调整时起作用,您需要将这些值设置为与文档和面板成比例。如果面板大于文档,则滚动条将被禁用。

// horizontal scrollbar
float horizontalProportion = GetInnerControlEffectiveSize
                             (innerControl, panel, vScrollBar, hScrollBar).Width
                             / (float)GetPanelContentEffectiveSize
                             (panel, vScrollBar, hScrollBar).Width;
if (horizontalProportion >= 1)
{
    hScrollBar.Enabled = true;
    SetHScrollBarSmallAndLargeChange(horizontalProportion, hScrollBar);
    hScrollBar.Value = 0;
    innerControl.Left = 0;
}
else
{
    hScrollBar.LargeChange = 100;
    hScrollBar.SmallChange = 100;
    hScrollBar.Enabled = false;
    hScrollBar.Value = 0;
    innerControl.Left = 0;
}
Debug.WriteLine("HScrollBar Enabled : " + hScrollBar.Enabled);
Debug.WriteLine("HScrollBar LargeChange :" + hScrollBar.LargeChange);
Debug.WriteLine("HScrollBar SmallChange :" + hScrollBar.SmallChange);

// vertical scrollbar
float verticalProportion = GetInnerControlEffectiveSize
                           (innerControl, panel, vScrollBar, hScrollBar).Height
                           / (float)GetPanelContentEffectiveSize
                           (panel, vScrollBar, hScrollBar).Height;
if (verticalProportion >= 1)
{
    vScrollBar.Enabled = true;
    SetVScrollBarSmallAndLargeChange(verticalProportion, vScrollBar);
    vScrollBar.Value = 0;
    innerControl.Top = 0;
}
else
{
    vScrollBar.LargeChange = 100;
    vScrollBar.SmallChange = 100;
    vScrollBar.Enabled = false;
    vScrollBar.Value = 0;
    innerControl.Top = 0;
}
Debug.WriteLine("VScrollBar Enabled : " + vScrollBar.Enabled);
Debug.WriteLine("VScrollBar LargeChange :" + vScrollBar.LargeChange);
Debug.WriteLine("VScrollBar SmallChange :" + vScrollBar.SmallChange);

当坐标改变时

当坐标改变时,您正在移动文档中的插入点。如果您超出 视口,您将永远看不到插入点。然后滚动条应该自动移动以显示插入点。

为此,我有一个函数可以将位置转换为值。此函数名为 ConvertToValue。它将位置(文档的左上角)转换为值,作为比例运算。

/// <summary>
/// Converts a position to a scrollbar value
/// </summary>
/// <param name="coordinate">position (x or y)</param>
/// <param name="maximale">maximale position</param>
/// <returns>the value by percent</returns>
public static int ConvertToValue(int panelContent, 
              int innerControl, int coordinate, int LargeChange, int nTurn = 1)
{
    return (int)(coordinate / (nTurn * (float)(innerControl - panelContent)) * 
                              (float)(100 - LargeChange));
}

函数 WhenCoordinatesChanged 是托管布局和逻辑操作以仅在您超出窗口可见边界时移动 scrollbar 的函数。因此,我们首先验证插入点是否可见。我们有一个名为 VerifyIfNotViewed 的函数。

public static bool VerifyIfNotViewed(int innerControl, 
                   int panelContent, int view, int coordinate)
{
    Debug.WriteLine("View : " + view + ", PanelContent : " + 
                     panelContent + ", Coordinate : " + coordinate + ", 
                     NotViewed : " + ((coordinate - view) > panelContent || 
                     coordinate < view));
    return (coordinate - view) > panelContent || coordinate < view;
}

下面,我展示了将窗口移动到文档中某个位置的想法以及如何计算文档的新左上角。在图像中显示的数字 1 处,该位置在文档的更下方。为了计算 Value,我只需调用 ConvertToValue 函数并将滚动条(垂直或水平)的 Value 设置为该结果。在数字 2 处,该位置在窗口的前面。我只需调用 ConvertToValue 函数并将值设置为该结果。但是,要了解位置是否可见,1 对于 VerifyIfNotViewed 返回 true,因为 (coordinate - view) > panelContent2 返回 true,因为 coordinate < view

为了调试,我使用函数

Debug.WriteLine(string? s);

调试输出是读取值而不是调试器的唯一解决方案,因为它不断引发,并且焦点事件在我切换调试器和窗口之间时每次都会发生。

结论

这是 C# 中用于 Windows 窗体的 ScrollBars 的实现,用于查看超出窗口边界的文档。

使用此实现有三个优点

  1. 自动文档导航:在键入时无缝调整文档视图,无需在交互过程中手动操作滚动条。

  2. 高效且封装的设计:通过将布局和逻辑过程封装在两个类中,该实现提供了一种简化的方法来导入和集成此功能到您的应用程序中,使您免于大量的调试工作。这种简化的集成可以为您节省宝贵的开发时间,可能半小时或更长。

  3. 灵活适应:代码的组织结构便于修改逻辑和布局,可轻松适应带填充或其他变化的文档。

通过运用我的解决问题的能力,我采用了全面的方法来剖析和理解滚动条行为的复杂性,确保了强大有效的解决方案。

历史

  • 2023 年 8 月 25 日:初始版本
© . All rights reserved.