View-based NSTableView с строками с динамическими высотами

У меня есть приложение с NSTableView на основе NSTableView . Внутри этого представления таблицы у меня есть строки, у которых есть ячейки, у которых есть контент, состоящий из многострочного NSTextField с включенным NSTextField слов. В зависимости от текстового содержимого NSTextField размер строк, необходимых для отображения ячейки, будет отличаться.

Я знаю, что могу реализовать метод NSTableViewDelegatetableView:heightOfRow: вернуть высоту, но высота будет определяться на основе обертывания слов, используемого на NSTextField . Оболочка слова NSTextField аналогично основана на том, насколько широка NSTextField …, которая определяется шириной NSTableView .

Soooo … Я думаю, мой вопрос … что такое хороший шаблон дизайна для этого? Кажется, что все, что я пробую, заканчивается запутанным беспорядком. Поскольку TableView требует знания высоты ячеек, чтобы выложить их … и NSTextField нуждается в знаниях о его макете, чтобы определить перенос слов … и клетке нужно знание обертывания слов, чтобы определить ее высоту … это круговой беспорядок … и это сводит меня с ума .

Предложения?

Если это имеет значение, конечный результат также будет иметь редактируемые NSTextFields которые будут изменять размер, чтобы приспособиться к тексту внутри них. У меня уже есть работа над уровнем представления, но tableview еще не корректирует высоты ячеек. Я noteHeightOfRowsWithIndexesChanged только я получу проблему с высотой, я буду использовать метод – noteHeightOfRowsWithIndexesChanged чтобы информировать таблицу о том, как изменилась высота … но она все равно собирается спросить делегата о высоте … следовательно, мой quandry.

Заранее спасибо!

10 Solutions collect form web for “View-based NSTableView с строками с динамическими высотами”

Это проблема с курицей и яйцом. Таблица должна знать высоту строки, потому что она определяет, где будет находиться данное представление. Но вы хотите, чтобы представление уже было вокруг, чтобы вы могли использовать его для определения высоты строки. Итак, что на первом месте?

Ответ заключается в том, чтобы сохранить дополнительный NSTableCellView (или любой вид, который вы используете в качестве своего «представления ячеек»), только для измерения высоты представления. В tableView:heightOfRow: делегировать, получить доступ к вашей модели для «строки» и установить objectValue в NSTableCellView . Затем установите ширину представления как ширину таблицы, и (как бы вы этого ни захотели) определите требуемую высоту для этого вида. Верните это значение.

Не вызывайте noteHeightOfRowsWithIndexesChanged: из метода делегата tableView:heightOfRow: или viewForTableColumn:row: :! Это плохо, и это вызовет мега-неприятности.

Чтобы динамически обновлять высоту, то вам следует отреагировать на изменение текста (через цель / действие) и пересчитать вычисленную высоту этого представления. Теперь не динамически изменяйте NSTableCellView (или какой-либо вид, который вы используете в качестве своего «представления ячейки»). Таблица должна контролировать рамку этого представления, и вы будете бороться с табличным представлением, если попытаетесь установить его. Вместо этого в вашем целевом / действии для текстового поля, где вы вычисляете высоту, вызовите noteHeightOfRowsWithIndexesChanged: который позволит таблице resize этой отдельной строки. Предполагая, что у вас есть настройка авторезистирующей маски прямо на subviews (то есть: subviews из NSTableCellView ), все должно измениться в порядке! Если нет, сначала начните работу с маской для изменения размеров подзонов, чтобы все было правильно с переменной высотой строки.

Не забывайте, что noteHeightOfRowsWithIndexesChanged: анимирует по умолчанию. Чтобы сделать его неживым:

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

PS: Я больше отвечаю на вопросы, размещенные на форумах Apple Dev, чем переполнение стека.

PSS: я написал представление, основанное на NSTableView

Это стало намного проще в macOS 10.13 с .usesAutomaticRowHeights . Подробности здесь: https://developer.apple.com/library/content/releasenotes/AppKit/RN-AppKit/#10_13 (В разделе «Автоматические высоты строки NSTableView»).

В основном вы просто выбираете свой NSTableView или NSOutlineView в редакторе раскадровки и выбираете этот параметр в инспекторе размеров:

введите описание изображения здесь

Затем вы устанавливаете материал в свой NSTableCellView чтобы иметь верхние и нижние ограничения для ячейки, и ваша ячейка изменит размер, чтобы она соответствовала автоматически. Не требуется код!

Ваше приложение будет игнорировать любые высоты, указанные в heightOfRow ( NSTableView ) и heightOfRowByItem ( NSOutlineView ). Вы можете видеть, какие высоты вычисляются для ваших строк автоматического макета с помощью этого метода:

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

Для тех, кто хочет получить больше кода, вот полное решение, которое я использовал. Спасибо corbin dunn за то, что указали мне в правильном направлении.

Мне нужно было установить высоту в основном в зависимости от того, насколько высок NSTextView в моем NSTableViewCell .

В моем подclassе 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 который является controllerом моей ячейки (подclass 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; } с -(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; } с -(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 ; } 

Основываясь на ответе Корбина (btw, спасибо, проливая некоторый свет на это):

Swift 3, View-Based NSTableView с автоматической компоновкой для macOS 10.11 (и выше)

Моя настройка: у меня есть NSTableCellView который выложен с помощью Auto-Layout. Он содержит (помимо других элементов) многострочный NSTextField который может содержать до двух строк. Поэтому высота представления всей ячейки зависит от высоты этого текстового поля.

Я обновляю сообщение в виде таблицы, чтобы обновить высоту в двух случаях:

1) При изменении размера таблицы:

 func tableViewColumnDidResize(_ notification: Notification) { let allIndexes = IndexSet(integersIn: 0.. 

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 можно использовать как новую высоту.

Я искал решение довольно долгое время и придумал следующий, который отлично работает в моем случае:

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

Поскольку я использую пользовательский 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 

Вы видели RowResizableViews ? Он довольно старый, и я его не тестировал, но он тем не менее может работать.

Вот что я сделал, чтобы исправить это:

Источник: ознакомьтесь с документацией XCode в разделе «Высота строки nstableview». Вы найдете пример исходного кода с именем «TableViewVariableRowHeights / TableViewVariableRowHeightsAppDelegate.m»

(Примечание: я смотрю на столбец 1 в виде таблицы, вам нужно настроить, чтобы посмотреть в другом месте)

in Delegate.h

 IBOutlet NSTableView *ideaTableView; 

в Делегат.

table view delegates контролируют высоту строки

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

Надеюсь, это поможет.

Заключительное примечание: это не поддерживает изменение ширины столбца.

Это очень похоже на то, что я должен был сделать ранее. Хотел бы я сказать, что я придумал простое, элегантное решение, но, увы, я этого не сделал. Не из-за отсутствия попыток. Поскольку вы уже заметили, что UITableView должен знать высоту до того, как создаваемые ячейки действительно сделают все, они кажутся довольно круговыми.

Мое лучшее решение заключалось в том, чтобы подталкивать логику к ячейке, потому что, по крайней мере, я мог бы выделить class, необходимый для понимания того, как были выложены ячейки. Метод, подобный

 + (CGFloat) heightForStory:(Story*) story 

сможет определить, насколько высока эта ячейка. Конечно, это связано с измерением текста и т. Д. В некоторых случаях я разработал способы кэширования информации, полученной в ходе этого метода, которая затем может быть использована при создании ячейки. Это было лучшее, что я придумал. Это проблема бешенства, хотя, как кажется, должен быть лучший ответ.

Вот решение, основанное на ответе JanApotheker, измененное как cellView.fittingSize.height не вернул мне правильную высоту. В моем случае я использую стандартный NSTableCellView , NSAttributedString для текста textField ячейки и одну таблицу столбцов с ограничениями для текстового поля ячейки, установленного в IB.

По моему мнению, я заявляю:

 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 – это постоянное значение, mimimumCellHeight 30, для резервного копирования, но никогда не используется. NSAttributedString – мой модельный массив NSAttributedString .

Это отлично работает для моих нужд. Спасибо за все предыдущие ответы, которые указали мне в правильном направлении для этой надоедливой проблемы.

Давайте будем гением компьютера.