原文地址:http://www.keakon.net/2011/11/15/%E5%9C%A8MacOSX%E7%9A%84%E5%B1%8F%E5%B9%95%E6%9C%80%E4%B8%8A%E5%B1%82%E7%BB%98%E5%9B%BE
其实Mac OS X和iOS都有一个window level的概念。当window level相同时,根据出现的顺序,后出现的窗口会叠放在先出现的窗口上面;而当window level不同时,越大的排在越上面。在系统预设的值当中,最低的是普通窗口(NSNormalWindowLevel),值为0;最高的是屏保(NSScreenSaverWindowLevel),值为1000。一般而言我们也没必要覆盖屏保,不过实际上它的取值访问可以更广,至少可以到CGShieldingWindowLevel(),在Lion上它的值貌似是2^31-1。
于是只要创建一个window level很高的透明窗口,在接收到鼠标事件时进行绘图,这样就能显示鼠标拖动的轨迹了。
找了一番后我发现Magic Pen这个小程序,于是拿它修改了一番。
先实现我们要的窗口类:
#import <AppKit/AppKit.h> @interface FloatPanel : NSPanel - (id)initWithContentRect:(NSRect)contentRect; @end #import "FloatPanel.h" @implementation FloatPanel - (id)initWithContentRect:(NSRect)contentRect { self = [super initWithContentRect:contentRect styleMask:(NSBorderlessWindowMask | NSNonactivatingPanelMask) backing:NSBackingStoreBuffered defer:NO]; if (self) { self.backgroundColor = NSColor.clearColor; self.level = CGShieldingWindowLevel(); self.opaque = NO; self.hasShadow = NO; self.hidesOnDeactivate = NO; } return self; } @end
这个窗口被设置为没有边框和阴影,背景色透明,level为最高,非当前窗口时也处于激活状态(否则第一次鼠标点击会被当成选中窗口)。
再为其实现一个view,它内部保存了显示到窗口的画,并接受鼠标事件:
#import <Cocoa/Cocoa.h> @interface PanelView : NSView { NSColor *color; NSImage *image; NSPoint lastLocation; NSUInteger radius; } @end #import "PanelView.h" @implementation PanelView - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { color = [NSColor.blueColor retain]; image = [[NSImage alloc] initWithSize:frame.size]; radius = 2; } return self; } - (void)drawRect:(NSRect)dirtyRect { [image drawInRect:NSScreen.mainScreen.frame fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0]; } - (void)drawCircleAtPoint:(NSPoint)point { [image lockFocus]; NSBezierPath *path = [NSBezierPath bezierPathWithOvalInRect:NSMakeRect(point.x - radius, point.y - radius, radius * 2, radius * 2)]; [color set]; [path fill]; [image unlockFocus]; } - (void)drawLineFromPoint:(NSPoint)point1 toPoint:(NSPoint)point2 { [image lockFocus]; NSBezierPath *path = [NSBezierPath bezierPath]; path.lineWidth = radius * 2; [color setStroke]; [path moveToPoint:point1]; [path lineToPoint:point2]; [path stroke]; [image unlockFocus]; } - (void)mouseDown:(NSEvent *)event { lastLocation = [self convertPoint:self.window.mouseLocationOutsideOfEventStream fromView:nil]; [self drawCircleAtPoint:lastLocation]; } - (void)mouseDragged:(NSEvent *)event { NSPoint newLocation = [self convertPoint:self.window.mouseLocationOutsideOfEventStream fromView:nil]; [self drawCircleAtPoint:newLocation]; [self drawLineFromPoint:lastLocation toPoint:newLocation]; [self setNeedsDisplayInRect:NSMakeRect(fmin(lastLocation.x - radius, newLocation.x - radius), fmin(lastLocation.y - radius, newLocation.y - radius), abs(newLocation.x - lastLocation.x) + radius * 2, abs(newLocation.y - lastLocation.y) + radius * 2)]; lastLocation = newLocation; } - (void)mouseUp:(NSEvent *)event { [image release]; image = [[NSImage alloc] initWithSize:NSScreen.mainScreen.frame.size]; [self setNeedsDisplay:YES]; } - (void)dealloc { [super dealloc]; [color release]; [image release]; } @end
不将其直接绘制到屏幕的原因是可能会擦除之前绘制过的路径,所以需要一个图像来保存。
此外,在用NSBezierPath来绘图时,需要lockFocus图像,否则不会绘制到图像里。
而在接收鼠标事件时,mouseDown只需要记录lastLocation和画点,mouseDragged需要连线,mouseUp则清除图像。
最后把这个窗口显示出来:
#include <Carbon/Carbon.h> #import "AppDelegate.h" #import "FloatPanel.h" #import "PanelView.h" @implementation AppDelegate @synthesize window; - (void)dealloc { [super dealloc]; [window release]; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { NSRect frame = NSScreen.mainScreen.frame; window = [[FloatPanel alloc] initWithContentRect:frame]; NSView *view = [[PanelView alloc] initWithFrame:frame]; window.contentView = view; [view release]; window.ignoresMouseEvents = NO; [window makeKeyAndOrderFront:nil]; } @end
注意ignoresMouseEvents要设为NO,因为透明窗口默认是不接收鼠标事件的。
现在运行一下程序,它可以工作了,可是没法传递到下层窗口;而且都无法选中菜单来退出,只能按下Command+Q快捷键。
其实不能传递到下层窗口是必然的,window server在决定了哪个窗口能处理这个鼠标事件后,就会将这个事件传递给它,之后就不管了。而要规避这个限制,就只能用CGEventTap来获取鼠标事件再绘图,因为它能在window server之前进行处理。
于是把上次的代码复制过来,简单地修改一下:
static CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { static AppDelegate *delegate; if (delegate == nil) { delegate = NSApplication.sharedApplication.delegate; } NSView *view = delegate.window.contentView; NSEvent *mouseEvent = [NSEvent eventWithCGEvent:event]; switch (mouseEvent.type) { case NSRightMouseDown: [view mouseDown:mouseEvent]; break; case NSRightMouseDragged: [view mouseDragged:mouseEvent]; break; case NSRightMouseUp: [view mouseUp:mouseEvent]; break; default: return event; break; } return NULL; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { NSRect frame = NSScreen.mainScreen.frame; window = [[FloatPanel alloc] initWithContentRect:frame]; NSView *view = [[PanelView alloc] initWithFrame:frame]; window.contentView = view; [view release]; [window makeKeyAndOrderFront:nil]; CGEventMask eventMask = CGEventMaskBit(kCGEventRightMouseDown) | CGEventMaskBit(kCGEventRightMouseDragged) | CGEventMaskBit(kCGEventRightMouseUp); CFMachPortRef eventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, eventMask, eventCallback, NULL); CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0); CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes); CGEventTapEnable(eventTap, true); CFRelease(eventTap); CFRelease(runLoopSource); }
要注意的是这次window本身需要忽略鼠标事件,所以ignoresMouseEvents需要为YES。
再测试一下,会发现不能正常画线。于是打开PanelView,找到mouseDragged:等方法,改成如下实现:
NSPoint newLocation = event.locationInWindow;
再运行一下,终于搞定了。不过还需要处理屏幕分辨率改变和切换space的事件,这些都可以在Stack Overflow找到答案。
其中space切换可以用一行代码搞定:
window.collectionBehavior = NSWindowCollectionBehaviorCanJoinAllSpaces;