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

阶段进度条

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.47/5 (5投票s)

2007年4月2日

CPOL

17分钟阅读

viewsIcon

57670

downloadIcon

1124

一个自定义控件,用于通过多边形提供阶段进度。

Screenshot - dmPhaseProgressBar.jpg

引言

这是一个使用 VB.NET 构建的自定义控件。该控件旨在允许用户实现任意数量的处理阶段,并查看他们还有多远、他们当前处于哪个“阶段”,以及选择性地指示用户何时准备好进入下一个“阶段”。

背景

我一直在做一个关于需求管理的个人项目,并研究了许多不同的方法来为用户提供友好且视觉上吸引人的方式来处理如此复杂的东西,例如软件需求和这些需求的项⽬管理。我的屏幕之一是需求及其当前“阶段”的摘要。我想要一些使用颜色并且完全交互的东西。我不喜欢大多数应用程序中标准的矩形外观和感觉,因为对我来说,它没有指示方向。我还偶然发现了形状相似的东西,只是发现它们使用了重叠在多边形形状上的矩形。这些导致能够单击一个多边形(例如尾部),它会假定您指的是另一个多边形。我还想研究一下自定义控件。还有什么比创建一个更好的方法来做到这一点。

Using the Code

代码分为三个主要区域

  • ddPhaseProgressBarItem - 保存每个多边形信息的类。
  • dmPhaseProgressBar - 作为 ddPhaseProgressBarItem 类集合的容器的自定义控件。它负责每个多边形项的组织、外观和感觉。它决定了“阶段”集合的选定索引。
  • 演示窗体 - 用于实现自定义控件并添加自定义“阶段”列表。这是一个交互式设置,允许用户检查控件的功能。

步骤 1:多边形项

ddPhaseProgressBarItem 类包含绘制多边形所需的所有信息。Polygons 属性是一个用于绘制两个形状的点数组:FillPolygonDrawPolygon。您必须在执行 Draw 方法之前执行 Fill,以确保边框对用户可见。

此控件实现了四种基本形状。

注意:使用标题图形的第四行可查看所有三种形状集。

形状集 1:一个阶段项

当进度条只有一个选项卡时,将使用此形状。这可能非常违反直觉,因为这是一个进度条。但是,有时这可能是一个要求。

此多边形形状是简单的矩形。

形状集 2:两个阶段项

此形状集包含从现在开始使用的两个特殊情况形状。左侧形状是五点多边形。右侧形状也是五点多边形。

第一个多边形是一个简单的矩形,右侧在矩形中间向下并向外延伸形成一个点。

第二个多边形是一个简单的矩形,左侧在矩形中间向下并向内延伸形成一个点。

形状集 3:三个或更多阶段项

当有三个或更多选项卡时,将使用此形状集。中间选项卡将重复使用,直到所有阶段都已定义。

第一个和最后一个多边形与形状集 #2 相同。

中间多边形是一个简单的矩形,左侧在中间向下并向内延伸形成一个点,右侧在中间向下并向外延伸形成一个点。

步骤 2:自定义控件

自定义控件是保存 ddPhaseProgressBarItem 集合以及确定整体形状的地方。既然形状集已经定义,我们可以使用这些定义来确定遵循哪个形状集,然后报告每个多边形的*状态。我们将保留额外的*状态以允许“闪烁”,通过添加一个计时器控件,以便通知用户下一个“阶段”已准备就绪。例如:在执行从 MS Excel 导入文本时,我们有四个主要步骤可以用在这里表示。请参见标题图形中的底部栏。

正如您可能已经猜到的,因为这是一个控件,所以大部分工作都在 OnPaint 方法中完成。您是正确的。这些步骤在控件初始化时完成,而步骤 2-5 在 OnPaint 事件中重复执行。

  1. 为每个多边形设置值(TextBorderColorBackgroundColor 等)。
  2. 确定控件中的额外空间,以便在多边形之间共享。
  3. 确定每个多边形的形状。
  4. 将每个多边形绘制到屏幕上。
  5. 在多边形上绘制文本。

步骤 2.1:为每个多边形设置值

此步骤是设置所有要从默认值更改的值,并添加要添加到控件的每个多边形的文本。我预设了一些值,以便开发人员可以在设计时看到控件正在运行,因此您需要先使用 clearPolygons 方法将其清除。然后,您生成一个新的多边形项对象 ddPhaseProgressBarItem,设置您想要的所有值,直到添加完所有项。

此示例显示所有属性都使用随机值进行设置

Me.SuspendLayout()
Dim PhaseProgressBarItem As New ddPhaseProgressBar.PhaseProgressBarItem
PhaseProgressBarItem.Text = "File Selection"
PhaseProgressBarItem.Font = New Font("Arial", 10, FontStyle.Regular)
PhaseProgressBarItem.Font_Selected = New Font("Arial", 12, FontStyle.Bold)
PhaseProgressBarItem.FontColor = Color.FromArgb(&HFF000000)
PhaseProgressBarItem.FontColor_Selected = Color.FromArgb(&HFF0000FF)
PhaseProgressBarItem.BackColor = Color.FromArgb(&H78FFFFCC)
PhaseProgressBarItem.BackColor_Selected = Color.FromArgb(&H78FFFF00)
PhaseProgressBarItem.BorderColor = Color.Black
PhaseProgressBarItem.BorderColor_Selected = Color.BlueViolet
PhaseProgressBar1.AddPhase(PhaseProgressBarItem)

PhaseProgressBarItem = New ddPhaseProgressBar.PhaseProgressBarItem
PhaseProgressBarItem.Text = "Import Type"
PhaseProgressBarItem.Font = New Font("Arial", 10, FontStyle.Regular)
PhaseProgressBarItem.Font_Selected = New Font("Arial", 12, FontStyle.Bold)
PhaseProgressBarItem.FontColor = Color.FromArgb(&HFF000000)
PhaseProgressBarItem.FontColor_Selected = Color.FromArgb(&HFF0000FF)
PhaseProgressBarItem.BackColor = Color.FromArgb(&H78FFFFCC)
PhaseProgressBarItem.BackColor_Selected = Color.FromArgb(&H78FFFF00)
PhaseProgressBarItem.BorderColor = Color.Black
PhaseProgressBarItem.BorderColor_Selected = Color.Blue
PhaseProgressBar1.AddPhase(PhaseProgressBarItem)

PhaseProgressBarItem = New ddPhaseProgressBar.PhaseProgressBarItem
PhaseProgressBarItem.Text = "Delimiters"
PhaseProgressBarItem.Font = New Font("Arial", 10, FontStyle.Regular)
PhaseProgressBarItem.Font_Selected = New Font("Arial", 12, FontStyle.Bold)
PhaseProgressBarItem.FontColor = Color.FromArgb(&HFF000000)
PhaseProgressBarItem.FontColor_Selected = Color.FromArgb(&HFF0000FF)
PhaseProgressBarItem.BackColor = Color.FromArgb(&H78FFFFCC)
PhaseProgressBarItem.BackColor_Selected = Color.FromArgb(&H78FFFF00)
PhaseProgressBarItem.BorderColor = Color.Black
PhaseProgressBarItem.BorderColor_Selected = Color.DodgerBlue
PhaseProgressBar1.AddPhase(PhaseProgressBarItem)

PhaseProgressBarItem = New ddPhaseProgressBar.PhaseProgressBarItem
PhaseProgressBarItem.Text = "Sample Output"
PhaseProgressBarItem.Font = New Font("Arial", 10, FontStyle.Regular)
PhaseProgressBarItem.Font_Selected = New Font("Arial", 12, FontStyle.Bold)
PhaseProgressBarItem.FontColor = Color.FromArgb(&HFF000000)
PhaseProgressBarItem.FontColor_Selected = Color.FromArgb(&HFF0000FF)
PhaseProgressBarItem.BackColor = Color.FromArgb(&H78FFFFCC)
PhaseProgressBarItem.BackColor_Selected = Color.FromArgb(&H78FFFF00)
PhaseProgressBarItem.BorderColor = Color.Black
PhaseProgressBarItem.BorderColor_Selected = Color.IndianRed
PhaseProgressBar1.AddPhase(PhaseProgressBarItem)

Me.ResumeLayout()

我使用 SuspendLayoutResumeLayout 来提高速度,因为我希望最大限度地减少控件重绘时的闪烁。

步骤 2.2:填充额外空间

此步骤是确定控件的长度,然后确定多边形的基本总长度,以便我们可以确定需要均匀分布在所有多边形上的额外空间,以填充控件的总体长度。这样,用户/程序员就可以调整控件大小,而不会改变可见的总空间,也无需修改文本间隔值即可正确居中文本。

我们通过测量每个多边形的文本加上预设文本缓冲区值来做到这一点,将它们加在一起得到我称之为基本总长度的值。该值减去控件的宽度将得到需要由我们的二次文本缓冲区值填充的“空间”。将该结果除以多边形数量,即可得到新的二次文本缓冲区值。

使用 .NET 函数 MeasureString(要测量的字符串, 使用的字体),我们得到一个二维对象,它提供我们要测量的文本的总体高度和宽度。由于此版本的控件(嘿,我不能为您做一切 :D)只关心宽度,所以我们将围绕它进行工作。由于我选择的形状集,我们只需要处理文本宽度加上文本缓冲区值;进入和退出矩形的点实际上是互补的,最后一个多边形不指向外部,所以我们在测量时不必考虑它们。

这是确定二次文本缓冲区(TextBufferForCentering)的代码函数

Private Function DetermineTextCenterBuffer(ByVal g As Graphics) As Boolean
    'As the control can be bigger than the entire length of the phases, 

    ' we need to get that extra space and evenly distribute it to 

    ' all the phases. 

    Dim returnValue As Boolean = True
    Dim overallLength As Single = Me.Width
    Dim phasesLength As Single = 0
    Dim sFThis As SizeF

    For i As Integer = 1 To m_Phases.Count
        sFThis = g.MeasureString(m_Phases.Item(i).Text, m_Phases.Item(i).CurrentFont)
        If i = 1 And m_Phases.Count = i Then 'one tab

            phasesLength += sFThis.Width + Me.TextBuffer + 2
        ElseIf i = m_Phases.Count Then 'last tab

            phasesLength += sFThis.Width + Me.TextBuffer + Me.PointLength + 2
        ElseIf i = 1 Then 'first tab

            phasesLength += sFThis.Width + Me.TextBuffer + 2
        Else 'middle tabs

            phasesLength += sFThis.Width + Me.TextBuffer + Me.PointLength + 2
        End If
    Next

    Me.TextBufferForCentering = (overallLength - phasesLength) / m_Phases.Count

    DetermineTextCenterBuffer = returnValue
End Function

步骤 2.3:确定每个多边形的形状

此步骤是在控件的最终绘制阶段确定每个多边形的形状。使用先前定义的形状集(见上文),我们现在可以使用一些信息来绘制形状。

  • 文本宽度
  • 文本缓冲区
  • 二次文本缓冲区(TextBufferForCentering 值)
  • 控件高度
  • 点长度(多边形点超出矩形的距离)

每个多边形都有一个我们首先必须确定的起始点。这是使用相同的方法完成的,但增加了 TextBufferForCentering 值,正如我们在确定多边形总体长度时所做的那样。但是,这一次,我们只计算当前正在处理的多边形之前的多边形。例如,在确定第三个多边形的起始点时,我们将确定前两个多边形的总体长度,并将起始点放在第二个多边形最右侧点的末端。请记住,我们没有使用从矩形伸出的点,因为它们是互补的。我们只在确定实际点时使用点长度,而不是起始点。是的,这很混乱,但这就是为什么有时用代码解释更容易。DeterminePhasePoly 函数是完成这项工作的地方。

对于本节,我们将假设我们正在处理绘制三个以上多边形解决方案中的第三个多边形。

正如您在此代码片段中所见,我们根据前一个多边形的长度来确定多边形的起始点。

Dim startPoint As Single = 0
...
For i As Integer = 2 To ItemPosition
    'measure the previous poly's text width

    iPrevious = i - 1
    sF = g.MeasureString(m_Phases.Item(iPrevious).Text, _
                         m_Phases.Item(iPrevious).CurrentFont)
    If iPrevious = 1 Then 'first tab

        startPoint += sF.Width + Me.TextBuffer + _
                      Me.TextBufferForCentering + 2
    Else ' middle tabs

        startPoint += sF.Width + Me.TextBuffer + _
                      Me.TextBufferForCentering + Me.PointLength + 2
    End If
Next i

现在我们有了起始点,我们可以用数学方法确定其余的点。我们将以顺时针方式(x 和 y 坐标)绘制,因此点从起始点开始确定(这是我们最复杂的多边形)

  • 点 1:x = 起始点,Y = 0
  • 点 2:x = 起始点 + 文本宽度 + 文本缓冲区 + 文本居中缓冲区 + 点长度,y = 0
  • 点 3:x = 点 2 + 点长度,y = 控件高度的 1/2
  • 点 4:x = 与点 2 相同,但 y = 控件高度
  • 点 5:x = 起始点,y = 控件高度
  • 点 6:x = 起始点 + 点长度,y = 控件高度的 1/2

确定所有多边形点后,我们将这些值分配给多边形的*对象值。

m_Phases.Item(i).PolyPoints = DeterminePhasePoly(g, i)

步骤 2.4:绘制多边形

最后一步是实际绘制多边形并放置文本。万岁!

我们提取要绘制的多边形的所有属性。我选择从右到左绘制多边形,因为多边形会重叠。绘制时,最后绘制的是最顶部的项。因此,如果您想让项“堆叠”在一起,则必须先绘制最底部的项,然后向上绘制。绘制每个多边形时也必须执行相同的操作。

penColor = m_Phases.Item(i).CurrentBorderColor
pPen = New Pen(penColor, 2)
bFontBrush = New SolidBrush(m_Phases.Item(i).CurrentFontColor)
bBrush = New SolidBrush(m_Phases.Item(i).CurrentBackColor)
fFont = m_Phases.Item(i).CurrentFont
sText = m_Phases.Item(i).Text

绘制单个多边形时,必须按顺序绘制

  1. 背景填充
  2. border
  3. 文本

您在阅读代码时可能会注意到的一个有趣之处是,我修改了 ddPhaseProgressBarItem 以根据多边形的“Selected”*状态、CurrentBorderColorCurrentFontColorCurrentFontCurrentBackColor 返回颜色和文本。这使我能够简化绘制代码,因为我只请求当前值,而不必在绘制阶段测试多边形是否“已选定”。我让对象本身决定它想要呈现的颜色和文本。

'draw background

g.FillPolygon(bBrush, m_Phases.Item(i).PolyPoints)
'draw border

g.DrawPolygon(pPen, m_Phases.Item(i).PolyPoints)

步骤 2.5:放置文本

放置文本是完成控件绘制部分的最后一步。但是,我们需要正确地居中文本,而不仅仅是随意放置它并期望它能优雅地处理。

再次,我们必须知道我们正在处理的形状才能正确确定其文本中心点。您可能会注意到我们实际上是在找到文本开始的最左侧点,而不是多边形的中心。

If i = m_Phases.Count And i = 1 Then 'only one tab

    xForText = m_Phases.Item(i).polypoints(0).X + _
              (Me.TextBuffer / 2) + (Me.TextBufferForCentering / 2)
        yForText = (Me.Height - fFont.Height) / 2
ElseIf i = m_Phases.Count Then 'last tab

        xForText = m_Phases.Item(i).polypoints(0).X + _
                  (Me.TextBuffer / 2) + (Me.TextBufferForCentering / 2) + _
                   Me.PointLength
        yForText = (Me.Height - fFont.Height) / 2
ElseIf i = 1 Then 'first tab

        xForText = m_Phases.Item(i).polypoints(0).X + (Me.TextBuffer / 2) _
                   + (Me.TextBufferForCentering / 2) + (Me.PointLength / 3)
        yForText = (Me.Height - fFont.Height) / 2
Else 'all middle tabs 

        xForText = m_Phases.Item(i).polypoints(0).X + (Me.TextBuffer / 2) + _
                  (Me.TextBufferForCentering / 2) + Me.PointLength + _
                  (Me.PointLength / 3)
        yForText = (Me.Height - fFont.Height) / 2
End If

现在,在正确的位置绘制文本

'draw the text for this polygon in its proper location
g.DrawString(sText, fFont, bFontBrush, xForText, yForText)

我们完成了!控件现在已绘制到屏幕上。

演示项目

我创建了一个演示项目,让您作为读者/开发人员能够很好地了解这个控件。我将控件放在窗体上,使用了控件的所有功能,此外我还添加了一个我没有在本篇文章中介绍的闪烁功能。这是我在概述之后添加的内容,我认为在观看我如何构建控件时,这是一个值得您自己研究的好东西。它并不是一个真正必需的功能。

加载项目并在不更改源代码的情况下运行它,您应该会看到控件与标题图像(见上文)中的顶部行阶段进度条匹配。通过单击一个多边形,它应该更改为选定的颜色和字体,并设置其右侧多边形的闪烁*状态。当然,除非您处于最后阶段,否则没有多边形会被设置为闪烁。

用户单击了哪个多边形?

运行演示后,您可能会觉得我错过了一个非常重要的代码部分。您如何知道单击了哪个多边形,以便我可以处理 OnClick 事件?嗯,我故意跳过了这一点,因为我没有编写那部分。我找到了一个关于如何确定一个点是否在多边形内部的极好资源,作者是 Darel Rex Finley,可以在这里找到。尽管我不得不将其从 C++ 转换为 VB.NET,但它仍然是他的研究使一个非常复杂的过程变得简单。阅读他的文章以完全理解它。在撰写本文时,它处理了非常复杂的多边形,甚至处理了由具有交叉点的多边形创建的空隙。

关注点

由于我之前没有做过多少图形相关的控件,所以我发现观察绘制项目的顺序很有趣。基本上是从后往前。它还引导我使用一个名为 PenWidth(在本示例代码中没有)的新属性,使所有多边形实际接触,而不是重叠。您会注意到我的许多计算中都有 +2 用于绘制和点放置。那就是笔宽被考虑在内。

文本放置是一个很好的挑战,因为我改变了绘制多边形的方式,因为文本可能会落入选项卡的尖端,这可能会导致文本和线条重叠。我当然不希望那样。因此,我在基本形状中均匀地增加了更多空间,但仍然将文本偏移,使其稍微推入多边形的尖端。

一些控件让我烦恼,无论是 Web 还是应用程序,它们的复杂形状都只是为了好看,而没有为 Click 事件覆盖实际形状。例如,我见过形状与我的相似的选项卡,当单击多边形尖端时,它要么不响应单击,要么将单击响应到错误的多边形。这是因为单击的映射区域实际上是一个矩形,而不是相同的多边形形状。我知道绘制矩形更容易,但是如果您花时间为用户生成一个多边形,那么您应该使用相同多边形进行单击事件。

调试这个也很烦人,因为控件的图形性质。我发现了一个 bug,它实际上在绘制两个或多个选项卡环境中的最后一个选项卡时遇到了问题。我将最后一个选项卡的点设置在控件的左上角,而不是最后一个选项卡的末端(我的复制粘贴失误),并且因为它先绘制(记住,我从右到左绘制),所以它在我所有其他选项卡的下方。问题是,当我在使用 Darel 的方法测试鼠标单击时,我从左到右进行测试。我没有“如果为真,则中断”行,因此它会为多个多边形获得“是,在此多边形中”的值:我单击的多边形和它下面的形状不正确的那个。注释掉我不想绘制的所有多边形的绘制阶段,只显示我想要的那一个,就显示了问题,但我认为将来在此控件中添加不透明度值会很好。

Bug

目前,我只看到一项可以被视为控件中的 bug。

控件并未完全处理“额外空间”,即多边形长度与控件长度之间的差值。有时控件没有填满所有空间。我知道这是因为数学计算存在小数点后的值,这些值累加起来,导致控件末端右侧出现几像素的空间。我正在努力处理这些额外的空间,并且很可能会将其添加到最后一个多边形中。

此控件的想法

既然我们正在谈论新事物,这里有一些我想到过的可能很有趣的想法,可以添加到这个控件中,使其更具可用性。

响应最小高度和宽度。通过为整个控件添加最小高度和宽度的属性,我们可以更好地处理窗体大小调整,并触发事件以防止控件变得太小(垂直或水平)。我们可以根据字体(大小和系列)和实际文本组合而成的控件可能达到的最长长度来计算。为了使此工作起作用,我们必须限制功能,或者考虑多种可能的情况。我想到的一种是限制可以与基础字体不同的多边形数量。当一个多边形被“选中”时,字体可能会改变,导致文本的高度和宽度增加。在绘制控件时,会考虑到这一点,但这是逐个情况进行的。控件的总长度会发生变化,这就是为什么我必须添加二次间隔器来居中文本。如果我们不限制控件,那么诸如“如果 poly1 和 poly2 被选中,则最小长度为 X”之类的场景就必须各自运行,然后确定最长的长度。一个简单的答案可能是只需进行所有可能的选定与未选定的组合,并确定最高数字,就完成了。但是,这样对吗?不完全是,但对于政府工作来说已经足够了。

更好的着色。我在这里读了几篇文章,并且看到一个选项卡控件进行了一些数学计算来为选项卡着色,以使其具有那种闪亮或玻璃反射的外观和感觉。我也不喜欢只是从标准的 RGB 值中获取颜色,但这是这个控件的起点。使用更好看的颜色符合用户对美观和功能的需求。漂亮的东西比功能更容易销售,看看 Windows 和 Linux 就知道了 :D。

添加 Visual Studio 设计时控件功能。除了文本间距值等简单属性之外,我希望开发人员能够在设计时添加阶段。就像 DataGridView 控件一样。您可以将列添加到集合中,这在这里会很方便,这样您就可以添加阶段、颜色、文本等,而无需在运行时进行。我可以看到这个控件被静态地用于显示导入向导的步骤以及类似性质的内容,这些内容不一定需要在运行时动态添加/删除阶段。

全局设置值。我认为一些用户会从为该控件使用一组全局值中受益。如果用户可以简单地设置全局值,如背景颜色、选定背景颜色和边框颜色,而无需为每个多边形设置,这将加快控件从头开始的编码速度。虽然我的演示代码显示您可以独立设置所有这些项,但我当然可以看到一些情况,其中具有匹配的“选定”背景颜色、字体等的单个背景颜色将设置为整个控件的单一标准。

历史

  • 2007 年 3 月 - 首次发布。

版权所有 © 2007 Dan Morris。保留所有权利。未经我明确许可,请勿发布到其他网站。根据本网站的政策和程序链接到本文。

© . All rights reserved.