很久以前,Apple和IBM公司成立了一个公司Taligent,开发一些类似Cocoa的工具和库,在Taligent已经比较成熟的时候,在一个商业交流会议上,我遇到了该公司的一个工程师.我请他开发一个简单的程序:<wbr>在一个window上创建一个button. 当点击这个button, 在textfield上面显示 "hello world".这个工程师创建了一个工程,并且开始疯狂的开始了类继承:从window和button以及event handler等基本类继承自己的子类. 然后写了n多的代码让button和textfield显示在window上. 45分钟后,我必须得离开了,可以他仍然没有完成. 在那时候我就断定,这个公司没有前途.果然,若干年后,他们关门大吉了.</wbr>
很多的C++和Java工具库和Taligent工具库思想差不多.开发者需要从标准类中继承,编写很多的代码来控制window上显示的控件. 很多类似的工具库确实在使用.
如果使用AppKit framework. 我们很少去继承类去处理windows ,buttons或是events. 相反,我们会使用现有的类. 同样你也不需要编写代码去控制window中的控件.nib文件能包含所有的信息. 整个程序只会包含少数几行重要的代码.开始,你会决定很惊讶.但是,随着时间,你会发现它的优雅和美妙.
<wbr></wbr>
通过学习NSControl可以很好的理解Appkit framework. NSButton,NSSlider,NSTextView和NSColorWell都是NSControl的子类. 每个control都包含target和action. target是一个指向其它对象的指针.action是会发给target的message(selector).<wbr>回忆一下我们在第二章给两个按钮设置的target和action: 我们把foo对象设置成两个按钮的target.一个按钮的action设置成 seed;另一个设置成generate. 如图5.1</wbr>
action方法接受一个参数: sender.该参数可以让接收者(target)知道是哪一个控件发送的这个action消息. 通常,我们会访问sender来获得更多的信息.比如,一个check box 在勾选的时候发送action消息. 而接受者接受到消息后会访问checkbox,得到它是否是选中的.
- (IBAction)toggleFoo:(id)sender
{
<wbr><wbr><wbr> BOOL isOn =[sender state];</wbr></wbr></wbr>
<wbr><wbr><wbr> ...</wbr></wbr></wbr>
}
要更好得理解NSControl, 我们要进一步了解它的父类继承关系:NSControl继承NSView. 而NSView又继承NSResponder. NSResponder的父类就是NSObject.这个继承关系中的每个节点类都增加了一些实现去做某些功能.如图5.3
在继承关系根部是NSObject. 所有的类都从它继承,同时继承了NSObject实现了一些基本方法:retain, release, dealloc,和init.NSResponder是NSObject的子类. 它实现了一些事件处理的方法,象mouseDown: keyDown:等等.NSView是NSResponder的子类. NSView描述了window上的一块区域,来显示自己.我们可以创建NSView的子类来画图,或是让用户拖拽数据.而NSContro继承NSView,并增加了target和action.
<wbr></wbr>
-- 一些常用的NSControl子类
在使用控件前,我们简要的来学习下3个常用控件类:NSButton, NSSlider,NSTextFeild.
<wbr></wbr>
- NSButton
NSButton实例对象可以有几个不同的外表: 椭圆形,方形,复选框.在点击它们时,它们有不同的行为. 还可以给按钮设置图标和声音. 在Interface Builder中选择NSButton的Attributes Inspector 如图5.4
我们可能经常给按钮发送3个消息:
<wbr></wbr>
- (void)setEnabled:(BOOL)yn
用户可以点击enabled的按钮, disabled的按钮会是灰色的.
<wbr></wbr>
- (int)state
一般对于复选框而言,如果按钮是勾选的, 返回NSOnState(1) ,没有勾选则为NSOffState (0).
<wbr></wbr>
- (void)setState:(int)aState
该方法可以使复选框勾选或不勾选.
<wbr></wbr>
--NSSlider
NSSlider实例-slider可以是横向或是纵向.它可以设置成当拖动时连续不断的发送消息,或是只有当拖动结束(用户mouse up)才发送消息.slider还可以设置标尺,把拖动的改变值限制在一个刻度 图 5.5.同时我们还可以创建圆形的slider
两个常用方法
<wbr></wbr>
- (void)setFloatValue:(float)x
移动slide到x位置
<wbr></wbr>
- (float)floatValue
得到当前值(位置)
<wbr></wbr>
-- NSTextField
NSTextField实例对象文本框,能让用户输入单行文本.文本框可以是可编辑(容许输入),也可以是不可编辑.通常不可编辑的文本框就是窗体上的文本标签.相对于button和slider.文本框相对复杂一些,以后我们还会讨论到其中奥秘.图5.6是在Interface Builder里面 NSTextField的属性
我们看到如果当文本框为空的时候,会自动包含了灰色的站位文本
<wbr></wbr>
NSSecureTextField是NSTextField的子类,处理类似密码文本.用户的输入会由*号代替.我们也不能拷贝,剪切其中的文本.
<wbr></wbr>
NSTextField常用方法:
- (NSString *)stringValue
- (void)setStringValue:(NSString*)aString
得到和设置文本框中的文本
<wbr></wbr>
- (NSObject *)objectValue
- (void)setObjectValue:(NSObject *)obj
这些方法得到和设置文本框中任意对象类型数据
当你需要使用formatter时,这会很有帮助.NSFormatter负责把字符串转换为另外的类型.如果没有相关的NSFormatter指定,这些方法会使用对象obj的descripte方法返回的字符串.
举个例子: 我们需要一个文本框来让用户输入日期.我们不希望用户之间输入文本.而是一个NSCalendarDate对象.通过绑定一个NSDateFormatter,可以保证文本框的objectVaule方法返回一个NSCalendarDate对象,而setObjectValue:可以接受一个NSCalendarDate对象,NSDateFormatter会把NSCalendarDate对象转换成我们相应的文本(在23章,我们会创建自己的formatter)
图5.7显示了其他我们可能用到的控件. 试着吧它们拖放到你的window上,看看它们有什么属性.编译运行程序看看它们是怎么响应的吧.
开始SpeakLine例子
<wbr></wbr>
我们来创建一个简单的例子来试着使用控件. 这个例子容许用户在文本框中输入一行文本,然后使用MacOSX的 speech synthesizer来朗读这行文本. 当我们完成它后,样子如图5.8
图5.9是我们将要创建的对象,以及它们的关系图.其中所以一NS为前缀的是cocoaframework中已经有的类.我们创建了AppController类使用XCode,创建一个Cocoa Applictiaon. 并命名为SpeakLine.
<wbr></wbr>
布局界面 (nib file)
双击MainMenu.nib,打开Interface Builder. 从LibraryWindow中拖处一个文本框和两个按钮.双击文本框,修改文本为"Peter Piper picked a peck ofpickled peppers",(或是你希望朗读的文字.) 把按钮的标题改为 Speak 和 Stop. 如图5.8
<wbr></wbr>
回到XCode, 我们创建一个类: AppController.AppController将是两个按钮的target.每个按钮都会触发一个不同的action方法.编写AppController.h
#import<Cocoa/Cocoa.h>
<wbr></wbr>
@interface AppController : NSObject
{
<wbr><wbr><wbr> IBOutletNSTextField *textField;</wbr></wbr></wbr>
}
- (IBAction)sayIt:(id)sender;
- (IBAction)stopIt:(id)sender;
@end
<wbr></wbr>
在nib文件中创建一个AppController对象: 拖一个蓝色的NSObject正反体到docwindow. 在Identity Inspector中,把他的class设置成AppController. 如图5.10
-- 使用Interface Builder连接
对象连接就像我们做人员介绍: "小明,这是小强".如果你认为小强也有必要知道小明.你会说"小强,这位就是小明." 在InterfaceBuilder中,我们从个某对象拖动到它想知道的那个对象,从而建立连接.
<wbr></wbr>
比如.当用户点击Stop按钮, 按钮发送一个消息给AppController.那么,按钮对象就要"知道"AppController对象.这里,我们从按钮对象Control-Drap到AppController对象.这时会弹出一个面板,我们可以利用它来知道action为stopIt:如图5.11
同样的,我们Conrtrol-drap Speak按钮,设置action为sayIt:
<wbr></wbr>
为了能够朗读文本框中的文字, AppController对象需要得到文本框中的文本.因此,AppController对象有一个指向文本框的指针. Control-Click(按住Control点击鼠标).当outlets列表出现后,从textField拖动到文本框上.如图5.12
到现在,我们设置了几乎所有对象关系图 5.9中的对象连接. 除了speechSynth.它将通过代码而不是Interface Builder来连接.
<wbr></wbr>
-NSWindow的 initialFirstResponder outlet
当我们的程序运行,窗口出现后, 用户如果没有点击文本框,他没有办法输入文本.我们可以设置:当窗口弹出,哪一个view可以接受用户的键盘输入. Control-clickwindow图标.在弹出面板上拖拽initialFirstResponder到文本框
<wbr></wbr>
-- 实现AppController 类
<wbr></wbr>
现在我们来编写一些代码.回到XCode.打开AppController.h文件.给AppController添加一个NSSpeechSynthesizer类型的成员变量:speechSynth
#import<Cocoa/Cocoa.h>
<wbr></wbr>
@interface AppController : NSObject
{
<wbr><wbr><wbr> IBOutletNSTextField *textField;</wbr></wbr></wbr>
<wbr><wbr><wbr><strong>NSSpeechSynthesizer *speechSynth;</strong></wbr></wbr></wbr>
}
- (IBAction)sayIt:(id)sender;
- (IBAction)stopIt:(id)sender;
<wbr></wbr>
@end
<wbr></wbr>
打开AppController.m文件.在这里我们要让程序动起来
#import "AppController.h"
<wbr></wbr>
@implementation AppController
<wbr></wbr>
- (id)init
{
<wbr><wbr><wbr> [superinit];</wbr></wbr></wbr>
<wbr></wbr>
<wbr><wbr><wbr> // Logs canhelp the beginner understand what</wbr></wbr></wbr>
<wbr><wbr><wbr> // ishappening and hunt down bugs.</wbr></wbr></wbr>
<wbr><wbr><wbr>NSLog(@"init");</wbr></wbr></wbr>
<wbr></wbr>
<wbr><wbr><wbr> // Create anew instance of NSSpeechSynthesizer</wbr></wbr></wbr>
<wbr><wbr><wbr> // with thedefault voice.</wbr></wbr></wbr>
<wbr><wbr><wbr> speechSynth= [[NSSpeechSynthesizer alloc] initWithVoice:nil];</wbr></wbr></wbr>
<wbr><wbr><wbr> returnself;</wbr></wbr></wbr>
}
<wbr></wbr>
- (IBAction)sayIt:(id)sender
{
<wbr><wbr><wbr> NSString*string = [textField stringValue];</wbr></wbr></wbr>
<wbr></wbr>
<wbr><wbr><wbr> // Is thestring zero-length?</wbr></wbr></wbr>
<wbr><wbr><wbr> if ([stringlength] == 0) {</wbr></wbr></wbr>
<wbr><wbr><wbr><wbr><wbr><wbr><wbr>NSLog(@"string from %@ is of zero-length", textField);</wbr></wbr></wbr></wbr></wbr></wbr></wbr>
<wbr><wbr><wbr><wbr><wbr><wbr><wbr>return;</wbr></wbr></wbr></wbr></wbr></wbr></wbr>
<wbr><wbr><wbr> }</wbr></wbr></wbr>
<wbr><wbr><wbr> [speechSynthstartSpeakingString:string];</wbr></wbr></wbr>
<wbr><wbr><wbr> NSLog(@"Havestarted to say: %@", string);</wbr></wbr></wbr>
}
<wbr></wbr>
- (IBAction)stopIt:(id)sender
{
<wbr><wbr><wbr>NSLog(@"stopping");</wbr></wbr></wbr>
<wbr><wbr><wbr> [speechSynthstopSpeaking];</wbr></wbr></wbr>
}
@end
<wbr></wbr>
好了,编译运行我们的程序.现在我们可以开始朗读文本,并随时停止朗读
<wbr></wbr>
注意: 一个菜单项(NSMenuItem对象)同样有target和action.我们在这章所讨论的东西一样适合于菜单项.
<wbr></wbr>
思考: 通过代码来设置target
<wbr></wbr>
我们注意到,控件的action是一个selector. NSControl有一个方法:
- (void)setAction:(SEL)aSelector
<wbr></wbr>
那,我们怎么获取一个selector呢? 我们可以使用Objective-C编译指令@selector来查找selector[在那里查找呢.在self中,也就是当前类里面].比如,要设置一个按钮的action为drawMickey:.我们可以这样做
SEL mySelector;
mySelector = @selector(drawMickey:);
[myButton setAction:mySelector];
在编译的时候, @selector(drawMickey:)将会被selectordrawMickey: 来代替.
<wbr></wbr>
如果你要在运行时查找selector.可以使用NSSelectorFromString()
SEL mySelector;
mySelector =NSSelectorFromString(@"drawMickey:");
[myButtonsetTarget:someObjectWithADrawMicke<wbr>yMethod];</wbr>
[myButton setAction:mySelector];
<wbr></wbr>
挑战
<wbr></wbr>
这个练习一定对你意义非凡. 虽然前面在我的指示下能训练完成朗读的例子.这个练习毕竟是你自己完成的.多参考一些前面的例子.你一定行
<wbr></wbr>
创建一个只有一个窗口的程序(不是document-base).图5.13显示的是程序启动后,你没有任何输入时的样子.图5.14显示了,用户作了一些输入后的样子.
当用户输入一些文本,点击按钮. 下面的文本标签将显示输入的文本,并计算出文本的字符数.
<wbr></wbr>
怎么使用Cocoa现有的类去实现这些功能对你很重要.在这个练习中你会认识NSTextField类的两个方法
- (NSString *)stringValue;
- (void)setStringValue:(NSString*)aString;
<wbr></wbr>
同时你也会发现NSString的两个方法很有帮助
- (int)length;
+ (NSString *)stringWithFormat:(NSString*),...;
<wbr></wbr>
你还会创建一个controller对象, 并包含2个outlet和一个action. (恩,虽然有点难,但你一定能完成它. 加油!!)
<wbr></wbr>
<wbr></wbr>
--调试建议
<wbr></wbr>
现在你不再是简单的从书中拷贝代码,而是动手自己编写一些代码了.我想是时候给你一些代码调试的建议了
时刻注意console: 一旦Cocoa对象抛出异常,它会在console中记录相关信息.如果你没注意console.你就不能发现这些错误.
开发过程中使用debug 编译设定: release编译设定移除了一个debuggingsymbol. 因此调试器会没有办法正常调试
<wbr></wbr>
以下是一些常见问题及其解决方法:
<wbr></wbr>
1、没有任何响应: 你可能忘记了在Interface Builder做对象连接.因此,指针为nil.还记得给nil发消息什么都不会做
2、做了连接,还是没有响应: 方法名字是否有拼错. Objective-C是大小写敏感的.所以setFoo: 和 setfoo:是不同的方法. 设置一个断点,看看方法得到调用了么?
3、程序崩溃: 给一个已经释放调的对象发送消息,将会导致你的程序崩溃.(如果你使用了garbagecollector,问题将很难解决). 解决这类问题可能比较难-毕竟,产生问题的对象已经释放掉了.一个方法是设置对象僵化来替代释放.当我们给一个僵化的对象发送消息是,debugger会抛出异常显示一些描述, 比如"You triedto send the message -count to a freed instance of class Fido".并且debugger会停止在发送消息那行.
<wbr></wbr>
在XCode中双击executable . 在Info面板上添加两个环境变量:NSZombiesEnabled为YES. CFZombieLevel 为16. 如图5.15
4、对象没有释放,照样崩溃: 检查你的参数类型. 例如下面就会崩溃
int x = 5;
NSLog(@"x is %@", x);
<wbr></wbr>
5、没法通过Interface Builder做对象连接: 是不是.h 搞错了,是不是忘掉分号: .还是变量定义出错, 比如NSTabView写成NSTableView. 仔细找找看吧.