Подтвердить что ты не робот

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

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

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

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

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

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

Спасибо заранее!

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

Ответ 2

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

Мне нужно было установить высоту в основном в зависимости от того, насколько высока a 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 ;
}

Ответ 3

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

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

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

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

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.

Ответ 4

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

Ответ 5

Это стало намного проще в 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)
}

Ответ 6

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

Ответ 7

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

Источник: ознакомьтесь с документацией 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 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 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];
}

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

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

Ответ 8

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

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

Ответ 9

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

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

+ (CGFloat) heightForStory:(Story*) story

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

Ответ 10

Вот решение, основанное на ответе JanApotheker, измененное как cellView.fittingSize.height, не вернуло мне правильную высоту. В моем случае я использую стандартный NSTableCellView, NSAttributedString для текста textField ячейки и одну таблицу столбцов с ограничениями для ячейки 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 - это постоянное значение, равное 30, для резервного копирования, но никогда не используется. attributedStrings - мой модельный массив NSAttributedString.

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