原文地址: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
@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
@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
#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;