我错过了什么阻止我使用自动布局制作类似通用NSStackView的容器?

时间:2016-05-08 21:33:33

标签: objective-c macos cocoa autolayout nsview

我正在尝试使用自动布局 for OS X 来实现容器视图,其运行方式与NSStackView类似,但有一些差异,NSStackView无法处理(我还需要10.7支持)。我的规则是:

  • 子视图水平或垂直排列。
  • 在主要方向上,默认情况下,子视图采用其固有大小。
  • 如果子视图标记为"弹性",则在布置所有非弹性子视图后,将占用剩余的空间。多个弹性子视图获得剩余空间的相等分布。
  • 如果没有子视图是弹性的,则容器可以在主方向上增长,在最后一个子视图后显示空白空间。将具有相同方向的两个这样的堆叠嵌套优先于外层。
  • 沿着次要方向,子视图会夹到容器视图的边缘并自由增长。

我认为这可以通过一种简单的方式完成,将主要方向的视图一个接一个地连接起来,然后使用|[view]|视觉格式在次要方向上连接。使用内部内容大小为0x0的NSView作为最后一个视图来处理缺乏弹性视图。

这大部分都有效。遗憾的是,模糊性出现在表格的水平方向堆栈的嵌套树中(出于说明目的使用HTML表示)



.box {
  display: inline-block;
  border: 1px solid black;
  padding: 0.5em 0.5em 0.5em 0.5em;
}

.outermost-box {
  display: inline-block;
  border: 1px solid black;
  padding: 0.5em 0.5em 0.5em 0.5em;
  width: 100%;
}

<div class="outermost-box">
  <div class="box">
    <input type="button" value="These">
    <input type="button" value="Buttons" disabled>
  </div>
  <div class="box">
    <input type="button" value="are">
    <div class="box">
      <input type="button" value="in" disabled>
    </div>
  </div>
  <div class="box">
    <div class="box">
      <input type="button" value="nested">
      <div class="box">
        <input type="button" value="boxes" disabled>
      </div>
    </div>
  </div>
</div>
&#13;
&#13;
&#13;

其中所有堆栈都没有可伸缩的子视图。根据我的定义,最外面的盒子伸展,只有那个伸展。但是,自动布局会将额外空间随机分配给其中一个内框:

Auto Layout misbehaving part 1; notice how "nested boxes" is flush right with the edge of the window when it's supposed to be flush left next to "These buttons are in"

将额外视图更改为NSLayoutRelationLessThanOrEqual关系(last view.trailing <= superview.trailing)也无济于事。不过,我会为这篇文章的其余部分保留这个模型,因为我的下一次尝试是基于它的。

然后我决定尝试让容器询问它的superview是否应该扩展。这解决了上述问题,但引入了另一个问题,即容器的深链在水平和垂直之间交替:

Auto Layout misbehaving part 2

标有&#34;右边距测试&#34的按钮; 拉伸,但它们要么不伸展或伸展,要么剪掉两侧的视图(我现在没有截图;对不起)。

然后我决定同时在右边缘上同时使用<=和替代==约束,如果应该有额外的空间,则将==设置为低优先级。这个新的主要有效,但现在有一个奇怪的问题。如果我在上面显示的窗口上调整窗口大小,那么切换到第4页,我得到

Auto Layout misbehaving part 4

然后,如果我调整大小,我会

Auto Layout misbehaving part 5

即使在所有条件下都应该有底部空间。有时可以看到按钮的底部,并且可视化其垂直约束显示它认为它想要与单选按钮矩阵一样高(现在是NSMatrix;将其更改为一堆NSButtons将等到我修复所有这些自动布局问题)。

我真的不确定发生了什么或如何解决这些问题。我尝试使我提到的==约束具有可设置的&#34;真正的拥抱优先级&#34;它本身,但只是让事情以更加壮观的方式打破。&#39;

还有一些问题,标签视图的位置最初太低,需要几个布局周期来正确设置...

显示的所有内容都是使用这些容器完成的,NSBoxes包含一个子视图,NSTabs包含一个子视图。我将粘贴我的容器的代码,如下所示。

那么自动布局怎么样呢?我不明白我不能用明显的代码使它正常工作吗?或者NSStackView可以完成我想要的所有内容,我应该只使用它吗? (假设alignmentWidthHeight视为有效,而Interface Builder似乎并未将其视为有效。

谢谢!

// 15 august 2015
#import "uipriv_darwin.h"

// TODOs:
// - tab on page 2 is glitched initially and doesn't grow
// - page 3 doesn't work right; probably due to our shouldExpand logic being applied incorrectly

// TODOs to confirm
// - 10.8: if we switch to page 4, then switch back to page 1, check Spaced, and go back to page 4, some controls (progress bar, popup button) are clipped on the sides

@interface boxChild : NSObject
@property uiControl *c;
@property BOOL stretchy;
@property NSLayoutPriority oldHorzHuggingPri;
@property NSLayoutPriority oldVertHuggingPri;
- (NSView *)view;
@end

@interface boxView : NSView {
    uiBox *b;
    NSMutableArray *children;
    BOOL vertical;
    int padded;

    NSLayoutConstraint *first;
    NSMutableArray *inBetweens;
    NSLayoutConstraint *last, *last2;
    NSMutableArray *otherConstraints;

    NSLayoutAttribute primaryStart;
    NSLayoutAttribute primaryEnd;
    NSLayoutAttribute secondaryStart;
    NSLayoutAttribute secondaryEnd;
    NSLayoutAttribute primarySize;
    NSLayoutConstraintOrientation primaryOrientation;
    NSLayoutConstraintOrientation secondaryOrientation;
}
- (id)initWithVertical:(BOOL)vert b:(uiBox *)bb;
- (void)onDestroy;
- (void)removeOurConstraints;
- (void)forAll:(void (^)(uintmax_t i, boxChild *b))closure;
- (boxChild *)child:(uintmax_t)i;
- (BOOL)isVertical;
- (void)append:(uiControl *)c stretchy:(int)stretchy;
- (void)delete:(uintmax_t)n;
- (int)isPadded;
- (void)setPadded:(int)p;
@end

struct uiBox {
    uiDarwinControl c;
    boxView *view;
};

@implementation boxChild

- (NSView *)view
{
    return (NSView *) uiControlHandle(self.c);
}

@end

@implementation boxView

- (id)initWithVertical:(BOOL)vert b:(uiBox *)bb
{
    self = [super initWithFrame:NSZeroRect];
    if (self != nil) {
        // the weird names vert and bb are to shut the compiler up about shadowing because implicit this/self is stupid
        self->b = bb;
        self->vertical = vert;
        self->children = [NSMutableArray new];
        self->inBetweens = [NSMutableArray new];
        self->otherConstraints = [NSMutableArray new];

        if (self->vertical) {
            self->primaryStart = NSLayoutAttributeTop;
            self->primaryEnd = NSLayoutAttributeBottom;
            self->secondaryStart = NSLayoutAttributeLeading;
            self->secondaryEnd = NSLayoutAttributeTrailing;
            self->primarySize = NSLayoutAttributeHeight;
            self->primaryOrientation = NSLayoutConstraintOrientationVertical;
            self->secondaryOrientation = NSLayoutConstraintOrientationHorizontal;
        } else {
            self->primaryStart = NSLayoutAttributeLeading;
            self->primaryEnd = NSLayoutAttributeTrailing;
            self->secondaryStart = NSLayoutAttributeTop;
            self->secondaryEnd = NSLayoutAttributeBottom;
            self->primarySize = NSLayoutAttributeWidth;
            self->primaryOrientation = NSLayoutConstraintOrientationHorizontal;
            self->secondaryOrientation = NSLayoutConstraintOrientationVertical;
        }
    }
    return self;
}

- (void)onDestroy
{
    boxChild *bc;
    uintmax_t i, n;

    [self removeOurConstraints];
    [self->first release];
    [self->inBetweens release];
    [self->last release];
    [self->last2 release];
    [self->otherConstraints release];

    n = [self->children count];
    for (i = 0; i < n; i++) {
        bc = [self child:i];
        uiControlSetParent(bc.c, NULL);
        uiDarwinControlSetSuperview(uiDarwinControl(bc.c), nil);
        uiControlDestroy(bc.c);
    }
    [self->children release];
}

- (void)removeOurConstraints
{
    [self removeConstraint:self->first];
    [self removeConstraints:self->inBetweens];
    [self removeConstraint:self->last];
    [self removeConstraint:self->last2];
    [self removeConstraints:self->otherConstraints];
}

- (void)forAll:(void (^)(uintmax_t i, boxChild *b))closure
{
    uintmax_t i, n;

    n = [self->children count];
    for (i = 0; i < n; i++)
        closure(i, [self child:i]);
}

- (boxChild *)child:(uintmax_t)i
{
    return (boxChild *) [self->children objectAtIndex:i];
}

- (BOOL)isVertical
{
    return self->vertical;
}

// TODO something about spinbox hugging
- (void)updateConstraints
{
    uintmax_t i, n;
    BOOL hasStretchy;
    NSView *firstStretchy = nil;
    CGFloat padding;
    NSView *prev, *next;
    NSLayoutConstraint *c;
    NSLayoutPriority priority;

    [super updateConstraints];
    [self removeOurConstraints];

    n = [self->children count];
    if (n == 0)
        return;
    padding = 0;
    if (self->padded)
        padding = 8.0;      // TODO named constant

    // first, attach the first view to the leading
    prev = [[self child:0] view];
    self->first = mkConstraint(prev, self->primaryStart,
        NSLayoutRelationEqual,
        self, self->primaryStart,
        1, 0,
        @"uiBox first primary constraint");
    [self addConstraint:self->first];
    [self->first retain];

    // next, assemble the views in the primary direction
    // they all go in a straight line
    // also figure out whether we have stretchy controls, and which is the first
    if ([self child:0].stretchy) {
        hasStretchy = YES;
        firstStretchy = prev;
    } else
        hasStretchy = NO;
    for (i = 1; i < n; i++) {
        next = [[self child:i] view];
        if (!hasStretchy && [self child:i].stretchy) {
            hasStretchy = YES;
            firstStretchy = next;
        }
        c = mkConstraint(next, self->primaryStart,
            NSLayoutRelationEqual,
            prev, self->primaryEnd,
            1, padding,
            @"uiBox later primary constraint");
        [self addConstraint:c];
        [self->inBetweens addObject:c];
        prev = next;
    }

    // and finally end the primary direction
    self->last = mkConstraint(prev, self->primaryEnd,
        NSLayoutRelationLessThanOrEqual,
        self, self->primaryEnd,
        1, 0,
        @"uiBox last primary constraint");
    [self addConstraint:self->last];
    [self->last retain];

    // if there is a stretchy control, add the no-stretchy view
    self->last2 = mkConstraint(prev, self->primaryEnd,
        NSLayoutRelationEqual,
        self, self->primaryEnd,
        1, 0,
        @"uiBox last2 primary constraint");
    priority = NSLayoutPriorityRequired;
    if (!hasStretchy) {
        BOOL shouldExpand = NO;
        uiControl *parent;

        parent = uiControlParent(uiControl(self->b));
        if (parent != nil)
            if (self->vertical)
                shouldExpand = uiDarwinControlChildrenShouldAllowSpaceAtBottom(uiDarwinControl(parent));
            else
                shouldExpand = uiDarwinControlChildrenShouldAllowSpaceAtTrailingEdge(uiDarwinControl(parent));
        if (shouldExpand)
            priority = NSLayoutPriorityDefaultLow;
    }
    [self->last2 setPriority:priority];
    [self addConstraint:self->last2];
    [self->last2 retain];

    // next: assemble the views in the secondary direction
    // each of them will span the secondary direction
    for (i = 0; i < n; i++) {
        prev = [[self child:i] view];
        c = mkConstraint(prev, self->secondaryStart,
            NSLayoutRelationEqual,
            self, self->secondaryStart,
            1, 0,
            @"uiBox start secondary constraint");
        [self addConstraint:c];
        [self->otherConstraints addObject:c];
        c = mkConstraint(prev, self->secondaryEnd,
            NSLayoutRelationEqual,
            self, self->secondaryEnd,
            1, 0,
            @"uiBox end secondary constraint");
        [self addConstraint:c];
        [self->otherConstraints addObject:c];
    }

    // finally, set sizes for stretchy controls
    if (hasStretchy)
        for (i = 0; i < n; i++) {
            if (![self child:i].stretchy)
                continue;
            prev = [[self child:i] view];
            if (prev == firstStretchy)
                continue;
            c = mkConstraint(prev, self->primarySize,
                NSLayoutRelationEqual,
                firstStretchy, self->primarySize,
                1, 0,
                @"uiBox stretchy sizing");
            [self addConstraint:c];
            [self->otherConstraints addObject:c];
        }
}

- (void)append:(uiControl *)c stretchy:(int)stretchy
{
    boxChild *bc;
    NSView *childView;

    bc = [boxChild new];
    bc.c = c;
    bc.stretchy = stretchy;
    childView = [bc view];
    bc.oldHorzHuggingPri = horzHuggingPri(childView);
    bc.oldVertHuggingPri = vertHuggingPri(childView);

    uiControlSetParent(bc.c, uiControl(self->b));
    uiDarwinControlSetSuperview(uiDarwinControl(bc.c), self);
    uiDarwinControlSyncEnableState(uiDarwinControl(bc.c), uiControlEnabledToUser(uiControl(self->b)));

    // if a control is stretchy, it should not hug in the primary direction
    // otherwise, it should *forcibly* hug
    if (stretchy)
        setHuggingPri(childView, NSLayoutPriorityDefaultLow, self->primaryOrientation);
    else
        // TODO will default high work?
        setHuggingPri(childView, NSLayoutPriorityRequired, self->primaryOrientation);
    // make sure controls don't hug their secondary direction so they fill the width of the view
    setHuggingPri(childView, NSLayoutPriorityDefaultLow, self->secondaryOrientation);

    [self->children addObject:bc];
    [bc release];       // we don't need the initial reference now

    [self setNeedsUpdateConstraints:YES];
}

- (void)delete:(uintmax_t)n
{
    boxChild *bc;
    NSView *removedView;

    bc = [self child:n];
    removedView = [bc view];

    uiControlSetParent(bc.c, NULL);
    uiDarwinControlSetSuperview(uiDarwinControl(bc.c), nil);

    setHorzHuggingPri(removedView, bc.oldHorzHuggingPri);
    setVertHuggingPri(removedView, bc.oldVertHuggingPri);

    [self->children removeObjectAtIndex:n];

    [self setNeedsUpdateConstraints:YES];
}

- (int)isPadded
{
    return self->padded;
}

- (void)setPadded:(int)p
{
    CGFloat padding;
    uintmax_t i, n;
    NSLayoutConstraint *c;

    self->padded = p;

    // TODO split into method (using above code)
    padding = 0;
    if (self->padded)
        padding = 8.0;
    n = [self->inBetweens count];
    for (i = 0; i < n; i++) {
        c = (NSLayoutConstraint *) [self->inBetweens objectAtIndex:i];
        [c setConstant:padding];
    }
    // TODO call anything?
}

@end

static void uiBoxDestroy(uiControl *c)
{
    uiBox *b = uiBox(c);

    [b->view onDestroy];
    [b->view release];
    uiFreeControl(uiControl(b));
}

uiDarwinControlDefaultHandle(uiBox, view)
uiDarwinControlDefaultParent(uiBox, view)
uiDarwinControlDefaultSetParent(uiBox, view)
uiDarwinControlDefaultToplevel(uiBox, view)
uiDarwinControlDefaultVisible(uiBox, view)
uiDarwinControlDefaultShow(uiBox, view)
uiDarwinControlDefaultHide(uiBox, view)
uiDarwinControlDefaultEnabled(uiBox, view)
uiDarwinControlDefaultEnable(uiBox, view)
uiDarwinControlDefaultDisable(uiBox, view)

static void uiBoxSyncEnableState(uiDarwinControl *c, int enabled)
{
    uiBox *b = uiBox(c);

    if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(b), enabled))
        return;
    [b->view forAll:^(uintmax_t i, boxChild *bc) {
        uiDarwinControlSyncEnableState(uiDarwinControl(bc.c), enabled);
    }];
}

uiDarwinControlDefaultSetSuperview(uiBox, view)

static BOOL uiBoxChildrenShouldAllowSpaceAtTrailingEdge(uiDarwinControl *c)
{
    uiBox *b = uiBox(c);

    // return NO if this box is horizontal so nested horizontal boxes don't lead to ambiguity
    return [b->view isVertical];
}

static BOOL uiBoxChildrenShouldAllowSpaceAtBottom(uiDarwinControl *c)
{
    uiBox *b = uiBox(c);

    // return NO if this box is vertical so nested vertical boxes don't lead to ambiguity
    return ![b->view isVertical];
}

void uiBoxAppend(uiBox *b, uiControl *c, int stretchy)
{
    [b->view append:c stretchy:stretchy];
}

void uiBoxDelete(uiBox *b, uintmax_t n)
{
    [b->view delete:n];
}

int uiBoxPadded(uiBox *b)
{
    return [b->view isPadded];
}

void uiBoxSetPadded(uiBox *b, int padded)
{
    [b->view setPadded:padded];
}

static uiBox *finishNewBox(BOOL vertical)
{
    uiBox *b;

    uiDarwinNewControl(uiBox, b);

    b->view = [[boxView alloc] initWithVertical:vertical b:b];

    return b;
}

uiBox *uiNewHorizontalBox(void)
{
    return finishNewBox(NO);
}

uiBox *uiNewVerticalBox(void)
{
    return finishNewBox(YES);
}

1 个答案:

答案 0 :(得分:0)

我自己解决了这个问题。

主要的变化是,不是通过对自我的约束而是在其超级视图上的有余空间之后。 superview询问self是否应该占用额外空间,如果是,则分配额外空间(使用&gt; =约束到superview边缘而不是==约束)。

各种其他小修正修复了边缘情况。特别是,我到处都是

relation = NSLayoutRelationSomething;
if (condition)
    relation = NSLayoutRelationSomethingElse;
constraint = [NSLayoutConstraint constraintWithArg:arg arg:arg
    relation:relation
    ...]

我改为使用两个约束,根据条件设置它们的优先级。 这应该是自动布局的最佳做法,因为它运作良好......

非常感谢!