objective c - 基于视图的NSTableView,其行具有动态高度

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

我知道我可以实现NSTableViewDelegate方法-tableView:heightOfRow:来返回高度,但高度将根据NSTextField上使用的自动换行确定.NSTextField的自动换行同样基于NSTextFields的宽度...这是... 由noteHeightOfRowsWithIndexesChanged的宽度决定。

Soooo ......我想我的问题是......对于这个有什么好的设计模式? 似乎我尝试的一切都变成了一个令人费解的混乱。 由于TableView需要知道细胞的高度才能将它们排出......而NSTextFields需要知道它的布局以确定自动换行...并且细胞需要知道自动换行来确定它的高度......这是一个圆形的混乱 ......它让我疯了。

建议?

如果重要,最终结果也将具有可编辑的NSTextFields,它将调整大小以适应其中的文本。 我已经在视图级别上工作了,但是tableview还没有调整单元格的高度。 我想一旦我得到了高度问题,我将使用-noteHeightOfRowsWithIndexesChanged方法通知表格视图高度发生变化......但是它仍然会向代表询问高度...因此,我的quandry。

提前致谢!

Jiva DeVoe asked 2019-09-17T21:20:55Z
10个解决方案
127 votes

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

答案是保留额外的noteHeightOfRowsWithIndexesChanged:(或者您正在使用的任何视图作为“单元格视图”),仅用于测量视图的高度。 在noteHeightOfRowsWithIndexesChanged:委托方法中,访问“row”的模型并在NSTableCellView上设置NSTableCellView。然后将视图的宽度设置为表格的宽度,(但是你想要这样做)计算出该视图所需的高度。 返回那个值。

请勿在委托方法noteHeightOfRowsWithIndexesChanged:NSTableCellView中致电noteHeightOfRowsWithIndexesChanged:! 这很糟糕,会造成巨大的麻烦。

要动态更新高度,那么您应该做的是响应文本更改(通过目标/操作)并重新计算该视图的计算高度。 现在,不要动态更改noteHeightOfRowsWithIndexesChanged:的高度(或者您用作“单元格视图”的任何视图)。 该表必须控制该视图的框架,如果您尝试设置它,您将对抗tableview。 相反,在计算高度的文本字段的目标/操作中,请调用noteHeightOfRowsWithIndexesChanged:,这将使表格调整该行的大小。 假设您在子视图上设置了自动调整掩码(即:NSTableCellView的子视图),事情应该调整大小! 如果没有,首先处理子视图的大小调整掩码,以使变量行高度正确。

不要忘记默认情况下noteHeightOfRowsWithIndexesChanged:动画。 为了使它不具有动画效果:

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

PS:我对Apple Dev Forums上发布的问题的反应比堆栈溢出更多。

PSS:我写了基于NSTableView的视图

corbin dunn answered 2019-09-17T21:21:52Z
27 votes

在macOS 10.13中使用heightOfRow可以轻松实现。详细信息如下:[https://developer.apple.com/library/content/releasenotes/AppKit/RN-AppKit/#10_13](在标题为“NSTableView Automatic”的部分中 行高“”。

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

enter image description here

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

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

func outlineView(_ outlineView: NSOutlineView, didAdd rowView: NSTableRowView, forRow row: Int) {
  print(rowView.fittingSize.height)
}
Clifton Labrum answered 2019-09-17T21:22:38Z
7 votes

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

我需要设置的高度大多与我NSTableCellViewIBAnnotationTableViewCell的高度有关。

在我的IBAnnotationTableViewCell的子类中,我通过调用NSTableCellView临时创建一个新单元格

- (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 ;
}
Sunkas answered 2019-09-17T21:23:34Z
7 votes

根据Corbin的回答(顺便说一下,对此有所了解):

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

我的设置:我有一个使用自动布局布局的fittingSize。 它包含(除了其他元素之外)多行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
}

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

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

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

JanApotheker answered 2019-09-17T21:24:53Z
3 votes

我正在寻找一个解决方案很长一段时间,并提出了以下一个解决方案,这在我的情况下非常有用:

- (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;
    }
}
vomako answered 2019-09-17T21:25:21Z
3 votes

由于我使用自定义NSTableCellView并且我可以访问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
Klajd Deda answered 2019-09-17T21:25:49Z
2 votes

你看过RowResizableViews了吗? 它已经很老了,我还没有测试过,但它可能仍然有用。

Tim answered 2019-09-17T21:26:16Z
2 votes

这是我为解决它所做的工作:

来源:查看XCode文档,在“行高nstableview”下。 您将找到名为“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];
}

我希望这有帮助。

最后注意:这不支持更改列宽。

laurent answered 2019-09-17T21:27:31Z
1 votes

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

我最好的解决方案是将逻辑推向细胞,因为至少我可以找出理解细胞如何布局所需的类。 像这样的方法

+ (CGFloat) heightForStory:(Story*) story

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

vagrant answered 2019-09-17T21:28:13Z
1 votes

这是一个基于JanApotheker的答案的解决方案,修改为mimimumCellHeight并没有为我返回正确的高度。 在我的情况下,我使用标准attributedStringsNSAttributedString用于单元格的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

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

jbaraga answered 2019-09-17T21:29:13Z
translate from https://stackoverflow.com:/questions/7504546/view-based-nstableview-with-rows-that-have-dynamic-heights