NSTableView basato sulla visualizzazione con righe con altezze dinamiche


84

Ho un'applicazione con una visualizzazione NSTableViewin essa. All'interno di questa visualizzazione tabella, ho righe con celle con contenuto costituito da più righe NSTextFieldcon il ritorno a capo automatico abilitato. A seconda del contenuto testuale di NSTextField, la dimensione delle righe necessarie per visualizzare la cella varierà.

So di poter implementare il NSTableViewDelegatemetodo - tableView:heightOfRow:per restituire l'altezza, ma l'altezza sarà determinata in base alla parola a capo usata nel file NSTextField. La parola wrapping di NSTextFieldè analogamente basata su quanto è larga la NSTextField... che è determinata dalla larghezza di NSTableView.

Soooo ... immagino che la mia domanda sia ... qual è un buon modello di design per questo? Sembra che tutto quello che provo finisca per essere un pasticcio contorto. Poiché il TableView richiede la conoscenza dell'altezza delle celle per disporle ... e la NSTextFieldconoscenza del suo layout per determinare il ritorno a capo automatico ... e la cella ha bisogno della conoscenza del ritorno a capo automatico per determinarne l'altezza ... è un pasticcio circolare ... e mi sta facendo impazzire .

Suggerimenti?

Se è importante, il risultato finale sarà anche modificabile NSTextFieldsche verrà ridimensionato per adattarsi al testo al loro interno. Ho già questo lavoro a livello di vista, ma la tableview non regola ancora le altezze delle celle. Immagino che una volta risolto il problema dell'altezza, userò il noteHeightOfRowsWithIndexesChangedmetodo - per informare la vista della tabella che l'altezza è cambiata ... ma continuerà a chiedere al delegato l'altezza ... quindi, il mio dilemma.

Grazie in anticipo!


Se sia la larghezza che l'altezza della vista possono cambiare con il loro contenuto, definirle è complicato perché devi iterare (ma poi puoi memorizzare nella cache). Ma il tuo caso sembra avere un'altezza variabile per una larghezza costante. Se la larghezza è nota (cioè basata sulla larghezza della finestra o della vista), allora niente qui è complicato. Mi manca perchè la larghezza è variabile?
Rob Napier,

Solo l'altezza può cambiare con il contenuto. E ho già risolto il problema. Ho una sottoclasse NSTextField che regola automaticamente l'altezza. Il problema sta nel riportare la conoscenza di tale regolazione al delegato della vista tabella, in modo che possa aggiornare l'altezza di conseguenza.
Jiva DeVoe,

@ Jiva: chiedendomi se hai già risolto questo problema.
Joshua Nozzi

Sto cercando di fare qualcosa di simile. Mi piace l'idea di una sottoclasse NSTextField che regola la propria altezza. Che ne dici di aggiungere una notifica (delegato) del cambiamento di altezza a quella sottoclasse e monitorarla con una classe appropriata (origine dati, delegato di struttura, ...) per ottenere informazioni sulla struttura?
John Velman

Una domanda altamente correlata, con risposte che mi hanno aiutato più di quelle qui: Altezza riga NSTableView basata su NSStrings .
Ashley

Risposte:


133

Questo è il problema della gallina e delle uova. La tabella deve conoscere l'altezza della riga perché ciò determina dove si troverà una data vista. Ma vuoi che una vista sia già in giro in modo da poterla utilizzare per calcolare l'altezza della riga. Allora, cosa viene prima?

La risposta è di mantenere un extra NSTableCellView(o qualunque vista tu stia usando come "visualizzazione cella") in giro solo per misurare l'altezza della vista. Nel tableView:heightOfRow:metodo delegato, accedi al tuo modello per "riga" e imposta l' objectValueopzione NSTableCellView. Quindi imposta la larghezza della vista come la larghezza della tua tabella e (comunque vuoi farlo) calcola l'altezza richiesta per quella vista. Restituisci quel valore.

Non chiamare noteHeightOfRowsWithIndexesChanged:da nel metodo delegato tableView:heightOfRow:o viewForTableColumn:row:! Questo è negativo e causerà grossi guai.

Per aggiornare dinamicamente l'altezza, quello che dovresti fare è rispondere alla modifica del testo (tramite l'obiettivo / azione) e ricalcolare l'altezza calcolata di quella vista. Ora, non modificare dinamicamente l' NSTableCellViewaltezza di (o qualunque vista tu stia usando come "vista cella"). La tabella deve controllare il frame di quella vista e, se tenterai di impostarla, combatterai contro la vista tabella. Invece, nel tuo obiettivo / azione per il campo di testo in cui hai calcolato l'altezza, chiama noteHeightOfRowsWithIndexesChanged:, che consentirà alla tabella di ridimensionare quella singola riga. Supponendo che tu abbia la configurazione della maschera di ridimensionamento automatico direttamente sulle sottoviste (ad esempio: sottoviste del NSTableCellView), le cose dovrebbero ridimensionarsi bene! In caso contrario, prima lavora sulla maschera di ridimensionamento delle viste secondarie per ottenere le cose giuste con altezze di riga variabili.

Non dimenticare che si noteHeightOfRowsWithIndexesChanged:anima per impostazione predefinita. Per renderlo non animato:

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

PS: rispondo più alle domande pubblicate sui forum di sviluppo di Apple che allo stack overflow.

PSS: ho scritto NSTableView basato sulla visualizzazione


Grazie per la risposta. All'inizio non l'avevo visto ... L'ho contrassegnata come la risposta corretta. I forum di sviluppo sono fantastici, li uso anche io.
Jiva DeVoe

"calcolare l'altezza richiesta per quella vista. Restituire quel valore." .. Questo è il mio problema :(. Sto usando tableView basato sulla visualizzazione e non ho idea di come farlo!
Mazyod

@corbin Grazie per la risposta, questo è quello che avevo sulla mia app. Tuttavia, ho riscontrato un problema relativo a Mavericks. Dato che sei chiaramente esperto in questo argomento, ti dispiacerebbe controllare la mia domanda? Qui: stackoverflow.com/questions/20924141/…
Alex

1
@corbin c'è una risposta canonica per questo, ma usando layout automatico e vincoli, con NSTableViews? C'è una potenziale soluzione con l'auto-layout; chiedendosi se è abbastanza buono senza le API RatedHeightForRowAtIndexPath (che non è in Cocoa)
ZS

3
@ZS: Sto usando Auto Layout e ho implementato la risposta di Corbin. In tableView:heightOfRow:, ho impostato la mia visualizzazione della cella di riserva con i valori della riga (ho solo una colonna) e ritorno cellView.fittingSize.height. fittingSizeè un metodo NSView che calcola la dimensione minima della vista che soddisfa i suoi vincoli.
Peter Hosey

43

Questo è diventato molto più semplice in macOS 10.13 con .usesAutomaticRowHeights. I dettagli sono qui: https://developer.apple.com/library/content/releasenotes/AppKit/RN-AppKit/#10_13 (nella sezione intitolata "NSTableView Automatic Row Heights").

Fondamentalmente devi solo selezionare il tuo NSTableViewo NSOutlineViewnell'editor dello storyboard e selezionare questa opzione in Impostazioni dimensioni:

inserisci qui la descrizione dell'immagine

Quindi imposti le cose nel tuo NSTableCellViewper avere vincoli superiore e inferiore alla cella e la tua cella verrà ridimensionata per adattarsi automaticamente. Nessun codice richiesto!

La tua app ignorerà le altezze specificate in heightOfRow( NSTableView) e heightOfRowByItem( NSOutlineView). Puoi vedere quali altezze vengono calcolate per le righe del layout automatico con questo metodo:

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

@tofutim: se funziona correttamente con celle di testo avvolte se si utilizzano i vincoli corretti e quando ci si assicura che l'impostazione Larghezza preferita di ogni NSTextField con altezza dinamica, sia impostata su Automatico. Vedi anche stackoverflow.com/questions/46890144/…
Ely

Penso che @tofutim sia giusto, questo non funzionerà se metti NSTextView in TableView, prova ad archiviarlo ma non funziona. Funziona per altri componenti, ad esempio NSImage ..
Albert

Ho usato un'altezza fissa per una riga ma non funziona per me
Ken Zira

2
Dopo alcuni giorni che ho capito perché non funziona per me, ho scoperto: funziona SOLO quando TableCellViewNON è modificabile. Non importa è controllato in Attributes [Behavior: Editable] o Bindings Inspector [Conditionally Set Editable: Yes]
Łukasz

Voglio solo sottolineare quanto sia importante avere vincoli di altezza effettivi nella cella della tabella: il mio no, quindi il fittingSize calcolato della cella era 0,0 e ne è seguita la frustrazione.
green_knight

9

Sulla base della risposta di Corbin (btw grazie per far luce su questo):

Swift 3, NSTableView basato su visualizzazione con layout automatico per macOS 10.11 (e versioni successive)

Il mio setup: ho un NSTableCellViewlayout con layout automatico. Contiene (oltre ad altri elementi) una multilinea NSTextFieldche può avere fino a 2 righe. Pertanto, l'altezza della visualizzazione dell'intera cella dipende dall'altezza di questo campo di testo.

Aggiornamento dico alla vista tabella di aggiornare l'altezza in due occasioni:

1) Quando la vista tabella viene ridimensionata:

func tableViewColumnDidResize(_ notification: Notification) {
        let allIndexes = IndexSet(integersIn: 0..<tableView.numberOfRows)
        tableView.noteHeightOfRows(withIndexesChanged: allIndexes)
}

2) Quando l'oggetto del modello dati cambia:

tableView.noteHeightOfRows(withIndexesChanged: changedIndexes)

Ciò farà sì che la vista tabella chieda al suo delegato la nuova altezza di riga.

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
}

Per prima cosa, dobbiamo ottenere il nostro oggetto modello per row ( entity) e l'identificatore di visualizzazione cella appropriato. Quindi controlliamo se abbiamo già creato una vista per questo identificatore. Per fare ciò dobbiamo mantenere un elenco con le visualizzazioni delle celle per ogni identificatore:

// We need to keep one cell view (per identifier) around
fileprivate var savedTableCellViews = [String : NSTableCellView]()

Se non ne viene salvato nessuno, è necessario crearne uno nuovo (e memorizzarlo nella cache). Aggiorniamo la vista cella con il nostro oggetto modello e gli diciamo di ridisporre tutto in base alla larghezza della vista tabella corrente. L' fittingSizealtezza può quindi essere utilizzata come nuova altezza.


Questa è la risposta perfetta. Se hai più colonne, ti consigliamo di impostare i limiti sulla larghezza della colonna, non sulla larghezza della tabella.
Vojto

Inoltre, durante l'impostazione cellView.bounds.size.width = tableView.bounds.size.width, è una buona idea reimpostare l'altezza su un numero basso. Se si prevede di riutilizzare questa vista fittizia, impostarla su un numero basso "reimposterà" le dimensioni di adattamento.
Vojto

7

Per chiunque desideri più codice, ecco la soluzione completa che ho usato. Grazie Corbin Dunn per avermi indicato nella giusta direzione.

Avevo bisogno di impostare l'altezza principalmente in relazione a quanto fosse alto a NSTextViewnel mio NSTableViewCell.

Nella mia sottoclasse di NSViewControllercreo temporaneamente una nuova cella chiamandooutlineView: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;
}

Nel mio IBAnnotationTableViewCellche è il controller per la mia cella (sottoclasse di NSTableCellView) ho un metodo di configurazione

-(void)setupWithPDFAnnotation:(PDFAnnotation*)annotation;

che imposta tutti i punti vendita e imposta il testo dalle mie PDFAnnotations. Ora posso calcolare "facilmente" l'altezza usando:

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

Dov'è heightForWidth: forString: implementato?
ZS

Mi dispiace per quello. Aggiunto alla risposta. Ho provato a trovare il post SO originale da cui l'ho ricevuto ma non sono riuscito a trovarlo.
Sunkas

1
Grazie! Sta quasi funzionando per me, ma mi dà risultati imprecisi. Nel mio caso, la larghezza che sto inserendo nel NSTextContainer sembra sbagliata. Stai usando il layout automatico per definire le sottoview di NSTableViewCell? Da dove viene la larghezza di "self.contentTextView"?
ZS

Ho pubblicato una domanda sul mio problema qui: stackoverflow.com/questions/22929441/…
ZS

3

Stavo cercando una soluzione per un po 'di tempo e ho trovato la seguente, che funziona alla grande nel mio caso:

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

Nel mio caso volevo espandere una data colonna verticalmente in modo che il testo che mostro su quell'etichetta si adatti perfettamente. Quindi ottengo la colonna in questione e prendo il carattere e la larghezza da essa.
Klajd Deda

3

Dato che uso personalizzato NSTableCellViewe ho accesso alla NSTextFieldmia soluzione è stata quella di aggiungere un metodo 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

Grazie! Sei il mio salvatore! Questo è l'unico che funziona correttamente. Ho passato un'intera giornata a capirlo.
Tommy


2

Ecco cosa ho fatto per risolverlo:

Fonte: guarda nella documentazione di XCode, sotto "row height nstableview". Troverai un codice sorgente di esempio denominato "TableViewVariableRowHeights / TableViewVariableRowHeightsAppDelegate.m"

(Nota: sto guardando la colonna 1 nella vista tabella, dovrai modificare per cercare altrove)

in Delegate.h

IBOutlet NSTableView            *ideaTableView;

in Delegate.m

la visualizzazione tabella delega il controllo dell'altezza delle righe

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

è necessario anche per effettuare la nuova altezza della riga (metodo delegato)

- (void)controlTextDidEndEditing:(NSNotification *)aNotification
{
    [ideaTableView reloadData];
}

Spero che possa aiutare.

Nota finale: questo non supporta la modifica della larghezza della colonna.


4
Sfortunatamente quel codice di esempio Apple (che è attualmente contrassegnato come versione 1.1) utilizza una tabella basata su celle, non una tabella basata su visualizzazioni (di cui tratta questa domanda). Le chiamate in codice -[NSTableView preparedCellAtColumn:row], che i documenti dicono "è disponibile solo per le visualizzazioni di tabella basate su NSCell". L'utilizzo di questo metodo su una tabella basata sulla visualizzazione produce questo output di log in fase di esecuzione: "Errore NSTableView basato sulla visualizzazione: readyCellAtColumn: riga: è stato chiamato. Registra un bug con il backtrace da questo log o interrompi l'utilizzo del metodo".
Ashley

1

Ecco una soluzione basata sulla risposta di JanApotheker, modificata in quanto cellView.fittingSize.heightnon restituiva l'altezza corretta per me. Nel mio caso sto usando lo standard NSTableCellView, un NSAttributedStringper il testo textField della cella e una tabella a colonna singola con vincoli per il textField della cella impostato in IB.

Nel mio view controller, dichiaro:

var tableViewCellForSizing: NSTableCellView?

In viewDidLoad ():

tableViewCellForSizing = tableView.make(withIdentifier: "My Identifier", owner: self) as? NSTableCellView

Infine, per il metodo delegato 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è una costante impostata su 30, per il backup, ma mai effettivamente utilizzata. attributedStringsè il mio array modello di NSAttributedString.

Funziona perfettamente per le mie esigenze. Grazie per tutte le risposte precedenti, che mi hanno indirizzato nella giusta direzione per questo fastidioso problema.


1
Molto buona. Un problema. La tabella non può essere modificabile o textfield.fittingSize.height non funzionerà e restituirà zero. Imposta table.isEditable = false in viewdidload
jiminybob99

0

Questo suona molto come qualcosa che dovevo fare in precedenza. Vorrei poterti dire che ho trovato una soluzione semplice ed elegante ma, ahimè, non l'ho fatto. Non per mancanza di tentativi però. Come hai già notato, la necessità di UITableView di conoscere l'altezza prima della costruzione delle celle fa sembrare tutto abbastanza circolare.

La mia migliore soluzione era spingere la logica nella cella, perché almeno potevo isolare la classe necessaria per capire come erano disposte le celle. Un metodo come

+ (CGFloat) heightForStory:(Story*) story

sarebbe in grado di determinare quanto doveva essere alta la cella. Ovviamente ciò ha comportato la misurazione del testo, ecc. In alcuni casi ho escogitato modi per memorizzare nella cache le informazioni acquisite durante questo metodo che potrebbero essere utilizzate quando la cella è stata creata. È stato il meglio che mi è venuto in mente. È un problema esasperante anche se sembra che dovrebbe esserci una risposta migliore.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.