文章目录
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: 方法来分发,那么我们就可以在个方法中,去捕获鼠标点击事件。
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)
参考资料
- [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)
- Cocoa Event Handling Guide: Event Architecture - Event Dispatch
--- EOF ---
so long time not see you write
想不到啥好写的,哈哈。