基于视图的NSTableView具有dynamic高度的行

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

我知道我可以实现NSTableViewDelegate方法 – tableView:heightOfRow:返回高度,但高度将根据NSTextField上使用的单词换行确定。 NSTextField换行同样基于NSTextField的宽度,这是由NSTableView的宽度决定的。

Soooo …我想我的问题是…什么是一个好的devise模式呢? 似乎我所尝试的一切都变成了一团糟。 由于TableView需要知道单元格的高度,所以需要知道它的布局以确定单词换行…而单元格需要单词换行的知识来确定它的高度…这是一个圆形的混乱…这让我疯狂

build议?

如果重要的话,最终的结果还将有可编辑的NSTextFields ,将resize,以适应其中的文本。 我已经在视图级别上工作了,但是tableview还没有调整单元格的高度。 一旦我得到了高度问题的解决scheme,我将使用 – noteHeightOfRowsWithIndexesChanged方法来通知表视图的高度改变…但它仍然要问代表的高度…因此,我的困惑。

提前致谢!

这是一个鸡和鸡蛋的问题。 该表需要知道行高度,因为这决定了给定视图的位置。 但是你想要一个已经存在的视图,所以你可以使用它来找出行高。 那么,首先呢?

答案是保留一个额外的NSTableCellView (或者任何您用作“单元格视图”的视图),只是为了测量视图的高度。 在tableView:heightOfRow: delegate方法中,访问您的模型'row'并在NSTableCellView上设置objectValue 。 然后将视图的宽度设置为您的表格的宽度,然后(但是您要这样做)找出该视图所需的高度。 返回该值。

不要调用noteHeightOfRowsWithIndexesChanged:从委托方法tableView:heightOfRow:viewForTableColumn:row: 这是不好的,会造成巨大的麻烦。

要dynamic更新高度,那么您应该做的是响应文本更改(通过目标/操作),并重新计算您的计算高度的视图。 现在,不要dynamic地改变NSTableCellView的高度(或任何您用作“单元格视图”的视图)。 该表必须控制该视图的框架,如果您尝试设置该表格,则会与表格视图进行对抗。 相反,在您计算高度的文本字段的目标/操作中,请调用noteHeightOfRowsWithIndexesChanged:它将使表格调整该单个行的大小。 假设你在子视图(即: NSTableCellView子视图)上设置了自动确定屏蔽的设置,事情应该调整好! 如果没有,首先在子视图的resize的面具上工作,以变化的行高获得正确的东西。

不要忘记, noteHeightOfRowsWithIndexesChanged:默认为animation。 为了使其不具有生命力:

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

PS:我对苹果开发者论坛发布的问题做出了更多回应,而不是堆栈溢出。

PSS:我写了基于NSTableView的视图

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

我需要设置的高度主要与NSTableViewCellNSTextView的高度有关。

在我的NSViewController子类我临时创build一个新的单元格通过调用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; } 

在我的单元格控制器( NSTableCellView子类)的NSTableCellView我有一个setup方法

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

它build立了所有网点,并从我的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 ; } 

基于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 )的模型对象和适当的单元视图标识符。 然后我们检查是否已经为这个标识符创build了一个视图。 要做到这一点,我们必须维护每个标识符的单元格视图的列表:

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

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

由于我使用自定义NSTableCellView ,我有权访问NSTextField我的解决scheme是在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 

你有没有看RowResizableViews ? 这是相当老,我没有testing它,但它可能仍然工作。

以下是我所做的修复:

资料来源:在“row height 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]; } 

我希望这有帮助。

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

我正在寻找一个解决scheme相当长的一段时间,并提出了下面的一个,这在我的情况很好:

 - (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; } } 

这听起来很像我以前做的事情。 我希望我能告诉你,我提出了一个简单,优雅的解决scheme,但是,唉,我没有。 不是因为缺乏尝试。 正如你已经注意到UITableView在构build单元格之前知道高度的需要,真的使它看起来很圆。

我最好的解决办法是把逻辑推向单元格,因为至less我可以分离出什么类需要了解单元格的布局。 类似的方法

 + (CGFloat) heightForStory:(Story*) story 

将能够确定细胞有多高。 当然,这涉及到测量文本等。在某些情况下,我devise了一些方法来caching在这个方法中获得的信息,然后可以在创build单元格时使用这些信息。 这是我想出的最好的。 这是一个令人愤怒的问题,但似乎应该有一个更好的答案。

这里是基于JanApotheker的答案的一个解决scheme,修改为cellView.fittingSize.height没有返回正确的高度。 在我的情况下,我正在使用标准的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模型数组。

这完全符合我的需求。 感谢所有以前的答案,这些问题指出了我正确的方向。

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

基本上,您只需在故事板编辑器中selectNSTableViewNSOutlineView ,然后在“大小”检查器中select此选项:

在这里输入图像描述

然后你设置你的NSTableCellView的东西对单元格有顶部和底部约束,你的单元格将会自动resize。 无需代码!

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

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