【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 2937|回复: 0

【转】我们一起学MAC编程吧(1)

[复制链接]
发表于 2012-12-5 22:55:42 | 显示全部楼层 |阅读模式
我从旧书摊捡起那本一块砖头大小的《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控件,还没有完全掌握。初始化后图片的位置不正,需要拖动一下才正常。这里并不是特别重要,尤其对我们初学者来说,所以我也没有深究。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2025-4-19 06:39

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表