如何从另一个矩形中减去一个矩形?

11

我正在尝试确定桌面的工作区域,即使任务栏被隐藏。

我有两个矩形,一个是屏幕的边界,另一个是任务栏的边界。我需要从屏幕矩形中减去任务栏矩形,以确定桌面可用的工作区域。实际上,我想要的是Screen.WorkingArea,但当任务栏被隐藏时不应包括在内。

假设屏幕矩形是X,Y,W,H = 0,0,1680,1050,任务栏矩形是X,Y,W,H = 0,1010,1680,40。我需要从第一个矩形中减去第二个矩形,以确定工作区域为0,0,1680,1010

任务栏可以位于屏幕的任何一个侧面,我知道一定有比确定任务栏位置并使用单独的代码行为每个可能的位置生成新的矩形更好的方法。

12个回答

15
假设矩形2被包含在矩形1中(如果不是,则使用两个矩形的交集作为矩形2):
-------------------------
|      rectangle 1      |
|                       |
|     -------------     |
|     |rectangle 2|     |
|     -------------     |
|                       |
|                       |
-------------------------

如果你从矩形1中减去矩形2,你将得到一个带有洞的区域。
-------------------------
|                       |
|                       |
|     -------------     |
|     |    hole   |     |
|     -------------     |
|                       |
|                       |
-------------------------

这个区域可以分解成4个矩形:

-------------------------
|          A            |
|                       |
|-----------------------|
|  B  |   hole    |  C  |
|-----------------------|
|                       |
|          D            |
-------------------------

如果矩形1和矩形2有三条边重合,那么从被减去的区域中会得到1个矩形(这是您的情况)。一般情况下,最多会得到4个矩形。
Objective-C实现(抱歉,此时我没有Visual Studio):
// returns the rectangles which are part of rect1 but not part of rect2
NSArray* rectSubtract(CGRect rect1, CGRect rect2)
{
    if (CGRectIsEmpty(rect1)) {
        return @[];
    }
    CGRect intersectedRect = CGRectIntersection(rect1, rect2);

    // No intersection
    if (CGRectIsEmpty(intersectedRect)) {
        return @[[NSValue valueWithCGRect:rect1]];
    }

    NSMutableArray* results = [NSMutableArray new];

    CGRect remainder;
    CGRect subtractedArea;
    subtractedArea = rectBetween(rect1, intersectedRect, &remainder, CGRectMaxYEdge);

    if (!CGRectIsEmpty(subtractedArea)) {
        [results addObject:[NSValue valueWithCGRect:subtractedArea]];
    }

    subtractedArea = rectBetween(remainder, intersectedRect, &remainder, CGRectMinYEdge);
    if (!CGRectIsEmpty(subtractedArea)) {
        [results addObject:[NSValue valueWithCGRect:subtractedArea]];
    }

    subtractedArea = rectBetween(remainder, intersectedRect, &remainder, CGRectMaxXEdge);
    if (!CGRectIsEmpty(subtractedArea)) {
        [results addObject:[NSValue valueWithCGRect:subtractedArea]];
    }

    subtractedArea = rectBetween(remainder, intersectedRect, &remainder, CGRectMinXEdge);
    if (!CGRectIsEmpty(subtractedArea)) {
        [results addObject:[NSValue valueWithCGRect:subtractedArea]];
    }

    return results;
}

// returns the area between rect1 and rect2 along the edge
CGRect rectBetween(CGRect rect1, CGRect rect2, CGRect* remainder, CGRectEdge edge)
{
    CGRect intersectedRect = CGRectIntersection(rect1, rect2);
    if (CGRectIsEmpty(intersectedRect)) {
        return CGRectNull;
    }

    CGRect rect3;
    float chopAmount = 0;
    switch (edge) {
        case CGRectMaxYEdge:
            chopAmount = rect1.size.height - (intersectedRect.origin.y - rect1.origin.y);
            if (chopAmount > rect1.size.height) { chopAmount = rect1.size.height; }
            break;
        case CGRectMinYEdge:
            chopAmount = rect1.size.height - (CGRectGetMaxY(rect1) - CGRectGetMaxY(intersectedRect));
            if (chopAmount > rect1.size.height) { chopAmount = rect1.size.height; }
            break;
        case CGRectMaxXEdge:
            chopAmount = rect1.size.width - (intersectedRect.origin.x - rect1.origin.x);
            if (chopAmount > rect1.size.width) { chopAmount = rect1.size.width; }
            break;
        case CGRectMinXEdge:
            chopAmount = rect1.size.width - (CGRectGetMaxX(rect1) - CGRectGetMaxX(intersectedRect));
            if (chopAmount > rect1.size.width) { chopAmount = rect1.size.width; }
            break;
        default:
            break;
    }

    CGRectDivide(rect1, remainder, &rect3, chopAmount, edge);

    return rect3;
}

非常优雅的策略。这节省了我大量的时间。谢谢! - thedayturns

6

C#的简单解决方案

这是一个快速的、延迟执行的、易于理解的、跨平台的,并且可以轻松移植到其他语言的C#解决方案。

编辑:修正(subtrahend.IsEmpty)的情况,使代码不返回零面积的矩形。请查看评论以获取返回相邻矩形的方案。

    //-------------------------
    //|          A            |
    //|-----------------------|
    //|  B  |   hole    |  C  |
    //|-----------------------|
    //|          D            |
    //-------------------------
    public static IEnumerable<Rectangle> Subtract(this 
        Rectangle minuend,
        Rectangle subtrahend)
    {
        subtrahend.Intersect(minuend);
        //if (subtrahend.IsEmpty)
        //if (subtrahend.Size.IsEmpty)
        if ((subtrahend.Width | subtrahend.Height) == 0)
        {
            yield return minuend;
            yield break;
        }

        //A
        var heightA = subtrahend.Top - minuend.Top;
        if (heightA > 0)
            yield return new Rectangle(
                minuend.Left,
                minuend.Top,
                minuend.Width,
                heightA);

        //B
        var widthB = subtrahend.Left - minuend.Left;
        if (widthB > 0)
            yield return new Rectangle(
                minuend.Left,
                subtrahend.Top,
                widthB,
                subtrahend.Height);

        //C
        var widthC = minuend.Right - subtrahend.Right;
        if (widthC > 0)
            yield return new Rectangle(
                subtrahend.Right,
                subtrahend.Top,
                widthC,
                subtrahend.Height);

        //D
        var heightD = minuend.Bottom - subtrahend.Bottom;
        if (heightD > 0)
            yield return new Rectangle(
                minuend.Left,
                subtrahend.Bottom,
                minuend.Width,
                heightD);
    }

1
谢谢您的贡献,但是当回复一个已经有多个答案的老问题时,您应该提到您的解决方案与已经提出的答案相比有何不同/更好之处。 - Stanislas

2
你可以使用 System.Drawing.Region。它有一个 Exclude 方法,该方法以 System.Drawing.Rectangle 作为参数,并具有一些很棒的其他功能。
获取结果会稍微困难一些...我认为你只能使用区域来测试可见性,或者你可以获取其边界 - 但是你必须为此定义自己的 Graphics(我不知道在这里使用什么参数)。

http://msdn.microsoft.com/en-us/library/system.drawing.region_methods.aspx


2

这是一种简单的扩展,用于从彼此之间减去矩形。

public static Rectangle[] Subtract(this Rectangle source, Rectangle[] subtractions)
{
    Region tmp = new Region(source);

    foreach (var sub in subtractions)
    {
        tmp.Xor(sub);
    }
    Region src = new Region(source);
    src.Intersect(tmp);
    return src.GetRegionScans(new Matrix()).Select(Rectangle.Ceiling).ToArray();
}

1

除非矩形的三边重合,否则从一个矩形中减去另一个矩形将得到一个不是矩形的形状,因此“减去矩形”的一般解决方案并没有太多意义。

三边重合的解决方案:

给定矩形(Ax,Ay,Aw,Ah)和(Bx,By,Bw,Bh):

(max(Ax,Bx),max(Ay,By),min(Ax + Aw,Bx + Bw)- max(Ax,Bx),min(Ay + Ah,By + Bh)- max(Ay,By)

已编辑。


1
哎呀,这里有个问题,应该是交集而不是差集。 - Forrest Voight

1
我不确定是否有比你提到的方法更好的方法。问题在于一般情况下,从另一个矩形区域中减去一个矩形区域会在中间留下一个空洞,因此结果并不是真正的矩形。在您的情况下,您知道任务栏恰好适合屏幕矩形的一侧,因此“最佳”的方法确实是找出它是哪一侧,并从该侧减去宽度/高度。

我已经决定用这种方式来做了。我本来希望有一种更优雅的数学方法,但这种方法也完全可以。 - Chris Thompson
但我们可以利用这些知识以统一的方式计算工作区,无需事先进行任何测试(请参见我的答案)。 - d7samurai

1

从一个矩形中减去另一个矩形将产生一个矩形列表

正如Hai Feng Kao所说,减法的结果可以看作是一个List<Rect>

以下是C#的实现:

    public static List<Rect> Subtract(this Rect rect, Rect subtracted)
    {
        if (rect.HasNoArea())
        {
            return _emptyList;
        }
        if (rect.Equals(subtracted))
        {
            return new List<Rect>{new Rect(0, 0, 0, 0)};
        }
        Rect intersectedRect = rect;
        intersectedRect.Intersect(subtracted);
        if (intersectedRect.HasNoArea())
        {
            return new List<Rect> { rect };
        }
        List<Rect> results = new List<Rect>();
        var topRect = GetTopRect(rect, subtracted);
        if (!topRect.HasNoArea())
        {
            results.Add(topRect);
        }
        var leftRect = GetLeftRect(rect, subtracted);
        if (!leftRect.HasNoArea())
        {
            results.Add(leftRect);
        }
        var rightRect = GetRightRect(rect, subtracted);
        if (!rightRect.HasNoArea())
        {
            results.Add(rightRect);
        }
        var bottomRect = GetBottomRect(rect, subtracted);
        if (!bottomRect.HasNoArea())
        {
            results.Add(bottomRect);
        }
        return results;
    }

    public static bool HasNoArea(this Rect rect)
    {
        return rect.Height < tolerance || rect.Width < tolerance;
    }

    private static Rect GetTopRect(Rect rect, Rect subtracted)
    {
        return new Rect(rect.Left, rect.Top, rect.Width, Math.Max(subtracted.Top, 0));
    }

    private static Rect GetRightRect(Rect rect, Rect subtracted)
    {
        return new Rect(subtracted.Right, subtracted.Top, Math.Max(rect.Right - subtracted.Right, 0), subtracted.Height);
    }

    private static Rect GetLeftRect(Rect rect, Rect subtracted)
    {
        return new Rect(rect.Left, subtracted.Top, Math.Max(subtracted.Left - rect.Left, 0), subtracted.Height);
    }

    private static Rect GetBottomRect(Rect rect, Rect subtracted)
    {
        return new Rect(rect.Left, subtracted.Bottom, rect.Width, Math.Max(rect.Height - subtracted.Bottom, 0));
    }

请注意,Rect 是一个 struct,因此它是按值复制的。 公差可以是任何足够小的值(如10^-6)。
这些结果经过了多个单元测试的测试。
在性能方面,肯定有改进的空间。

1
受前面的答案启发,我想到了以下内容:
public Rectangle[] Subtract(Rectangle[] subtractions)
{// space to chop = this panel
    Region src = new Region(Bounds); 
    foreach (var sub in subtractions)
    {
        Region tmp = src.Clone();
        tmp.Xor(sub);
        src.Intersect(tmp);                
    }
    return src.GetRegionScans(new Matrix()).Select(Rectangle.Ceiling).ToArray();
}

祝您有美好的一天。

0
Rect CalcWorkArea(Rect scrn, Rect tbar)
{
    Rect work;

    work.X = (tbar.X + tbar.W) % scrn.W; 
    work.Y = (tbar.Y + tbar.H) % scrn.H; 
    work.W = work.X + scrn.W - (tbar.W % scrn.W); 
    work.H = work.Y + scrn.H - (tbar.H % scrn.H);

    return work;
}

0

我猜我对您试图推导什么感到困惑。.NET将桌面的当前工作区报告为显示器分辨率减去任务栏所占用的空间。

  • 如果任务栏没有隐藏,桌面的工作区域是整个显示器分辨率减去任务栏的大小。
  • 如果任务栏被设置成隐藏状态,桌面的工作区域是整个显示器分辨率。
  • 如果任务栏停靠在顶部或左侧,则桌面的工作区域是整个显示器分辨率减去任务栏的大小,并适当地在X/Y方向上移动。

确定屏幕的工作区已经在.NET中公开(包括任务栏停靠在顶部或左侧时的移位X/Y坐标)。

     // set the coordinate to the upper-left of the screen
     Rectangle r = Screen.GetWorkingArea(new Point(0, 0));

     // the resulting rectangle will show the deviation in X/Y
     // and also the dimensions of the desktop (minus the Taskbar)
     MessageBox.Show
     (
        r.ToString()
     );

是的,我知道。我需要知道在任务栏显示时可见的桌面区域,即使启用了自动隐藏。如果启用了自动隐藏并且任务栏正在显示,则WorkingArea仍然是整个桌面,我的弹出窗口会被任务栏遮盖。当它显示时,我需要将我的弹出窗口移动到任务栏上方,无论自动隐藏是否开启。 - Chris Thompson
好的,我不会问你为什么想要那种行为。但是你的回复更清楚了,无论任务栏的位置如何(即使在自动隐藏期间),你都希望在有效的工作区域内,如果任务栏被显示出来。我建议使用Win32调用(通过P/Invoke)两次切换任务栏的自动隐藏(从而将其恢复到原始设置),并取两个矩形中较小的一个。 - Michael

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