NSImageView 总是显示在任何其他类型的视图之上

NSImageView always displays on top of any other kind of view

我正在开发一个数据库应用程序,它有一个与 Interface Builder 非常相似的图形编辑器。然而,与 IB 不同的是,这个编辑器可以从编辑器模式切换到实时模式,用户界面是完全可操作的(可以单击按钮、编辑文本等)。为此,图形编辑器使用标准的 Appkit 界面 类 -- NSButton、NSTextView 等。编辑器本身是用 NSView 的自定义子类实现的。所有的用户界面元素都是这个自定义 NSView 的子视图,使用 addSubview: 方法添加新元素,使新元素成为最顶层的可见元素(注意——视图不是层支持的,只是常规视图) .用户还可以使用 Bring-to-Front 和 Send-to-Back 命令来更改子视图的顺序。这部电影展示了两个重叠的 NSButton 元素(出于说明目的,当然通常情况下你永远不会重叠它们),以及程序如何重新排列子视图以更改用户界面元素的 Z 顺序。

问题是,这适用于各种界面元素 除了 NSImageView。在之前的影片中有两个元素,一个 NSButton 和一个 NSImageView。 NSButton实际上一直"on top",NSImageView元素应该出现在按钮后面,但是无论子视图的顺序如何,NSImageView总是出现在最上面。

如果有两个重叠的 NSImageView 对象,它们之间的可见堆叠顺序是不可预测的,但它们总是会出现在所有其他对象之上,无论子视图的顺序是什么。

一个可能有用的线索是,如果我实现自己的自定义视图,直接在它的 drawRect: 方法中绘制图像,就可以正常工作。所以这是一个可能的解决方案,但我不愿意,因为这意味着重新实现 NSImageView 通常会处理的大量有用功能,其中一些非常复杂,比如支持动画 GIF 显示。除了这个 layering/z-order 问题,NSImageView 的其他一切都正常。

也许 NSImageView 在我没有要求的情况下使用了图层支持,所以它没有与我的其他对象正确混合?我找不到任何表明这一点的文件。我没有链接到 QuartzCore 框架。

下面是将 NSImageView 元素作为子视图添加到图形编辑器视图的代码。

- (void)objectDidAppearBelow:(NSView *)nextView
{
    FormView * formView = [FormWindowController currentFormView]; // get view element will be placed into
    NSScrollView * imageContainer = [[NSScrollView alloc] initWithFrame:insideBorderRect];
    ImageView * ixView = [[ImageView alloc] initWithFrame:[self insideFormObjectBorder:objectRectangle]];
    [ixView setOwnerObject:self];
    [imageContainer setDocumentView:ixView];    
    [imageContainer setAutoresizesSubviews:YES];
    [shapeView addSubview:imageContainer Below:nextView];
    imageDocumentView = ixView; // save weak reference to image view so it can be manipulated
}

在其他地方,NSButton(普通按钮、单选按钮等有多种变体)、NSTextView、NSTableView(用于列表和矩阵)、NSSlider、NSScroller、NSSegmentedControl,甚至 WebView 的代码几乎相同。所有其他的都可以正确处理重叠对象,包括 WebView,只有 NSImageView 不能按预期工作。

供我参考,这是 Panorama X 问题跟踪器中的 #429。

我在 WWDC 2018 上与 Apple 工程师讨论了这个问题。事实证明,正如我所怀疑的那样,在某些情况下,即使你没有要求,Appkit 也会为 NSImageView 使用图层支持!所以最好的解决方案是将所有视图切换到图层支持(Mojave 会自动发生)。

在这个特殊情况下,NSImageView 位于 NSScrollView 中,我没有提到它,因为我认为它不重要(我的错)。事实证明,在这种情况下,Appkit 认为使用层支持是一个好主意(以优化滚动)。因此,另一种解决此问题的方法是子类化 NSImageView(出于其他原因我已经这样做了)并添加此方法(由一位不愿透露姓名的 Apple 工程师当场编写)。

+ (BOOL)isCompatibleWithResponsiveScrolling {
    if (NSAppKitVersionNumber <= 1561. /* NSAppKitVersionNumber10_13 */) {
        return NO;
    } else {
        return YES;
    }
}

我确信这是 public 的全部内容,记录在 API,尽管文档很少(惊喜)。 What's new in OS X 10.9 发行说明中有一些关于响应式滚动的讨论。检查 NSAppKitVersionNumber 是为了确保在 Mojave 上 运行 时关闭此补丁,因为一切都是层支持的。