自定义表格视图中与NSStackView对齐的问题

问题描述 投票:0回答:1

对于我的跨平台项目(Mac 和 Windows),我在 Mac 上利用 Objective-C 中 AppKIt 中的 NSStackView 编写了一个通用的 C++ 集合视图(即表视图)(因为我可以轻松地将 ObjC 和 C++ 混合在一起)。

各个行本身就是一个水平 NSStackView,用于显示列标题和数据行的标题。

我还嵌入了一个搜索框和一个滚动视图来管理大量数据行。

实际的视图层次结构如下:

NSStackView(vertical)
  L NSSearchField
  L NSStackView(horizontal) - the header row
  L NSScrollView(vertical)
      L NSStackView(vertical)
           L NSStackView(horizontal) - the data row
           L ...

它的外观如下:

enter image description here

集合视图位于右侧的拆分视图内。 当我调整左视图的大小时,集合视图坚持分割线,但标题行保留在右侧(见下文):

enter image description here

为了显示层次结构信息,我为集合和行列表自定义了 NSStackView,isFlipped 返回 YES。

这是我的代码:

@interface FlippedStackView : NSStackView
{
}

- ( id ) initWithBackground: ( BOOL ) value;

- ( BOOL ) isFlipped;

@end

@implementation FlippedStackView

- ( id ) initWithBackground: ( BOOL ) value
{
    self = [ super init ];
    if( self )
    {
        if( value == YES )
        {
            self.spacing = 0.0;
            [ self.layer setBackgroundColor:[ NSColor windowBackgroundColor ].CGColor ];
        }
    }
    
    return self;
}

- ( BOOL ) isFlipped
{
    return YES;
}

@end

// --------------------

@implementation MacUICollectionView

- ( id ) initWithBackground: ( BOOL ) value
{
    M_LOG_FUNCTION( MacUICollectionView::initWithBackground )
    
    self = [ super init ];
    if( self )
    {
        // General layout setting
        self.translatesAutoresizingMaskIntoConstraints = NO;
        self.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
        self.orientation = NSUserInterfaceLayoutOrientationVertical;
        self.wantsLayer = YES;
        self.spacing = 0.0;
        
        if( value == YES )
        {
            self.spacing = 0.0;
            [ self.layer setBackgroundColor:[ NSColor windowBackgroundColor ].CGColor ];
        }

        // Initialize search field and header values
        _searchField = nil;
        _header = nil;

        // Create the scroll view
        NSScrollView * scrollView = [[NSScrollView alloc] init ];
        scrollView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
        scrollView.hasVerticalScroller = YES;
        scrollView.autohidesScrollers = YES;
        scrollView.drawsBackground = NO;

        // Get the contentView of the NSScrollView (which is the NSClipView)
        [ [ scrollView contentView] setDrawsBackground: NO ];
        
        // Create stack view for rows
        _stackViewForRows = [ [ FlippedStackView alloc ] initWithBackground: false ]; // Customize frame size
        _stackViewForRows.translatesAutoresizingMaskIntoConstraints = NO;
        _stackViewForRows.orientation = NSUserInterfaceLayoutOrientationVertical;
        _stackViewForRows.spacing = 0.0;

        // Add row stack view to scroll view
        scrollView.documentView = _stackViewForRows;
        
        [ self addView: scrollView inGravity:NSStackViewGravityTop ];
    }

    return self;
}

// --------------------

- ( BOOL ) isFlipped
{
    M_LOG_FUNCTION( MacUICollectionView::isFlipped )
    
    return YES;
}
// --------------------

- ( void ) displaySearchField: ( NSSearchField * ) value
{
    M_LOG_FUNCTION( MacUICollectionView::displaySearchField )
    
    if( _searchField == value )
        return;
    
    if( _searchField != nil )
        [ self removeView: _searchField ];

    if( value != nil )
    {
        [ self insertView: value atIndex: 0 inGravity: NSStackViewGravityLeading ];
        NSNumber * hMargin = @( ( CGFloat ) kMacUIStandardPadding );
        [ self addConstraints: [ NSLayoutConstraint constraintsWithVisualFormat: @"|->=hMargin-[value]->=hMargin-|"
                                                                        options: 0
                                                                        metrics: NSDictionaryOfVariableBindings( hMargin )
                                                                          views: NSDictionaryOfVariableBindings( value ) ] ];
        [ self addConstraints: [ NSLayoutConstraint constraintsWithVisualFormat: @"V:|->=hMargin-[value]->=hMargin-|"
                                                                        options: 0
                                                                        metrics: NSDictionaryOfVariableBindings( hMargin )
                                                                          views: NSDictionaryOfVariableBindings( value ) ] ];
    }
    _searchField = value;
}

// --------------------

- ( void ) displayHeader: ( NSView * ) value
{
    M_LOG_FUNCTION( MacUICollectionView::displayHeader )
    
    if( _header == value )
        return;

    if( _header != nil )
        // Remove the header
        [ self removeView: _header ];
    else
    {
        // Add the header
        [ self insertView: value atIndex: ( _searchField == nil ? 0 : 1 ) inGravity: NSStackViewGravityTop ];
    }

    _header = value;
}

// --------------------

- ( void ) addRow: ( NSView * ) value atIndex: ( int ) index
{
    M_LOG_FUNCTION( MacUICollectionView::addRow )
    
    [ _stackViewForRows insertView: value atIndex: index inGravity: NSStackViewGravityLeading ];
}

// --------------------

- ( void ) removeRow: ( NSView * ) value
{
    M_LOG_FUNCTION( MacUICollectionView::removeRow )
    
    [ _stackViewForRows removeView: value ];
}

@end

所以,我的问题是:如何强制标题行的 NSStackView 粘在左侧?我尝试了几种布局约束指令,但没有成功......

objective-c macos appkit nsstackview
1个回答
0
投票

谢谢@Willeke的建议。事实上,我刚刚意识到我根本不明白AppKit的自动布局功能是如何工作的!

所以,我没有在无休止的尝试中掉头发,而是在 ObjC 中创建了 UWP 的 StackPanel 的山寨版。现在它正在按预期工作:

enter image description here

这是我的 MacUIStackPanel 代码,以防万一:

enum MacUIStackPanelOrientation {
    MacUIVerticalStack,
    MacUIHorizontalStack
};

enum MacUIStackPanelAlignment {
    MacUICenterStack,
    MacUILeftStack,
    MacUIRightStack,
    MacUITopStack,
    MacUIBottomStack
};

// --------------------

@interface MacUIStackPanel : MacUIFlippedView
{
    MacUIStackPanelOrientation orientation;
    MacUIStackPanelAlignment alignment;

    BOOL autoResize;
    NSSize size;
    
    BOOL hasBackground;
    NSColor * backgroundColor;

    BOOL hasBorder;
    CoreServices::Coordinate borderThickness;
    NSColor * borderColor;

    CoreServices::Coordinate padding;
    CoreServices::Coordinate spacing;

    CoreServices::List< NSView * > children;
}

- ( id ) init;

- ( BOOL ) autoResize;
- ( void ) autoResize: ( BOOL ) value;

- ( MacUIStackPanelOrientation ) orientation;
- ( void ) orientation: ( MacUIStackPanelOrientation ) value;

- ( MacUIStackPanelAlignment ) alignment;
- ( void ) alignment: ( MacUIStackPanelAlignment ) value;

- ( BOOL ) hasBackground;
- ( void ) hasBackground: ( BOOL ) value;

- ( NSColor * ) backgroundColor;
- ( void ) backgroundColor: ( NSColor * ) value;

- ( BOOL ) hasBorder;
- ( void ) hasBorder: ( BOOL ) value;

- ( CoreServices::Coordinate ) borderThickness;
- ( void ) borderThickness: ( CoreServices::Coordinate ) value;

- ( NSColor * ) borderColor;
- ( void ) borderColor: ( NSColor * ) value;

- ( CoreServices::Coordinate ) padding;
- ( void ) padding: ( CoreServices::Coordinate ) value;

- ( CoreServices::Coordinate ) spacing;
- ( void ) spacing: ( CoreServices::Coordinate ) value;

- ( void ) addView: ( NSView * ) view;
- ( void ) insertView: ( NSView * ) view atIndex: ( CoreServices::Index ) index;
- ( void ) removeView: ( NSView * ) view;
- ( void ) removeViewAtIndex: ( CoreServices::Index ) index;
- ( Counter ) countOfViews;

- ( void ) updateLayout;

- ( void ) setFrame:( NSRect ) frameRect;
- ( void ) drawRect:( NSRect ) dirtyRect;

@end

@implementation MacUIStackPanel

- ( id ) init
{
    M_LOG_FUNCTION( MacUIStackPanel::init )
    

    self = [ super init ];
    if( self )
    {
        orientation = MacUIHorizontalStack;
        alignment = MacUICenterStack;

        hasBackground = NO;
        backgroundColor = [ NSColor controlBackgroundColor ];

        hasBorder = NO;
        borderThickness = 1.0;
        borderColor = [ NSColor gridColor ];

        padding = 0.0;
        spacing = 0.0;

        children.MakeEmpty();
        [ self setClipsToBounds: YES ];
    }
    
    return self;
}

// --------------------

- ( BOOL ) autoResize
{
    M_LOG_FUNCTION( MacUIStackPanel::autoResize )
    
    return autoResize;
}
// --------------------

- ( void ) autoResize: ( BOOL ) value
{
    M_LOG_FUNCTION( MacUIStackPanel::autoResize )
    
    if( autoResize == value )
        return;

    if( value == YES )
        [ self setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable ];
    else
        [ self setAutoresizingMask: NSViewNotSizable ];
    
    autoResize = value;
}

// --------------------

- ( BOOL ) isFlipped
{
    //    M_LOG_FUNCTION( MacUIStackPanel::isFlipped )
    
    return YES;
}

// --------------------

- ( MacUIStackPanelOrientation ) orientation
{
    M_LOG_FUNCTION( MacUIStackPanel::orientation )
    
    return orientation;
}

// --------------------

- ( void ) orientation: ( MacUIStackPanelOrientation ) value
{
    M_LOG_FUNCTION( MacUIStackPanel::orientation )
    
    if( orientation == value )
        return;

    orientation = value;
    alignment = MacUICenterStack;
        
    [ self updateLayout ];
}

// --------------------

- ( MacUIStackPanelAlignment ) alignment
{
    M_LOG_FUNCTION( MacUIStackPanel::alignment )
    
    return alignment;
}

// --------------------

- ( void ) alignment: ( MacUIStackPanelAlignment ) value
{
    M_LOG_FUNCTION( MacUIStackPanel::alignment )
    
    if( alignment == value )
        return;
    
    if( orientation == MacUIVerticalStack )
        switch( value )
        {
            case MacUICenterStack:
            case MacUILeftStack:
            case MacUIRightStack:
                break;
            default:
                return;
        }
    else
        switch( value )
        {
            case MacUICenterStack:
            case MacUITopStack:
            case MacUIBottomStack:
                break;
            default:
                return;
        }

    alignment = value;
    
    [ self updateLayout ];
}

// --------------------

- ( BOOL ) hasBackground
{
    M_LOG_FUNCTION( MacUIStackPanel::hasBackground )
    
    return hasBackground;
}

// --------------------

- ( void ) hasBackground: ( BOOL ) value
{
    M_LOG_FUNCTION( MacUIStackPanel::hasBackground )
    
    if( hasBackground == value )
        return;
    
    hasBackground = value;
}

// --------------------

- ( NSColor * ) backgroundColor
{
    M_LOG_FUNCTION( MacUIStackPanel::backgroundColor )
    
    return backgroundColor;
}

// --------------------

- ( void ) backgroundColor: ( NSColor * ) value
{
    M_LOG_FUNCTION( MacUIStackPanel::backgroundColor )
    
    if( backgroundColor == value )
        return;
    
    backgroundColor = value;
}

// --------------------

- ( BOOL ) hasBorder
{
    M_LOG_FUNCTION( MacUIStackPanel::hasBorder )
    
    return hasBorder;
}

// --------------------

- ( void ) hasBorder: ( BOOL ) value
{
    M_LOG_FUNCTION( MacUIStackPanel::hasBorder )
    
    if( hasBorder == value )
        return;
    
    hasBorder = value;
}

// --------------------

- ( CoreServices::Coordinate ) borderThickness
{
    M_LOG_FUNCTION( MacUIStackPanel::borderThickness )
    
    return borderThickness;
}

// --------------------

- ( void ) borderThickness: ( CoreServices::Coordinate ) value
{
    M_LOG_FUNCTION( MacUIStackPanel::hasBorder )
    
    if( borderThickness == value || value < 0.0 )
        return;
    
    borderThickness = value;
}

// --------------------

- ( NSColor * ) borderColor
{
    M_LOG_FUNCTION( MacUIStackPanel::borderColor )
    
    return borderColor;
}

// --------------------

- ( void ) borderColor: ( NSColor * ) value
{
    M_LOG_FUNCTION( MacUIStackPanel::borderColor )
    
    if( borderColor == value )
        return;
    
    borderColor = value;
}

// --------------------

- ( CoreServices::Coordinate ) padding
{
    M_LOG_FUNCTION( MacUIStackPanel::padding )
    
    return padding;
}

// --------------------

- ( void ) padding: ( CoreServices::Coordinate ) value
{
    M_LOG_FUNCTION( MacUIStackPanel::padding )
    
    if( value >= 0.0 && padding != value )
    {
        padding = value;
        
        [ self updateLayout ];
    }
}

// --------------------

- ( CoreServices::Coordinate ) spacing
{
    M_LOG_FUNCTION( MacUIStackPanel::spacing )
    
    return spacing;
}

// --------------------

- ( void ) spacing: ( CoreServices::Coordinate ) value
{
    M_LOG_FUNCTION( MacUIStackPanel::spacing )
    
    if( value >= 0.0 && spacing != value )
    {
        spacing = value;
        
        [ self updateLayout ];
    }
}

// --------------------

- ( void ) addView: ( NSView * ) view
{
    M_LOG_FUNCTION( MacUIStackPanel::addView )
    
    if( children.HasItem( view ) == true )
        return;
    
    children.AddItem( view );
    [ self addSubview: view ];
    
    [ self updateLayout ];
}

// --------------------

- ( void ) insertView: ( NSView * ) view atIndex: ( CoreServices::Index ) index
{
    M_LOG_FUNCTION( MacUIStackPanel::insertView )
    
    if( children.HasItem( view ) == true ||
       index < 0 || index > children.CountOfItems() )
        return;
    
    children.AddItemAtIndex( index, view );
    [ self addSubview: view ];
    
    [ self updateLayout ];
}

// --------------------

- ( void ) removeView: ( NSView * ) view
{
    M_LOG_FUNCTION( MacUIStackPanel::removeView )
    
    Index index;
    
    index = children.FindItem( view );
    if( index == kNullIndex )
        return;
    
    children.RemoveItemAtIndex( index );
    [ view removeFromSuperview ];
    
    [ self updateLayout ];
}

// --------------------

- ( void ) removeViewAtIndex: ( CoreServices::Index ) index
{
    M_LOG_FUNCTION( MacUIStackPanel::removeViewAtIndex )

    if( index < 0 || index >= children.CountOfItems() )
        return;

    NSView * view;
    
    view = * children.GetItemAtIndex( index );
    
    children.RemoveItemAtIndex( index );
    [ view removeFromSuperview ];
    
    [ self updateLayout ];
}

// --------------------

- ( Counter ) countOfViews
{
    M_LOG_FUNCTION( MacUIStackPanel::countOfViews )
    
    return children.CountOfItems();
}

// --------------------

- ( void ) updateLayout
{
    M_LOG_FUNCTION( MacUIStackPanel::updateLayout )

    M_LOG_FUNCTION_MESSAGE( LogLevel::Debug, "current size: width=%.1f, height=%.1f", size.width, size.height )
    M_LOG_FUNCTION_MESSAGE( LogLevel::Debug, "current frame: x=%.1f, y=%.1f, width=%.1f, height=%.1f",
                           [ self frame ].origin.x,
                           [ self frame ].origin.y,
                           [ self frame ].size.width,
                           [ self frame ].size.height )

    CoreServices::Coordinate current[2];
    bool direction;
    
    direction = ( orientation == MacUIVerticalStack );
    
    current[direction] = ( hasBorder ? borderThickness : 0.0 ) + padding;
    current[!direction] = 0.0;
    
    // First pass: position the children along the direction line
    M_LOG_FUNCTION_MESSAGE( LogLevel::Debug, "First pass: position the children along the direction line" )
    for( Index index = 0; index < children.CountOfItems(); index++ )
    {
        NSView * view;
        CoreServices::Point size;
        
        view = * children.GetItemAtIndex( index );
        size.SetX( [view frame].size.width );
        size.SetY( [view frame].size.height );
        
        M_LOG_FUNCTION_MESSAGE( LogLevel::Debug, "\t[%d] %p, width=%.1f height=%.1f", index, view, size[0], size[1] )
        
        if( index > 0 )
            current[direction] += spacing;
        
        [ view setFrameOrigin: NSMakePoint( !direction?current[0]:0.0, direction?current[1]:0.0 )];
        
        current[direction] += size[direction];
        current[!direction] = M_MAXIMUM( current[!direction], size[!direction] );
    }
    
    current[direction] += ( hasBorder ? borderThickness : 0.0 ) + padding;
    current[!direction] += 2 * ( ( hasBorder ? borderThickness : 0.0 ) + padding );

    if( autoResize == YES )
    {
        current[direction] = M_MAXIMUM( current[direction], direction ? self.frame.size.height : self.frame.size.width );
        current[!direction] = M_MAXIMUM( current[!direction], direction ? self.frame.size.width : self.frame.size.height );
    }

    size = NSMakeSize( current[0], current[1] );
    M_LOG_FUNCTION_MESSAGE( LogLevel::Debug, "new size: width=%.1f, height=%.1f", size.width, size.height )
    if( autoResize == NO )
        [ self setFrameSize: size ];
    
    // Second pass: update the alignemnt of the children
    M_LOG_FUNCTION_MESSAGE( LogLevel::Debug, "Second pass: update the alignemnt of the children" )
    for( Index index = 0; index < children.CountOfItems(); index++ )
    {
        NSView * view;
        CoreServices::Point size;
        CoreServices::Coordinate position[2];
        
        view = * children.GetItemAtIndex( index );
        size.SetX( [view frame].size.width );
        size.SetY( [view frame].size.height );
        position[0] = [view frame].origin.x;
        position[1] = [view frame].origin.y;

        switch( alignment )
        {
            case MacUICenterStack:
                position[!direction] = ( current[!direction] - size[!direction] ) / 2.0;
                break;
                
            case MacUILeftStack:
            case MacUITopStack:
                position[!direction] = ( hasBorder ? borderThickness : 0.0 ) + padding;
                break;
                
            case MacUIRightStack:
            case MacUIBottomStack:
                position[!direction] = current[!direction] - size[!direction] - ( ( hasBorder ? borderThickness : 0.0 ) + padding );
                break;
        }
        
        [ view setFrameOrigin: NSMakePoint( position[0], position[1] ) ];
        
        M_LOG_FUNCTION_MESSAGE( LogLevel::Debug, "\t[%d] %p, width=%.1f height=%.1f, x=%.1f, y=%.1f", index, view, size[0], size[1], position[0], position[1] )
    }
    
    [ self setNeedsDisplay: YES ];
}

// --------------------

- ( void ) setFrame:( NSRect ) frameRect
{
    M_LOG_FUNCTION( MacUIStackPanel::setFrame )
    
    char buffer[64];
    [ identifier getCString: buffer maxLength:64 encoding:NSUTF8StringEncoding ];
    
    M_LOG_FUNCTION_MESSAGE( LogLevel::Debug, "\'%s\' frameRect: x=%.1f, y=%.1f, width=%.1f, height=%.1f",
                           buffer,
                           frameRect.origin.x,
                           frameRect.origin.y,
                           frameRect.size.width,
                           frameRect.size.height )

    [ super setFrame: frameRect ];
    
    M_LOG_FUNCTION_MESSAGE( LogLevel::Debug, "\'%s\' frame: x=%.1f, y=%.1f, width=%.1f, height=%.1f",
                           buffer,
                           self.frame.origin.x,
                           self.frame.origin.y,
                           self.frame.size.width,
                           self.frame.size.height )

    if( autoResize == YES )
    {
        [ self updateLayout ];
        [ self setNeedsDisplay: YES ];
    }
}

// --------------------

- ( void ) drawRect:( NSRect ) dirtyRect
{
    M_LOG_FUNCTION( MacUIStackPanel::drawRect )
    
    M_LOG_FUNCTION_MESSAGE( LogLevel::Debug, "\tframe: x=%.1f, y=%.1f, width=%.1f, height=%.1f",
                           [ self frame ].origin.x,
                           [ self frame ].origin.y,
                           [ self frame ].size.width,
                           [ self frame ].size.height )
    M_LOG_FUNCTION_MESSAGE( LogLevel::Debug, "\tbounds: x=%.1f, y=%.1f, width=%.1f, height=%.1f",
                           [ self bounds ].origin.x,
                           [ self bounds ].origin.y,
                           [ self bounds ].size.width,
                           [ self bounds ].size.height )
    
    NSRect background;
    
    background = NSMakeRect( 0.0, 0.0, size.width, size.height );
    
    // Draw the background?
    if( hasBackground == YES )
    {
        [ backgroundColor set ];
        NSRectFill( background );
    }
    
    // Draw the border?
    if( hasBorder == YES )
    {
        NSBezierPath * borderPath = [ NSBezierPath bezierPathWithRect:NSInsetRect( background, borderThickness / 2.0, borderThickness / 2.0 ) ];
        [ borderColor setStroke ];
        [ borderPath setLineWidth: borderThickness ];
        [ borderPath stroke ];
    }

    [ super drawRect: dirtyRect ];
}

@end
© www.soinside.com 2019 - 2024. All rights reserved.