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
想不到啥好写的,哈哈。