第十八章: Image和鼠标事件
在前一章中,你在一些随机点间画线.编写成一个绘制程序会更有趣的. 为了能编写这样的程序,你必须要获取和处理鼠标事件
- NSResponder
NSView继承至NSResponder类. NSResponder类定义了所有的事件处理方法. 现在,我们只对鼠标事件感兴趣.至于键盘事件,留到下一章来讨论吧. NSResponder定义了如下方法:
- (void)mouseDown:(NSEvent *)theEvent;
- (void)rightMouseDown:(NSEvent *)theEvent;
- (void)otherMouseDown:(NSEvent *)theEvent;
- (void)mouseUp:(NSEvent *)theEvent;
- (void)rightMouseUp:(NSEvent *)theEvent;
- (void)otherMouseUp:(NSEvent *)theEvent;
- (void)mouseDragged:(NSEvent *)theEvent;
- (void)scrollWheel:(NSEvent *)theEvent;
- (void)rightMouseDragged:(NSEvent *)theEvent;
- (void)otherMouseDragged:(NSEvent *)theEvent;
注意,所有的参数都是NSEvent对象
-- NSEvent
一个事件对象包含了所有激发该时间相关的信息.当你要处理一个鼠标事件的时候,你应该会对这个方法感兴趣
- (NSPoint)locationInWindow
它返回事件发生的位置
- (unsigned int)modifierFlags
返回的整型告诉你用户按住了哪个键盘上的键,这会让程序员获得Control-click和Shift-Click. 下面是一个例子
- (void)mouseDown:(NSEvent *)e
{
unsigned int flags;
flags = [e modifierFlags];
if (flags & NSControlKeyMask) {
...handle control click...
}
if (flags & NSShiftKeyMask) {
...handle shift click...
}
}
下面是一些常量,你也可以使用AND(&)来修饰modifier flag
NSShiftKeyMask
NSControlKeyMask
NSAlternateKeyMask
NSCommandKeyMask
- (NSTimeInterval)timestamp
这个方法返回一个按秒计量的时间值, 指的是从机器启动开始,到该事件发送时的时间值. NSTimeInterval为double类型
- (NSWindow *)window
事件发生在哪个window上
- (int)clickCount
单击还是双击.
- (float)pressure
如果用户使用一个压力输入设备,这个方法将返回压力,0-1的值
- (float)deltaX;
- (float)deltaY;
- (float)deltaZ;
这些方法获得鼠标和滚轮的位置改变量
-- 获取鼠标事件
为了能获取到鼠标事件,你需要在StrechView.m中重载鼠标事件方法
#pragma mark Events
- (void)mouseDown:(NSEvent *)event
{
NSLog(@"mouseDown: %d", [event clickCount]);
}
- (void)mouseDragged:(NSEvent *)event
{
NSPoint p = [event locationInWindow];
NSLog(@"mouseDragged:%@", NSStringFromPoint(p));
}
- (void)mouseUp:(NSEvent *)event
{
NSLog(@"mouseUp:");
}
编译运行程序,试试双击,并检查输出的click count. 注意噢,首先是获取到第一次点击事件,它的的click count为1.接着第二次点击事件到了,它的click count为2
在XCode 的编辑窗口顶部又一个弹出菜单, 使用#pragma mark.可以给这个菜单添加一项,这样你或是阅读你代码的人更方便,
-- 使用 NSOpenPanel
如果在我们的view上显示一张图像,这会更有意思点. 不过首先,你的创建一个controller对象来从一个文件中读取图像数据. 这时是一个学习使用NSOpenPanel的好机会. 还记得RaiseMan函数中使用到了NSOpenPanel吧,那时是NSDocument类帮我们做的. 而这里,你将自己来使用NSOpenPanel.图18.1显示了你的程序将是什么样.
窗口下面的slide用来控制图像的透明度. 图18.2是对象关系图
--修改nib文件
在XCode中,创建一个新的Objective-c类,命名为AppController.在.h中,添加outlet StretchView和一个action,用来启动Open Panel
#import <Cocoa/Cocoa.h>
@class StretchView;
@interface AppController : NSObject
{
IBOutlet StretchView *stretchView;
}
- (IBAction)showOpenPanel:(id)sender;
打开MainMenu.nib. 从Libray中拖动一个对象到window. 在Identity Inspector中,将它的class设为AppController.如图18.3
拖动一个slider到window. 在Inspector中,设置范围为0-1. 勾选Continuous. 这个slider将用来控制图像显示的透明度-opaque如图18.4
将slider的value绑定到AppController的stretchView.opacity key path如图18.5
在AppController上Control-click,将他的outlet stretchView和window上的StretchView连接上如图18.6
回到nib文件中的main menu,打开File菜单,删除open菜单项以外的所有菜单项。然后Control拖拽该菜单项到AppController的action:showOpenPanel:(如图18.7)
保存文件
--编辑代码
编辑AppController.m文件如下:
#import "AppController.h"
#import "StretchView.h"
@implementation AppController
- (void)openPanelDidEnd:(NSOpenPanel *)openPanel
returnCode:(int)returnCode
contextInfo:(void *)x
{
// Did they choose "Open"?
if (returnCode == NSOKButton) {
NSString *path = [openPanel filename];
NSImage *image = [[NSImage alloc] initWithContentsOfFile:path];
[stretchView setImage:image];
[image release];
}
}
- (IBAction)showOpenPanel:(id)sender
{
NSOpenPanel *panel = [NSOpenPanel openPanel];
// Run the open panel
[panel beginSheetForDirectory:nil
file:nil
types:[NSImage imageFileTypes]
modalForWindow:[stretchView window]
modalDelegate:self
didEndSelector:
@selector(openPanelDidEnd:returnCode:contextInfo:)
contextInfo:NULL];
}
@end
看看启动sheet的那行代码。这是一个非常handy的方法:
- (void)beginSheetForDirectory:(NSString *)path
file:(NSString *)name
types:(NSArray *)types
modalForWindow:(NSWindow *)docWindow
modalDelegate:(id)delegate
didEndSelector:(SEL)didEndSelector
contextInfo:(void *)contextInfo
这个方法以薄板的方式弹出一个和docWindow相关文件打开面板。DidEndSelector有如下的声明:
- (void)openPanelDidEnd:(NSWindow *)sheet
returnCode:(int)returnCode
contextInfo:(void *)contextInfo;
一般在模式对话框的代理中实现该方法。参数path指定了文件浏览时将打开的初始位置。参数name制定了所选文件的初始名字。Path和name参数都可以为空。
- 在View中合成一个图像
你将需要修改StretchView来使用opacity和image。首先,在StretchView.h中声明变量和方法:
#import <Cocoa/Cocoa.h>
@interface StretchView : NSView
{
NSBezierPath *path;
NSImage *image;
float opacity;
}
@property (readwrite) float opacity;
- (void)setImage:(NSImage *)newImage;
- (NSPoint)randomPoint;
@end
然后在StretchView.m实现这些方法:
#pragma mark Accessors
- (void)setImage:(NSImage *)newImage
{
[newImage retain];
[image release];
image = newImage;
[self setNeedsDisplay:YES];
}
- (float)opacity
{
return opacity;
}
- (void)setOpacity:(float)x
{
opacity = x;
[self setNeedsDisplay:YES];
}
在每个方法的最后,通知view需要重画自己。在方法initWithFrame最后,将opacity设置为1.0:
[path closePath];
opacity = 1.0;
return self;
}
同样在StrechView.m中,你需要在drawRect:方法中合成图像:
- (void)drawRect:(NSRect)rect
{
NSRect bounds = [self bounds];
[[NSColor greenColor] set];
[NSBezierPath fillRect:bounds];
[[NSColor whiteColor] set];
[path fill];
if (image) {
NSRect imageRect;
imageRect.origin = NSZeroPoint;
imageRect.size = [image size];
NSRect drawingRect = imageRect;
[image drawInRect:drawingRect
fromRect:imageRect
operation:NSCompositeSourceOver
fraction:opacity];
}
}
注意到drawInRect:fromRect:operation:fraction:方法用力合成图像到view上,fraction决定了图像的透明度。
你需要在dealloc方法中释放image:
- (void)dealloc
{
[path release];
[image release];
[super dealloc];
}
编译并运行程序。你可以再/Developer/Examples/AppKit/Sketch中找到一些图像,当你打开一个图像时,它将出现在StretchView对象的左下方。
--View的坐标系统
最后的一点乐趣呢,来自通过拖拽鼠标修改image的显示位置和尺寸。MouseDown确定图像将要显示区域的其中一个角,而mouseUp确定了相对的另外一个角。最后程序将如图18.8.
每个view都有自己的坐标系统。缺省情况下,(0,0)为左下角。这和PDF以及PostScript一致。如果你愿意,你可以修改view的坐标系统。可以移动原点;修改刻度;以及旋转整个坐标系统。窗口也有一个坐标系统。
如果有两个view,a和b,而你需要将NSPoint p从b的坐标系统转换到a的坐标系统。可以这样做:
NSPoint q = [a convertPoint:p fromView:b];
如果b为空,将从window的坐标系统转换到a的坐标系统。
鼠标消息包含了窗口坐标系统下的位置信息。所以,你一般必须将位置点转换到本地坐标系统。你将创建成员变量来保存图像将显示的区域的角位置。
在StretchView.h中定义这些变量:
NSPoint downPoint;
NSPoint currentPoint;
mouseDown:的位置为downPoint,而mouseDragged:和mouseUp:将更新currentPoint。
编辑鼠标事件处理方法来更新downPoint和currentPoint:
- (void)mouseDown:(NSEvent *)event
{
NSPoint p = [event locationInWindow];
downPoint = [self convertPoint:p fromView:nil];
currentPoint = downPoint;
[self setNeedsDisplay:YES];
}
- (void)mouseDragged:(NSEvent *)event
{
NSPoint p = [event locationInWindow];
currentPoint = [self convertPoint:p fromView:nil];
[self setNeedsDisplay:YES];
}
- (void)mouseUp:(NSEvent *)event
{
NSPoint p = [event locationInWindow];
currentPoint = [self convertPoint:p fromView:nil];
[self setNeedsDisplay:YES];
}
Add a method to calculate the rectangle based on the two points:
- (NSRect)currentRect
{
float minX = MIN(downPoint.x, currentPoint.x);
float maxX = MAX(downPoint.x, currentPoint.x);
float minY = MIN(downPoint.y, currentPoint.y);
float maxY = MAX(downPoint.y, currentPoint.y);
return NSMakeRect(minX, minY, maxX-minX, maxY-minY);
}
(我不知道为什么,很多人都将最后一个方法输入错误。在继续前仔细再检查一次,否则结果将是令人失望的)
在StretchView.h中声明currentRect方法。
为了让用户在没有任何拖拽的情况下也能够看到图像,在setImage:方法中初始化downPoint和currentPoint:
- (void)setImage:(NSImage *)newImage
{
[newImage retain];
[image release];
image = newImage;
NSSize imageSize = [newImage size];
downPoint = NSZeroPoint;
currentPoint.x = downPoint.x + imageSize.width;
currentPoint.y = downPoint.y + imageSize.height;
[self setNeedsDisplay:YES];
}
在drawRect:中,让iamge显示在矩形区域内:
- (void)drawRect:(NSRect)rect
{
NSRect bounds = [self bounds];
[[NSColor greenColor] set];
[NSBezierPath fillRect:bounds];
[[NSColor whiteColor] set];
[path stroke];
if (image) {
NSRect imageRect;
imageRect.origin = NSZeroPoint;
imageRect.size = [image size];
NSRect drawingRect = [self currentRect];
[image drawInRect:drawingRect
fromRect:imageRect
operation:NSCompositeSourceOver
fraction:opacity];
}
}
编译运行程序。注意当拖动到边缘时,view不能滚动。如果能让scroll view能自动移动让用户看到他们拖拽到哪里了将更好一点。这个技术叫autoscrolling。
当用户拖到鼠标时,通过发送消息autoscroll:给clip view来使程序实现autoscrolling. 将事件作为该消息的参数。打开StretchView.m,在方法mouseDragged:中添加下面的代码:
- (void)mouseDragged:(NSEvent *)event
{
NSPoint p = [event locationInWindow];
currentPoint = [self convertPoint:p fromView:nil];
[self autoscroll:event];
[self setNeedsDisplay:YES];
}
编译运行程序。
注意,目前只有当你拖拽时才会自动滚动。为了实现更加平滑的自动滚动,很多开发者会生成一个计时器,在用户拖拽时周期性的给view发送autoscroll:消息。我们将在第24章讨论计时器。
思考:NSImage
大多数情况下,如读取图像,改变图像大小,以及上面练习中的在view中合成图像, NSimage足够胜任。
一个NSImage对象包含一个“表示”数组。例如,图像为一头绘制好的母牛。它可以绘制在PDF中,或是一个彩色位图中,或是一个黑白位图中。每个版本的“表示”都是一个NSImageRep子类的对象。你可以给图像添加一个“表示”,也可以从图像中删除一个“表示”。当你坐下了重写Adobe Photoshop程序时,你就会要维护图像的“表示”
下面是NSImageRep子类列表:
· NSBitmapImageRep
· NSEPSImageRep
· NSPICTImageRep
· NSCachedImageRep
· NSCustomImageRep
· NSPDFImageRep
虽然NSImageRep只有5个子类,不过请注意,NSImage大约可以支持24个类型的图像文件,其中包含了所有的常用图像格式:PICT, GIF, JPG, PNG, PDF, BMP, TIFF等等。
挑战:
创建一个新的基于文档的程序,来容许用户使用随机位置和大小绘制椭圆形。NSBezierPath包含下面的绘制方法:
NSBezierPath *)bezierPathWithOvalInRect:(NSRect)rect;
如果你想多做点,就给它添加保持和打开文件功能
如果你还想多做点,添加undo功能。