对于我的跨平台项目(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 ...
它的外观如下:
集合视图位于右侧的拆分视图内。 当我调整左视图的大小时,集合视图坚持分割线,但标题行保留在右侧(见下文):
为了显示层次结构信息,我为集合和行列表自定义了 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 粘在左侧?我尝试了几种布局约束指令,但没有成功......
谢谢@Willeke的建议。事实上,我刚刚意识到我根本不明白AppKit的自动布局功能是如何工作的!
所以,我没有在无休止的尝试中掉头发,而是在 ObjC 中创建了 UWP 的 StackPanel 的山寨版。现在它正在按预期工作:
这是我的 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