Pertanyaan NSTableView berbasis tampilan dengan baris yang memiliki ketinggian dinamis


Saya memiliki aplikasi dengan berbasis tampilan NSTableView di dalamnya. Di dalam tampilan tabel ini, saya memiliki baris yang memiliki sel yang memiliki konten yang terdiri dari banyak baris NSTextField dengan kata-bungkus diaktifkan. Tergantung pada konten tekstual dari NSTextField, ukuran baris yang dibutuhkan untuk menampilkan sel akan bervariasi.

Saya tahu bahwa saya dapat menerapkan NSTableViewDelegate metode -tableView:heightOfRow: untuk mengembalikan ketinggian, tetapi ketinggian akan ditentukan berdasarkan kata pembungkusan yang digunakan pada NSTextField. Membungkus kata dari NSTextField juga berdasarkan pada seberapa lebar NSTextField adalah ... yang ditentukan oleh lebar NSTableView.

Soooo ... Saya kira pertanyaan saya adalah ... apakah pola desain yang bagus untuk ini? Sepertinya semua yang aku coba berakhir menjadi kekacauan yang rumit. Karena TableView membutuhkan pengetahuan tentang ketinggian sel untuk meletakkan mereka ... dan NSTextField membutuhkan pengetahuan tentang tata letaknya untuk menentukan kata bungkus ... dan sel membutuhkan pengetahuan tentang kata bungkus untuk menentukan tingginya ... itu adalah kekacauan melingkar ... dan itu membuat saya gila.

Saran?

Jika itu penting, hasil akhirnya juga akan dapat diedit NSTextFields yang akan mengubah ukuran untuk menyesuaikan dengan teks di dalamnya. Saya sudah memiliki ini bekerja pada tingkat tampilan, tetapi tampilan tabel belum menyesuaikan ketinggian sel. Saya pikir setelah saya mendapatkan masalah ketinggian berhasil, saya akan menggunakan -noteHeightOfRowsWithIndexesChanged metode untuk menginformasikan tampilan tabel ketinggian berubah ... tetapi masih kemudian akan meminta delegasi untuk ketinggian ... karenanya, quandry saya.

Terima kasih sebelumnya!


75
2017-09-21 18:10


asal


Jawaban:


Ini adalah masalah ayam dan telur. Tabel perlu mengetahui tinggi baris karena yang menentukan di mana pandangan yang diberikan akan berbohong. Tetapi Anda ingin tampilan sudah ada sehingga Anda dapat menggunakannya untuk mengetahui tinggi baris. Jadi, mana yang lebih dulu?

Jawabannya adalah untuk menjaga ekstra NSTableCellView (atau tampilan apa pun yang Anda gunakan sebagai "tampilan sel" Anda) di sekitar hanya untuk mengukur ketinggian tampilan. Dalam tableView:heightOfRow: metode delegasi, akses model Anda untuk 'baris' dan atur objectValue di NSTableCellView. Kemudian atur lebar tampilan menjadi lebar meja Anda, dan (bagaimanapun Anda ingin melakukannya) cari ketinggian yang diperlukan untuk tampilan itu. Kembalikan nilai itu.

Jangan telepon noteHeightOfRowsWithIndexesChanged: dari dalam metode delegasi tableView:heightOfRow: atau viewForTableColumn:row: ! Itu buruk, dan akan menyebabkan masalah besar.

Untuk memperbarui ketinggian secara dinamis, maka yang harus Anda lakukan adalah menanggapi perubahan teks (melalui target / tindakan) dan menghitung ulang ketinggian yang dihitung dari tampilan tersebut. Sekarang, jangan ubah secara dinamis NSTableCellViewKetinggian (atau tampilan apa pun yang Anda gunakan sebagai "tampilan sel" Anda). Meja harus kontrol bingkai tampilan itu, dan Anda akan melawan tampilan tabel jika Anda mencoba mengaturnya. Sebagai gantinya, dalam target / tindakan Anda untuk bidang teks di mana Anda menghitung tinggi, panggil noteHeightOfRowsWithIndexesChanged:, yang akan membiarkan tabel mengubah ukuran baris individu. Dengan asumsi Anda memiliki pengaturan masker autoresizing Anda tepat di subview (yaitu subview dari NSTableCellView), semuanya harus diubah ukurannya! Jika tidak, pertama-tama kerjakan topeng pengubah ukuran subview untuk menyelesaikan segala hal dengan ketinggian baris variabel.

Jangan lupakan itu noteHeightOfRowsWithIndexesChanged: menjiwai secara default. Untuk membuatnya tidak bergerak:

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

PS: Saya menanggapi lebih banyak pertanyaan yang diposting di Forum Apple Dev daripada tumpukan overflow.

PSS: Saya menulis pandangan berdasarkan NSTableView


123
2017-11-08 16:57



Ini jauh lebih mudah macOS 10.13 dengan .usesAutomaticRowHeights. Detailnya ada di sini: https://developer.apple.com/library/content/releasenotes/AppKit/RN-AppKit/#10_13 (Di bagian berjudul "NSTableView Automatic Row Heights").

Pada dasarnya Anda hanya memilih Anda NSTableView atau NSOutlineView di editor storyboard dan pilih opsi ini di Inspektur Ukuran:

enter image description here

Maka Anda mengatur barang-barang di Anda NSTableCellView untuk memiliki kendala atas dan bawah ke sel dan sel Anda akan mengubah ukuran agar sesuai secara otomatis. Tidak perlu kode!

Aplikasi Anda akan mengabaikan ketinggian yang ditentukan heightOfRow (NSTableView) dan heightOfRowByItem (NSOutlineView). Anda dapat melihat apa yang tinggi dihitung untuk baris tata letak otomatis Anda dengan metode ini:

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

9
2017-11-01 23:27



Bagi siapa saja yang menginginkan lebih banyak kode, inilah solusi lengkap yang saya gunakan. Terima kasih corbin dunn karena mengarahkan saya ke arah yang benar.

Saya perlu mengatur ketinggian sebagian besar dalam kaitannya dengan seberapa tinggi a NSTextView di dalam saya NSTableViewCell adalah.

Dalam subkelas saya NSViewController Saya sementara membuat sel baru dengan menelepon 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;
}

Di dalam saya IBAnnotationTableViewCell yang merupakan pengontrol untuk sel saya (subkelas dari NSTableCellView) Saya memiliki metode pengaturan

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

yang mengatur semua outlet dan mengatur teks dari PDFAnnotations saya. Sekarang saya dapat "dengan mudah" menghitung tinggi menggunakan:

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



Berdasarkan jawaban Corbin (btw terima kasih memberi penjelasan tentang ini):

Swift 3, View-Based NSTableView dengan Auto-Layout untuk macOS 10.11 (dan lebih tinggi)

Pengaturan saya: Saya punya NSTableCellView yang ditata menggunakan Tata Letak Otomatis. Ini mengandung (selain elemen lain) multi-line NSTextField yang dapat memiliki hingga 2 baris. Oleh karena itu, tinggi keseluruhan tampilan sel bergantung pada ketinggian bidang teks ini.

Saya memperbarui memberitahu tampilan tabel untuk memperbarui ketinggian pada dua kesempatan:

1) Saat tampilan tabel mengubah ukuran:

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

2) Saat objek model data berubah:

tableView.noteHeightOfRows(withIndexesChanged: changedIndexes)

Ini akan menyebabkan tampilan tabel meminta delegasi untuk tinggi baris baru.

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
}

Pertama, kita perlu mendapatkan objek model untuk baris (entity) dan pengidentifikasi tampilan sel yang sesuai. Kami kemudian memeriksa apakah kami sudah membuat tampilan untuk pengidentifikasi ini. Untuk melakukan itu kita harus mempertahankan daftar dengan tampilan sel untuk setiap pengenal:

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

Jika tidak ada yang disimpan, kita perlu membuat (dan cache) yang baru. Kami memperbarui tampilan sel dengan objek model kami dan memberitahukannya untuk menata ulang semuanya berdasarkan lebar tampilan tabel saat ini. Itu fittingSize Ketinggian kemudian dapat digunakan sebagai ketinggian baru.


6
2018-03-17 09:34



Saya mencari solusi untuk beberapa waktu dan datang dengan yang berikut, yang bekerja dengan baik dalam kasus saya:

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



Karena saya menggunakan custom NSTableCellView dan saya memiliki akses ke NSTextField solusi saya adalah menambahkan metode 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



Apakah Anda sudah melihat RowResizableViews? Sudah cukup tua dan saya belum mengujinya tetapi tetap bisa berfungsi.


2
2017-09-21 21:13



Inilah yang telah saya lakukan untuk memperbaikinya:

Sumber: Lihat dokumentasi Xcode, di bawah "tinggi baris nstableview". Anda akan menemukan contoh kode sumber bernama "TableViewVariableRowHeights / TableViewVariableRowHeightsAppDelegate.m"

(Catatan: Saya melihat kolom 1 dalam tampilan tabel, Anda harus menyesuaikan untuk mencari di tempat lain)

di Delegasi.h

IBOutlet NSTableView            *ideaTableView;

di Delegasi.m

tampilan tabel mendelegasikan kontrol ketinggian baris

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

Anda juga perlu ini untuk mempengaruhi tinggi baris baru (metode yang didelegasikan)

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

Saya harap ini membantu.

Catatan terakhir: ini tidak mendukung perubahan lebar kolom.


2
2017-09-17 09:32



Ini terdengar sangat mirip dengan sesuatu yang harus saya lakukan sebelumnya. Saya berharap saya dapat memberitahu Anda bahwa saya datang dengan solusi sederhana dan elegan tetapi, sayangnya, saya tidak. Bukan karena kurang berusaha sekalipun. Seperti yang telah Anda perhatikan perlunya UITableView untuk mengetahui ketinggian sebelum sel-sel yang dibangun benar-benar membuat semuanya tampak cukup melingkar.

Solusi terbaik saya adalah mendorong logika ke sel, karena setidaknya saya bisa mengisolasi kelas apa yang diperlukan untuk memahami bagaimana sel-sel itu ditata. Metode seperti

+ (CGFloat) heightForStory:(Story*) story

akan dapat menentukan seberapa tinggi sel itu. Tentu saja itu termasuk mengukur teks, dll. Dalam beberapa kasus saya merancang cara untuk menyimpan informasi yang diperoleh selama metode ini yang kemudian dapat digunakan ketika sel dibuat. Itu yang terbaik yang saya dapatkan. Ini adalah masalah yang menyebalkan, karena sepertinya harus ada jawaban yang lebih baik.


1
2017-09-30 05:06



Berikut ini adalah solusi berdasarkan jawaban JanApotheker, dimodifikasi sebagai cellView.fittingSize.height tidak mengembalikan ketinggian yang tepat untuk saya. Dalam kasus saya, saya menggunakan standar NSTableCellView, sebuah NSAttributedString untuk teks textField sel, dan satu tabel kolom dengan batasan untuk textField sel yang ditetapkan dalam IB.

Dalam tampilan controller saya, saya menyatakan:

var tableViewCellForSizing: NSTableCellView?

Di viewDidLoad ():

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

Akhirnya, untuk metode delegasi 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 adalah set konstan ke 30, untuk cadangan, tetapi tidak pernah benar-benar digunakan. attributedStrings adalah array model saya NSAttributedString.

Ini bekerja sempurna untuk kebutuhan saya. Terima kasih untuk semua jawaban sebelumnya, yang mengarahkan saya ke arah yang benar untuk masalah yang mengganggu ini.


1
2018-05-29 16:56