当控件位于Splitter Container中时,如何找到其相对于父窗体的位置?

4

情境


我正在开发一个用户控件,用于镜像目标窗口。它内部使用Win32 DWM API来注册/注销缩略图,并在父窗体调整大小或移动时更新缩略图的位置和位置。

这是我的应用程序的结构:

enter image description here

在“正常”的条件下(也就是在操作系统中使用默认的Windows主题时),我的应用程序/用户控件能够按预期工作。我录制了下面的视频来演示用户控件的用法和行为,以便您更好地了解所有这些内容并查看控件是否按预期工作:

问题


问题出现在我设法为操作系统使用不同的主题时,具体来说是任何添加非隐藏窗口边框的主题。例如使用第三方软件如WindowBlinds(使用名为“Flat Dark”的主题),也可以通过修改注册表中的一些窗口度量值来复制Windows 10中的可见边框添加,但是我不记得如何通过注册表实现这一点,抱歉。

好吧,当在Windows 10中设法使用带有非隐藏边框的窗口(通过上述第三方软件或其他可能的方法)时,我用户控件类内部的算法用于检索其相对于父窗体的坐标就会出问题,然后我得到意外的坐标,因此DWM缩略图未在正确的位置绘制。

我录制了下面的视频,您可以看到其中的差异并了解问题:

在视频中,首先我展示了程序在“正常”条件下运行的情况,然后我关闭了程序,更改了操作系统主题,再次运行程序,从这一点开始,您可以看到DWM缩略图没有在正确的位置绘制...

所有我的推测都表明我遇到的问题与我的Form的客户机/非客户机区域有关,当应用非隐藏的Windows 10边框时。

为什么我这样认为呢?因为如果我更改主题以使用带有可见边框的窗口,然后像这样删除我的窗体的边框:

this.FormBorderStyle = FormBorderStyle.None;

当我的窗体没有边框时,我的应用程序能够正常工作,因此在这种特定情况下,问题必须与窗体的客户端/非客户端区域有关。但是,在这些情况下计算控件的相对位置时,我不知道自己做错了什么,当窗体有边框时。

源代码


最后,我在这里分享完整的解决方案,其中包括我正在开发的用户控件以及演示应用程序(与上面的视频相同)。

请注意,源代码是用VB.NET编写的,但是这个事实与我在这个问题中标记的语言无关,因为我接受C#或VB.NET的任何解决方案,请不要因为问题中标记的语言与共享解决方案使用的语言不同而责备我。

不需要下载和检查源代码,所有源代码中唯一相关的部分是relativePos的坐标赋值,如下所示:

Public Class ElektroDwmThumbnail : Inherits UserControl

    Protected Function GetThumbnailRectangle() As Rectangle
        Dim relativePos As Point = Me.ParentForm.PointToClient(Me.PointToScreen(Point.Empty))
        ' ...
        Dim dstRectangle As New Rectangle(relativePos, thumbnailSize)
        Return dstRectangle
    End Function

End Class

这在C#中将是:

public class ElektroDwmThumbnail: UserControl {

    protected Rectangle GetThumbnailRectangle() {
        Point relativePos = this.ParentForm.PointToClient(this.PointToScreen(Point.Empty));
        // ...
        Rectangle dstRectangle = new Rectangle(relativePos, thumbnailSize);
        return dstRectangle;
    }

}

...在我所解释的情况下,它会给relativePos分配意外的坐标,这就是我需要解决并询问的问题,我需要高效(通用地)确定我的用户控件相对于父窗体的真实相对坐标,而不考虑父窗体窗口的边框大小...


1
WindowBlinds因引起此类问题而相当臭名昭著。在您的代码中无法解决这个问题。我只能想到两个基本的解决方法:确保添加一个清单来声明您的应用程序为dpiAware,如果dpi虚拟化是原因,则可以解决该问题。以及在Win10之前的Windows版本中必须执行的操作,如果更改了主题并且非客户端区域度量发生了变化,则建议注销并重新登录。首先尝试这样做。 - Hans Passant
@Hans Passant 感谢您的评论,但是我不确定如何将问题归因于WindowBlinds还是算法。我的意思是,当使用一个主题或另一个主题时,我的程序的坐标计算返回完全相同的坐标。如果这是可以归因于WindowBlinds的问题,那么按照这个规则,所有控件的位置都会在操作系统中变得混乱吗?(因为所有控件将返回相同的坐标,无论使用哪个主题),从逻辑上讲,这种情况并没有发生,只有当我尝试计算此特定控件的坐标时才会发生。 - ElektroStudios
除非我做错了什么,否则我通过Visual Studio的“新项目”菜单添加了清单文件,然后我声明它为dpiAware,我还尝试了gdiScaling属性,我将它们设置为感知和不感知,结果是相同的。重新登录用户会话的第二个解决方法不是原因。 - ElektroStudios
我想强调的是,这个问题只会在使用第三方主题时出现,但仅当我的表单有边框时,也就是说,如果我制作一个无边框的表单,那么坐标计算就能正常工作,我的DWM缩略图会被绘制在正确的位置。然后... 当我尝试进行坐标计算时,边框大小计算或其他方面可能存在问题或遗漏... 我不知道。 - ElektroStudios
2个回答

5

无论控件类型,父控件或控件在树中的位置有多深,下面是一个扩展方法,可以帮助您查找相对于宿主窗体的控件边界:

using System;
using System.Drawing;
using System.Windows.Forms;

public static class ControlExtensions
{
    public static Rectangle GetBoundsRelativeToForm(this Control c)
    {
        if (c == null)
            throw new ArgumentNullException(nameof(c));

        var form = c.FindForm();
        if (form == null)
            throw new InvalidOperationException("The control is not located on a form.");

        var parent = c.Parent;
        if (parent == null)
            throw new InvalidOperationException("The control does not have a parent.");

        var p = form.PointToClient(parent.PointToScreen(c.Location));
        return new Rectangle(p, c.Size);
    }
}

例如:

举个例子:

var r = textBox1.GetBoundsRelativeToForm();

我复现了这个问题,发现位置计算是正确的。但是DwmRegisterThumbnail假定整个窗口区域为客户端区域,实际上应该使用客户端区域。

我认为这是主题的问题,作为一个快速解决办法,我这样更正了位置:

Dim p0 As Point = Me.ParentForm.PointToScreen(Point.Empty)
Dim p1 As Point = Me.ParentForm.DesktopLocation
Dim relativePos As Point = Me.ParentForm.PointToClient(Me.PointToScreen(Point.Empty)) 
relativePos.X += (p0.X - p1.X)
relativePos.Y += (p0.Y - p1.Y) 

事实上,使用这段代码,我将边框宽度和标题栏高度添加到结果中。

评论不适合进行长时间的讨论;此对话已被移至聊天室 - Bhargav Rao
由于标准主题没有这个问题,很可能是第三方主题的问题。目前我不知道如何检测主题是否存在此类问题,因此只分享针对问题中提到的特定主题的解决方法。希望能有所帮助 :) - Reza Aghaei

1
以下示例代码将帮助您查找控件相对于窗体的位置。窗体可以是无边框或有边框。
将以下方法用作Control类型的扩展,或者只需从参数中删除此关键字以使其成为普通方法。
public static Point GetPositionInForm(this Control ctrl)
{
    Point p = ctrl.Location;
    Control parent = ctrl.Parent;
    Form frm = ctrl.FindForm();
    Rectangle screenRectangle = frm.RectangleToScreen(frm.ClientRectangle);
    int titleHeight = screenRectangle.Top - frm.Top;
    int leftMargin = screenRectangle.Left - frm.Left;
    p.Offset(leftMargin, titleHeight);

    while (!(parent is Form))
    {
        p.Offset(parent.Location.X, parent.Location.Y);
        parent = parent.Parent;
    }
    return p;
}

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