我从旧书摊捡起那本一块砖头大小的《C语言大全》到现在整整过去了20年了。这20年间,我偶尔摆弄过其他一些语言,但绝大部分时间,我都在弄C/C++,近3-4年我已经不再使用MFC,而用WTL编程。我是个很不时髦的家伙,所以,尽管谷歌和苹果的兴起都看到了,但从没有想过要参与其中,直到最近,由于参与的一个项目,客户端有使用Ipad的打算,我才打算看看苹果编程究竟是怎么回事儿。 首先看到的就是Object-c,它的语法给我的第一印象相当怪异。以至于本来很容易弄明白的事情,花费了更多的时间。以我的年纪和经验,理解不是问题,--不管怎么说,它总还是C语言吧;但记忆力衰退明显,尤其是要记住许多细节,相当的困难了。花了两周之后,感觉到苹果漂亮面孔背后的强大,他们与开发者共享了几乎,几乎所有的一切,功能强到令我感动。于是我决定买一个MAC本本。现在MacBook Pro已经拿到了大概两个月了,查看了许许多多文档,下载并试验了几乎超过一半的苹果提供的例子程序,当然是相当粗略的。我甚至自己编了一个文本编辑器,当然自己没编什么代码,都是从这边拷贝到那边来实现,连接设置等等。 结果出来后我测试了下,吓了一跳,那个程序居然实现了很多我想都没有想过的功能。拷贝粘贴,字体颜色设置,图片,屏幕捕捉,甚至还有英文的语音阅读。完成所有这一切,程序的代码只有几十行。这还不算,浏览那些例子的时候,我也发现其他感兴趣的东西,表格,各种各样的控件,CoreData,PDF, 视频,相机,定位,地图,游戏 ……这使我产生了一个感觉,苹果的成功不是偶然的,将来他们或者会更加成功,尤其是现在苹果的本本的价格也到了跟ThinkPad差不多程度。 于是我决定好好学习下苹果编程。3个月的接触,我已经有了一个大致的印象。刚开始时,我的目光局限在IOS上,但是现在我认为如果要学习,还是从cocoa开始比较好。毕竟IOS只是MAC的一个子集而已。学习编程,重要的不是会编什么程序,而是你是否理解界面背后的逻辑。对于Mac OS而言,你要学习的也不是object-c,那只是基础,你要学习 Cocoa, Appkit, Foundation,CoreData等这些基础的 FrameWork,以及他们如何在Xcode里面被组织和使用,整个程序如何工作。 为了强化我的学习动力,我觉得开一个专门的博客来做这件事,毕竟当考虑到会有人看你写的那些东西,你才愿意把想法纪录到别人能看懂的程度。我过去也曾经记录过一些东西,后来,有些我自己也无法看明白了。就算看明白的,内容也想到凌乱。 闲话不说了。那么我就们开始吧。
一、第一个程序(查看图片1) 如果要讲编程,一般的例子总是从Hello Word开始的。但我不打算这么做,这也太简单了,如果你的程度跟不上这里,那么请另外查找资料补足。 我们的第一个例子,是一个查看本机图片文件的单窗口界面,显示图片,最好我们还能对图片进行一些处理,彩色变黑白之类的。 打开Xcode,选择Cocoa Application,[Next]。
product : 产品名称 classPrefix :你的分类前缀, 任意的,一般我取产品名称的头字母。 Use Automatic Reference Counting 使用自动参考计数。
关于“使用自动参考计数”,如果你不明白怎么回事。那么要去查查。内存问题一直是C语言最棘手之处,引用计数我最早在COM里面遇到过。它的实质就是解决内存泄漏。自动参考计数是一种先进的方法,基本你可以不用去管内存释放问题了。简单的说就是你alloc了一个对象,用完后不用理会,它会自动释放的。所以这个选项一定要选。
建立程序运行,你看到的就是这样的一个程序,它什么也不做。但是它已经是一个完整的程序了,你检查Xcode自动产生的代码,会发现主题只有一个应用的接口类和一个nib界面文件。
// BSAppDelegate.h @interface BSAppDelegate :NSObject <NSApplicationDelegate> @property (assign)IBOutletNSWindow *window; @end
// BSAppDelegate.m #import "BSAppDelegate.h"
@implementation BSAppDelegate - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // Insert code here to initialize your application } @end
主程序main也叫入口程序,几乎所有的程序都有它的身影。很明显它仅仅调用了一下NSApplicationMain函数,然后就返回了。你可能很想知道,这个叫做启动函数的哥们都做了什么呢?可以想象,它启动了主线程,调用了主界面产生了主窗口,并且建立了菜单。之后就进入事件循环,等待用户输入。
下面语句:
定义了一个窗口变量属性。显然它就是主窗口,如果你够细心你会发现最左边有一个同心原点,内心是实心的。如果是实心的就表示它已经跟界面上的窗口关联了。打开nib文件,找找看吧,要仔细。FIle's Owner 就象英文表达的,叫做文件属主,这个文件的属主如何确定呢?看看那个叫App Delegate 的,是这个家伙决定了nib的属主是BSAppDelegate这个类。为了搞清这里的关系,我费了不少气力,请你认真记住了。毕竟以后,你会看到另外的东东,比如 没有BSAppDelegate这个类代之以一个controler类,等等。
不能说的再多了。如果你还没明白,那么就记下来,将来慢慢体会。回到我们将要实现的程序, 我们的目标已经明确,就是要实现能够浏览机器上的图片,要实现这个有多种方法。比如你可以用一个打开文件对话框,就象finder, 它就有浏览能力,但是我不想这样,我希望是一个树型结构,象Windows里面的资源管理器,不过只显示图片文件。要实现这个功能,第一步我会想有这样的控件吗?或者别人有现成的控件?有没有例子代码? 要学软件,要点之一就是,不要怕向人学习,哪怕你是一个20年开发经验的老手,而你学习对象仅仅刚入门。一个好的开发者就要善于把别人的代码变成自己的,这个能力是学习软件最重要的能力之一。将一个功能从一个完整的例子里面剥离出来,并不象从别人那边拿过扳手拧上螺丝那么简单。
我找到的类似的代码例子是 OutlineView。你要先去下载了看看。 我们就使用OutlineView来实现这个目录结构。NSOutlineView是一个标准的控件。他们是这么介绍它的: “ NSOutlineView是NSTableView的一个子类,使用行和列的格式来显示分层数据,可以展开和折叠,如显示文件系统中的目录和文件。用户可以展开和折叠行,可以编辑值,并调整其大小和重新排列。”你应该去查关于这个控件更多的资料,我已经这么做了。 苹果引用数据的处理相当高明,它引入了一个数据源和接口的概念。以前数据源总是和一个数据表关联的。就好像它说:把数据准备好给我。而在这里NSOutlineView会对你说:当我需要什么的时候,我向你提问,你告诉我就行了,这种情况,就算你什么都没有,你都可以假装自己就是数据源。 如果我们把一个OutlineView控件拉到界面上,那么最终系统就会在window内部产生一个NSOutLineView对象,你可以定义一个变量指向它,如果你不打算针对这个变量做什么,也可以不定义变量,就算你不定义它也是存在的。你可以去看看 NSOutlineView和NSTableView的头文件代码,就算看不太明白也没什么关系,主要是我们要定义它需要的那个数据源和接口。
@interface NSTableView :NSControl <NSUserInterfaceValidations,NSTextViewDelegate,NSDraggingSource> { /* All instance variables are private */ 。。。。。。 id _delegate; id _dataSource; 。。。。。。 } 在OutlineView 例子里面,数据源是这样定义的 @interface DataSource :NSObject @end 仅仅定义成了NSObject的子类,比较上面两处代码,你应该大致可以理解了。原因是他们的原始数据定义成了id指针。 关于指针,我想这里应该好好说一下。指针是C语言中另一个难点。下面是我个人的理解,希望是对的:指针的本质是计算机内存中的物理位置,它指向内存的某处,就象从西安到广州的铁路,我们假设每根枕木都是一个字节,每根枕木都有一个编号,所谓指针就是这个编号,至于内存中,则可以保存变量,对象,甚至是函数或者类对象。从这个意义上说,所有指针本质上都是一样的。那么,指针在哪里变的不同了呢?嗯,是C语言的编译器。因为编译器的原因,比如它处理一个char *指针和一个long *指针, 你完全可以把两个指针强行转换,当然值可能并不是你期望的,编译器并不会产生错误。如果你对指针进行+1操作,情况就不同了。对于char 指针,加一操作之后它的编号也加1,而long指针的+1操作之后,它的指针编号增加了4.原因在于,一个long变量的字节数是4,而char是1,编译器是这么操作的。由于这个原因,指针才变的不同。因此,在你确切知道自己在做什么之后,你完全可以根据需要对指针进行转换。object-c中的id,相当于c语言中的void*。id是个指针,既然我们知道我们是把DataSource对象的指针给了它,那么我们就可以强制将它转换成我们需要的接口指针,前提是我们为那些函数编写了正确的代码。 下面就开始建立 DataSource这个类。由于磁盘上文件或者目录项的内存需要保存,需要一个类来完成,我们也建立了类BSFileSystemItem。
#import <Foundation/Foundation.h>
@interface BSFileSystemItem :NSObject { NSString *relativePath; BSFileSystemItem *parent; NSMutableArray *children; } + (BSFileSystemItem *)rootItem; - (NSInteger)numberOfChildren;// Returns -1 for leaf nodes - (BSFileSystemItem *)childAtIndex:(NSInteger)n;// Invalid to call on leaf nodes - (NSString *)fullPath; - (NSString *)relativePath; @end
#import "BSFileSystemItem.h"
@implementation BSFileSystemItem
static BSFileSystemItem *rootItem =nil; //#define IsALeafNode ((id)-1) - (id)initWithPath:(NSString *)path parent:(BSFileSystemItem *)obj { if (self = [superinit]) { relativePath = [[pathlastPathComponent]copy]; parent = obj; } return self; }
+ (BSFileSystemItem *)rootItem { if (rootItem ==nil)rootItem = [[BSFileSystemItemalloc]initWithPath:@"/"parent:nil]; returnrootItem; }
// Creates and returns the array of children // Loads children incrementally // - (NSArray *)children { if (children ==NULL) { NSFileManager *fileManager = [NSFileManagerdefaultManager]; NSString *fullPath = [selffullPath]; BOOL isDir, valid = [fileManagerfileExistsAtPath:fullPathisDirectory:&isDir]; if (valid && isDir) { NSArray *array = [fileManagercontentsOfDirectoryAtPath:fullPatherror:NULL]; if (!array) { // This is unexpected children = [[NSMutableArrayalloc]init]; }else { NSInteger cnt, numChildren = [arraycount]; children = [[NSMutableArrayalloc]initWithCapacity:numChildren]; for (cnt =0; cnt < numChildren; cnt++) { BSFileSystemItem *item = [[BSFileSystemItemalloc]initWithPath:[arrayobjectAtIndex:cnt]parent:self]; [childrenaddObject:item]; } } }else { children =nil;//IsALeafNode; } } returnchildren; }
- (NSString *)relativePath { returnrelativePath; }
- (NSString *)fullPath { returnparent ? [[parentfullPath]stringByAppendingPathComponent:relativePath] :relativePath; }
- (BSFileSystemItem *)childAtIndex:(NSInteger)n { return [[selfchildren]objectAtIndex:n]; }
- (NSInteger)numberOfChildren { id tmp = [selfchildren]; return (tmp ==nil) ? (-1) : [tmpcount]; }
@end
#import "BSDataSource.h" #import "BSFileSystemItem.h" @implementation BSDataSource // Data Source methods
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { return (item ==nil) ?1 : [itemnumberOfChildren]; }
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { return (item ==nil) ?YES : ([itemnumberOfChildren] != -1); }
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item { return (item ==nil) ? [BSFileSystemItemrootItem] : [(BSFileSystemItem *)itemchildAtIndex:index]; }
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { return (item ==nil) ?@"/" : (id)[itemrelativePath]; }
// Delegate methods - (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item { return NO; } @end
由于OutlineView的代码并不支持ARC,我给出的代码还是做了一些修改。不过并不困难。 编译如果没有错误,就可以设置他们了。你总得告诉NSOutlineView怎么使用你新加入的类。 将Object对象拖入到窗口界面,窗口外部,就可以了。你会看到
最下面增加了一个对象。这么做的意思是初始化的时候,window会为这个产生一个对象。我们当然也可以通过编程方式来设置,不过现在不必要。将这个对象的类修改成BSDataSource。 接下来需要将OutLineView控件拖放到窗口。按右键,设置dataSource和delegate 跟我们新加的对象连接。点击后面的圆圈,拖动箭头到蓝色对象。你看到下面的设置图,就说明对了。记得设置列的标题。
编译执行,目前看到这样的结果:
我们的目录树显示了你的电脑里面所有的东西,我们其实不需要这么多。我们仅仅想看那些图片,因此得研究下如何才能达成目标。稍微分析一下源代码,就会发现我们需要了解NSString,NSString里有一些和路径文件有关的代码。在任何编程语言,字符串都是必修课。所以我们就算在这里耽误些时间,也是值得的。 就算不能完全明白,看看NSString的头文件吧,总没什么损失。 到这里,我就顺便说一下 UNICODE 码。最初的计算机是8位的,因此那时候字符表也是8位的,就是我们都知道的ASCII码,对于字母系(主要是英语系)的国家来说,8位已经够了。80年代,计算机到了中国,如何在计算机里面表示汉字成了难题,我们给出的方案是双字节16位,用两个字节表示一个汉字,为了跟ASCII兼容,当时的国标,只使用了16进制A0上面的代码区,第一个汉字“啊”的代码A1A1,如果发现一个字符在128以下,那就是ASCII码,这是一种选择,没对错之分。不过这样限定了能够在电脑表示的汉字总数也就8000个左右,当时的字库只有7000多汉字,要知道中国的汉字总数有10万,那么很多字无法在电脑里出现是必然的。当时我记得,如果起名字超出字库都不给上户口,直到后来朱镕基当了总理,没有人敢让总理改名字,那么就想办法改进字库吧,还真找到了一个方法,当时台湾已经有BIg5码了,所以方法是现成的。改进的结果形成了新的国标gbk,还是两个字节表示一个汉字,头一个字节如果大于0X80就表示是汉字,第二个字不限。这样以来,128X256,去掉控制字符的话,总的汉字数也超过了两万。那时中国,日本台湾,中东国家等,都有自己的字符系统,他们相互冲突而且不能通用。就是为了解决这个问题,国际上发展出了UNCODE码,它是也2字节的。2字节,256X256 可以表示6万多字符,他们把不同的代码区分配给各个国家,这么以来,大家代码冲突的问题就解决了。不同之处在于,所有的字符都变成了双字节字符。 我们还是回到我们该做的事情。我看了NSString的定义之后,还是没有完全弄明白路径搜索问题。所以我们再看看其他代码。 NSFileManager 类,我初步看了下:在Cocoa应用程序,文件管理器对象通常是你与文件系统的交互的第一选择。您可以使用此对象定位,创建,复制和移动文件和目录。您也可以使用这个对象来获取有关的文件和目录信息,如它的大小,修改日期和BSD许可。您还可以使用文件管理器对象改变许多文件和目录的属性值。NSDirectoryEnumerator这个由NSFileManager过来的类,可以每次一个枚举目录的内容。
从代码看得出来,程序用它得fileExistsAtPath来判断一个路径是目录还是文件,如果是目录得话,就用contentsOfDirectoryAtPath方法得到目录下所有内容(目录或者文件)存放到一个数组,也就是children属性。我并不打算用NSDirectoryEnumerator做这个,要达到我们的目标,可以查找这个children数组的项,把目录跟图片文件找出来,其他得过滤掉。 当然这个效率可能会差一些,不过效率问题目前还不是我们关心的问题。
我想了解我们BSDataSource的工作过程,所以就象上面为每个函数设置了断点。启动程序后,首先停留在第一个函数,请回忆一下,NSOutlineView并不知道BSFileSystemItem这个类,因此可以理解这个时候的item参数是nil,这个函数是要告诉NSOutlineView当前项目下的子项数目。当前项为空,想必是表示这个是根目录了。继续执行程序,被调用的是第3个函数,这个函数目的是返回当前项的第index个子项,这个子项就是BSFileSystemItem的,也就是说,除了根目录其他目录是知道 BSFileSystemItem的。所以这个函数就会在item为nil时,建立根目录,如果是后续的目录,就返回children里面相应的项。继续执行,是第二个函数,这个函数是回答是否是可展开的,也就是下面是否有子项。再次调用的函数是第四个,这是列的名称,最后一个方法是回答是否可以编辑表列的,我们只是查看系统内容,当然这里回答NO. 有了这些知识,我们就可以去实现我们的想法了。
// Creates and returns the array of children // Loads children incrementally // - (NSArray *)children { if (children ==NULL) { NSFileManager *fileManager = [NSFileManagerdefaultManager]; NSString *fullPath = [selffullPath]; children = [[NSMutableArrayalloc]init];
BOOL isDir, valid = [fileManagerfileExistsAtPath:fullPathisDirectory:&isDir]; if (valid && isDir) { NSArray *array = [fileManagercontentsOfDirectoryAtPath:fullPatherror:NULL]; if (array) { NSInteger cnt, numChildren = [arraycount]; for (cnt =0; cnt < numChildren; cnt++) { NSString *childFile = [arrayobjectAtIndex:cnt]; NSString *childPath = [fullPathstringByAppendingPathComponent:childFile]; valid = [fileManagerfileExistsAtPath:childPathisDirectory:&isDir];
if ((valid && isDir) || ([[childPathpathExtension]isEqualToString:@"jpg"])) { BSFileSystemItem *item = [[BSFileSystemItemalloc]initWithPath: childFileparent:self]; [childrenaddObject:item]; } } } } } returnchildren; } 我只是修改了上面这个方法。希望你能看明白我做了什么,这个方法只处理了一种图片格式。不过对于我们的目标来说,应该算达到了目的。好了,接下来我们来显示图片,同时最好能给图片做点什么。还是老方法,我们找找有没有合适的控件,或者例子代码什么的。 在控件里找的了三个控件,Image Wall,Image Cell,ImageKit Image View, 第二个是用在表格控件里面的(甚至都无法拖入窗口),比较了1,3两个选项后,我决定选择ImageKit Image View。当然,选择第一个应该也没什么问题。 将ImageKit Image View控件拖入窗口布局一下,如果执行的话会出错,原因是缺少System/Library/Frameworks/Quartz.framework/ImageKit.framework 。
添加了Quartz.framework再次执行就看到上面的结果。颜色深的区域就是:ImageKit Image View,我在旁边留了点空白是打算将来放按钮。 接下来要考虑的就是,接收NSOutLineView的鼠标事件,然后根据用户的选择设置 ImageKit Image View 选择那个图片。苹果编程有几种方法可以处理这个任务,通常使用Delegate。 我通过NSOutlineViewDelegate接口找到了outlineView:shouldSelectItem,定义是这样: - (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item
我们在BSDataSource.m里面增加这个方法。该方法的内容参考了例子:lkImageViewDemo。
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item{ if(item ==nil) returnNO; NSURL * url = [NSURLfileURLWithPath: [itemfullPath]]; // open the sample files that's inside the application bundle [selfopenImageURL:url];
// customize the IKImageView... [_lkImagesetDoubleClickOpensImageEditPanel:YES]; [_lkImagesetCurrentToolMode:IKToolModeMove]; [_lkImagezoomImageToFit:self]; [_lkImagesetDelegate:self];
return YES; }
并增加了lkImageViewDemo里面一个函数- - (void)openImageURL: (NSURL*)url { // use ImageIO to get the CGImage, image properties, and the image-UTType // CGImageRef image =NULL; CGImageSourceRef isr =CGImageSourceCreateWithURL( (__bridgeCFURLRef)url,NULL);
if (isr) { NSDictionary *options = [NSDictionarydictionaryWithObject: (id)kCFBooleanTrue forKey: (id)kCGImageSourceShouldCache]; image =CGImageSourceCreateImageAtIndex(isr,0, (__bridgeCFDictionaryRef)options);
if (image) { _imageProperties = (NSDictionary*)CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(isr,0, (__bridgeCFDictionaryRef)_imageProperties));
_imageUTType = (__bridgeNSString*)CGImageSourceGetType(isr); } CFRelease(isr);
}
if (image) { [_lkImagesetImage: image imageProperties:_imageProperties]; CGImageRelease(image); } }
在BSDataSource.h增加了几个属性。
#import <Foundation/Foundation.h> #import <Quartz/Quartz.h> @class IKImageView; @interface BSDataSource :NSObject { NSDictionary* _imageProperties; NSString* _imageUTType; } @property IBOutletIKImageView *lkImage; @end
并将lkImage连接到ImageKit Image View。这些都处理完,如果编译成功的话,就有下面的效果了。 曾经说要增加一个按钮,将图片变成黑白之类的。我本来以为图片类里面会有这样的函数,调用一下就可以了。但当我真的去实现的时候,发现彩色图片变灰度相当麻烦。试了几次后,我决定放弃这个功能了。如果你有兴趣的话,可以自己试试看。
最后总结一下。整个过程还是学到了不少东西的。这个ImageKit Image View控件,还没有完全掌握。初始化后图片的位置不正,需要拖动一下才正常。这里并不是特别重要,尤其对我们初学者来说,所以我也没有深究。 |