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

其它问题:

  • 如何一进入就展开?
  • 如果设置缩进?
  • 如何设置递进的图标?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章