Pregunta NSTableView basado en la vista con filas que tienen alturas dinámicas


Tengo una aplicación basada en vistas NSTableView en eso. Dentro de esta vista de tabla, tengo filas que tienen celdas que tienen contenido que consiste en una fila múltiple NSTextField con word-wrap habilitado Dependiendo del contenido textual de la NSTextField, el tamaño de las filas necesarias para mostrar la celda variará.

Sé que puedo implementar el NSTableViewDelegate método -tableView:heightOfRow: para devolver la altura, pero la altura se determinará en función de la palabra envoltura utilizada en el NSTextField. La palabra envoltura de NSTextField se basa de manera similar en qué tan ancho NSTextField es ... que está determinado por el ancho de la NSTableView.

Soooo ... supongo que mi pregunta es ... ¿cuál es un buen patrón de diseño para esto? Parece que todo lo que intento termina siendo un desastre complicado. Dado que TableView requiere el conocimiento de la altura de las celdas para establecerlas ... y el NSTextField necesita conocimiento de su diseño para determinar la palabra envoltura ... y la célula necesita conocimiento de la palabra envoltura para determinar su altura ... es un desastre circular ... y me está conduciendo insano.

Sugerencias?

Si es importante, el resultado final también será editable NSTextFields eso cambiará el tamaño para ajustarse al texto dentro de ellos. Ya tengo este trabajo en el nivel de vista, pero la vista de tabla aún no ajusta las alturas de las celdas. Me imagino que una vez que obtenga el problema de la altura, usaré el -noteHeightOfRowsWithIndexesChanged El método para informar la vista de tabla cambió la altura ... pero aún así va a preguntar al delegado por la altura ... por lo tanto, mi quandry.

¡Gracias por adelantado!


75
2017-09-21 18:10


origen


Respuestas:


Este es un problema de huevo y pollo. La tabla necesita saber la altura de la fila porque eso determina dónde se ubicará una vista determinada. Pero desea que la vista ya esté disponible para que pueda usarla para calcular la altura de la fila. Entonces, ¿qué viene primero?

La respuesta es mantener un extra NSTableCellView (o cualquier vista que esté utilizando como su "vista de celda") solo para medir la altura de la vista. En el tableView:heightOfRow: delegar método, acceder a su modelo para 'fila' y configurar el objectValue en NSTableCellView. Luego, configure el ancho de la vista para que sea el ancho de su tabla, y (como quiera hacerlo) determine la altura requerida para esa vista. Devuelve ese valor.

No llame noteHeightOfRowsWithIndexesChanged: desde en el método delegado tableView:heightOfRow: o viewForTableColumn:row: ! Eso es malo y causará mega problemas.

Para actualizar dinámicamente la altura, lo que debe hacer es responder al cambio de texto (a través del objetivo / acción) y volver a calcular la altura calculada de esa vista. Ahora, no cambies dinámicamente el NSTableCellViewla altura (o la vista que esté usando como su "vista de celda"). La mesa debe controle el marco de esa vista, y luchará contra la vista de tabla si intenta configurarla. En cambio, en su destino / acción para el campo de texto donde calculó la altura, llame noteHeightOfRowsWithIndexesChanged:, lo que permitirá que la tabla cambie el tamaño de esa fila individual. Suponiendo que tiene su configuración de máscara de reajuste automático en las subvistas (es decir, las subcarpetas de NSTableCellView), las cosas deberían cambiar el tamaño bien! Si no, primero trabaje en la máscara de cambio de tamaño de las subvistas para hacer las cosas bien con alturas de fila variables.

No olvides eso noteHeightOfRowsWithIndexesChanged: anima de forma predeterminada. Para que no sea animado:

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

PD: respondo más a las preguntas publicadas en los foros de desarrolladores de Apple que al desbordamiento de la pila.

PSS: escribí la vista basada en NSTableView


123
2017-11-08 16:57



Esto se hizo mucho más fácil en macOS 10.13 con .usesAutomaticRowHeights. Los detalles están aquí: https://developer.apple.com/library/content/releasenotes/AppKit/RN-AppKit/#10_13 (En la sección titulada "NSTableView Automatic Row Heights").

Básicamente, simplemente selecciona tu NSTableView o NSOutlineView en el editor de guiones gráficos y seleccione esta opción en el Inspector de tallas:

enter image description here

Luego, configura las cosas en su NSTableCellView tener restricciones superiores e inferiores a la celda y su celda cambiará de tamaño para ajustarse automáticamente. ¡No se requiere código!

Su aplicación ignorará cualquier altura especificada en heightOfRow (NSTableView) y heightOfRowByItem (NSOutlineView) Puedes ver qué alturas se están calculando para tus filas de diseño automático con este método:

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

9
2017-11-01 23:27



Para cualquiera que quiera más código, aquí está la solución completa que utilicé. Gracias a Corbin Dunn por señalarme en la dirección correcta.

Necesitaba establecer la altura principalmente en relación con qué tan alto NSTextView en mi NSTableViewCell estaba.

En mi subclase de NSViewController Creo temporalmente una nueva célula llamando 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;
}

En mi IBAnnotationTableViewCell que es el controlador para mi celda (subclase de NSTableCellView) Tengo un método de configuración

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

que establece todos los puntos de venta y establece el texto de mi PDFAnotaciones. Ahora puedo calcutar "fácilmente" la altura 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 ;
}

7
2017-09-10 12:23



Basado en la respuesta de Corbin (gracias por cierto, arrojando algo de luz sobre esto):

Swift 3, NSTableView basado en vista con diseño automático para macOS 10.11 (y superior)

Mi configuración: tengo un NSTableCellView eso se presenta usando Auto-Layout. Contiene (además de otros elementos) una línea múltiple NSTextField eso puede tener hasta 2 filas. Por lo tanto, la altura de la vista de celda completa depende de la altura de este campo de texto.

Actualizo decir a la vista de tabla para actualizar la altura en dos ocasiones:

1) Cuando la vista de tabla cambia de tamaño:

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

2) Cuando el objeto del modelo de datos cambia:

tableView.noteHeightOfRows(withIndexesChanged: changedIndexes)

Esto hará que la vista de tabla solicite su delegado para la nueva altura de fila.

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
}

Primero, necesitamos obtener nuestro objeto modelo para la fila (entity) y el identificador de vista de celda apropiado. Luego verificamos si ya hemos creado una vista para este identificador. Para hacer eso tenemos que mantener una lista con vistas de celda para cada identificador:

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

Si no se guarda ninguno, necesitamos crear (y almacenar en caché) uno nuevo. Actualizamos la vista de celda con nuestro objeto de modelo y le pedimos que vuelva a diseñar todo en función del ancho de la vista de tabla actual. los fittingSize la altura se puede usar como la nueva altura.


6
2018-03-17 09:34



Estaba buscando una solución por bastante tiempo y se me ocurrió la siguiente, que funciona muy bien en mi 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;
    }
}

3
2017-08-25 09:41



Como uso personalizado NSTableCellView y tengo acceso a la NSTextField mi solución fue agregar un método en 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



¿Has echado un vistazo a RowResizableViews? Es bastante viejo y no lo he probado pero puede funcionar.


2
2017-09-21 21:13