问题
I am developing in and against Mac OS X 10.6 (Snow Leopard). When I double-click between two of my NSTableView's column headers, the column on the left autosizes, like you would expect.
I want to provide this in a context menu as well, but it seems there is no publicly accessible function to do this. I've Googled, and looked at the documentation for NSTableView, NSTableHeaderView, and NSTableColumn, but found nothing. I find it hard to believe they wouldn't expose something so useful when they obviously have the code written to do it.
I saw the -[NSTableColumn sizeToFit]
method, but that only takes the header's size into account. I would also settle for sending the double-click event to the NSTableHeaderView, but couldn't figure out how to do that, either.
Update - I realized it's important to mention I have an NSArrayController (subclass) providing the data to my table, so I don't have an NSTableViewDataSource
on which I could call -[tableView: objectValueForTableColumn: row:]
. That is the crux of the problem: each column is bound to a keypath of the array controller and that's how it gets its data, so it can't loop through its contents.
回答1:
You can get the length of each cell by getting the cells from the column.
NSCell myCell = [column dataCellForRow:i];
Then get the width of the cell.
width = [myCell cellSize].width;
After that you can set the width of the cell as the width of the column.
[column setMinWidth:width];
[column setWidth:width];
回答2:
The following category on NSTableColumn resizes columns to the same width that double-clicking on the separator would produce.
@implementation NSTableColumn (resize)
- (void) resizeToFitContents
{
NSTableView * tableView = self.tableView;
NSRect rect = NSMakeRect(0,0, INFINITY, tableView.rowHeight);
NSInteger columnIndex = [tableView.tableColumns indexOfObject:self];
CGFloat maxSize = 0;
for (NSInteger i = 0; i < tableView.numberOfRows; i++) {
NSCell *cell = [tableView preparedCellAtColumn:columnIndex row:i];
NSSize size = [cell cellSizeForBounds:rect];
maxSize = MAX(maxSize, size.width);
}
self.width = maxSize;
}
@end
回答3:
For view-based table views (OS X 10.7 and later), you have to use the NSTableView
method viewAtColumn:row:makeIfNecessary:
, and then get the size.
The following code works if your cell view is an NSTextField
. For other views you have to figure out the way to get its minimum size.
[tableView.tableColumns enumerateObjectsUsingBlock:
^(id obj, NSUInteger idx, BOOL *stop) {
NSTableColumn* column = (NSTableColumn*) obj;
CGFloat width = 0;
for (int row = 0; row < tableView.numberOfRows; row++) {
NSView* view = [tableView viewAtColumn: idx
row: row
makeIfNecessary: YES];
NSSize size = [[view cell] cellSize];
width = MAX(width, MIN(column.maxWidth, size.width));
}
column.width = width;
}];
}
回答4:
I found the following to work for me (written in Swift):
func resizeColumn(columnName: String) {
var longest:CGFloat = 0 //longest cell width
let columnNumber = tableView.columnWithIdentifier(columnName)
let column = tableView.tableColumns[columnNumber] as! NSTableColumn
for (var row = 0; row < tableView.numberOfRows; row++) {
var view = tableView.viewAtColumn(columnNumber, row: row, makeIfNecessary: true) as! NSTableCellView
var width = view.textField!.attributedStringValue.size.width
if (longest < width) {
longest = width
}
}
column.width = longest
viewTable.reloadData()
}
Given a column's identifier as an argument, it finds the cell in that column with the longest content, and resizes the column to fit that content.
回答5:
I needed to do this very thing and it turned out (for me at least) to be a bit more involved; this isn't perfect but pretty close ;-) p.s. Edited to handle image based cells
#define GBLBIN(x) [[NSUserDefaults standardUserDefaults] boolForKey:(x)]
- (IBAction)autosizeColumns:(id)sender
{
NSMutableArray * widths = [NSMutableArray array];
NSIndexSet * rows = [table selectedRowIndexes];
NSArray * columns = [table tableColumns];
BOOL deslectAll = NO;
NSNumber * col = nil;
int i, j;
// If no row's selected, then select all, the default
if (![rows count])
{
[table selectAll:sender];
rows = [table selectedRowIndexes];
deslectAll = YES;
}
// Use the selected rows from which we'll use the columns' formatted data widths
for (i=[rows lastIndex]; i != NSNotFound; i=[rows indexLessThanIndex:i])
{
for (j=0; j<[columns count]; j++)
{
NSTableColumn * column = [columns objectAtIndex:j];
id item = [[table dataSource]
tableView:table objectValueForTableColumn:column row:i];
NSCell * cell = [column dataCell];
float width, minWidth=10;
// Depending on the dataCell type (image or text) get the 'width' needed
if ([cell isKindOfClass:[NSTextFieldCell class]])
{
NSFont * font = ([cell font]
? [cell font] : [NSFont controlContentFontOfSize:(-1)]);
NSDictionary * attrs =
[NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
NSFormatter * formatter = [cell formatter];
NSString * string = item;
// We want a string, as IB would have formatted it...
if (![item isKindOfClass:[NSString class]])
if (formatter)
string = [formatter stringForObjectValue:item];
else
string = [NSString stringWithFormat:@"%@", item];
width = [string sizeWithAttributes:attrs].width + minWidth;
}
else
{
// We have NSButtonCell, NSImageCell, etc; get object's width
width = MAX(minWidth,[[cell image] size].width);
}
// First time seeing this column, go with minimum width
if (j == [widths count])
{
[widths addObject:[NSNumber numberWithFloat:minWidth]];
}
col = [widths objectAtIndex:j];
width = MAX([col floatValue], width);
[widths replaceObjectAtIndex:j withObject:[NSNumber numberWithInt:width]];
}
}
// Now go resize each column to its minimum data content width
for (j=0; j<[widths count]; j++)
{
NSTableColumn * column = [columns objectAtIndex:j];
float width = [[widths objectAtIndex:j] floatValue];
if (GBLBIN(@"debug"))
{
NSLog(@"col:%d '%@' %.f", j, [column identifier], width);
}
[column setWidth:width];
}
// Undo select all if we did it
if (deslectAll)
{
[table deselectAll:sender];
}
}
回答6:
Here are some extension methods to size columns by their content in C# for Monomac developers.
NOTE: this sizes columns by their content only, other considerations no considered (empty cols are 10px wide for eg).
Thanks to slashlos for the obj-c snippet.
public static void SizeColumnsByContent(this NSTableView tableView, int minWidth = 10) {
Contract.Requires(tableView != null);
Contract.Requires(tableView.DataSource != null);
var columns = tableView.TableColumns();
var widths = new List<int>();
for (int row=0; row< tableView.RowCount; row++) {
for (int col=0; col < tableView.ColumnCount; col++) {
// Determine what the fit width is for this cell
var column = columns[col];
var objectValue = tableView.DataSource.GetObjectValue(tableView, column, row);
var width = column.DataCell.DetermineFitWidth(objectValue, minWidth);
// Record the max width encountered for current coolumn
if (row == 0) {
widths.Add(width);
} else {
widths[col] = Math.Max(widths[col], width);
}
}
// Now go resize each column to its minimum data content width
for (int col=0; col < tableView.ColumnCount; col++) {
columns[col].Width = widths[col];
}
}
}
public static int DetermineFitWidth(this NSCell cell, NSObject objectValueAtCell, int minWidth = 10) {
int width;
if (cell is NSTextFieldCell) {
var font = cell.Font ?? NSFont.ControlContentFontOfSize(-1);
var attrs = NSDictionary.FromObjectAndKey(font, NSAttributedString.FontAttributeName);
// Determine the text on the cell
NSString cellText;
if (objectValueAtCell is NSString) {
cellText = (NSString)objectValueAtCell;
} else if (cell.Formatter != null) {
cellText = cell.Formatter.StringFor(objectValueAtCell).ToNSString();
} else {
cellText = objectValueAtCell.Description.ToNSString();
}
width = (int)cellText.StringSize(attrs).Width + minWidth;
} else if (cell.Image != null) {
// if cell has an image, use that images width
width = (int)Math.Max(minWidth, (int)cell.Image.Size.Width);
} else {
// cell is something else, just use its width
width = (int)Math.Max(minWidth, (int)cell.CellSize.Width);
}
return width;
}
回答7:
I had trouble implementing some of the other solutions... But I managed to get columns to automatically resize width when cells are created using the cell's string value. Seems to work pretty well so far!
- (id)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
NSString *identifier = [tableColumn identifier];
NSUInteger col = [keys indexOfObject:identifier];
// Get an existing cell with the MyView identifier if it exists
NSTextField *cell = [tableView makeViewWithIdentifier:identifier owner:self];
// Get cell data
NSDictionary *dict = [self.tableContents objectAtIndex:row];
NSString *stringValue = [dict objectForKey:[keys objectAtIndex:col]];
// There is no existing cell to reuse so create a new one
if (cell == nil) {
// Create the new NSTextField with a frame of the {0,0} with the width of the table.
// Note that the height of the frame is not really relevant, because the row height will modify the height.
NSRect rect = NSRectFromCGRect(CGRectMake(0, 0, 64, 24));
cell = [[NSTextField alloc] initWithFrame:rect];
[cell setBordered:NO];
[cell setBackgroundColor:[NSColor clearColor]];
[cell setAutoresizesSubviews:YES];
// autosize to fit width - update column min width
NSRect rectAutoWidth = [stringValue boundingRectWithSize:(CGSize){CGFLOAT_MAX, 24} options:NSStringDrawingUsesFontLeading|NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:cell.font}];
if ( tableColumn.minWidth < rectAutoWidth.size.width )
{
[tableColumn setMinWidth:rectAutoWidth.size.width+20]; // add padding
}
// The identifier of the NSTextField instance is set to MyView. This allows the cell to be reused.
cell.identifier = identifier;
}
// result is now guaranteed to be valid, either as a reused cell or as a new cell, so set the stringValue of the cell to the nameArray value at row
// set value
cell.stringValue = stringValue;
// Return the result
return cell;
}
来源:https://stackoverflow.com/questions/4674163/nstablecolumn-size-to-fit-contents