macOS 開發 - NSOutlineView 的簡單實現(純代碼)


一、簡述

NSOutlineView 繼承自 NSTableView, 比 NSTableView 多一個層級的概念,所以類似多層級樹狀的視圖一般使用這個類來實現。
在學習 NSOutlineView 之前,最好先學習 NSTableView 的實現。


二、一個簡單的 view-based Outlineview

這裏示例實現一個簡單的 view-based Outlineview
在這裏插入圖片描述


1、準備


@interface OutlineWindowController ()<NSOutlineViewDataSource, NSOutlineViewDelegate>

@property (nonatomic, strong) NSScrollView *scrollView;
@property (nonatomic, strong) NSOutlineView *outlineView;

@property (nonatomic, strong) NSArray *wholeTasks;

@end
  

2、數據準備


-(void)initData {
    
    //1級根節點
    TaskModel *model0  = [[TaskModel alloc] init];
    model0.title = @"任務0";
    model0.ID = @"0";
    
    TaskModel *model1 = [[TaskModel alloc]init];
    model1.title = @"任務1";
    model1.ID = @"1";
    
    TaskModel *model2 = [[TaskModel alloc]init];
    model2.title = @"任務2";
    model2.ID = @"2";
    
    //2級節點
    TaskModel *model10 = [[TaskModel alloc]init];
    model10.title = @"任務1-0";
    model10.ID = @"10";
    
    TaskModel *model11 = [[TaskModel alloc]init];
    model11.title = @"任務1-1";
    model11.ID = @"11";
    
    //3級節點
    TaskModel *model100 = [[TaskModel alloc]init];
    model100.ID = @"100";
    model100.title = @"任務1-0-0";
    
    TaskModel *model110 = [[TaskModel alloc]init];
    model110.ID = @"110";
    model110.title = @"任務1-1-0";
    
    //設置子節點
    self.wholeTasks = @[model0,model1,model2];
    model1.subTaskArray = @[model10, model11];
    model10.subTaskArray = @[model100];
    model11.subTaskArray = @[model110]; 
}



3、創建視圖


- (void)testOutLine{
    
    NSTableColumn *tableColumn = [[NSTableColumn alloc] init];
    tableColumn.resizingMask = NSTableColumnAutoresizingMask;
    
    NSOutlineView *outlineView = [[NSOutlineView alloc] init];
    outlineView.allowsColumnResizing = YES;
    outlineView.headerView = nil;
    outlineView.columnAutoresizingStyle = NSTableViewFirstColumnOnlyAutoresizingStyle;
    
    outlineView.usesAlternatingRowBackgroundColors = YES;//背景顏色的交替,一行白色,一行灰色。
    [outlineView addTableColumn:tableColumn];
    
    outlineView.delegate = self;
    outlineView.dataSource = self;
    
    outlineView.autosaveExpandedItems = YES;
    
    /* */
    NSScrollView *scrollView = [[NSScrollView alloc] initWithFrame:NSMakeRect(10, 10, 300, 250)];
    scrollView.documentView = outlineView;
    scrollView.hasVerticalScroller = YES;
    scrollView.autohidesScrollers = YES;
    
    self.scrollView = scrollView;
    self.outlineView = outlineView;
    
    [self.window.contentView addSubview:self.scrollView];
}



4、實現 datasource protocol

// 每一層級節點包含的下一級節點的數量。
-(NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
    
    NSInteger num = 0;
    
    if (!item) {
        num = self.wholeTasks.count;
        NSLog(@"root num : %ld",(long)num);
    }else{
        
       TaskModel *model = (TaskModel *)item;
        num = model.subTaskArray.count;
        NSLog(@"%@ - num : %ld",model.ID,(long)num);
    }

    return num;
}

/*
 item 爲nil空時表示獲取頂級節點模型。
 每一層級節點的模型對象爲item時,根據 item 獲取子節點模型。
 
  這個地方用於向自定義的cellView傳遞數據,如果return item.title,則那邊接收到的“self.objectValue”就是item.title。

 */
-(id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
    
    TaskModel *childModel;
    if(item) {
        TaskModel *model = (TaskModel *)item;
        childModel = model.subTaskArray[index];
    }else{
        childModel = self.wholeTasks[index];
    }
    
    NSLog(@"index : %ld , item : %@ , childModel : %@",index,item,childModel.ID);
    
    return childModel;
}

// 節點是否可以 展開/摺疊。
-(BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
    
    //count 大於0表示有子節點,需要允許Expandable
    if(!item) {
        return [self.wholeTasks count] > 0 ;
    }else {
        
        TaskModel *model = (TaskModel *)item;
        return [model.subTaskArray count] > 0;
    }
}



PS:

  • 代理方法中的 item 代表該項要使用的 item, 如果爲空代表這是 root 節點,返回我們數據源第一層的數據即可。這個在大部分的方法中都會出現,可以多體會它的意思。
  • outlineView:child:ofItem: 需要返回該節點的子 item;如果返回了該 item, 程序可能會進入死循環;
  • outlineView:objectValueForTableColumn:byItem: 對於 view-based 是可選,對於 cell-based 是必須實現。

5、實現 delegate protocol


// 是否是組元素
- (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item {
    
    if (!item) {
        return self.wholeTasks.count;
    }
    
    TaskModel *model = (TaskModel *)item;
    return model.subTaskArray.count;
}

/*
 view 視圖
 和 NSTableView 中的方法 tableView:viewForTableColumn:row: 一樣;
 */
- (nullable NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(nullable NSTableColumn *)tableColumn item:(id)item{
    
    CustomTableCellView *cell = [CustomTableCellView cellWithTableView:outlineView owner:self];
    NSLog(@"item : %@",item);
    TaskModel *model = (TaskModel *)item;
    
    cell.titleLabel.stringValue = model.ID;
    
    return cell;
}

/*
 rowView 視圖
 */
-(NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item{

    CustomTableRowView *rowView = [CustomTableRowView rowViewWithTableView:outlineView];
    //    [rowView msSetLayerColor:[NSColor orangeColor]];

    return rowView;
}

// 自定義行高
- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item{
    
    return 30;
}

// 選擇節點後的通知
- (void)outlineViewSelectionDidChange:(NSNotification *)notification{
    
    NSLog(@"--");
    NSOutlineView *outlineView = notification.object;
    
    NSInteger row = [outlineView selectedRow];
    
    TaskModel *model = (TaskModel *)[outlineView itemAtRow:row];
    
    NSLog(@"name = %@",model.title);
}

6、其他類的實現

item 的類:TaskModel

@interface TaskModel : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *ID;
@property (nonatomic, strong) NSArray *subTaskArray;

@end

自定義 cellView

@interface CustomTableCellView : NSTableCellView
@property (nonatomic, strong) NSTextField *titleLabel;

+ (instancetype)cellWithTableView:(NSTableView *)tableView owner:(nullable id)owner;

@end

@implementation CustomTableCellView
static NSString * CustomTableCellID = @"CustomTableCellID";

+ (instancetype)cellWithTableView:(NSTableView *)tableView owner:(nullable id)owner{
    
    CustomTableCellView *cell = [tableView makeViewWithIdentifier:CustomTableCellID owner:owner];
    if (!cell) {
        cell = [[CustomTableCellView alloc]init];
        cell.identifier = CustomTableCellID;
        [cell setUpViews];
    }
    
    return cell;
}


- (void)setUpViews{
    
    NSTextField *titleLabel = [[NSTextField alloc]initWithFrame:NSMakeRect(0, 0, 200, 30)];
    [self addSubview:titleLabel];
    titleLabel.stringValue = @"123";
    titleLabel.editable = NO;
    titleLabel.bordered = NO;
    self.titleLabel = titleLabel;
    
    self.wantsLayer = YES;
//    self.layer.backgroundColor = [NSColor yellowColor].CGColor;
    self.layer.backgroundColor = [NSColor whiteColor].CGColor;
    
}

@end

自定義 rowView

@interface CustomTableRowView : NSTableRowView

+ (instancetype)rowViewWithTableView:(NSTableView *)tableView;

@end

@implementation CustomTableRowView

static NSString * customTableRowViewID = @"customTableRowViewID";

+ (instancetype)rowViewWithTableView:(NSTableView *)tableView{
    CustomTableRowView *rowView = [tableView makeViewWithIdentifier:customTableRowViewID owner:self];
    if (rowView == nil) {
        rowView = [[CustomTableRowView alloc] init];
        rowView.identifier = customTableRowViewID;
        
        //        [rowView setUpViews];
    }
    
    return rowView;
}

//自定義 row 背景色
- (void)setBackgroundColor:(NSColor *)backgroundColor {
        super.backgroundColor = [NSColor whiteColor];
//    super.backgroundColor = [NSColor orangeColor];
}

- (void)drawRect:(NSRect)dirtyRect {
    [super drawRect:dirtyRect];
    
    // Drawing code here.
}


// 自定義 row 被選中的背景色
-(void)drawSelectionInRect:(NSRect)dirtyRect {
    
    if (self.selectionHighlightStyle != NSTableViewSelectionHighlightStyleNone) {
        [[NSColor lightGrayColor] setFill];
        
        NSBezierPath *path = [NSBezierPath bezierPathWithRect:NSInsetRect(self.bounds, 0, 0)];
        [path fill];
        [path stroke];
    }
}


@end

其它問題:

  • 如何一進入就展開?
  • 如果設置縮進?
  • 如何設置遞進的圖標?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章