wake-up-neo.com

Ansichtsbasiertes NSTableView mit Zeilen mit dynamischen Höhen

Ich habe eine Anwendung mit einem ansichtsbasierten NSTableView darin. In dieser Tabellenansicht befinden sich Zeilen mit Zellen, deren Inhalt aus einem mehrzeiligen NSTextField mit aktiviertem Zeilenumbruch besteht. Abhängig vom Textinhalt von NSTextField variiert die Größe der Zeilen, die zum Anzeigen der Zelle benötigt werden.

Ich weiß, dass ich die NSTableViewDelegate -Methode implementieren kann - tableView:heightOfRow:, Um die Höhe zurückzugeben, aber die Höhe wird basierend auf dem in NSTextField verwendeten Zeilenumbruch bestimmt. Der Zeilenumbruch von NSTextField basiert auf der Breite von NSTextField, die durch die Breite von NSTableView bestimmt wird.

Soooo… ich denke meine Frage ist… was ist ein gutes Designmuster dafür? Es scheint, als wäre alles, was ich versuche, ein kompliziertes Durcheinander. Da für die Tabellenansicht Kenntnisse über die Höhe der Zellen erforderlich sind, um sie anzuordnen, muss für NSTextField das Layout des Zeilenumbruchs bekannt sein, und für die Zelle sind Kenntnisse über den Zeilenumbruch erforderlich, um die Höhe zu bestimmen … Es ist ein Kreislauf… und es treibt mich an verrückt.

Vorschläge?

Wenn es darauf ankommt, verfügt das Endergebnis auch über ein bearbeitbares NSTextFields, dessen Größe an den darin enthaltenen Text angepasst wird. Ich habe dies bereits auf der Ansichtsebene, aber die Tabellenansicht passt die Höhe der Zellen noch nicht an. Sobald ich das Problem mit der Größe gelöst habe, werde ich die Methode -noteHeightOfRowsWithIndexesChanged verwenden, um die Tabellenansicht über die geänderte Größe zu informieren. Aber dann wird der Delegierte immer noch nach der Größe gefragt .

Danke im Voraus!

79
Jiva DeVoe

Dies ist ein Problem mit Huhn und Ei. Die Tabelle muss die Zeilenhöhe kennen, da dies bestimmt, wo eine bestimmte Ansicht liegen wird. Sie möchten jedoch, dass eine Ansicht bereits vorhanden ist, damit Sie die Zeilenhöhe ermitteln können. Also, was kommt zuerst?

Die Antwort ist, ein zusätzliches NSTableCellView (oder eine andere Ansicht, die Sie als "Zellenansicht" verwenden) zu verwenden, um nur die Höhe der Ansicht zu messen. Greifen Sie in der Delegierungsmethode tableView:heightOfRow: Auf Ihr Modell für 'row' zu und setzen Sie objectValue auf NSTableCellView. Stellen Sie dann die Breite der Ansicht auf die Breite Ihrer Tabelle ein und ermitteln Sie (wie auch immer Sie dies tun möchten) die erforderliche Höhe für diese Ansicht. Diesen Wert zurückgeben.

Rufen Sie nicht noteHeightOfRowsWithIndexesChanged: In der Delegate-Methode tableView:heightOfRow: Oder viewForTableColumn:row: Auf! Das ist schlimm und wird Mega-Ärger verursachen.

Um die Höhe dynamisch zu aktualisieren, müssen Sie auf die Änderung des Texts (über das Ziel/die Aktion) reagieren und die berechnete Höhe dieser Ansicht neu berechnen. Ändern Sie jetzt nicht dynamisch die Höhe von NSTableCellView (oder die Ansicht, die Sie als "Zellenansicht" verwenden). Die Tabelle muss steuert den Frame dieser Ansicht, und Sie bekämpfen die Tabellenansicht, wenn Sie versuchen, sie festzulegen. Rufen Sie stattdessen in Ihrem Ziel/Ihrer Aktion für das Textfeld, in dem Sie die Höhe berechnet haben, noteHeightOfRowsWithIndexesChanged: Auf, damit die Tabelle die Größe dieser einzelnen Zeile ändert. Angenommen, Sie haben Ihre Autoresizing-Maske direkt in Unteransichten eingerichtet (d. H. Unteransichten des NSTableCellView), sollte die Größe der Objekte in Ordnung sein! Wenn nicht, arbeiten Sie zuerst an der Größenänderungsmaske der Unteransichten, um die richtigen Ergebnisse mit variablen Zeilenhöhen zu erzielen.

Vergessen Sie nicht, dass noteHeightOfRowsWithIndexesChanged: Standardmäßig animiert ist. Um es nicht zu animieren:

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

PS: Ich beantworte mehr Fragen, die in den Apple Dev Forums) gepostet wurden, als Stapelüberlauf.

PSS: Ich habe das view-basierte NSTableView geschrieben

128
corbin dunn

Dies wurde in macOS 10.13 mit .usesAutomaticRowHeights Viel einfacher. Die Details finden Sie hier: https://developer.Apple.com/library/content/releasenotes/AppKit/RN-AppKit/#10_1 (Im Abschnitt "NSTableView Automatic Row Heights").

Grundsätzlich wählen Sie einfach Ihr NSTableView oder NSOutlineView im Storyboard-Editor aus und wählen diese Option im Größeninspektor:

enter image description here

Anschließend legen Sie die Elemente in Ihrem NSTableCellView so fest, dass die obere und untere Grenze für die Zelle gelten. Die Größe Ihrer Zelle wird dann automatisch angepasst. Kein Code erforderlich!

Ihre App ignoriert alle in heightOfRow (NSTableView) und heightOfRowByItem (NSOutlineView) angegebenen Höhen. Mit dieser Methode können Sie sehen, welche Höhen für Ihre automatischen Layoutzeilen berechnet werden:

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

Für alle, die mehr Code wollen, ist hier die vollständige Lösung, die ich verwendet habe. Danke corbin dunn, der mich in die richtige richtung geleitet hat.

Ich musste die Höhe hauptsächlich in Bezug auf die Höhe eines NSTextView in meinem NSTableViewCell einstellen.

In meiner Unterklasse von NSViewController erstelle ich vorübergehend eine neue Zelle, indem ich outlineView:viewForTableColumn:item: Aufrufe.

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

In meinem IBAnnotationTableViewCell, der der Controller für meine Zelle ist (Unterklasse von NSTableCellView), habe ich eine Setup-Methode

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

hiermit werden alle Ausgänge eingerichtet und der Text aus meinen PDFAnnotations festgelegt. Jetzt kann ich die Höhe "leicht" berechnen mit:

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

Basierend auf Corbins Antwort (nebenbei ein wenig Licht ins Dunkel):

Swift 3, View-basiertes NSTableView mit automatischem Layout für macOS 10.11 (und höher)

Mein Setup: Ich habe ein NSTableCellView, das mit Auto-Layout angelegt wurde. Es enthält (neben anderen Elementen) ein mehrzeiliges NSTextField, das bis zu 2 Zeilen enthalten kann. Daher hängt die Höhe der gesamten Zellenansicht von der Höhe dieses Textfelds ab.

Ich aktualisiere die Tabellenansicht, um die Höhe zweimal zu aktualisieren:

1) Wenn die Größe der Tabellenansicht geändert wird:

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

2) Wenn sich das Datenmodellobjekt ändert:

tableView.noteHeightOfRows(withIndexesChanged: changedIndexes)

Dies veranlasst die Tabellenansicht, ihren Stellvertreter nach der neuen Zeilenhöhe zu fragen.

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
}

Zunächst müssen wir unser Modellobjekt für die Zeile (entity) und den entsprechenden Zellansichtsbezeichner abrufen. Wir prüfen dann, ob wir für diese Kennung bereits eine Sicht angelegt haben. Dazu müssen wir für jeden Bezeichner eine Liste mit Zellansichten führen:

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

Wenn keine gespeichert ist, müssen wir eine neue erstellen (und zwischenspeichern). Wir aktualisieren die Zellenansicht mit unserem Modellobjekt und weisen es an, alles basierend auf der aktuellen Breite der Tabellenansicht neu zu gestalten. Die fittingSize Höhe kann dann als neue Höhe verwendet werden.

7
JanApotheker

Ich habe lange nach einer Lösung gesucht und die folgende gefunden, die in meinem Fall hervorragend funktioniert:

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

Da ich benutzerdefiniertes NSTableCellView verwende und Zugriff auf das NSTextField habe, bestand meine Lösung darin, eine Methode für NSTextField hinzuzufügen.

@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
Klajd Deda

Haben Sie sich RowResizableViews angesehen? Es ist ziemlich alt und ich habe es nicht getestet, aber es kann trotzdem funktionieren.

2
Tim

Folgendes habe ich getan, um das Problem zu beheben:

Quelle: Schauen Sie in die XCode-Dokumentation unter "row height nstableview". Sie finden einen Beispielquellcode mit dem Namen "TableViewVariableRowHeights/TableViewVariableRowHeightsAppDelegate.m".

(Hinweis: Ich schaue in Spalte 1 in der Tabellenansicht nach, du musst zwicken, um woanders hinzuschauen.)

in Delegate.h

IBOutlet NSTableView            *ideaTableView;

in Delegate.m

die Tabellenansicht delegiert die Steuerung der Zeilenhöhe

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

sie benötigen dies auch, um die neue Zeilenhöhe zu bewirken (delegierte Methode)

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

Ich hoffe das hilft.

Letzte Anmerkung: Das Ändern der Spaltenbreite wird nicht unterstützt.

2
laurent

Hier ist eine Lösung basierend auf der Antwort von JanApotheker, modifiziert als cellView.fittingSize.height hat mir nicht die richtige Höhe zurückgegeben. In meinem Fall verwende ich den Standard NSTableCellView, einen NSAttributedString für den textField-Text der Zelle und eine einspaltige Tabelle mit Einschränkungen für das in IB festgelegte textField der Zelle.

In meinem View Controller erkläre ich:

var tableViewCellForSizing: NSTableCellView?

In viewDidLoad ():

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

Zum Schluss für die tableView-Delegate-Methode:

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 ist eine Konstante, die für die Sicherung auf 30 gesetzt, aber nie verwendet wird. attributedStrings ist mein Modellarray von NSAttributedString.

Dies funktioniert perfekt für meine Bedürfnisse. Vielen Dank für all die vorherigen Antworten, die mich für dieses lästige Problem in die richtige Richtung gelenkt haben.

1
jbaraga

Das hört sich nach etwas an, was ich vorher tun musste. Ich wünschte, ich könnte Ihnen sagen, dass ich eine einfache, elegante Lösung gefunden hätte, aber leider nicht. Nicht aus Mangel an Versuchen. Wie Sie bereits bemerkt haben, muss UITableView die Höhe kennen, bevor die Zellen erstellt werden, sodass alles recht kreisförmig erscheint.

Meine beste Lösung bestand darin, die Logik auf die Zelle zu übertragen, da ich zumindest isolieren konnte, welche Klasse benötigt wurde, um zu verstehen, wie die Zellen angeordnet waren. Eine Methode wie

+ (CGFloat) heightForStory:(Story*) story

würde bestimmen können, wie groß die Zelle sein musste. Dazu gehörte natürlich das Messen von Text usw. In einigen Fällen entwickelte ich Methoden zum Zwischenspeichern von Informationen, die bei der Erstellung der Zelle verwendet werden konnten. Das war das Beste, was ich mir ausgedacht habe. Es ist jedoch ein ärgerliches Problem, da es anscheinend eine bessere Antwort geben sollte.

1
vagrant