在Mac OS X的屏幕最上层绘图

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


 

你可能感兴趣的:(Cocoa_Learning)