Vista NSTableView basada en vistas con filas que tienen alturas dinámicas
Tengo una aplicación con una vista basada en NSTableView
en ella. Dentro de esta vista de tabla, tengo filas que tienen celdas que tienen contenido que consiste en una fila múltiple NSTextField
con ajuste de palabra habilitado. Dependiendo del contenido textual de NSTextField
, el tamaño de las filas necesarias para mostrar la celda variará.
Sé que puedo implementar el método NSTableViewDelegate
- tableView:heightOfRow:
para devolver la altura, pero la altura se determinará en función del ajuste de palabras utilizado en el NSTextField
. El envoltorio de la NSTextField
se basa de manera similar en cuán ancho es el NSTextField
which que está determinado por el ancho del NSTableView
.
Soooo guess supongo que mi pregunta es what ¿qué es un buen patrón de diseño para esto? Parece que todo lo que intento termina siendo un enrevesado desastre. Dado que la vista de tabla requiere el conocimiento de la altura de las celdas para colocarlas... y el NSTextField
necesita conocimiento de su diseño para determinar el ajuste de línea the y la celda necesita conocimiento del ajuste de línea para determinar su altura it es un lío circular and y me está volviendo loco.
Sugerencias?
Si es importante, el resultado final también tendrá NSTextFields
editable que se redimensionará para ajustarse al texto dentro de ellos. Ya tengo esto trabajando en el nivel de vista, pero la vista de tabla todavía no ajusta las alturas de las celdas. Me imagino que una vez que tenga el problema de altura resuelto, usaré el método - noteHeightOfRowsWithIndexesChanged
para informar a la vista de tabla de la altura cambiada but pero todavía va a preguntar al delegado por la altura hence por lo tanto, mi quandry.
Gracias de antemano!
10 answers
Este es un problema de pollo y huevo. La tabla necesita conocer la altura de la fila porque eso determina dónde se encuentra una vista dada. Pero desea que una vista ya esté disponible para que pueda usarla para averiguar la altura de la fila. Entonces, ¿qué viene primero?
La respuesta es mantener un NSTableCellView
extra (o cualquier vista que esté utilizando como su "vista de celda") alrededor solo para medir la altura de la vista. En el método delegado tableView:heightOfRow:
, acceda a su modelo para 'row' y establezca objectValue
en NSTableCellView
. A continuación, establecer el ancho de la vista debe ser el ancho de la tabla y (como quiera hacerlo) calcular la altura requerida para esa vista. Devuelve ese valor.
No llame a noteHeightOfRowsWithIndexesChanged:
desde el método delegado tableView:heightOfRow:
o viewForTableColumn:row:
! Eso es malo, y causará mega-problemas.
Para actualizar dinámicamente la altura, entonces lo que debe hacer es responder al cambio de texto (a través del objetivo/acción) y recalcular la altura calculada de esa vista. Ahora, no cambie dinámicamente la altura de NSTableCellView
(o lo que sea vista que está utilizando como su"vista de celda"). La tabla debe controlar el marco de esa vista, y luchará contra la vista de tabla si intenta configurarla. En su lugar, en su objetivo/acción para el campo de texto donde calculó la altura, llame a noteHeightOfRowsWithIndexesChanged:
, lo que permitirá que la tabla cambie el tamaño de esa fila individual. Suponiendo que usted tiene su configuración de máscara de autoresizing derecha en subviews (es decir: subviews de la NSTableCellView
), las cosas deben cambiar de tamaño bien! Si no, primero trabaje en la máscara de cambio de tamaño de las subviews para obtener cosas derecha con alturas de fila variables.
No olvides que noteHeightOfRowsWithIndexesChanged:
se anima por defecto. Para que no se anime:
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0];
[tableView noteHeightOfRowsWithIndexesChanged:indexSet];
[NSAnimationContext endGrouping];
PD: Respondo más a las preguntas publicadas en los Foros de Apple Dev que a stack overflow.
PSS: Escribí la vista basada en NSTableView
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-09 14:23:41
Esto 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 solo tienes que seleccionar tu NSTableView
o NSOutlineView
en el editor de storyboard y seleccionar esta opción en el Inspector de tamaño:
Luego configura las cosas en su NSTableCellView
para que tengan restricciones superiores e inferiores a la la celda y su celda cambiarán de tamaño para que quepa automáticamente. No se requiere código!
Su aplicación ignorará cualquier altura especificada en heightOfRow
(NSTableView
) y heightOfRowByItem
(NSOutlineView
). Puede ver qué alturas se calculan para sus filas de diseño automático con este método:
func outlineView(_ outlineView: NSOutlineView, didAdd rowView: NSTableRowView, forRow row: Int) {
print(rowView.fittingSize.height)
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-01 23:27:09
Para cualquiera que quiera más código, aquí está la solución completa que usé. Gracias Corbin Dunn por señalarme en la dirección correcta.
Necesitaba establecer la altura principalmente en relación con qué tan alto era un NSTextView
en mi NSTableViewCell
.
En mi subclase de NSViewController
Creo temporalmente una nueva celda llamando a 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 todas las salidas y establece el texto de mi PDFAnnotations. Ahora puedo "fácilmente" calcular 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 ;
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-04-07 18:58:08
Basado en la respuesta de Corbin (por cierto, gracias por arrojar algo de luz sobre esto):
Swift 3, Vista NSTableView basada en vistas con Diseño automático para macOS 10.11 (y superior)
Mi configuración: Tengo un NSTableCellView
que se presenta usando Auto-Layout. Contiene (además de otros elementos) un NSTextField
multilínea que 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 decirle a la vista de tabla que actualice 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 pida a su delegado la nueva altura de la 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 comprobamos si ya hemos creado una vista para este identificador. Para ello 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 ninguno es salvo, nosotros necesidad de crear (y caché) una nueva. Actualizamos la vista de celda con nuestro objeto modelo y le decimos que reorganice todo en función del ancho de vista de la tabla actual. La altura fittingSize
se puede usar como la nueva altura.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-17 09:49:08
Estuve buscando una solución durante 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;
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-25 09:41:20
Dado que 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
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-01 13:42:25
¿Has visto RowResizableViews? Es bastante viejo y no lo he probado, pero puede funcionar.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-09-21 21:13:26
Esto es lo que he hecho para arreglarlo:
Fuente: Busque en la documentación de XCode, en "row height nstableview". Encontrará un código fuente de ejemplo llamado " TableViewVariableRowHeights / TableViewVariableRowHeightsAppDelegate.m "
(Nota: Estoy mirando la columna 1 en la vista de tabla, tendrá que ajustar para buscar en otro lugar)
En Delegado.h
IBOutlet NSTableView *ideaTableView;
En Delegado.m
La vista de tabla delega el control de la altura de la fila
- (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;
}
Usted también necesita esto para efectuar la nueva altura de fila (método delegado)
- (void)controlTextDidEndEditing:(NSNotification *)aNotification
{
[ideaTableView reloadData];
}
Espero que esto ayude.
Nota final: esto no admite el cambio de ancho de columna.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-09-17 09:32:56
Esto se parece mucho a algo que tenía que hacer anteriormente. Desearía poder decirles que se me ocurrió una solución simple y elegante, pero, por desgracia, no lo hice. No por falta de intentarlo. Como ya ha notado, la necesidad de UITableView para saber la altura antes de que las celdas se construyan realmente hace que todo parezca bastante circular.
Mi mejor solución fue empujar la lógica a la célula, porque al menos podía aislar qué clase necesitaba para entender cómo se diseñaban las células. Método como
+ (CGFloat) heightForStory:(Story*) story
Sería capaz de determinar la altura de la célula tenía que ser. Por supuesto que eso implicaba medir texto, etc. En algunos casos, ideé formas de almacenar en caché la información obtenida durante este método que luego podría usarse cuando se creó la celda. Eso fue lo mejor que se me ocurrió. Sin embargo, es un problema exasperante, ya que parece que debería haber una mejor respuesta.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-09-30 05:06:37
Aquí hay una solución basada en la respuesta de JanApotheker, modificada como cellView.fittingSize.height
no estaba devolviendo la altura correcta para mí. En mi caso estoy usando el estándar NSTableCellView
, un NSAttributedString
para el texto TextField de la celda, y una tabla de una sola columna con restricciones para el TextField de la celda establecido en IB.
En mi controlador de vista, declaro:
var tableViewCellForSizing: NSTableCellView?
En viewDidLoad ():
tableViewCellForSizing = tableView.make(withIdentifier: "My Identifier", owner: self) as? NSTableCellView
Finalmente, para el método delegado 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
es un conjunto constante a 30, para la copia de seguridad, pero nunca se usó. attributedStrings
es mi matriz modelo de NSAttributedString
.
Esto funciona perfectamente para mis necesidades. Gracias por todas las respuestas anteriores, que me señalaron en la dirección correcta para este molesto problema.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-29 16:56:14