NSTableView на основі перегляду з рядками, які мають динамічну висоту


84

У мене є програма з переглядом NSTableView. Усередині цього подання таблиці у мене є рядки, які містять комірки, вміст яких складається з багаторядків NSTextFieldіз увімкненим перенесенням слів. Залежно від текстового вмісту NSTextField, розмір рядків, необхідних для відображення комірки, буде змінюватися.

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

Ооооо ... Думаю, моє питання полягає в тому ... який хороший шаблон дизайну для цього? Здається, все, що я намагаюся, закінчується заплутаним безладом. Оскільки TableView вимагає знання висоти клітинок, щоб їх розкласти ... і NSTextFieldзнання його макета вимагає визначення переносу слів ... а клітинка потребує знання обгортки слів, щоб визначити його висоту ... це круговий безлад ... і це зводить мене з розуму .

Пропозиції?

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

Спасибі заздалегідь!


Якщо як ширина, так і висота подання можуть змінюватися з їх вмістом, то їх визначення складне, оскільки вам доведеться виконати ітерацію (але тоді ви можете кешувати). Але, здається, у вашому корпусі змінюється висота на постійну ширину. Якщо ширина відома (тобто на основі ширини вікна або виду), то тут нічого складного. Мені не вистачає, чому ширина варіюється?
Роб Нейпір,

Тільки висота може змінюватися залежно від вмісту. І я вже вирішив цю проблему. У мене є підклас NSTextField, який автоматично регулює його висоту. Проблема полягає у отриманні знань про це коригування до делегата подання таблиці, тому він може відповідно оновити висоту.
Jiva DeVoe

@Jiva: Цікаво, чи ти це ще не вирішив.
Джошуа Ноцці

Я намагаюся зробити щось подібне. Мені подобається ідея підкласу NSTextField, який регулює власну висоту. Як щодо додавання (делегованого) повідомлення про зміну висоти до цього підкласу та моніторингу цього за допомогою відповідного класу (джерело даних, делегат структури, ...), щоб отримати інформацію до контуру?
Джон Велман,

Дуже пов’язане питання з відповідями, які допомогли мені більше, ніж тут: Висота рядка NSTableView на основі NSStrings .
Ешлі,

Відповіді:


132

Це проблема курки та яєць. Таблиця повинна знати висоту рядка, оскільки це визначає, де буде знаходитися даний вигляд. Але ви хочете, щоб вигляд уже був навколо, щоб ви могли використовувати його для визначення висоти рядка. Отже, що першим?

Відповідь полягає в тому, щоб залишити додатковий NSTableCellView(або будь-який вигляд, який ви використовуєте як свій «вигляд комірки») навколо, лише для вимірювання висоти вигляду. У tableView:heightOfRow:методі делегатів перейдіть до вашої моделі для 'row' та встановіть значення objectValueon 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


Дякую за відповідь. Я не бачив його спочатку ... Я позначив це як правильну відповідь. Форуми розробників чудові, я їх теж використовую.
Jiva DeVoe

"визначити необхідну висоту для цього подання. Повернути це значення." .. Це моя проблема :(. Я використовую view-based tableView, і я не уявляю, як це зробити!
Mazyod,

@corbin Дякую за відповідь, ось що я мав у своєму додатку. Однак я виявив із цим проблему щодо Mavericks. Оскільки ви чітко розбираєтесь у цьому питанні, не могли б ви перевірити моє запитання? Тут: stackoverflow.com/questions/20924141/…
Олексій

1
@corbin чи існує канонічна відповідь на це, але використовуючи автоматичне розміщення та обмеження за допомогою NSTableViews? Існує потенційне рішення з автоматичним розміщенням; цікаво, чи це достатньо добре без розрахункових APIHeightForRowAtIndexPath (це не в какао)
ZS

3
@ZS: Я використовую Auto Layout і реалізував відповідь Корбіна. У tableView:heightOfRow:, я налаштовую свій запасний вигляд комірки зі значеннями рядка (у мене лише один стовпець), і повертаюся cellView.fittingSize.height. fittingSizeє методом NSView, який обчислює мінімальний розмір подання, що задовольняє його обмеження.
Пітер Хосі,

43

Це стало набагато простіше в 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)
}

@tofutim: Якщо правильно працює із загорнутими текстовими клітинками, якщо ви використовуєте правильні обмеження та коли переконайтесь, що налаштування "Бажана ширина" кожного NSTextField з динамічною висотою встановлено на "Автоматично". Дивіться також stackoverflow.com/questions/46890144/…
Елі,

Мені здається, @tofutim має рацію, це не спрацює, якщо ви покладете NSTextView у TableView, він спробує його архівувати, але це не працює. Хоча це працює для іншого компонента, наприклад NSImage ..
Альберт,

Я використовував фіксовану висоту для ряду, але це не працює для мене
Кен Зіра

2
Після кількох днів з'ясування, чому це не працює у мене, я виявив: він працює ТІЛЬКИ коли TableCellViewНЕ можна редагувати. Не має значення, перевіряється в атрибутах [Поведінка: можна редагувати] або Інспектор
Лукаш

Я просто хочу підкреслити, наскільки важливо мати фактичні обмеження висоти в комірці таблиці: у мене цього не було, тому обчислюваний fittingSize клітинки становив 0,0 і виникало розлад.
green_knight

9

На основі відповіді Корбіна (до речі, дякую, проливаючи це світло):

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висоту можна використовувати як нову висоту.


Це ідеальна відповідь. Якщо у вас кілька стовпців, вам потрібно встановити межі на ширину стовпця, а не на ширину таблиці.
Войто

Крім того, як ви встановили cellView.bounds.size.width = tableView.bounds.size.width, непогано скинути висоту до мінімального значення. Якщо ви плануєте повторно використовувати цей фіктивний вигляд, встановивши його на низьке число, "скине" розмір, що підходить.
Войто

7

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

Мені потрібно встановити висоту в основному по відношенню до як високо 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 ;
}

Де реалізовано heightForWidth: forString:
ZS

Вибач за те. Додав це до відповіді. Спробував знайти оригінальний пост SO, я отримав це, але не зміг знайти.
Sunkas

1
Дякую! Це майже працює для мене, але дає мені неточні результати. У моєму випадку ширина, яку я подаю в NSTextContainer, здається неправильною. Чи використовуєте ви автоматичне розміщення для визначення підпрозорів вашого NSTableViewCell? Звідки береться ширина "self.contentTextView"?
ZS

Я розмістив тут запитання про свою проблему: stackoverflow.com/questions/22929441/…
ZS

3

Я досить довго шукав рішення і придумав наступне, яке чудово працює в моєму випадку:

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

У моєму випадку я хотів розширити даний стовпець вертикально, щоб текст, який я відображаю на цій мітці, відповідав усьому. Тому я отримую відповідний стовпець і захоплюю шрифт та ширину з нього.
Klajd Deda

3

Оскільки я використовую користувальницькі 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

Дякую! Ти мій рятівник! Це єдине, що робить роботи коректно. Я витратив цілий день, щоб це зрозуміти.
Томмі

2

Ви бачили RowResizableViews ? Він досить старий, і я його не тестував, але все-таки це може спрацювати.


2

Ось що я зробив, щоб це виправити:

Джерело: Перегляньте документацію XCode у розділі "Висота рядка nstandview". Ви знайдете зразок вихідного коду з назвою "TableViewVariableRowHeights / TableViewVariableRowHeightsAppDelegate.m"

(Примітка: Я переглядаю стовпець 1 у поданні таблиці, вам доведеться налаштувати, щоб шукати деінде)

в Delegate.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];
}

Сподіваюся, це допоможе.

Заключна примітка: це не підтримує зміну ширини стовпця.


Ви рок! :) Дякую.
флагман

4
На жаль, зразок коду Apple (який в даний час позначений як версія 1.1) використовує таблицю на основі комірок, а не таблицю на основі перегляду (про яку йдеться в цьому питанні). Виклики коду -[NSTableView preparedCellAtColumn:row], які, як кажуть у документах, "доступні лише для переглядів таблиць на основі NSCell". Використання цього методу в таблиці, що базується на поданні, видає цей журнал під час виконання: "Викликана помилка NSTableView на основі перегляду: викликаноCellAtColumn: row: викликано. Будь ласка, зареєструйте помилку з зворотною трасою з цього журналу або припиніть використовувати метод".
Ешлі

1

Ось рішення, засноване на відповіді JanApotheker, змінене, оскільки cellView.fittingSize.heightне повертало правильну висоту для мене. У моєму випадку я використовую стандарт NSTableCellView, an NSAttributedStringдля тексту textField комірки, і одну таблицю стовпця з обмеженнями для textField комірки, встановлену в IB.

У своєму контролері подання я заявляю:

var tableViewCellForSizing: NSTableCellView?

In 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
Дуже добре. Одне питання. Таблицю не можна редагувати або textfield.fittingSize.height не буде працювати і поверне нуль. Встановити table.isEditable = false у viewdidload
jiminybob99

0

Це дуже схоже на те, що мені доводилося робити раніше. Я хотів би сказати вам, що я придумав просте, елегантне рішення, але, на жаль, я цього не зробив. Не через відсутність спроб, хоча. Як ви вже помітили необхідність UITableView знати висоту до побудови комірок, насправді все це здається цілком круглим.

Моїм найкращим рішенням було підштовхувати логіку до комірки, тому що принаймні я міг відокремити, який клас потрібен, щоб зрозуміти, як розміщені клітини. Такий метод, як

+ (CGFloat) heightForStory:(Story*) story

змогли б визначити, якою висотою повинна бути камера. Звичайно, це стосувалось вимірювання тексту тощо. У деяких випадках я розробляв способи кешування інформації, отриманої під час цього методу, який потім можна було б використовувати при створенні комірки. Це було найкраще, що я придумав. Це викликає неприємність, однак, як видається, відповідь повинна бути кращою.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.