Mac OS X 中实现支持任意位置拖动的不规则窗体

Mac OS X 中实现支持任意位置拖动的不规则窗体

在开发 Mac OS App 时,根据视觉设计师的需要,有些时候会要实现一些不规则的窗口。一般不规则窗口都不包含 Mac OS X 标准窗口的标题栏,这样用户就没有办法通过拖动标题栏来拖动窗口。

为了能让用户拖动一个无标题栏的窗口,我们就需要自己去处理这个移动窗口的过程。另外,由于窗口有可能是不规则的,那么同样就没有办法拖动窗口边缘来改变窗口大小了,同样需要自己进行处理。

因为要处理鼠标点击事件,因此我们先创建一个继承于 NSWindow 的类 JWBorderlessWindow,以下代码都是此类的代码实现。

初步实现

//
//  JWBorderlessWindow
//
//  Created by Xu Jiwei on 12-9-3.
//  Copyright (c) 2012 xujiwei.com. All rights reserved.
//

@implementation JWBorderlessWindow

- (BOOL)canBecomeKeyWindow {
    return YES;
}


- (BOOL)canBecomeMainWindow {
    return YES;
}


- (NSRect)resizeAreaRect {
    const CGFloat resizeBoxSize = 20.0;
    
    // 窗口右下角 20x20 的区域为改变窗口的区域
    NSRect frame = [self frame];
    NSRect resizeRect = NSMakeRect(frame.size.width - resizeBoxSize, 0,
                                   resizeBoxSize, resizeBoxSize);
    
    return resizeRect;
}


// 处理鼠标按下事件
- (void)mouseDown:(NSEvent *)event {
    NSPoint pointInView = [event locationInWindow];
    
    // 判断是否点击在改变窗口大小区域
    BOOL resize = NSPointInRect(pointInView, [self resizeAreaRect]);
    
    NSWindow *window = self;
    NSPoint originalMouseLocation = [window convertBaseToScreen:[event locationInWindow]];
    NSRect originalFrame = [window frame];
    
    while (YES) {
        // 捕获鼠标拖动或鼠标按键弹起事件
        NSEvent *newEvent = [window nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
        
        if ([newEvent type] == NSLeftMouseUp) {
            break;
        }
        
        // 计算鼠标移动的偏移
        NSPoint newMouseLocation = [window convertBaseToScreen:[newEvent locationInWindow]];
        NSPoint delta = NSMakePoint(newMouseLocation.x - originalMouseLocation.x,
                                    newMouseLocation.y - originalMouseLocation.y);
        
        NSRect newFrame = originalFrame;
        
        if (!resize) {
            // 移动窗口
            newFrame.origin.x += delta.x;
            newFrame.origin.y += delta.y;
            
        } else {
            NSSize maxSize = [window maxSize];
            NSSize minSize = [window minSize];
            
            // 改变窗口大小
            newFrame.size.width += delta.x;
            newFrame.size.height -= delta.y;
            
            // 控制窗口大小在限制范围内
            newFrame.size.width = MIN(MAX(newFrame.size.width, minSize.width), maxSize.width);
            newFrame.size.height = MIN(MAX(newFrame.size.height, minSize.height), maxSize.height);
            newFrame.origin.y -= newFrame.size.height - originalFrame.size.height;
        }
        
        [window setFrame:newFrame display:YES animate:NO];
    }
}

@end

以上代码借鉴自 RoundWindow 示例代码。

这些代码就实现了点击窗口就可以拖动窗口,点击 resizeAreaRect 所返回的区域就可以改变窗口大小。

但是在用的时候,会发现一个问题,如果这个窗口上有控件的话,例如有按钮、文本框等控件,那么就鼠标点击在按钮上时,就无法拖动这个窗口了。

有些时候可能整个窗口是一个列表,没有空余的地方露出 Window 本身,那想要拖动窗口就会变得很麻烦了。

实现任意位置移动

因为 NSWindow 的 mouseDown: 方法,只会在点击在窗口本身上时,才会被调用,而如果点击在了按钮或者文本框上,这个鼠标事件就不会传递到 NSWindow 上。

根据《Cocoa Event Handling Guide》可以知道,如果有事件过来,会由 NSApp 分发到 NSWindow,再通过 NSWindow,使用 sendEvent: 方法来分发,那么我们就可以在个方法中,去捕获鼠标点击事件。

Path of a mouse event
via Cocoa Event Handling Guide

//
//  JWBorderlessWindow
//
//  Created by Xu Jiwei on 12-9-3.
//  Copyright (c) 2012 xujiwei.com. All rights reserved.
//

@interface JWBorderlessWindow : NSWindow {
    BOOL            mouseDraggedForMoveOrResize;
    BOOL            mouseDownInResizeArea;
    NSPoint         mouseDownLocation;
    NSRect          mouseDownWindowFrame;
}
@end

@implementation JWBorderlessWindow

- (BOOL)canBecomeKeyWindow {
    return YES;
}


- (BOOL)canBecomeMainWindow {
    return YES;
}


- (NSRect)resizeAreaRect {
    const CGFloat resizeBoxSize = 20.0;
    
    NSRect frame = [self frame];
    NSRect resizeRect = NSMakeRect(frame.size.width - resizeBoxSize, 0,
                                   resizeBoxSize, resizeBoxSize);
    
    return resizeRect;
}


- (void)handleMoveOrResize:(NSEvent *)event {
    NSWindow *window = self;
    
    //
    // Work out how much the mouse has moved
    //
    NSPoint newMouseLocation = [window convertBaseToScreen:[event locationInWindow]];
    NSPoint delta = NSMakePoint(newMouseLocation.x - mouseDownLocation.x,
                                newMouseLocation.y - mouseDownLocation.y);
    
    NSRect newFrame = mouseDownWindowFrame;
    
    if (!mouseDownInResizeArea) {
        //
        // Alter the frame for a drag
        //
        newFrame.origin.x += delta.x;
        newFrame.origin.y += delta.y;
        
    } else {
        NSSize maxSize = [window maxSize];
        NSSize minSize = [window minSize];
        
        newFrame.size.width += delta.x;
        newFrame.size.height -= delta.y;
        
        newFrame.size.width = MIN(MAX(newFrame.size.width, minSize.width), maxSize.width);
        newFrame.size.height = MIN(MAX(newFrame.size.height, minSize.height), maxSize.height);
        newFrame.origin.y -= newFrame.size.height - mouseDownWindowFrame.size.height;
    }
    
    [window setFrame:newFrame display:YES animate:NO];
}


- (void)sendEvent:(NSEvent *)event {
    // 处理单击事件,实现在窗口任意位置移动窗口
    if (event.type == NSLeftMouseDown) {
        mouseDraggedForMoveOrResize = NO;
        mouseDownLocation = [self convertBaseToScreen:[event locationInWindow]];
        mouseDownWindowFrame = [self frame];
        mouseDownInResizeArea = NSPointInRect([event locationInWindow], [self resizeAreaRect]);
        
        BOOL keepOn = YES;
        
        while (keepOn) {
            NSEvent *newEvent = [self nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)
                                                  untilDate:[NSDate distantFuture]
                                                     inMode:NSEventTrackingRunLoopMode
                                                    dequeue:NO];
            switch (newEvent.type) {
                case NSLeftMouseDragged:
                    // 处理鼠标移动事件
                    [self handleMoveOrResize:newEvent];
                    // 把事件从队列中删除
                    [self nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
                    mouseDraggedForMoveOrResize = YES;
                    break;
                    
                case NSLeftMouseUp:
                    // 如果不是正在移动窗口或改变窗口大小,就把事件继续分发
                    if (!mouseDraggedForMoveOrResize) {
                        [super sendEvent:event];
                    }
                    keepOn = NO;
                    break;
                    
                default:
                    keepOn = NO;
                    break;
            }
        }
        
    } else {
        [super sendEvent:event];
    }
}

@end

这时候又会有另外一个问题,虽然可以在窗口任意位置都可以拖动窗口,但是在 NSTextField 或者 NSTextView 获取焦点时,如果想用鼠标拖动光标来选择文本,是没办法做到的,在拖动的时候变成了在拖动整个窗口。

好吧,继续解决问题。

修正 NSTextField 及 NSTextView 兼容性

- (void)sendEvent:(NSEvent *)event {
    // 处理单击事件,实现在窗口任意位置移动窗口
    // 判断鼠标点击所在位置的 view,如果是 NSTextView,就不处理,直接继续传递事件
    NSView *targetView = [self.contentView hitTest:[event locationInWindow]];
    if (event.type == NSLeftMouseDown && ![targetView isKindOfClass:[NSTextView class]]) {
        // 省略,与前一段代码相同
        
    } else {
        [super sendEvent:event];
    }
}

完成,在 NSTextField 或者 NSTextView 获得焦点时,可以用鼠标来拖动选中内容了。

结语

下载文中示例程序:JWBorderlessWindowTest (39KB)

参考资料

  1. [http: //cocoawithlove.com/2008/12/drawing-custom-window-on-mac-os-x.html](http: //cocoawithlove.com/2008/12/drawing-custom-window-on-mac-os-x.html)
  2. Cocoa Event Handling Guide: Event Architecture - Event Dispatch

--- EOF ---

发表评论?

2 条评论。

  1. so long time not see you write

回复给 yangkuang ¬
取消回复


注意 - 你可以用以下 HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>