web-dev-qa-db-fra.com

NSTableView basé sur une vue avec des lignes qui ont des hauteurs dynamiques

J'ai une application contenant un NSTableView basé sur une vue. Dans cette vue de tableau, j'ai des lignes qui ont des cellules dont le contenu est composé d'un NSTextField à plusieurs lignes avec Word-wrap activé. Selon le contenu textuel du NSTextField, la taille des lignes nécessaires pour afficher la cellule variera.

Je sais que je peux implémenter la méthode NSTableViewDelegate -tableView:heightOfRow: pour renvoyer la hauteur, mais la hauteur sera déterminée en fonction de l'habillage Word utilisé sur le NSTextField. L'habillage Word du NSTextField est basé sur la largeur du NSTextField… qui est déterminé par la largeur du NSTableView.

Soooo ... Je suppose que ma question est ... quel est un bon modèle de conception pour cela? Il semble que tout ce que j'essaie soit un gâchis alambiqué. Étant donné que TableView nécessite une connaissance de la hauteur des cellules pour les disposer ... et que le NSTextField a besoin de connaître sa disposition pour déterminer le retour à la ligne Word ... et que la cellule a besoin de connaître le retour à la ligne pour déterminer sa hauteur … C'est un gâchis circulaire… et ça me rend fo.

Suggestions?

Si cela est important, le résultat final aura également NSTextFields modifiable qui sera redimensionné pour s'adapter au texte qu'il contient. J'ai déjà ce travail au niveau de la vue, mais la table ne règle pas encore les hauteurs des cellules. Je suppose qu'une fois que le problème de hauteur sera résolu, j'utiliserai la méthode -noteHeightOfRowsWithIndexesChanged pour informer la table de la hauteur modifiée… mais il faudra quand même demander au délégué la hauteur… d'où mon dilemme .

Merci d'avance!

79
Jiva DeVoe

Ceci est un poulet et le problème des œufs. La table doit connaître la hauteur de la ligne car cela détermine où se trouvera une vue donnée. Mais vous voulez qu'une vue soit déjà présente afin que vous puissiez l'utiliser pour déterminer la hauteur de la ligne. Alors, qui vient en premier?

La réponse est de garder un NSTableCellView supplémentaire (ou la vue que vous utilisez comme "vue de cellule") juste pour mesurer la hauteur de la vue. Dans le tableView:heightOfRow: méthode déléguée, accédez à votre modèle pour 'row' et définissez objectValue sur NSTableCellView. Définissez ensuite la largeur de la vue comme étant la largeur de votre table et (quelle que soit la façon dont vous le fassiez) déterminez la hauteur requise pour cette vue. Renvoie cette valeur.

N'appelez pas noteHeightOfRowsWithIndexesChanged: à partir de la méthode déléguée tableView:heightOfRow: ou viewForTableColumn:row:! C'est mauvais et cela causera des méga-ennuis.

Pour mettre à jour dynamiquement la hauteur, ce que vous devez faire est de répondre au changement de texte (via la cible/action) et de recalculer la hauteur calculée de cette vue. Maintenant, ne modifiez pas dynamiquement la hauteur de NSTableCellView (ou la vue que vous utilisez comme "vue de cellule"). La table doit contrôler le cadre de cette vue, et vous combattrez la table si vous essayez de la définir. Au lieu de cela, dans votre cible/action pour le champ de texte où vous avez calculé la hauteur, appelez noteHeightOfRowsWithIndexesChanged:, qui permettra au tableau de redimensionner cette ligne individuelle. En supposant que votre masque de redimensionnement automatique soit configuré directement sur les sous-vues (c'est-à-dire: les sous-vues du NSTableCellView), les choses devraient bien se redimensionner! Si ce n'est pas le cas, commencez par travailler sur le masque de redimensionnement des sous-vues pour bien faire les choses avec des hauteurs de ligne variables.

N'oubliez pas que noteHeightOfRowsWithIndexesChanged: anime par défaut. Pour ne pas l'animer:

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

PS: je réponds plus aux questions postées sur les Apple Dev Forums que le débordement de pile.

PSS: j'ai écrit la vue basée sur NSTableView

128
corbin dunn

Cela est devenu beaucoup plus facile dans macOS 10.13 avec .usesAutomaticRowHeights. Les détails sont ici: https://developer.Apple.com/library/content/releasenotes/AppKit/RN-AppKit/#10_1 (Dans la section intitulée "NSTableView Automatic Row Heights").

Fondamentalement, vous sélectionnez simplement votre NSTableView ou NSOutlineView dans l'éditeur de storyboard et sélectionnez cette option dans l'inspecteur de taille:

enter image description here

Ensuite, vous définissez les éléments dans votre NSTableCellView pour avoir des contraintes supérieure et inférieure à la cellule et votre cellule sera redimensionnée pour s'adapter automatiquement. Aucun code requis!

Votre application ignorera toutes les hauteurs spécifiées dans heightOfRow (NSTableView) et heightOfRowByItem (NSOutlineView). Vous pouvez voir quelles hauteurs sont calculées pour vos lignes de disposition automatique avec cette méthode:

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

Pour tous ceux qui veulent plus de code, voici la solution complète que j'ai utilisée. Merci corbin dunn de m'avoir pointé dans la bonne direction.

Je devais régler la hauteur principalement par rapport à la hauteur d'un NSTextView dans mon NSTableViewCell.

Dans ma sous-classe de NSViewController je crée temporairement une nouvelle cellule en appelant 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;
}

Dans mon IBAnnotationTableViewCell qui est le contrôleur de ma cellule (sous-classe de NSTableCellView) j'ai une méthode de configuration

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

qui configure tous les points de vente et définit le texte de mes PDFAnnotations. Maintenant, je peux "facilement" calculer la hauteur en utilisant:

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

Sur la base de la réponse de Corbin (btw merci de faire la lumière sur ce point):

Swift 3, NSTableView basé sur la vue avec mise en page automatique pour macOS 10.11 (et supérieur)

Ma configuration: J'ai un NSTableCellView qui est disposé à l'aide de la mise en page automatique. Il contient (en plus d'autres éléments) un NSTextField multi-lignes qui peut avoir jusqu'à 2 lignes. Par conséquent, la hauteur de la vue de cellule entière dépend de la hauteur de ce champ de texte.

Je mets à jour la vue de la table pour mettre à jour la hauteur à deux reprises:

1) Lorsque la vue du tableau est redimensionnée:

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

2) Lorsque l'objet du modèle de données change:

tableView.noteHeightOfRows(withIndexesChanged: changedIndexes)

Cela entraînera la vue de table demander à son délégué la nouvelle hauteur de ligne.

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
}

Tout d'abord, nous devons obtenir notre objet modèle pour la ligne (entity) et l'identificateur de vue de cellule approprié. Nous vérifions ensuite si nous avons déjà créé une vue pour cet identifiant. Pour ce faire, nous devons maintenir une liste avec des vues de cellule pour chaque identifiant:

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

Si aucun n'est enregistré, nous devons en créer (et mettre en cache) un nouveau. Nous mettons à jour la vue des cellules avec notre objet modèle et lui demandons de tout réorganiser en fonction de la largeur de la vue de table actuelle. La hauteur fittingSize peut alors être utilisée comme nouvelle hauteur.

7
JanApotheker

Je cherchais une solution depuis un certain temps et j'ai trouvé la suivante, qui fonctionne très bien dans mon cas:

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

Étant donné que j'utilise NSTableCellView personnalisé et que j'ai accès à NSTextField, ma solution consistait à ajouter une méthode sur 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
Klajd Deda

Avez-vous jeté un œil à RowResizableViews ? Il est assez ancien et je ne l'ai pas testé mais il peut néanmoins fonctionner.

2
Tim

Voici ce que j'ai fait pour y remédier:

Source: Regardez dans la documentation XCode, sous "row height nstableview". Vous trouverez un exemple de code source nommé "TableViewVariableRowHeights/TableViewVariableRowHeightsAppDelegate.m"

(Remarque: je regarde la colonne 1 en mode tableau, vous devrez modifier pour regarder ailleurs)

dans Delegate.h

IBOutlet NSTableView            *ideaTableView;

dans Delegate.m

la vue de table délègue le contrôle de la hauteur de ligne

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

vous en avez également besoin pour effectuer la nouvelle hauteur de ligne (méthode déléguée)

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

J'espère que ça aide.

Remarque finale: cela ne prend pas en charge la modification de la largeur des colonnes.

2
laurent

Voici une solution basée sur la réponse de JanApotheker, modifiée par cellView.fittingSize.height ne retournait pas la bonne hauteur pour moi. Dans mon cas, j'utilise le standard NSTableCellView, un NSAttributedString pour le texte textField de la cellule et un tableau à une seule colonne avec des contraintes pour le textField de la cellule défini dans IB.

Dans mon contrôleur de vue, je déclare:

var tableViewCellForSizing: NSTableCellView?

Dans viewDidLoad ():

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

Enfin, pour la méthode déléguée 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 est une constante définie sur 30, pour la sauvegarde, mais jamais réellement utilisée. attributedStrings est mon tableau de modèles de NSAttributedString.

Cela fonctionne parfaitement pour mes besoins. Merci pour toutes les réponses précédentes, qui m'ont indiqué dans la bonne direction pour ce problème embêtant.

1
jbaraga

Cela ressemble beaucoup à quelque chose que je devais faire auparavant. J'aimerais pouvoir vous dire que j'ai trouvé une solution simple et élégante mais, hélas, je ne l'ai pas fait. Pas faute d'avoir essayé. Comme vous avez déjà remarqué la nécessité pour UITableView de connaître la hauteur avant la construction des cellules, tout semble assez circulaire.

Ma meilleure solution était de pousser la logique vers la cellule, car au moins je pouvais isoler quelle classe avait besoin pour comprendre comment les cellules étaient disposées. Une méthode comme

+ (CGFloat) heightForStory:(Story*) story

serait en mesure de déterminer la hauteur de la cellule. Bien sûr, cela impliquait de mesurer du texte, etc. Dans certains cas, j'ai imaginé des façons de mettre en cache les informations obtenues au cours de cette méthode qui pourraient ensuite être utilisées lors de la création de la cellule. C'était le meilleur que j'ai trouvé. C'est un problème exaspérant, car il semble qu'il devrait y avoir une meilleure réponse.

1
vagrant