NSTableView на основе представления со строками, имеющими динамическую высоту - PullRequest
79 голосов
/ 21 сентября 2011

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

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

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

Предложения

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

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

Ответы [ 10 ]

128 голосов
/ 08 ноября 2011

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

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

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

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

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

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

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

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

28 голосов
/ 02 ноября 2017

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

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

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]()

Если ни один не сохранен, нам нужно создать (и кэшировать) новый. Мы обновляем представление ячейки с нашим объектом модели и говорим ему перестроить все на основе текущей ширины представления таблицы. Высота fittingSize может быть использована в качестве новой высоты.

7 голосов
/ 10 сентября 2013

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

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

, который устанавливает все розетки и устанавливает текст из моих PDF-аннотаций. Теперь я могу «легко» рассчитать высоту, используя:

-(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 голосов
/ 01 февраля 2016

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

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

- (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;
    }
}
2 голосов
/ 17 сентября 2012

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

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

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

Последнее замечание: не поддерживается изменение ширины столбца.

2 голосов
/ 22 сентября 2011

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

1 голос
/ 29 мая 2017

Вот решение, основанное на ответе 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.

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

1 голос
/ 30 сентября 2011

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

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

+ (CGFloat) heightForStory:(Story*) story

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

...