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

创建你自己的运行时可移动 Windows 窗体控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.47/5 (48投票s)

2006年7月11日

CPOL

14分钟阅读

viewsIcon

402986

downloadIcon

5577

一个关于构建用户可以在运行时移动的 Windows 窗体标签控件的教程。

Control testing sandbox screenshot

引言

这些问题已经被问过不止一次了:“我怎么才能创建自己的控件?”或者“我怎么才能创建一个可以在运行时移动的标签控件?”这两个问题的答案都非常简单!我将带您一步一步地创建自己的控件,一个不仅在设计时,而且在运行时都可以移动的标签控件。我将向您展示如何使用现有的 Label 控件并在此基础上进行扩展。我还将向您展示如何构建一个应用程序来测试您的新控件,并观察控件的构建过程。但是,在我们深入细节之前,我们需要一些规范。毕竟,如果我们不知道这个新的 MovableLabel 控件应该做什么,我们怎么能给它添加任何代码呢?所以,我们想要什么,更重要的是,我们“需要”这个东西做什么?

一些规范

  • 首先,我们希望它具有标准 Label 控件提供的所有功能。我们只是增加了通过鼠标在运行时移动它的能力。创建自定义控件的头号规则是,如果已经存在一个控件具备您需要的大部分功能,那就继承它!
  • 我们需要在鼠标按钮按下并停留在我们的 Label 控件上时移动它。
  • 我们需要某种指示来表明当鼠标悬停在控件上时,该控件是可移动的。
  • 当鼠标按钮被按下时,我们还需要更改鼠标光标,以向用户表明我们正处于“移动”模式。

开始编码

继承自 Label

我们列表上的第一件事是,将标准 Label 控件的所有功能引入到我们的 MovableLabel 控件中。首先创建一个新的 Windows 类库项目,并将其命名为 Movable Controls Library。在解决方案资源管理器中,将 Class1.vb 文件重命名为 MovableLabel.vb。这将允许您创建自己的运行时可移动控件库。您所要做的就是为 Movable Controls Library 项目添加另一个类,以表示您将要创建的每个控件。

现在,由于我们将创建一个 Windows 窗体控件,我们需要导入一些内容。System.Windows.Forms 命名空间为我们提供了对标准 Windows 窗体 Label 控件的访问,以及与 Windows 窗体交互所需的所有小组件。要添加此项,请转到“项目”菜单,然后单击“添加引用...”。确保选择了“.NET”选项卡,然后在列表中向下滚动找到 System.Windows.Forms.dll 文件。双击它以添加引用。

我们从鼠标事件参数中获取的鼠标坐标都将以 Point 结构的形式传递。因此,我们将需要 System.Drawing 命名空间来访问 Point 结构。执行相同的步骤,添加对 System.Drawing.dll 文件的引用。

在完成了这些引用之后,我们只需要稍微修改代码,导入这两个 DLL 文件公开的命名空间,并告诉我们的 MovableLabel 控件它继承自哪个控件类。

Imports System.Windows.Forms
Imports System.Drawing

Public Class MovableLabel
    Inherits System.Windows.Forms.Label

End Class

就这些!现在我们有了一个类,它的功能与标准的 Label 控件完全相同。但是,我们无法这样测试它。如果您尝试按 [F5] 启动项目,您会收到一条错误消息,提示“输出类型为类库的项目无法直接启动...”类库需要有东西来实例化它才能进行测试。我们将不得不向我们的解决方案中添加第二个项目来测试我们的新控件。

创建沙盒

什么是“沙盒”?我将一个小的应用程序称为“沙盒”,用于测试类库或某些其他代码想法的功能。由于我们正在创建一个新的 Windows 窗体控件,我们将需要一个 Windows 窗体应用程序来测试我们的控件。转到“文件”菜单,单击“添加...”,然后单击“新建项目...”。选择“VB.NET Windows 应用程序”,并将其命名为“Movable Controls Sandbox”。这将向我们的解决方案中添加第二个项目。请注意,在解决方案资源管理器中,新项目名称会比我们其他项目名称加粗。这意味着这是启动项目。当您按 [F5] 开始运行代码的调试器时,这个项目将首先启动。如果出于任何原因需要更改启动项目,只需在解决方案资源管理器中右键单击项目名称,然后在弹出的上下文菜单中单击“设置为启动项目”。

沙盒项目需要进行一些修改,以便我们在运行它时知道我们在看什么。在解决方案资源管理器中,找到名为“Form1.vb”的文件并将其重命名为“MainForm.vb”。然后双击新文件名以在设计器中打开窗体。在解决方案资源管理器下方的属性窗格中,找到“外观”部分下的 Text 属性,并将其更改为“Movable Controls Sandbox”。

现在,我们需要将我们的新控件的一个实例放置在窗体上,以便到目前为止可以看到它是否正常工作。如果您还没有这样做,请按 [Ctrl]+[Shift]+B 来生成解决方案。这将检查解决方案中的所有项目是否已编译为可执行代码,并与源代码的最新更改保持同步。它还将把我们的新控件添加到设计器窗口左侧的工具箱中。打开工具箱,找到并展开名为“Movable Controls Library”的部分。其中,您应该会找到 MovableLabel 控件。双击工具箱中的 MovableLabel 控件。设计器应将控件的一个新实例添加到窗体中。将控件移动到窗体大约中间的位置。然后,查看解决方案资源管理器下的属性窗格。请注意,您在普通 Label 控件中找到的所有属性都在这里。我们有一个可以工作的 Label 控件,只是类名不同!

为了以后方便起见,找到 Text 属性并将其更改为“This is a runtime movable label control”。然后,找到属性列表底部附近的 AutoSize 属性,并将其更改为“False”。我们将把控件调整到窗体上一个更大的尺寸,以便单击和拖动。所以抓住我们新控件的一个角柄,将其拖动,直到控件的轮廓大致呈方形,并且您可以在框中阅读完整的文本。现在,通过添加一些颜色并将文本居中,使 MovableLabel 看起来更漂亮一些。

找到 BackColor 属性,在其值框中键入“Green”,然后找到 ForeColor 并将其更改为“White”。将 BorderStyle 更改为 FixedSingle,并将 TextAlign 属性更改为 MiddleCenter。您应该得到一个看起来像这样的结果

Sandbox application screenshot

现在按 [F5] 运行沙盒应用程序。您会发现我们的 MovableLabel 看起来和感觉都与其他任何 Label 控件一样——静态且无聊!让我们开始为它添加一些新功能!单击窗体上的小“X”来关闭沙盒应用程序并停止调试器。

用户反馈

静态模式

我们的 MovableLabel 需要能够向用户提供某种反馈。由于我们的控件有两种模式:Static(静态)和 Movable(可移动),我们应该向用户提供一些反馈,让他们知道控件处于什么模式。

Static 模式是指控件静止不动,等待鼠标执行我们感兴趣的操作。在这种情况下,Static 模式应监听 MouseDown 事件。此事件的触发将促使控件切换到 Movable 模式。在我们等待此事件发生时,我们应该通过将鼠标指针移到我们的控件上来设置光标,例如指向性手形,以向用户提供我们在 Static 模式下等待的提示。由于我们继承的 Label 控件已经公开了 Cursor 属性,它就可以做到这一点,我们所要做的就是在我们控件的构造函数中设置 Cursor 的值。这样,每一个 MovableLabel 实例都将在添加到窗体的那一刻初始化自己以显示此手形光标。单击编辑器窗口中的“MovableLabel.vb”选项卡,并在 Inherits 行和 End Class 之间添加以下代码。

    Public Sub New()
        ' Call the constructor of the base Label class to properly
        ' initialize our control
        MyBase.New()
        ' Setup the Cursor property so that, by default, when the
        ' mouse passes over our control, the cursor changes to a hand
        Me.Cursor = Cursors.Hand
    End Sub

现在按 [F5] 查看结果。您应该会发现,每当鼠标指针移到 MovableLabel 上方时,光标就会变成手形指针,当鼠标移开控件时,光标又会恢复原状。

此实现方式**不会**剥夺用户在 Static 模式下将 Cursor 属性更改为他们喜欢的任何光标的能力。它只是设置了在设计模式下未使用指定光标时的**默认**光标。试试看。您只需将另一个 MovableLabel 控件拖到窗体上,然后在属性窗格中查看 Cursor 属性。请注意,它不再显示“Default”(默认)。我们继续...

继承背景介绍

那个 MyBase.New() 行是怎么回事?那行在那里是因为我们使用了从 Label 类继承而来的所有代码、属性和方法。Label 类已经变成了我们的奴隶。我们将 Label 类包装在我们自己的代码层中,并通过该代码层来操作 Label。但它仍然是一个单独的类,有其自身的功能规则和功能需求。因此,所有 Windows 窗体控件都有一个 InitializeComponent() 方法,当它们被实例化时会被调用。此调用几乎总是在控件的构造函数或 New 方法中出现。由于 Label 类几乎完成了我们需要完成的所有工作,因此我们必须告诉 Label 正确初始化自身,以便控件的其余部分能够正常工作。我们所做的就是调用我们继承的类,也就是所谓的“基类”的构造函数。在 VB.NET 语法中,MyBase 始终指向我们继承的基类。因此,要调用基类的构造函数,我们使用 MyBase.New()

可移动模式

这里是真正开始为这个控件添加代码的地方。我们需要添加支持,以便在我们的控件上按下鼠标左键时更改鼠标指针,并在释放鼠标按钮时将其恢复为原来的样子。这可以通过处理 Label 类公开的 MouseDownMouseUp 事件轻松完成。

首先,我们需要在类中添加一个字段,该字段用于跟踪我们 Label 控件当前 Cursor 的值。当 MouseDown 事件触发时,我们将设置此值,然后在 MouseUp 事件触发时用它来恢复 Cursor。在 Inherits 行和 Public Sub New() 行之间添加以下代码。

    ' Used to store the current cursor shape before we start
    ' to move our control
    Private m_CurrentCursor As Cursor

现在我们必须添加代码以在适当的时间更改光标。记住,我们正在控制一个包装好的 Label 类,所以它将通过 MyBase 标识符引发我们看到的所有事件。我们需要在按下鼠标按钮时保存当前的光标状态,更改光标以表示我们已进入 Movable 模式,然后在释放鼠标按钮时将其恢复为我们找到的状态。

要添加 MouseDownMouseUp 事件处理程序,建议从编辑器窗口顶部的两个下拉列表中选择它们。这是为了确保您正确地处理了事件参数签名。如果它们不匹配,您将在编译代码时遇到问题。在左侧的下拉列表中,选择“(MovableLabel Events)”,旁边有一个小闪电图标。这将仅显示我们类可用的事件。在右侧的下拉列表中,选择 MouseDown。这将自动生成一个空的事件处理程序,并为该事件提供适当的参数签名。对 MouseUp 执行相同的操作。用以下代码填充空的处理程序。

    Private Sub MovableLabel_MouseDown( _
                ByVal sender As Object, _
                ByVal e As System.Windows.Forms.MouseEventArgs) _
                Handles MyBase.MouseDown
        ' Check to see if the correct button has been pressed
        If e.Button = Windows.Forms.MouseButtons.Left Then
            ' If so, the save the current cursor and
            m_CurrentCursor = MyBase.Cursor
            ' replace it with our new image that says were in Movable mode
            MyBase.Cursor = Cursors.SizeAll
        End If
    End Sub

    Private Sub MovableLabel_MouseUp( _
                ByVal sender As Object, _
                ByVal e As System.Windows.Forms.MouseEventArgs) _
                Handles MyBase.MouseUp
        ' The button was released, so we're going back to Static mode.
        ' Restore the cursor image to the way we found it when the mouse
        ' button was pressed
        MyBase.Cursor = m_CurrentCursor
    End Sub

按 [F5] 查看您的成果。确保将鼠标按钮按在 MovableLabel 控件上,并观察发生了什么。

跟踪鼠标

好的。现在,我们开始实际让 MovableLabel 移动!我将从一个简短的解释开始,说明我们将如何根据鼠标位置计算标签的新位置。

Control Math Layout

当用户在我们的控件上按下鼠标按钮时,会触发 MouseDown 事件。在 MouseEventArgs 中,我们将获得鼠标相对于我们控件左上角(点 2)的位置(点 3)。由于我们希望鼠标指针在移动过程中保持在控件内的这个位置,因此我们需要存储在这里获取的位置。稍后我们将使用它来维护控件与鼠标位置之间的这种关系。

计算控件位置时,我们需要鼠标相对于我们控件的父容器(在本例中为 MainForm 客户端区域)的位置(点 1)。在我们将要获得的 MouseEventArgs 中,我们仍然会获得鼠标在移动控件内的位置,但这对我们没有用。我们需要将该位置转换为父控件内的点。我们通过调用我们控件的父容器的方法 PointToClient 来实现这一点。Label 类以及任何继承自 Control 的类都公开了一个 Parent 属性,该属性返回一个对持有我们控件的任何控件容器的引用,或者 ParentPointToClient 方法会将任何相对于其子控件(点 2)内部的点(点 3)转换为相对于其自身左上角(点 1)的相同点。

现在我们有了计算控件新位置所需的两个信息(点 2)。Point2 = (Point3 相对于 Point1) - (Point 3 相对于 Point 2),或者 Point2 = (Point3 相对于 Point1) - (从 MouseDown 事件保存的坐标)。

向代码添加一个 MouseMove 事件处理程序。现在,只要鼠标在我们的控件上并且移动了哪怕一个像素,都会触发此事件,无论鼠标按钮是否按下。我们不希望在 Static 模式下执行任何操作,因此我们需要一个标志在此事件处理程序中检查以查看我们处于什么模式。如果我们处于 Movable 模式,我们需要计算控件的新位置。如果我们处于 Static 模式,我们什么也不做,让事件处理程序终止。所以,让我们将这个标志添加到我们的类字段中。将类代码顶部的字段代码更改为如下所示:

    ' Used to store the current cursor shape when we start
    ' to move the control
    Private m_CurrentCursor As Cursor
    ' Holds the mouse position relative to the inside of
    ' our control when the mouse button goes down
    Private m_CursorOffset As Point
    ' Used by the MoveMove event handler to show that the
    ' setup to move the control has completed
    Private m_Moving As Boolean

已设置移动信号标志,但现在我们需要在适当的地方管理该标志的状态。由于我们的 Movable 模式只能在 MouseDown 事件内进入,因此 m_Moving 标志应在此事件中设置为 True。修改 MouseDown 事件代码,如下所示。我们还将添加在事件触发时保存鼠标在控件内位置的代码。

    Private Sub MovableLabel_MouseDown( _
                ByVal sender As Object, _
                ByVal e As System.Windows.Forms.MouseEventArgs) _
                Handles MyBase.MouseDown
        ' Check to see if the correct button has been pressed
        If e.Button = Windows.Forms.MouseButtons.Left Then
            ' If so, save the current cursor and
            m_CurrentCursor = MyBase.Cursor

            ' replace it with our new image that says were in Movable mode
            MyBase.Cursor = Cursors.SizeAll

            ' Save the location of the mouse pointer relative to the top-left
            ' corner of our control
            m_CursorOffset = e.Location

            ' Set the mode flag to signal the MouseMove event handler that it
            ' needs to now calculate new positions for our control
            m_Moving = True
        End If
    End Sub

修改 MouseUp 事件处理程序,使其如下所示:

    Private Sub MovableLabel_MouseUp( _
                ByVal sender As Object, _
                ByVal e As System.Windows.Forms.MouseEventArgs) _
                Handles MyBase.MouseUp
        ' The button was released, so we're going back to Static mode.
        m_Moving = False

        ' Restore the cursor image to the way we found it when the mouse
        ' button was pressed
        MyBase.Cursor = m_CurrentCursor
    End Sub

快完成了!填充 MouseMove 事件处理程序:

    Private Sub MovableLabel_MouseMove( _
                ByVal sender As Object, _
                ByVal e As System.Windows.Forms.MouseEventArgs) _
                Handles MyBase.MouseMove
        ' Check which mode we're in. If we're supposed to be moving
        ' our control
        If m_Moving Then
            ' get the screen position of the mouse pointer and map it
            ' to the position relative to the top-left corner of our
            ' parent container
            Dim clientPosition As Point = _
                MyBase.Parent.PointToClient
                (System.Windows.Forms.Cursor.Position)

            ' Calculate the new position of our control, maintaining
            ' the relative position stored by the MoveDown event
            Dim adjustedLocation As New Point( _
                clientPosition.X - m_CursorOffset.X, _
                clientPosition.Y - m_CursorOffset.Y)

            ' Set the new position of our control
            MyBase.Location = adjustedLocation
        End If
    End Sub

按 [F5] 运行它,看看您是否能移动您新的 MovableLabel 控件!

未来扩展

本文构建的代码是文章顶部下载中提供的解决方案的简化版本。下载中的 MovableLabel 类包含大量附加功能,例如:

  • 支持停留在父容器的边界内
  • 在调整父容器大小时保持边界限制
  • 添加一个属性来打开和关闭父容器边界支持
  • 公开 ValueChanged 事件并维护该事件订阅者列表
  • 支持在设计时将控件从一个父容器移动到另一个父容器
  • 设计器序列化支持,用于在设计时维护属性值
  • 设置新的属性默认值

本文中编写的完整的 MovableControl.vb 文件

Imports System.Windows.Forms
Imports System.Drawing

Public Class MovableLabel
    Inherits System.Windows.Forms.Label

    ' Used to store the current cursor shape when we start
    ' to move the control
    Private m_CurrentCursor As Cursor

    ' Holds the mouse position relative to the inside of
    ' our control when the mouse button goes down
    Private m_CursorOffset As Point

    ' Used by the MoveMove event handler to show that the
    ' setup to move the control has completed
    Private m_Moving As Boolean

    Public Sub New()
        ' Call the constructor of the base Label class to properly
        ' initialize our control
        MyBase.New()
        ' Setup the Cursor property so that, by default, when the
        ' mouse passes over our control, the cursor changes to a hand
        Me.Cursor = Cursors.Hand
    End Sub

    Private Sub MovableLabel_MouseDown( _
                ByVal sender As Object, _
                ByVal e As System.Windows.Forms.MouseEventArgs) _
                Handles MyBase.MouseDown
        ' Check to see if the correct button has been pressed
        If e.Button = Windows.Forms.MouseButtons.Left Then
            ' If so, save the current cursor and
            m_CurrentCursor = MyBase.Cursor

            ' replace it with our new image that says were in Movable mode
            MyBase.Cursor = Cursors.SizeAll

            ' Save the location of the mouse pointer relative to the top-left
            ' corner of our control
            m_CursorOffset = e.Location

            ' Set the mode flag to signal the MouseMove event handler that it
            ' needs to now calculate new positions for our control
            m_Moving = True
        End If
    End Sub

    Private Sub MovableLabel_MouseMove( _
                ByVal sender As Object, _
                ByVal e As System.Windows.Forms.MouseEventArgs) _
                Handles MyBase.MouseMove
        ' Check which mode we're in. If we're supposed to be moving
        ' our control
        If m_Moving Then
            ' get the screen position of the mouse pointer and map it
            ' to the position relative to the top-left corner of our
            ' parent container
            Dim clientPosition As Point = _
                MyBase.Parent.PointToClient
                    (System.Windows.Forms.Cursor.Position)

            ' Calculate the new position of our control, maintaining
            ' the relative position stored by the MoveDown event
            Dim adjustedLocation As New Point( _
                clientPosition.X - m_CursorOffset.X, _
                clientPosition.Y - m_CursorOffset.Y)

            ' Set the new position of our control
            MyBase.Location = adjustedLocation
        End If
    End Sub

    Private Sub MovableLabel_MouseUp( _
                ByVal sender As Object, _
                ByVal e As System.Windows.Forms.MouseEventArgs) _
                Handles MyBase.MouseUp
        ' The button was released, so we're going back to Static mode.
        m_Moving = False

        ' Restore the cursor image to the way we found it when the mouse
        ' button was pressed
        MyBase.Cursor = m_CurrentCursor
    End Sub

End Class

本文专门为 The Code Project 撰写。如果您在任何其他网站上发现本文(部分或全部),这是公然的版权侵犯。请通过 webmaster@codeproject.com 通知 CodeProject 员工。谢谢。

© . All rights reserved.