Nstableview на основе представления со строками с динамическими высотами

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

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

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

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

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

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

10 ответов


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

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

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

чтобы динамически обновлять высоту, то, что вы должны сделать, это ответить на изменение текста (через цель/действие) и пересчитать вычисленную высоту этого представления. Не надо. динамическое изменение NSTableCellViewвысота (или любой вид, который вы используете в качестве "вида ячейки"). В таблице должны управляйте рамкой этого представления, и вы будете бороться с tableview, если попытаетесь установить его. Вместо этого в вашем целевом/действии для текстового поля, где вы вычисляли высоту, вызовите noteHeightOfRowsWithIndexesChanged:, что позволит таблице изменить размер этой отдельной строки. Предполагая, что у вас есть настройка маски авторезирования прямо на подвидах (т. е.: подвиды 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 в Редакторе раскадровки и выберите эту опцию в Инспекторе размеров:

enter image description here

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

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

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

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

мне нужно было установить высоту в основном по отношению к тому, как высоко 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. Теперь я могу "легко" calcutate высоты через:

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

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

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

моя настройка: у меня есть NSTableCellView это выложено с помощью Auto-Layout. Он содержит (помимо других элементов) многострочный 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]()

если ни один не сохранен, нам нужно создать (и кэшировать) новый. Мы обновляем представление ячейки нашим объектом модели и говорим ему перестроить все на основе текущей ширины представления таблицы. The 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;
    }
}

так как я использую custom 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.м"

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

в делегат.h

IBOutlet NSTableView            *ideaTableView;

в делегат.м

посмотреть в таблице делегатов управление высотой строки

    - (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 знать высоту до строящихся ячеек действительно делает все это довольно круглым.

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

+ (CGFloat) heightForStory:(Story*) story

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


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

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