如何将用户窗体与当前单元格对齐?

13

我有一个UserForm,里面包含一个MonthView。当我点击指定范围内的单元格时,它就会打开。 这个SO线程 给了我基本的脚本,但是它没有将UserForm放在我期望的位置。

这是我放置在特定工作表中的脚本,用于在范围B3:C2000内单击任何单元格时打开UserForm。

Private Sub Worksheet_SelectionChange(ByVal Target As Range)
    Set oRange = Range("B3:C2000")
    If Not Intersect(Target, oRange) Is Nothing Then
        frmCalendar.Show
        frmCalendar.Top = ActiveCell.Offset(0, 0).Top
        frmCalendar.Left = ActiveCell.Offset(0, 1).Left
    End If
End Sub

问题1:我将UserForm的StartUpPosition属性设置为0 - Manual - 这样做正确吗?

问题2:在打开工作簿后第一次单击指定范围内的任何单元格时,UserForm总是在屏幕的左上角打开。为什么?

问题3:在单击指定范围内的任何单元格时,对于之后的任何单击,UserForm都相对于先前活动的单元格而不是我刚刚单击的单元格打开。如何使它相对于刚刚单击的单元格而不是相对于先前活动的单元格打开?

问题4:为什么看起来它会将UserForm的底部对齐而不是顶部?

执行以下步骤后:
1- 单击单元格C15
2- UserForm打开
3- 关闭UserForm
4- 单击单元格16
5- UserForm打开

这是我看到的:

原始结果

编辑:实施J. Garth的解决方案后(并将偏移属性更改为(0,2):

正确的结果


有些东西告诉我,两组坐标中的一组可能没有使用你所期望的单位。如果你的屏幕截图可以突出或指出你期望表单左上角与选定单元格的确切位置,那就太好了。 - Mathieu Guindon
3个回答

8

问题1:我将UserForm StartUpPosition属性设置为0 - 手动 - 这正确吗? 是的,这是正确的。在下面的代码中,我在代码中设置了此属性。

问题2:当我单击指定范围内的任何单元格时,在打开工作簿后第一次,UserForm总是打开在屏幕的最上方左侧。为什么? 我认为这个答案与问题#3有些相关。那似乎是表单要打开的默认位置。您现在的代码方式,尝试在Worksheet_SelectionChange事件中设置表单顶部和左侧坐标不起作用,因为实际上从未设置坐标。坐标的设置需要移动到用户窗体初始化事件中。

问题3:当我单击指定范围内的任何单元格时,在第一次之后的任何单击中,UserForm都会相对于先前活动的单元格打开,而不是我刚刚单击的单元格。如何使其相对于刚刚单击的单元格而不是相对于先前活动的单元格打开? 这个问题也与代码放错位置有关。如上所述,协调设置需要在用户窗体初始化事件中进行。至于为什么它引用以前的活动单元格,我的猜测是活动单元格实际上直到工作表选择更改事件完成之后才会更改。因此,由于您正在尝试在此事件中设置坐标(即 - 在事件完成之前),因此您将获得先前的活动单元格。同样,将代码移动到正确位置可以解决此问题。

问题4:为什么它似乎对齐UserForm的底部而不是顶部? 在涉及单元格(范围)和用户窗体时,似乎存在“顶部”定义的差异。单元格的顶部是从第一行开始测量的,而用户窗体的顶部似乎是从Excel应用程序的顶部开始测量的。因此,在其他单词中,如果activecell.top和userform.top都等于144,则它们将位于屏幕上的不同位置。这是因为activecell的顶部距离Excel电子表格中的第一行向下144个点,而userform的顶部距离Excel应用程序的顶部向下144个点(即 - Excel窗口的顶部),它比activecell.top的起始点(电子表格中的第一行)更高。我们可以通过添加用户窗体的高度加上活动单元格的高度来调整它。

Sheet模块代码

Private Sub Worksheet_SelectionChange(ByVal target As Range)

    Dim oRange As Range

    Set oRange = Range("B3:C2000")
    If Not Intersect(target, oRange) Is Nothing Then
        frmCalendar.Show
    End If

End Sub

用户表单代码

Private Sub UserForm_Initialize()

    With Me
        .StartUpPosition = 0
        .Top = ActiveCell.Top + ActiveCell.Height + .Height
        .Left = ActiveCell.Offset(0, 1).Left
    End With

End Sub

1
太好了!我对VBA非常陌生——我可以阅读代码并通常能跟上,但是理解初始化事件的重要性或时机这样的东西仍在开发中。我还在学习如何知道将每个代码片段放在哪里。但是你的解释非常清晰,帮助我更好地理解正在发生的事情。 - Trevor D
1
@YinCognyto 这些都是很好的观点。然而,我对SO的理解是它是一个获取特定编码问题答案的地方。它不意味着成为一个“考虑我的所有业务用例,并编写完整代码来处理它们”的网站。所提出的问题已经得到了解决,并且提供的解决方案足够说明用户表单和定位如何工作,从而使某人能够进行一些研究并编写进一步处理各种情况的代码。如果有人在尝试扩展代码时遇到困难,我想他们可以打开一个新主题并参考此页面并寻求帮助。 - J. Garth
1
@J. Garth,您说得对。我将提供一个答案,涵盖有关此类问题的几乎所有内容,即使不是为了彻底,至少也是因为我自己花了相当长的时间从众多来源收集代码和有用信息,直到我成功地组装出一个完整的解决方案,用于我的工作簿。拥有这样一个每次都有效的解决方案将是很好的,这样其他用户就不必浏览数十页并为每一页上的数学问题进行计算 - 考虑到该主题的信息稀缺性。 - Yin Cognyto
@YinCognyto 太棒了。我认为那会非常有帮助。 - J. Garth
@YinCognyto 谢谢你对此事的关注。当人们考虑到这些事情时,感觉真好。:) 还要感谢你抽出时间来扩展这个话题。我认为这将有助于很多人,因为这似乎是一个难以正确实施的领域,如果用户创建了你列举的任何条件,然后遇到问题,你可能会帮他们省去重新工作的麻烦。 - J. Garth
显示剩余2条评论

7
J. Garth的答案解释得很好,但正如我在评论中提到的,虽然它适用于此特定情况,但在其他情况下(例如缩放级别更改、目标范围在表格初始可见范围之外的拆分/冻结窗格),它会失败。更不用说它没有考虑标题行/列(也受缩放级别更改影响)以及在设置位置时包围表单的3D“边框/框架”。
我花了几天时间寻找一个完整的答案来覆盖所有可能性,而几乎在所有情况下都将表单的位置设置得非常接近正确位置的唯一答案是由nerv编写的这个,作为这个讨论在MSDN论坛上的结果-很明显,大部分功劳归于他。我将其与其他来自各种其他来源的信息和代码“合并”以避免硬编码变量,使代码32位和64位兼容,并覆盖神秘的包围表单的3D框架问题。 表格代码
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
    UserForm1.Show
End Sub

用户窗体代码

Private Sub UserForm_Initialize()
  Dim pointcoordinates As pointcoordinatestype, horizontaloffsetinpoints As Double, verticaloffsetinpoints As Double
    With Me
        horizontaloffsetinpoints = (.Width - .InsideWidth) / 2
        verticaloffsetinpoints = 1 
        Call GetPointCoordinates(ActiveCell, pointcoordinates)
        .StartUpPosition = 0
        .Top = pointcoordinates.Top - verticaloffsetinpoints
        .Left = pointcoordinates.Left - horizontaloffsetinpoints
    End With
End Sub

模块代码

Private Const LOGPIXELSX = 88
Private Const LOGPIXELSY = 90
Public Type pointcoordinatestype
    Left As Double
    Top As Double
    Right As Double
    Bottom As Double
End Type
Private pixelsperinchx As Long, pixelsperinchy As Long, pointsperinch As Long, zoomratio As Double
#If VBA7 And Win64 Then
    Private Declare PtrSafe Function GetDeviceCaps Lib "gdi32" (ByVal hdc As LongPtr, ByVal nIndex As Long) As Long
    Private Declare PtrSafe Function GetDC Lib "user32" (ByVal hwnd As LongPtr) As LongPtr
    Private Declare PtrSafe Function ReleaseDC Lib "user32" (ByVal hwnd As LongPtr, ByVal hdc As LongPtr) As Long
#Else
    Private Declare Function GetDeviceCaps Lib "gdi32" (ByVal hdc As Long, ByVal nIndex As Long) As Long
    Private Declare Function GetDC Lib "user32" (ByVal hwnd As Long) As Long
    Private Declare Function ReleaseDC Lib "user32" (ByVal hwnd As Long, ByVal hdc As Long) As Long
#End If

Private Sub ConvertUnits()
  Dim hdc As LongPtr
    hdc = GetDC(0)
    pixelsperinchx = GetDeviceCaps(hdc, LOGPIXELSX) ' Usually 96
    pixelsperinchy = GetDeviceCaps(hdc, LOGPIXELSY) ' Usually 96
    ReleaseDC 0, hdc
    pointsperinch = Application.InchesToPoints(1)   ' Usually 72
    zoomratio = ActiveWindow.Zoom / 100
End Sub

Private Function PixelsToPointsX(ByVal pixels As Long) As Double
    PixelsToPointsX = pixels / pixelsperinchx * pointsperinch
End Function

Private Function PixelsToPointsY(ByVal pixels As Long) As Double
    PixelsToPointsY = pixels / pixelsperinchy * pointsperinch
End Function

Private Function PointsToPixelsX(ByVal points As Double) As Long
    PointsToPixelsX = points / pointsperinch * pixelsperinchx
End Function

Private Function PointsToPixelsY(ByVal points As Double) As Long
    PointsToPixelsY = points / pointsperinch * pixelsperinchy
End Function

Public Sub GetPointCoordinates(ByVal cellrange As Range, ByRef pointcoordinates As pointcoordinatestype)
  Dim i As Long
    ConvertUnits
    Set cellrange = cellrange.MergeArea
    For i = 1 To ActiveWindow.Panes.Count
        If Not Intersect(cellrange, ActiveWindow.Panes(i).VisibleRange) Is Nothing Then
            pointcoordinates.Left = PixelsToPointsX(ActiveWindow.Panes(i).PointsToScreenPixelsX(cellrange.Left))
            pointcoordinates.Top = PixelsToPointsY(ActiveWindow.Panes(i).PointsToScreenPixelsY(cellrange.Top))
            pointcoordinates.Right = pointcoordinates.Left + cellrange.Width * zoomratio
            pointcoordinates.Bottom = pointcoordinates.Top + cellrange.Height * zoomratio
            Exit Sub
        End If
    Next
End Sub

以上大部分内容都不需要解释,它们都运作得非常完美 - 至少根据我所测试的。唯一仍然让我有点困扰的是,由于某种原因,表单框架在奇数行上(即它比所需单元格网线低1像素)并不完全符合要求,而在偶数行上则一切顺利。如果有人能找出原因,请与我分享这个谜团,因为我怀疑这不是一个简单的舍入问题...

编辑: 今天在使用计时器时,我想到了如何避免上面发生的奇偶行之间的差异:这只是一个声明点值和输出(以及缩放比例)As Double(即浮点数)而不是As Long(即整数)。这是我的愚蠢错误 - 我已经正确地编辑了代码来纠正它。我添加了一个verticaloffsetinpoints变量来调整那个奇怪的(但这次一致的)“比预期低1像素”的垂直故障,但我还没有找到解释。


很棒的第二个答案,对于有关发布什么内容的讨论,你做了出色的教学评论。我喜欢你引用了重要来源的方式,通常会复制足够的代码以直接进行解释,但在你的情况下,你编写了新的代码,并清晰地向读者展示了。赞扬OP提出了清晰的问题。@YinCognyto - ElderDelp
还要向第一个答案致敬!@J. Garth - ElderDelp
@ElderDelp 很高兴你喜欢它,如果它对你有所帮助那就更好了 ;) 我认为,提供免费工作成果的人最起码应该得到适当的荣誉,这也是引用参考文献的原因。至于详细,这也是因为我自己需要正确的代码,在我的工作簿中,我有冻结窗格、一个 InkPicture 控件来检测缩放级别的变化,等等,因此相应地重新定位表单非常关键。 - Yin Cognyto
这篇文章太棒了!非常感谢,当然还要感谢nerv。令人惊讶的是,他原始的代码带有硬编码值,却非常简单,只用了普通的VBA而没有WinAPI。将代码存储在自定义的UserForm类中以实现更方便的重用效果会更好。祝好! - Rafał B.
@RafałB。感谢您的赞赏,当然您是对的。原始代码对我也非常有帮助,但由于我的情况具有特殊性,我不得不进行改进和添加。它肯定能够完成工作,这是肯定的。 - Yin Cognyto

0
通过在Module1中声明GetDeviceCaps、GetDC和ReleaseDC函数,我将用户窗体与单击的活动单元格对齐。(我已经检查了在32位和64位版本的Excel中的代码)

enter image description here

Type POINTAPI
    X As Long
    Y As Long
End Type

#If VBA7 Then
    Declare PtrSafe Function GetDeviceCaps Lib "gdi32" (ByVal hDc As LongPtr, ByVal nIndex As Long) As Long
    Declare PtrSafe Function GetDC Lib "user32" (ByVal hwnd As LongPtr) As LongPtr
    Declare PtrSafe Function ReleaseDC Lib "user32" (ByVal hwnd As LongPtr, ByVal hDc As LongPtr) As Long
    Dim hDc As LongPtr
#Else
    Declare Function GetDeviceCaps Lib "gdi32" (ByVal hDc As Long, ByVal nIndex As Long) As Long
    Declare Function GetDC Lib "user32" (ByVal hwnd As Long) As Long
    Declare Function ReleaseDC Lib "user32" (ByVal hwnd As Long, ByVal hDc As Long) As Long
    Dim hDc As Long
#End If
...

代码和示例文件的来源


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