本文的目的,是研究有没有可能在一个TableView中呈现树形数据,尤其是树形菜单。众多的网络资料都强调,Cocoa框架不支持树形视图,苹果推荐程序员使用TableViewController+NavigationController的方式展现树形菜单。如果2-3层的树形数据还可以忍受,万一层次稍多一点,必须反复的用导航按钮在视图中转来转去,显然并不太方便。何况笔者认为2-3层的导航也要切换多次视图,也是一种浪费。
一、搭建基本框架 1
二、实现树节点 2
三、实现树 3
四、实现TreeViewCell 4
五、 在TreeViewController中展现树视图 6
六、 一些改进 8
七、 进一步的封装 10
一、搭建基本框架
1、新建Winddow-Based-Application项目TreeView;
2、删除MainWindow.xib,删除plist中Main nib file base name;
3、修改main.m: int retVal = UIApplicationMain(argc, argv, nil, @"TreeViewAppDelegate");
4、修改TreeViewAppDelegate,删除属性window的声明,删除window的synthesize语句。增加变量声明: TreeViewController* rootViewController;
修改(BOOL)application: didFinishLaunchingWithOptions方法 :
window=[[UIWindow alloc]initWithFrame:CGRectMake(0, 0, 320, 480)];
rootViewController=[[TreeViewController alloc]init];
[window addSubview:rootViewController.view];
[window makeKeyAndVisible];
return YES;
5、新建类TreeViewController,继承UIViewController.
二、实现树节点
1、树由节点构成。树节点是一种链表结构。它包含有父节点、子节点等内容,同时应实现节点添加等操作。
2、新建TreeNode类。
===============.h文件==============
#import <Foundation/Foundation.h>
@interface TreeNode : NSObject {
TreeNode* p_node;//父节点
NSMutableArray* children;//子节点
id data;//节点可以包含任意数据
NSString* title;//节点要显示的文字
NSString* key;//主键,在树中唯一
BOOL expanded;//标志:节点是否已展开,保留给TreeViewCell使用的
}
@property (retain) TreeNode* p_node;
@property (retain) id data;
@property (retain) NSString *title,*key;
@property (assign) BOOL expanded;
@property (retain) NSMutableArray* children;
-(int) deep;//hasChildren的访问方法
-(BOOL)hasChildren;
//子节点的添加方法
-(void)addChild:(TreeNode*)child;
-(int)childrenCount;
@end
===============.m文件==============
#import "TreeNode.h"
@implement ation TreeNode
@synthesize p_node,children,data,title,key,expanded;
-(id)init{
if (self=[super init]) {
p_node=nil;
children=nil;
key=nil;
}
return self;
}
-(void)addChild:(TreeNode *)child{
if (children==nil) {
children=[[NSMutableArray alloc]init];
}
child.p_node=self;
[children addObject:child];
}
-(int)childrenCount{
return children==nil?0:children.count;
}
-(int)deep{
return p_node==nil?0:[p_node.deep]+1;
}
-(BOOL)hasChildren{
if(children==nil || children.count==0)
return NO;
else return YES;
}
@end
三、实现树
1、节点其实就是一种树,有父节点、子节点。但树的最大用处在于遍历树、查找任意子节点。我们可以在TreeNode中增加遍历树的操作。
2、在TreeNode的头文件中增加方法声明:
+(TreeNode*)findNodeByKey:(NSString*)_key :(TreeNode*)node;
+(void)getNodes:(TreeNode*)root :(NSMutableArray*) array;
两个方法都使用递归对树节点进行遍历 ,不同的是前者在查找到key相同的节点返回,而后者则直接把树的所有节点添加到数组中返回。
4、 findNodeByKey 和getNodes 方法:
+(TreeNode*)findNodeByKey:(NSString*)_key :(TreeNode*)node{
if ([_key isEqualToString:[node key]]) {//如果node就匹配,返回node
return node;
}else if([node hasChildren]){//如果node有子节点,查找node 的子节点
for(TreeNode* each in [node children]){
NSLog(@"retrieve node:%@ %@",each.title,each.key);
TreeNode* a=[TreeNode findNodeByKey:_key :each];
if (a!=nil) {
return a;
}
}
}
//如果node没有子节点,则查找终止,返回nil
return nil;
}
+(void)getNodes:(TreeNode*)root :(NSMutableArray*) array{
[array addObject:root];
if ([root hasChildren]) {
for(TreeNode* each in [root children]){
[TreeNode getNodes:each :array];
}
}
return;
}
四、实现TreeViewCell
1、新建类TreeViewCell.
2、TreeViewCell.h文件:
#import <UIKit/UIKit.h>
#import "TreeNode.h"
@interface TreeViewCell : UITableViewCell {
UIButton* btnExpand;//按钮:用于展开子节点
SEL onExpand;//selector:点击“+”展开按钮时触发
TreeNode* treeNode;//每个单元格表示一个节点
UILabel* label;//标签:显示节点title
id owner;//表示 onExpand方法委托给哪个对象
UIImageView* imgIcon;//图标
}
@property (assign) SEL onExpand;
@property (retain) id owner;
@property (retain) UIImageView* imgIcon;
-(void)setTreeNode:(TreeNode *)node;
@end
3、TreeViewCell.m文件:
#import "TreeViewCell.h"
@implementation TreeViewCell
@synthesize onExpand,imgIcon,owner;
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if ((self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) {
// Initialization code
}
return self;
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
-(void)onExpand:(id)sender{
if ([treeNode hasChildren]) {//如果有子节点
//NSLog(@"%d",[treeNode hasChildren]);
treeNode.expanded=!treeNode.expanded;//切换“展开/收起”状态
if(treeNode.expanded){//若展开状态设置“+/-”号图标
[btnExpand setImage:[UIImage imageNamed:
@"minus.png"] forState:UIControlStateNormal];
}else {
[btnExpand setImage:[UIImage imageNamed:
@"plus.png"] forState:UIControlStateNormal];
}
if(owner!=nil && onExpand!=nil)//若用户设置了onExpand属性则调用
[owner performSelector:onExpand withObject:treeNode];
}
}
-(void)setTreeNode:(TreeNode *)node{
treeNode=node;
if (label==nil) {
//NSLog(@"label is nil");
imgIcon=[[UIImageView alloc]initWithFrame:
CGRectMake(20+(15*node.deep), 6, 32, 32)];
label=[[UILabel alloc]initWithFrame:
CGRectMake(50+(15*node.deep), 0, 200,36)];
btnExpand=[[UIButton alloc]initWithFrame:CGRectMake((15*node.deep), 5, 32, 32)];
[btnExpand addTarget:self action:@selector(onExpand:)
forControlEvents:UIControlEventTouchUpInside];
[imgIcon setImage:[UIImage imageNamed:@"folder_small.png"]];
[self addSubview:label];
[self addSubview:imgIcon];
[self addSubview:btnExpand];
}else {
[label setFrame:CGRectMake(50+(15*node.deep), 0, 200, 36)];
[imgIcon setFrame:CGRectMake(20+(15*node.deep), 6, 32, 32)];
[btnExpand setFrame:CGRectMake(15*node.deep, 5, 32, 32)];
}
if ([node hasChildren]) {
NSLog(@"node has children");
if ([node expanded]) {
[btnExpand setImage:[UIImage imageNamed:@"minus.png"]
forState:UIControlStateNormal];
}else {
UIImage *img=[UIImage imageNamed:@"plus.png"];
//NSLog(@"%d",img==nil);
[btnExpand setImage:img
forState:UIControlStateNormal];
}
}else {
[btnExpand setImage:nil forState:UIControlStateNormal];
}
[label setText:node.title];
}
- (void)dealloc {
[super dealloc];
}
@end
五、 在TreeViewController中展现树视图
1、接下来应该建立一个TableViewController,使用我们的TreeViewCell。新建类TreeViewController。
2、TreeViewController.h文件:
#import <Foundation/Foundation.h>
#import "TreeNode.h"
@interface TreeViewController : UITableViewController
<UITableViewDelegate,UITableViewDataSource>
{
TreeNode* tree;
NSMutableArray* nodes;
}
@end
3、TreeViewController.m文件:
#import "TreeViewController.h"
#import "TreeViewCell.h"
@implementation TreeViewController
-(void)viewDidLoad{
[super viewDidLoad];
tree=[[TreeNode alloc]init];
tree.deep=0;
tree.title=@"根节点";
TreeNode* node[10];
for (int i=0; i<10; i++) {
node[i]=[[TreeNode alloc]init];
node[i].title=[NSString stringWithFormat:@"节点%d",i];
node[i].key=[NSString stringWithFormat:@"%d",i];
}
[node[0] addChild:node[1]];
[node[0] addChild:node[2]];
[node[0] addChild:node[3]];
[node[2] addChild:node[4]];
[node[2] addChild:node[5]];
[node[2] addChild:node[6]];
[node[6] addChild:node[7]];
[node[6] addChild:node[8]];
[node[3] addChild:node[9]];
[tree addChild:node[0]];
nodes=[[NSMutableArray alloc]init];
[TreeNode getNodes:tree :nodes];
}
#pragma mark ===table view datasource methods====
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
return 1;
}
-(NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section{
return nodes.count;
}
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString* cellid=@"cell";
TreeViewCell* cell=(TreeViewCell*)[tableView dequeueReusableCellWithIdentifier:
cellid];
if (cell==nil) {
cell=[[[TreeViewCell alloc]initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:cellid]autorelease];
}
TreeNode* node=[nodes objectAtIndex:indexPath.row];
[cell setOwner:self];
[cell setOnExpand:@selector(onExpand:)];
[cell setTreeNode:node];
return cell;
}
-(void)onExpand:(TreeNode*)node{
nodes=[[NSMutableArray alloc]init];
[TreeNode getNodes:tree :nodes];
[self.tableView reloadData];
}
@end
首先,继承TableViewController并实现UITableViewDelegate和UITableViewDataSource协议。
在viewDidLoad方法中,我们使用TreeNode构建了一棵树,并把树的根节点和所有需要展开的节点放到nodes数组中(请看TreeNode的getNodes方法是怎么定义的)。注意,因为一开始所有节点的expanded总是false(不展开),所以node数组中除了根节点外,没有其他元素。
tableView的数据源方法没有什么特别的。但对于TreeView,我们还需要实现一个方法(这里是onExpand方法,但其实叫什么名字无所谓),然后对所有cell使用setOnExpand把这个方法的selector传递给TreeViewCell,在TreeViewCell中,这个方法会在展开(+号)按钮点击时触发。
-(void)onExpand:(TreeNode*)node方法有一点特殊,它带了一个参数。由于在TreeViewCell中,触发该方法时用到了 performSelector:withObject:方式而不是普通的performSelect:发送,所以TreeViewCell有可能把这个单元格所包含的TreeNode对象传递到TreeViewController的onExpand:来。从而可以通过这个参数读到各个单元格的modal数据。
六、 一些改进>
1.无论我们需不需要,TreeView上总是会显示一个“根节点”,哪怕这个根节点并没有什么实际的用途。
如果我们可以控制节点是否需要显示就好了。要实现这一点,需要在TreeViewCell中增加一个新的变量:
BOOL hidden;//标志,节点是否隐藏
然后修改getNodes方法,将 [array addObject:root]; 修改为:
if(![root hidden])//只有节点被设置为“不隐藏”的时候才返回节点
[array addObject:root];
最后,把TreeViewController的loadView方法稍作修改,使根节点隐藏但同时展开:
tree.hidden=YES;
tree.expanded=YES;
这样,根节点不显示了,显示的是它已被展开的子节点“节点0” 。
2、节点左边的文件夹图标真是另人讨厌,我们可以把它替换成自己的图片吗?只需要在 tableView: cellForRowAtIndexPath:方法中修改TreeViewcell的image属性。
NSString* filename=[NSString
stringWithFormat:@"%d.png",[node.key intValue]+1];
UIImage* img=[UIImage imageNamed:filename];
[cell.imgIcon setImage:img];
但记住这些操作必须在[cell setTreeNode:node];语句之后,因为setTreeNode方法会将节点的image属性设为默认的文件夹图片,在此之前修改显然是没有用的:
图片似乎了大一点,把它们从(40*40)调整为默认的32*32 就好。
3.最后还有一个问题,上一级和下一级之间的缩进不是那么明显,我们可以调节缩进吗?
在TreeViewCell.h中,声明两个静态方法:
+(int) indent;
+(void)setIndent:(int)value;
在TreeViewCell.m中(注意,是在implementation,而不是interface中),声明一个静态变量: static int indent=15;//默认缩进值15
同时,实现那两个静态访问方法:
+(int)indent{
return indent;
}
+(void)setIndent:(int)value{
indent=value;
}
在setTreeNode方法中,替换所有的 15*node.deep 为 indent*node.deep
在TreeViewController的viewDidLoad方法中,增加一句: [TreeViewCell setIndent:45];
现在的缩进显然大多了:
七、 进一步的封装
1、为了使我们的TreeView类更容易被程序员们使用,我们应当对其进行必要的封装。这样程序员们可以通过简单的继承或者实现某个我们定义的协议来使用它。这两种方式我们都可以采用,但我决定使用第一种,也就是把我们的实现封装成一个可以继承的超类,程序员要想使用它,必须继承并覆盖一系列的方法,这是似乎更容易使用些。
2、我们在interface中声明了三个给使用者覆盖的方法:
//如果你想呈现自己的树,在子类中覆盖此方法
-(void)initTree;
//如果你想在选中某一个节点时,发生自定义行为,在子类中覆盖此方法
-(void)onSelectedRow:(NSIndexPath *)indexPath;
//如果你想定义自己的单元格视图(比如更换默认的文件夹图标),在子类中覆盖此方法
-(void)configCell:(TreeViewCell *)cell :(TreeNode *)node;
然后修改implementation中的几个地方,调用这三个方法:
-(void)viewDidLoad{
[super viewDidLoad];
[self initTree];
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
[self onSelectedRow:indexPath];
}
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString* cellid=@"cell";
TreeViewCell* cell=(TreeViewCell*)[tableView dequeueReusableCellWithIdentifier:
cellid];
if (cell==nil) {
cell=[[[TreeViewCell alloc]initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:cellid]autorelease];
}
TreeNode* node=[nodes objectAtIndex:indexPath.row];
[cell setOwner:self];
[cell setOnExpand:@selector(onExpand:)];
[cell setTreeNode:node];
[self configCell:cell :node];
return cell;
}
但是在initTree中必须放一些代码,这样当用户什么代码也没写的情况下,有一棵最基本的树显示在视图中:
-(void)initTree{
//NSLog(@"initTree===");
tree=[[TreeNode alloc]init];
tree.deep=0;
tree.title=@"根节点";
TreeNode* node[10];
for (int i=0; i<10; i++) {
node[i]=[[TreeNode alloc]init];
node[i].title=[NSString stringWithFormat:@"节点%d",i];
node[i].key=[NSString stringWithFormat:@"%d",i];
}
[node[0] addChild:node[1]];
[node[0] addChild:node[2]];
[node[0] addChild:node[3]];
[node[2] addChild:node[4]];
[node[2] addChild:node[5]];
[node[2] addChild:node[6]];
[node[6] addChild:node[7]];
[node[6] addChild:node[8]];
[node[3] addChild:node[9]];
[tree addChild:node[0]];
nodes=[[NSMutableArray alloc]init];
[TreeNode getNodes:tree :nodes];
}
3、现在测试一下怎样通过继承来展现我们自己的树。新建TreeViewTestController类,继承TreeViewController。先不加入任何代码,运行效果如下:
4、覆盖父类方法initTree:
-(void)initTree{
[TreeViewCell setIndent:25];
[TreeViewCell setIcoWidth:40];
[TreeViewCell setIcoHeight:40];
[TreeViewCell setLabelMarginLeft:10];
tree=[[TreeNode alloc]init];
tree.deep=0;
tree.title=@"根节点";
tree.hidden=YES;
tree.expanded=YES;
//节点:播放最多
TreeNode* node01=[[TreeNode alloc]init];
node01.title=@"播放最多";
node01.key=@"01";
node01.expanded=YES;
//子节点:今日播放最多
TreeNode* node011=[[TreeNode alloc]init];
node01 1.title=@"今日播放最多";
node011.key=@"001";
node011.data=RANK_PLAY_TODAY;
[node01 addChild:node011];
//子节点:本周播放最多
TreeNode* node012=[[TreeNode alloc]init];
node012.title=@"本周播放最多";
node012.key=@"002";
node012.data=RANK_PLAY_WEEKLY;
[node01 addChild:node012];
//子节点:本月播放最多
TreeNode* node013=[[TreeNode alloc]init];
node013.title=@"本月播放最多";
node013.key=@"003";
node013.data=RANK_PLAY_MONTHLY;
[node01 addChild:node013];
//节点:评论最多
TreeNode* node02=[[TreeNode alloc]init];
node02.title=@"评论最多";
node02.key=@"02";
node02.expanded=YES;
//子节点:今日评论最多
TreeNode* node021=[[TreeNode alloc]init];
node021.title=@"今日评论最多";
node021.key=@"021";
node021.data=RANK_REMARK_TODAY;
[node02 addChild:node021];
//子节点:本周评论最多
TreeNode* node022=[[TreeNode alloc]init];
node022.title=@"本周评论最多";
node022.key=@"022";
node022.data=RANK_REMARK_WEEKLY;
[node02 addChild:node022];
//子节点:本月评论最多
TreeNode* node023=[[TreeNode alloc]init];
node023.title=@"本月评论最多";
node023.key=@"023";
node023.data=RANK_REMARK_MONTHLY;
[node02 addChild:node023];
[tree addChild:node01];
[tree addChild:node02];
nodes=[[NSMutableArray alloc]init];
[TreeNode getNodes:tree :nodes];
}
5、覆盖父类方法 configCell:
//如果你想定义自己的单元格视图(比如更换默认的文件夹图标),在子类中覆盖此方法
-(void)configCell:(TreeViewCell*)cell :(TreeNode*)node{
NSPredicate* predicate=[NSPredicate predicateWithFormat:
@"SELF IN{'01','02'}"];
if ([predicate evaluateWithObject:node.key]) {
NSString* filename=[NSString stringWithFormat:@"%@.png",node.key];
UIImage* img=[UIImage imageNamed:filename];
[cell.imgIcon setImage:img];
}else {
[cell.imgIcon setImage:nil];
}
}
运行效果如下: