基于视图的NSTableView具有动态高度的行


84

我有一个基于视图的应用程序NSTableView。在此表视图中,我有一些行,这些行的单元格内容由NSTextField启用了自动换行的多行组成。根据的文本内容,NSTextField显示单元格所需的行大小会有所不同。

我知道我可以实现该NSTableViewDelegate方法-tableView:heightOfRow:返回高度,但是高度将基于上的自动换行来确定NSTextField。的自动换行NSTextField类似地基于NSTextFieldis的宽度……由的宽度确定NSTableView

太...我想我的问题是...对此有什么好的设计模式?似乎我尝试的所有操作都变得混乱不堪。由于TableView需要了解单元格的高度才能对其进行布局...并且NSTextField需要了解其布局才能确定自动换行...并且单元格需要了解自动换行才能确定其高度...这是一个圆形的混乱局面...这让我发疯

有什么建议吗?

如果重要的话,最终结果也将具有可编辑的内容NSTextFields,可以调整其大小以适应其中的文本。我已经在视图级别进行此操作,但是tableview尚未调整单元格的高度。我认为一旦解决了高度问题,我将使用-noteHeightOfRowsWithIndexesChanged方法通知表格视图高度已更改……但是仍然要向委托人询问高度……因此,我很困惑。

提前致谢!


如果视图的宽度和高度都可以随其内容而改变,则定义它们很复杂,因为您必须进行迭代(但随后可以进行缓存)。但是您的箱子的高度似乎在不断变化,宽度不变。如果知道宽度(即基于窗口或视图的宽度),那么这里没有什么复杂的。我想知道为什么宽度可变吗?
罗布·纳皮尔

只有高度可以随内容改变。而且我已经解决了这个问题。我有一个NSTextField子类,可以自动调整其高度。问题在于将有关调整的知识返回给表视图的委托,因此它可以相应地更新高度。
吉瓦·德沃

@Jiva:想知道您是否已经解决了这个问题。
2011年

我正在尝试做类似的事情。我喜欢NSTextField子类调整其自身高度的想法。如何向该子类添加(委托)高度变化通知并使用某个适当的类(数据源,大纲委托等)监视该通知,以将信息获取到大纲?
John Velman 2011年

一个高度相关的问题,答案比这里提供的帮助更多:NSTableView基于NSStrings的行高
Ashley 2014年

Answers:


132

这是鸡和鸡蛋的问题。表格需要知道行高,因为它确定了给定视图的位置。但是您希望视图已经存在,因此可以使用它来计算行高。那么,哪个先出现?

答案是保留一个额外的NSTableCellView(或用作“单元格视图”的任何视图)周围,仅用于测量视图的高度。在tableView:heightOfRow:委托方法中,访问模型的“行”并设置objectValueon NSTableCellView。然后将视图的宽度设置为表的宽度,然后(无论您要执行什么操作)找出该视图所需的高度。返回该值。

请勿noteHeightOfRowsWithIndexesChanged:在委托方法tableView:heightOfRow:viewForTableColumn:row:!中调用 那是不好的,并且会引起大麻烦。

要动态更新高度,那么您应该做的是响应文本更改(通过目标/操作)并重新计算该视图的计算高度。现在,不要动态更改NSTableCellView的高度(或任何用作“单元格视图”的视图)。该表必须控制该视图的框架,如果尝试设置该视图,您将与之抗争。相反,在计算高度的文本字段的目标/操作中,调用 noteHeightOfRowsWithIndexesChanged:,这将使表调整该行的大小。假设您已经在子视图(即的子视图NSTableCellView)上设置了自动调整大小的蒙版,那么应该可以调整大小!如果不是,请首先对子视图的调整大小蒙版进行操作,以使行高可变的东西正确无误。

不要忘记noteHeightOfRowsWithIndexesChanged:默认情况下会设置动画。要使其不动画,请执行以下操作:

[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0];
[tableView noteHeightOfRowsWithIndexesChanged:indexSet];
[NSAnimationContext endGrouping];

PS:我对Apple Dev论坛上发布的问题的回答要多于堆栈溢出。

PSS:我写了基于视图的NSTableView


感谢您的回复。我本来没看过……我将其标记为正确答案。开发论坛很棒,我也使用它们。
吉瓦·德沃

“找出该视图所需的高度。返回该值。” ..那是我的问题:(。我正在使用基于视图的tableView,但我不知道该怎么做!
Mazyod 2012年

@corbin感谢您的回答,这就是我在应用程序上所拥有的。但是,我发现有关小牛的问题。既然您显然精通此主题,您可以介意检查我的问题吗?此处:stackoverflow.com/questions/20924141/…–
亚历克斯(Alex)

1
@corbin对此有一个规范的答案,但是在NSTableViews中使用自动布局和约束吗?自动布局有一个潜在的解决方案。想知道如果没有估计的HeightHeightForRowAtIndexPath API是否足够好(可可中不存在)
ZS

3
@ZS:我正在使用自动布局并实现了Corbin的答案。在中tableView:heightOfRow:,我用行的值(我只有一列)设置备用单元格视图,然后返回cellView.fittingSize.heightfittingSize是一种NSView方法,用于计算满足其约束的视图的最小大小。
Peter Hosey

43

MacOS 10.13中使用,这变得容易得多.usesAutomaticRowHeights。详细信息在这里:https : //developer.apple.com/library/content/releasenotes/AppKit/RN-AppKit/#10_13(在标题为“ NSTableView自动行高”的部分中)。

基本上,您只需在Storyboard编辑器中选择NSTableViewNSOutlineView,然后在Size Inspector中选择以下选项:

在此处输入图片说明

然后,将其中的内容设置NSTableCellView为对单元格具有顶部和底部约束,并且单元格将调整大小以自动适应。无需代码!

您的应用将忽略heightOfRowNSTableView)和heightOfRowByItemNSOutlineView)中指定的任何高度。您可以使用以下方法查看为自动布局行计算出的高度:

func outlineView(_ outlineView: NSOutlineView, didAdd rowView: NSTableRowView, forRow row: Int) {
  print(rowView.fittingSize.height)
}

@tofutim:如果使用正确的约束,并且当您确保每个具有动态高度的NSTextField的Preferred Width设置设置为Automatic时,如果可正确使用换行的文本单元格。另请参见stackoverflow.com/questions/46890144/...
伊利

我觉得@tofutim是正确的,如果将NSTextView放到TableView中,它将尝试将其存档,但它将无法正常工作。它适用于其他组件,例如NSImage ..
Albert

我连续使用了固定的高度,但对我却不起作用
Ken Zira

2
经过几天的弄清楚为什么它对我不起作用,我发现:它仅在TableCellView不可编辑时才起作用。或绑定检查:不要紧,在属性[编辑行为]检查[有条件地设置可编辑:是]
卢卡斯

我只想强调在表单元格中具有实际高度限制的重要性:我的没有,因此单元格的计算出的fittingSize为0.0,随后产生了挫败感。
green_knight

9

基于Corbin的答案(顺便说一句,感谢您对此发表一些看法):

Swift 3,基于视图的NSTableView,带有适用于macOS 10.11(及更高版本)的自动布局

我的设置:我有一个NSTableCellView使用自动版式布局的布局。它包含(除其他元素外)多行NSTextField,最多可包含2行。因此,整个单元格视图的高度取决于此文本字段的高度。

我更新告诉表格视图两次来更新高度:

1)当表格视图调整大小时:

func tableViewColumnDidResize(_ notification: Notification) {
        let allIndexes = IndexSet(integersIn: 0..<tableView.numberOfRows)
        tableView.noteHeightOfRows(withIndexesChanged: allIndexes)
}

2)当数据模型对象更改时:

tableView.noteHeightOfRows(withIndexesChanged: changedIndexes)

这将导致表格视图向其委托要求新的行高。

func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {

    // Get data object for this row
    let entity = dataChangesController.entities[row]

    // Receive the appropriate cell identifier for your model object
    let cellViewIdentifier = tableCellViewIdentifier(for: entity)

    // We use an implicitly unwrapped optional to crash if we can't create a new cell view
    var cellView: NSTableCellView!

    // Check if we already have a cell view for this identifier
    if let savedView = savedTableCellViews[cellViewIdentifier] {
        cellView = savedView
    }
    // If not, create and cache one
    else if let view = tableView.make(withIdentifier: cellViewIdentifier, owner: nil) as? NSTableCellView {
        savedTableCellViews[cellViewIdentifier] = view
        cellView = view
    }

    // Set data object
    if let entityHandler = cellView as? DataEntityHandler {
        entityHandler.update(with: entity)
    }

    // Layout
    cellView.bounds.size.width = tableView.bounds.size.width
    cellView.needsLayout = true
    cellView.layoutSubtreeIfNeeded()

    let height = cellView.fittingSize.height

    // Make sure we return at least the table view height
    return height > tableView.rowHeight ? height : tableView.rowHeight
}

首先,我们需要获取模型对象的行(entity)和适当的单元格视图标识符。然后,我们检查是否已经为该标识符创建了视图。为此,我们必须维护一个列表,其中包含每个标识符的单元格视图:

// We need to keep one cell view (per identifier) around
fileprivate var savedTableCellViews = [String : NSTableCellView]()

如果没有保存,则需要创建(并缓存)一个新的。我们使用模型对象更新单元格视图,并告诉它根据当前表视图宽度重新布局所有内容。fittingSize然后可以将该高度用作新的高度。


这是完美的答案。如果您有多列,则需要将边界设置为列宽,而不是表宽。
Vojto

另外,按照您的设置cellView.bounds.size.width = tableView.bounds.size.width,最好将高度重置为较小的数字。如果您打算重复使用此虚拟视图,则将其设置为较小的数字将“重置”装配大小。
Vojto

7

对于想要更多代码的人,这里是我使用的完整解决方案。感谢corbin dunn为我指出正确的方向。

我需要在关系大多设定的高度有多高一NSTextView的我NSTableViewCell了。

在我的子类中,NSViewController通过调用临时创建一个新单元格outlineView:viewForTableColumn:item:

- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
{
    NSTableColumn *tabCol = [[outlineView tableColumns] objectAtIndex:0];
    IBAnnotationTableViewCell *tableViewCell = (IBAnnotationTableViewCell*)[self outlineView:outlineView viewForTableColumn:tabCol item:item];
    float height = [tableViewCell getHeightOfCell];
    return height;
}

- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
    IBAnnotationTableViewCell *tableViewCell = [outlineView makeViewWithIdentifier:@"AnnotationTableViewCell" owner:self];
    PDFAnnotation *annotation = (PDFAnnotation *)item;
    [tableViewCell setupWithPDFAnnotation:annotation];
    return tableViewCell;
}

在我的IBAnnotationTableViewCell单元格(的子类NSTableCellView)的控制器中,我有一个设置方法

-(void)setupWithPDFAnnotation:(PDFAnnotation*)annotation;

这将设置所有出口,并从我的PDFAnnotations中设置文本。现在,我可以使用以下方法“轻松”计算高度:

-(float)getHeightOfCell
{
    return [self getHeightOfContentTextView] + 60;
}

-(float)getHeightOfContentTextView
{
    NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:[self.contentTextView font],NSFontAttributeName,nil];
    NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:[self.contentTextView string] attributes:attributes];
    CGFloat height = [self heightForWidth: [self.contentTextView frame].size.width forString:attributedString];
    return height;
}

- (NSSize)sizeForWidth:(float)width height:(float)height forString:(NSAttributedString*)string
{
    NSInteger gNSStringGeometricsTypesetterBehavior = NSTypesetterLatestBehavior ;
    NSSize answer = NSZeroSize ;
    if ([string length] > 0) {
        // Checking for empty string is necessary since Layout Manager will give the nominal
        // height of one line if length is 0.  Our API specifies 0.0 for an empty string.
        NSSize size = NSMakeSize(width, height) ;
        NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:size] ;
        NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:string] ;
        NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init] ;
        [layoutManager addTextContainer:textContainer] ;
        [textStorage addLayoutManager:layoutManager] ;
        [layoutManager setHyphenationFactor:0.0] ;
        if (gNSStringGeometricsTypesetterBehavior != NSTypesetterLatestBehavior) {
            [layoutManager setTypesetterBehavior:gNSStringGeometricsTypesetterBehavior] ;
        }
        // NSLayoutManager is lazy, so we need the following kludge to force layout:
        [layoutManager glyphRangeForTextContainer:textContainer] ;

        answer = [layoutManager usedRectForTextContainer:textContainer].size ;

        // Adjust if there is extra height for the cursor
        NSSize extraLineSize = [layoutManager extraLineFragmentRect].size ;
        if (extraLineSize.height > 0) {
            answer.height -= extraLineSize.height ;
        }

        // In case we changed it above, set typesetterBehavior back
        // to the default value.
        gNSStringGeometricsTypesetterBehavior = NSTypesetterLatestBehavior ;
    }

    return answer ;
}

- (float)heightForWidth:(float)width forString:(NSAttributedString*)string
{
    return [self sizeForWidth:width height:FLT_MAX forString:string].height ;
}

heightForWidth:forString:在哪里实现?
ZS

对于那个很抱歉。将其添加到答案。试图找到我从中得到的原始SO帖子,但找不到。
2014年

1
谢谢!它几乎对我有用,但是却给我不准确的结果。就我而言,输入NSTextContainer的宽度似乎是错误的。您是否正在使用自动布局来定义NSTableViewCell的子视图?“ self.contentTextView”的宽度从何而来?
2014年

我发布了关于我的问题的问题在这里:stackoverflow.com/questions/22929441/...
ZS

3

我一直在寻找一种解决方案,并提出了以下解决方案,在我的情况下效果很好:

- (double)tableView:(NSTableView *)tableView heightOfRow:(long)row
{
    if (tableView == self.tableViewTodo)
    {
        CKRecord *record = [self.arrayTodoItemsFiltered objectAtIndex:row];
        NSString *text = record[@"title"];

        double someWidth = self.tableViewTodo.frame.size.width;
        NSFont *font = [NSFont fontWithName:@"Palatino-Roman" size:13.0];
        NSDictionary *attrsDictionary =
        [NSDictionary dictionaryWithObject:font
                                    forKey:NSFontAttributeName];
        NSAttributedString *attrString =
        [[NSAttributedString alloc] initWithString:text
                                        attributes:attrsDictionary];

        NSRect frame = NSMakeRect(0, 0, someWidth, MAXFLOAT);
        NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
        [[tv textStorage] setAttributedString:attrString];
        [tv setHorizontallyResizable:NO];
        [tv sizeToFit];

        double height = tv.frame.size.height + 20;

        return height;
    }

    else
    {
        return 18;
    }
}

在我的情况下,我想垂直扩展给定的列,以使该标签上显示的文本适合所有内容。因此,我得到了该列并从中获取字体和宽度。
克拉伊德达(Klajd Deda)

3

由于我使用了customNSTableCellView并且可以访问NSTextField我的解决方案,因此需要在上添加一个方法NSTextField

@implementation NSTextField (IDDAppKit)

- (CGFloat)heightForWidth:(CGFloat)width {
    CGSize size = NSMakeSize(width, 0);
    NSFont*  font = self.font;
    NSDictionary*  attributesDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
    NSRect bounds = [self.stringValue boundingRectWithSize:size options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading attributes:attributesDictionary];

    return bounds.size.height;
}

@end

谢谢!你是我的救星!这是使制作正确的唯一方法。我花了一整天的时间来解决这个问题。
汤米


2

这是我为修复它所做的事情:

来源:在“行高nstableview”下查看XCode文档。您将找到一个名为“ TableViewVariableRowHeights / TableViewVariableRowHeightsAppDelegate.m”的示例源代码。

(注意:我正在表格视图中查看第1列,您必须进行调整才能在其他地方查看)

在Delegate.h中

IBOutlet NSTableView            *ideaTableView;

在Delegate.m中

表格视图委托控制行高

    - (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row {
    // Grab the fully prepared cell with our content filled in. Note that in IB the cell's Layout is set to Wraps.
    NSCell *cell = [ideaTableView preparedCellAtColumn:1 row:row];

    // See how tall it naturally would want to be if given a restricted with, but unbound height
    CGFloat theWidth = [[[ideaTableView tableColumns] objectAtIndex:1] width];
    NSRect constrainedBounds = NSMakeRect(0, 0, theWidth, CGFLOAT_MAX);
    NSSize naturalSize = [cell cellSizeForBounds:constrainedBounds];

    // compute and return row height
    CGFloat result;
    // Make sure we have a minimum height -- use the table's set height as the minimum.
    if (naturalSize.height > [ideaTableView rowHeight]) {
        result = naturalSize.height;
    } else {
        result = [ideaTableView rowHeight];
    }
    return result;
}

您还需要这样做来影响新的行高(委托方法)

- (void)controlTextDidEndEditing:(NSNotification *)aNotification
{
    [ideaTableView reloadData];
}

我希望这有帮助。

最后说明:这不支持更改列宽。


你摇滚!:) 谢谢。
旗手

4
不幸的是,Apple示例代码(当前标记为1.1版)使用的是基于单元格的表,而不是基于视图的表(此问题与之相关)。代码调用-[NSTableView preparedCellAtColumn:row],文档说“仅适用于基于NSCell的表视图”。在基于视图的表上使用此方法将在运行时生成以下日志输出:“已调用基于视图的NSTableView错误:prepareCellAtColumn:row:。请记录此日志中包含回溯的错误,或停止使用该方法。”
Ashley

1

这是一个基于JanApotheker答案的解决方案,修改后因为cellView.fittingSize.height没有为我返回正确的身高。在我的情况下,我使用standard NSTableCellView,一个NSAttributedString用于单元格的textField文本,以及一个对IB中设置的单元格textField具有约束的单列表。

在我的视图控制器中,我声明:

var tableViewCellForSizing: NSTableCellView?

在viewDidLoad()中:

tableViewCellForSizing = tableView.make(withIdentifier: "My Identifier", owner: self) as? NSTableCellView

最后,对于tableView委托方法:

func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
    guard let tableCellView = tableViewCellForSizing else { return minimumCellHeight }

    tableCellView.textField?.attributedStringValue = attributedString[row]
    if let height = tableCellView.textField?.fittingSize.height, height > 0 {
        return height
    }

    return minimumCellHeight
}

mimimumCellHeight为备份设置为30的常数,但从未实际使用过。attributedStrings是我的模型数组NSAttributedString

这完全可以满足我的需求。感谢您之前的所有回答,这为我解决这个令人讨厌的问题指明了正确的方向。


1
很好。一个问题。表格不可编辑,否则textfield.fittingSize.height将不起作用并将返回零。在viewdidload中设置table.isEditable = false
jiminybob99

0

这听起来很像我之前要做的事情。我希望我能告诉你,我想出了一个简单,优雅的解决方案,但是,可惜,我没有。不是因为缺乏尝试。正如您已经注意到的,需要UITableView在构建单元格之前先知道其高度,这实际上使它们看起来都非常圆。

我最好的解决方案是将逻辑推入单元,因为至少我可以隔离理解单元布局方式所需的类。像

+ (CGFloat) heightForStory:(Story*) story

就能确定单元的高度。当然,这涉及到测量文本等。在某些情况下,我设计了一些方法来缓存在此方法期间获得的信息,这些信息可以在创建单元格时使用。那是我想到的最好的。尽管看起来应该有一个更好的答案,但这是一个令人毛骨悚然的问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.