Frage View-based NSTableView mit Zeilen, die dynamische Höhen haben


Ich habe eine Anwendung mit einem Blick basiert NSTableView drin. In dieser Tabellenansicht befinden sich Zeilen mit Zellen, deren Inhalt aus mehreren Zeilen besteht NSTextField mit aktiviertem Word-Wrap. Abhängig vom Textinhalt des NSTextFieldDie Größe der Zeilen, die zum Anzeigen der Zelle benötigt werden, variiert.

Ich weiß, dass ich das umsetzen kann NSTableViewDelegate Methode -tableView:heightOfRow: um die Höhe zurückzugeben, aber die Höhe wird basierend auf dem Wortumbruch bestimmt, das auf dem verwendet wird NSTextField. Die Wortumhüllung des NSTextField Ähnliches gilt auch für die Breite des NSTextField ist ... was durch die Breite der NSTableView.

Soooo ... Ich denke meine Frage ist ... was ist ein gutes Designmuster dafür? Es scheint, als würde alles, was ich versuche, zu einer verwickelten Unordnung werden. Da das TableView Kenntnisse über die Höhe der Zellen benötigt, um sie auszulegen, und NSTextField braucht Wissen von seinem Layout, um den Wortumbruch zu bestimmen ... und die Zelle braucht das Wissen des Wortwickels, um ihre Höhe zu bestimmen ... es ist eine kreisförmige Verwirrung ... und sie treibt mich wahnsinnig.

Vorschläge?

Wenn es darauf ankommt, wird das Endergebnis auch editierbar sein NSTextFields das passt sich an den Text in ihnen an. Ich arbeite bereits auf der Ansichtsebene, aber die Tabellenansicht passt die Höhe der Zellen noch nicht an. Ich denke, sobald ich die Höhe Problem ausgearbeitet habe, werde ich die -noteHeightOfRowsWithIndexesChanged Methode, um die Tabelle anzuzeigen, die Höhe geändert ... aber es wird immer noch dann den Delegierten für die Höhe fragen ... daher meine Quandry.

Danke im Voraus!


75
2017-09-21 18:10


Ursprung


Antworten:


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

Die Antwort ist, ein Extra zu behalten NSTableCellView (oder die Ansicht, die Sie als "Zellenansicht" verwenden), um die Höhe der Ansicht zu messen. In dem tableView:heightOfRow: Delegate - Methode, greifen Sie auf Ihr Modell für 'Zeile' zu und setzen Sie die objectValue auf NSTableCellView. Stellen Sie dann die Breite der Ansicht auf die Breite Ihres Tisches ein, und (wie auch immer Sie es tun möchten) berechnen Sie die erforderliche Höhe für diese Ansicht. Gebe diesen Wert zurück.

Ruf nicht an noteHeightOfRowsWithIndexesChanged: von in der Delegiertenmethode tableView:heightOfRow: oder viewForTableColumn:row: ! Das ist schlecht und wird Mega-Probleme verursachen.

Um die Höhe dynamisch zu aktualisieren, müssen Sie auf den Text reagieren, der sich ändert (über das Ziel / die Aktion), und die berechnete Höhe dieser Ansicht neu berechnen. Ändern Sie jetzt nicht dynamisch die NSTableCellViewdie Höhe (oder die Ansicht, die Sie als Ihre "Zellenansicht" verwenden). Der Tisch Muss Kontrollieren Sie den Rahmen dieser Ansicht und Sie werden gegen die Tabellenansicht kämpfen, 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, auf noteHeightOfRowsWithIndexesChanged:, wodurch die Tabelle die Größe dieser einzelnen Zeile ändert. Angenommen, Sie haben die Einrichtung der Autoresierungsmaske direkt auf Teilansichten (z. B. Unteransichten der NSTableCellView), sollte sich die Größe gut ändern! Wenn nicht, arbeiten Sie zunächst an der Größenänderungsmaske der Teilansichten, um die richtigen Zeilenhöhen zu erhalten.

Vergiss das nicht noteHeightOfRowsWithIndexesChanged: wird standardmäßig animiert. Um es nicht zu animieren:

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

PS: Ich reagiere mehr auf Fragen, die in den Apple Dev Foren als Stapelüberlauf gepostet werden.

PSS: Ich habe das View-basierte NSTableView geschrieben


123
2017-11-08 16:57



Das ist viel einfacher geworden macOS 10.13 mit .usesAutomaticRowHeights. Die Details sind hier: https://developer.apple.com/library/content/releasenotes/AppKit/RN-AppKit/#10_13 (Im Abschnitt "NSTableView Automatic Row Heights").

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

enter image description here

Dann stellst du das Zeug in deine NSTableCellView Obere und untere Beschränkungen für die Zelle haben und Ihre Zelle wird automatisch angepasst. Kein Code erforderlich!

Deine App ignoriert alle angegebenen Höhen heightOfRow (NSTableView) und heightOfRowByItem (NSOutlineView). 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)
}

9
2017-11-01 23:27



Für alle, die mehr Code möchten, hier ist die vollständige Lösung, die ich verwendet habe. Danke, Corbin Dunn, dass du mich in die richtige Richtung weist.

Ich musste die Höhe meist in Relation zur Höhe setzen NSTextView in meinem NSTableViewCell war.

In meiner Unterklasse von NSViewController Ich erstelle vorübergehend eine neue Zelle, indem ich anrufe 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;
}

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

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

was alle Steckdosen einrichtet und den Text von meinen PDFAnnotations setzt. Jetzt kann ich "leicht" die Höhe 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
2017-09-10 12:23



Basiert auf Corbins Antwort (ich danke dafür, etwas Licht ins Dunkel zu bringen):

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

Mein Setup: Ich habe eine NSTableCellView das ist mit Auto-Layout ausgelegt. Es enthält (neben anderen Elementen) eine Mehrfachzeile NSTextField das kann bis zu 2 Zeilen haben. Daher hängt die Höhe der gesamten Zellenansicht von der Höhe dieses Textfelds ab.

Ich aktualisiere die Tabellenansicht, um die Höhe bei zwei Gelegenheiten zu aktualisieren:

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

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 Delegaten für die neue 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
}

Zuerst müssen wir unser Modellobjekt für die Reihe bekommen (entity) und die entsprechende Zellenansichtskennung. Wir prüfen dann, ob wir bereits eine Sicht für diesen Bezeichner erstellt haben. Dazu müssen wir für jeden Bezeichner eine Liste mit Zellenansichten pflegen:

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

Wenn keine gespeichert wird, müssen wir eine neue erstellen (und zwischenspeichern). Wir aktualisieren die Zellenansicht mit unserem Modellobjekt und teilen ihm mit, dass alles basierend auf der aktuellen Tabellenansichtsbreite neu angeordnet werden soll. Das fittingSize Höhe kann dann als neue Höhe verwendet werden.


6
2018-03-17 09:34



Ich habe lange nach einer Lösung gesucht und die folgende gefunden, die in meinem Fall sehr gut 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
2017-08-25 09:41



Da benutze ich Brauch NSTableCellView und ich habe Zugang zu den NSTextField Meine Lösung bestand darin, eine Methode hinzuzufügen 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
2018-02-01 05:11



Hast du gesehen? RowResizableViews? Es ist ziemlich alt und ich habe es nicht getestet, aber es kann trotzdem funktionieren.


2
2017-09-21 21:13



Hier ist, was ich getan habe, um es zu beheben:

Quelle: Sehen Sie in der XCode-Dokumentation unter "Zeilenhöhe nstableview" nach. Sie finden einen Beispielquellcode namens "TableViewVariableRowHeights / TableViewVariableRowHeightsAppDelegate.m"

(Hinweis: Ich schaue Spalte 1 in der Tabellenansicht an, Sie müssen zwicken, um woanders zu suchen)

in Delegierte.h

IBOutlet NSTableView            *ideaTableView;

in Delegat.m

Tabellenansicht delegiert Kontrolle 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.

Abschließende Anmerkung: Dies unterstützt keine Änderung der Spaltenbreite.


2
2017-09-17 09:32



Das hört sich sehr nach etwas an, was ich vorher machen musste. Ich wünschte, ich könnte Ihnen sagen, dass ich eine einfache, elegante Lösung gefunden habe, aber leider nicht. Nicht aus Mangel an versuchen, obwohl. Wie Sie bereits bemerkt haben, dass UITableView die Höhe vor dem Bau der Zellen kennen muss, ist es ziemlich kreisförmig.

Meine beste Lösung bestand darin, Logik in die Zelle zu pushen, weil ich zumindest feststellen konnte, welche Klasse benötigt wurde, um zu verstehen, wie die Zellen aufgebaut waren. Eine Methode wie

+ (CGFloat) heightForStory:(Story*) story

wäre in der Lage zu bestimmen, wie groß die Zelle sein musste. Natürlich beinhaltete das das Messen von Text usw. In einigen Fällen habe ich Möglichkeiten gefunden, Informationen, die während dieser Methode gewonnen wurden, zwischenzuspeichern, die dann verwendet werden könnten, wenn die Zelle erstellt wurde. Das war das Beste, was mir einfiel. Es ist jedoch ein ärgerliches Problem, da es scheint, dass es eine bessere Antwort geben sollte.


1
2017-09-30 05:06



Hier ist eine Lösung basierend auf JanApothekers Antwort, modifiziert als cellView.fittingSize.height gab nicht die richtige Höhe für mich zurück. In meinem Fall verwende ich den Standard NSTableCellView, ein NSAttributedString für den textField-Text der Zelle und eine einzelne Spaltentabelle mit Einschränkungen für das in IB definierte textField der Zelle.

Aus meiner Sicht Controller, erkläre ich:

var tableViewCellForSizing: NSTableCellView?

In viewDidLoad ():

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

Abschließend für die Delegate-Methode 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 ist eine Konstante auf 30 gesetzt, für Backup, aber nie wirklich benutzt. attributedStrings ist meine Modellreihe von NSAttributedString.

Das funktioniert perfekt für meine Bedürfnisse. Danke für all die vorherigen Antworten, die mich für dieses lästige Problem in die richtige Richtung wiesen.


1
2018-05-29 16:56