macOS UI 列表!階層(樹狀)顯示 NSOutlineView

列表!階層(樹狀)顯示 NSOutlineView

NSOutlineView 是 NSTableView 的子類別,但它具有階層(樹狀)顯示的功能,可以將子項目展開或關閉,然而NSTableView實現方式使用陣列來表示,NSOutlineView具有子項目的關系,會使用自訂類型的物件來儲存整個結構的資料當做DataSource,另外以下文章會以階層來描述具有樹狀、階層結構的說明。

自訂結構資料來源

具有階層的結構來說,一個結構分為上層與下層,當下層中有資料時,下層的角色即為轉為上層,然而資料則為下層以此類推,所以就能先完成一個基礎結構:

這裡以一間公司的組成來說明,一間公司內可能會有很多部門,部門中有主管,主管又分為一級主管,一級主管下有二級主管,二級主管下有它所帶領的同仁,整 個組織結構下來可以先訂定一個結構資料物件名為Staff:

//-----------start-----------
//
//  Staff.h
//  NSOutlineViewDemo
//
//  Created by danny on 2015/3/3.
//  Copyright (c) 2015年 danny. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface Staff : NSObject

+ (id)staffName:(NSString*)name;
- (id)initWithName:(NSString*)name;
- (BOOL)isBoss;
- (NSString*)name;
- (void)addStaff:(Staff*)staff;
- (NSArray*) getStaffs;
- (NSInteger) getStaffNumbers;
@end

//------------end------------

以員工為主要物件,員工也能是老闆(isBoss),而老闆下也會有員工(addStaff:),將幾個功能實作為method:

//-----------start-----------
//
//  Staff.m
//  NSOutlineViewDemo
//
//  Created by danny on 2015/3/3.
//  Copyright (c) 2015年 danny. All rights reserved.
//

#import "Staff.h"

@implementation Staff
{
    NSMutableArray *_staffInStaff;
    NSString *_name;
}

- (id)init {
    return [self initWithName:@"name"];
}

+ (id)staffName:(NSString*)name {
    return [[Staff alloc] initWithName:name];
}

//設定員工名字
- (id)initWithName:(NSString*)name {
    self = [super init];
    if (self) {
        // Initialize self.
        _staffInStaff = [NSMutableArray array];
        _name = [name copy];
    }
    return self;
}


- (NSString*)name {
    return _name;
}

//是否為老闆,此員工下還有掛員工即為老闆
- (BOOL)isBoss {
    return (_staffInStaff.count>0?YES:NO);
}

- (void)addStaff:(Staff*)staff {
    [_staffInStaff addObject:staff];
}

- (NSInteger)getStaffNumbers {
    return [_staffInStaff count];
}

- (NSArray*)getStaffs {
    return [_staffInStaff copy];
}

- (NSString*)description {
    return _name;
}

@end


//------------end------------

配置NSOutlineView UI

增加NSOutlineView

將NSOutlineView拖至IB:

因我們只使用一個Table Column,所以刪除Table Column使之保持1個Table Column

已經刪除結果如下:

刪除Table Cell View

此次並無要客製化,所以先將Table Cell View刪除,保留Text Cell讓階層顯示資料時只顯示其字串內容:

如果你有使用Table Cell View客制化顯示內容時,就可以實際像如下:

實作上比較覆雜,這往後有機會再介紹,上圖片結果為開發者Perspx實作好的PXSourceList的Demo,有興趣可以下載回來試試。

物件與UI進行連結

以上準備工作完成後,接下來要將UI與物件及程式整個做連結完成一個Demo。

NSOutlineView連結

程式中加上IBOutlet準備與UI做連結:

@interface AppDelegate ()
:
:
@property (weak) IBOutlet NSOutlineView *outlineV;
:
:
@end

進行與UI的連結:

選擇剛建立好的outlineV IBOutlet:

連結完成如圖下:

寫程式產生內容

首先利用建立好的Staff物件進行建立員工資料:

//-----------start-----------
    Staff *dept1 = [Staff staffName:@"王董"];
    [dept1 addStaff:[Staff staffName:@"小王"]];
    [dept1 addStaff:[Staff staffName:@"楊吉"]];
//------------end------------

目前有個部門主管為王董,它帶領了小王楊吉,所以先建立主管,因它也是員工,所以以Staff物件建立,之後因它帶領2位同仁,所以直接將同仁加入王董Staff物件中,此時王董的角色為部門主管也是公司員工,這裡結構來說是以員工主,所以顯示的都是以名字為階層,並非部門,上面例子可以知道王董下有小王楊吉

之後再將整個部門結構加入公司,公司以陣列的型態以便能加上很多部門,先將dept1加入:

//-----------start-----------
    company = [NSMutableArray array];
    Staff *dept1 = [Staff staffName:@"王董"];
    [dept1 addStaff:[Staff staffName:@"小王"]];
    [dept1 addStaff:[Staff staffName:@"楊吉"]];
    [company addObject:dept1];
//------------end------------

依照此模式範例要加入三個部門,其中二個部門下有同仁,其一個部門只有主管自已一個人:

//-----------start-----------
    company = [NSMutableArray array];
    Staff *dept1 = [Staff staffName:@"王董"];
    [dept1 addStaff:[Staff staffName:@"小王"]];
    [dept1 addStaff:[Staff staffName:@"楊吉"]];
    [company addObject:dept1];
    Staff *dept2 = [Staff staffName:@"張市"];
    [dept2 addStaff:[Staff staffName:@"林林"]];
    [dept2 addStaff:[Staff staffName:@"陳林"]];
    [company addObject:dept2];
    Staff *dept3 = [Staff staffName:@"枸本"];
    [company addObject:dept3];
//------------end------------

加入後呈現結果如下圖結構:

不過加入結構後,此時要以此結構當做DataSource,讓NSOutletView使用時能取得結構上的資料:

//-----------start-----------
//.m
    _outlineV.dataSource = self;
    _outlineV.delegate = self;
//------------end------------

.h要增加DataSource、Delegate:

//-----------start-----------
@interface AppDelegate : NSObject <NSApplicationDelegate, NSOutlineViewDataSource,NSOutlineViewDelegate>
//------------end------------

將NSOutletView指定為self,並在該物件下實作DataSource需要的內容:

//-----------start-----------
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
    if (!item) {
        return [company count];//無item內容為第一層,所以顯示第一層的Staff數量
    }
    return [(Staff*)item getStaffNumbers];//非第一層時會將目前這層的物件傳入,此時我們列出這層下的Staff數量
}
//顯示index陣列值中的內容
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
    if (!item) {
        return [company objectAtIndex:index];//無item內容為第一層,所以顯示第一層的內容
    }
    return [[(Staff*)item getStaffs] objectAtIndex:index];//非第一層時會將目前這層的物件傳入,此時我們列出這層下是否有還有
}
//返回YES代表下層還有物件要列出
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
    if (!item) {
        return NO;//無item內容時代表已經無下層物件
    }
    return [(Staff*)item isBoss];//非第一層時會將目前這層的物件傳入,此時我們列出這層下還有Staff時會將isBoss=YES
}

/* NOTE: this method is optional for the View Based OutlineView.
 */
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
    NSLog(@"%@",item);
    //提供Staff的名字
    return [(Staff*)item name];
}
//------------end------------

程式內容:

//-----------start-----------
//
//  AppDelegate.h
//  NSOutlineViewDemo
//
//  Created by danny on 2015/3/2.
//  Copyright (c) 2015年 danny. All rights reserved.
//

#import <Cocoa/Cocoa.h>

@interface AppDelegate : NSObject <NSApplicationDelegate, NSOutlineViewDataSource,NSOutlineViewDelegate>


@end
//------------end------------

//-----------start-----------
//
//  AppDelegate.m
//  NSOutlineViewDemo
//
//  Created by danny on 2015/3/2.
//  Copyright (c) 2015年 danny. All rights reserved.
//

#import "AppDelegate.h"
#import "Staff.h"

@interface AppDelegate ()

@property (weak) IBOutlet NSWindow *window;
@property (weak) IBOutlet NSOutlineView *outlineV;

@end

@implementation AppDelegate
{
    NSMutableArray *company;
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
    company = [NSMutableArray array];
    Staff *dept1 = [Staff staffName:@"王董"];
    [dept1 addStaff:[Staff staffName:@"小王"]];
    [dept1 addStaff:[Staff staffName:@"楊吉"]];
    [company addObject:dept1];
    Staff *dept2 = [Staff staffName:@"張市"];
    [dept2 addStaff:[Staff staffName:@"林林"]];
    [dept2 addStaff:[Staff staffName:@"陳林"]];
    [company addObject:dept2];
    Staff *dept3 = [Staff staffName:@"枸本"];
    [company addObject:dept3];
    _outlineV.dataSource = self;
    _outlineV.delegate = self;

}

- (void)applicationWillTerminate:(NSNotification *)aNotification {
    // Insert code here to tear down your application
}

- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
    if (!item) {
        return [company count];//無item內容為第一層,所以顯示第一層的Staff數量
    }
    return [(Staff*)item getStaffNumbers];//非第一層時會將目前這層的物件傳入,此時我們列出這層下的Staff數量
}
//顯示index陣列值中的內容
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
    if (!item) {
        return [company objectAtIndex:index];//無item內容為第一層,所以顯示第一層的內容
    }
    return [[(Staff*)item getStaffs] objectAtIndex:index];//非第一層時會將目前這層的物件傳入,此時我們列出這層下是否有還有
}
//返回YES代表下層還有物件要列出
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
    if (!item) {
        return NO;//無item內容時代表已經無下層物件
    }
    return [(Staff*)item isBoss];//非第一層時會將目前這層的物件傳入,此時我們列出這層下還有Staff時會將isBoss=YES
}

/* NOTE: this method is optional for the View Based OutlineView.
 */
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
    NSLog(@"%@",item);
    //提供Staff的名字
    return [(Staff*)item name];
}


@end

//------------end------------

結論

NSOutlineView的使用與NSTableView方式很相近,都是需要提供DataSource、Delegate的方法,可以先參考列出正在執行的應用程式文章中的篇例程式來理解一下NSTableView的使用方式再接著重新看此篇文章會比較容易理解。

範例程式-NSOutlineViewDemo