Jun 25

MAC应用多窗口应用,窗口位置对齐/吸附等细节处理笔记

Lrdcq , 2018/06/25 11:04 , 程序 , 閱讀(6820) , Via 本站原創
接上,继续存代码开发多窗口mac应用。作为一个看板/大盘类应用,不同模块的快速拼装组合移动的支持肯定是必要的。而作为桌面应用,因为一个看板模块就是一个NSWindow,因此就牵扯出窗口位置对齐/吸附等细节处理了。

目前实现的效果如下:

點擊在新視窗中瀏覽此圖片

整个功能设计分为三步:1. 吸附,一个窗口靠近另一个窗口时,判定是否可吸附并自动贴近。 2. 拖动,实现吸附的窗口跟随拖动。3. 窗口状态的储存与恢复

吸附

吸附功能,用人话描述,就是当窗口拖拽结束的时候,判断它是否靠近另一个窗口,如果靠近到一定距离,就贴上去。

1. 事件触发:
- 窗口拖拽结束,可以用 NSWindowDelegate 的 - (void)windowDidMove:(NSNotification *)notification,它描述的是窗口每一次移动,即frame改变之后的事件。那么明显,存在问题是在拖动的过程中,每一次拖动都会触发这个回掉,因此还需要做一定的截流并且判断当前是否是拖动状态,才能准确的得到窗口拖拽结束事件。

- 如果从用户事件角度,mac上用户窗口拖拽结束的时候,肯定是鼠标离开的时候,因此使用NSWindow的 - (void)mouseUp:(NSEvent *)event 事件就可以拿到了。不过用户其他交互,比如普通的点击之类的也会触发这个,因此如果要注意明确是拖拽的事件结束的话,需要判断上一次事件是什么,就是要结合- (void)mouseDragged:(NSEvent *)event 事件一起做标记才行。

2. 关系维护:
- 首先定义,一个窗口只能吸附到一个父亲级窗口上。因此整个窗口吸附关系可以通过一棵树(或者是森林)来表示。当然对于独立漂泊的窗口,即独立的森林根节点,我们维护起来没啥意义,因此一个简单的map就可以满足我们全部需要 NSDictionary<NSString *, NSString *> *stickPairs,其中键值对表示的是key窗口吸附到value窗口。并且由于每个窗口只能吸附一个,因此key肯定是唯一的,也符合map定义。

3. 吸附判断:
- 当触发事件发生的时候,我们只需要根据当前window的frame和其他窗口的frame做一下对比,当然就知道了。目前判断逻辑如下:
#define XGFE_STICK_DIS 10.0 //这个是吸附的判定距离

#define XGFE_STICK_CLOSE(a,b) (ABS((a) - (b)) < XGFE_STICK_DIS)

//这是一个工具方法,递归的找到传入的root窗口名字所吸附的所有窗口的名字并放入arr中
- (void)foundAllStickWindowsByName:(NSString *)name toArray:(NSMutableArray<NSString *> *)arr {
    for (NSString *key in _stickPairs) {
        if ([_stickPairs[key] isEqualToString:name] && ![arr containsObject:key]) {
            [arr addObject:key];
            [self foundAllStickWindowsByName:key toArray:arr];
        }
    }
}

- (void)releaseWindow:(XGFEBoardBaseWindow *)window inWindows:(NSArray<XGFEBoardBaseWindow *> *)windows {
    CGRect thisRect = window.frame;//当前窗口的rect
    CGPoint thisRectNew = thisRect.origin;//用于缓存吸附刷新的新位置
    BOOL hasStick = NO;//是否判定到吸附的标志位

    NSMutableArray<NSString *> *names = [NSMutableArray array];
    [self foundAllStickWindowsByName:window.nameOfStick toArray:names];
    //拿到了当前窗口已经下的所有窗口names。我们不和吸附我的窗口做循环吸附

    for (XGFEBoardBaseWindow *w in windows) {
        if (w == window) {//是自己,跳过
            continue;
        }
        if ([names containsObject:w.nameOfStick]) {//已经被自己吸附的窗口,跳过
            continue;
        }
        CGRect pairRect = w.frame;
        //x方向吸附的判断
        if (thisRect.origin.x + thisRect.size.width >= pairRect.origin.x - XGFE_STICK_DIS && thisRect.origin.x < pairRect.origin.x + pairRect.size.width + XGFE_STICK_DIS) {
            if (XGFE_STICK_CLOSE(thisRect.origin.y, pairRect.origin.y)) {
                thisRectNew.y = pairRect.origin.y;
                hasStick = YES;
            } else if (XGFE_STICK_CLOSE(thisRect.origin.y, pairRect.origin.y + pairRect.size.height)) {
                thisRectNew.y = pairRect.origin.y + pairRect.size.height;
                hasStick = YES;
            } else if (XGFE_STICK_CLOSE(thisRect.origin.y + thisRect.size.height, pairRect.origin.y)) {
                thisRectNew.y = pairRect.origin.y - thisRect.size.height;
                hasStick = YES;
            } else if (XGFE_STICK_CLOSE(thisRect.origin.y + thisRect.size.height, pairRect.origin.y + pairRect.size.height)) {
                thisRectNew.y = pairRect.origin.y + pairRect.size.height - thisRect.size.height;
                hasStick = YES;
            }
        }
        //y方向吸附的判断
        if (thisRect.origin.y + thisRect.size.height >= pairRect.origin.y - XGFE_STICK_DIS && thisRect.origin.y < pairRect.origin.y + pairRect.size.height + XGFE_STICK_DIS) {
            if (XGFE_STICK_CLOSE(thisRect.origin.x, pairRect.origin.x)) {
                thisRectNew.x = pairRect.origin.x;
                hasStick = YES;
            } else if (XGFE_STICK_CLOSE(thisRect.origin.x, pairRect.origin.x + pairRect.size.width)) {
                thisRectNew.x = pairRect.origin.x + pairRect.size.width;
                hasStick = YES;
            } else if (XGFE_STICK_CLOSE(thisRect.origin.x + thisRect.size.width, pairRect.origin.x)) {
                thisRectNew.x = pairRect.origin.x - thisRect.size.width;
                hasStick = YES;
            } else if (XGFE_STICK_CLOSE(thisRect.origin.x + thisRect.size.width, pairRect.origin.x + pairRect.size.width)) {
                thisRectNew.x = pairRect.origin.x + pairRect.size.width - thisRect.size.width;
                hasStick = YES;
            }
        }
        //如果发生了吸附,则刷新自己并且结束查找
        if (hasStick) {
            [window setFrameOrigin:thisRectNew];
            [_stickPairs setObject:w.nameOfStick forKey:window.nameOfStick];
            break;
        }
    }
    if (!hasStick) {//如果没有发生吸附,就是包括之前可以吸到的窗口也没有,那要主动解除任何吸附关系
        [_stickPairs removeObjectForKey:window.nameOfStick];
    }
}

拖动

即通过上文说的foundAllStickWindowsByName方法拿到当前吸着自己的所有窗口,并且在拖动事件发生的时候,让它们跟着自己一起动。

1. 做了以上工作,我们都知道了窗口拖动中的事件,可以用windowDidMove与mouseDragged均可。基本思路是可以在这些事件之间拿到每次移动的距离,并且同步移动其他的窗口。实现代码为:
- (void)movingWindow:(XGFEBoardBaseWindow *)window inWindows:(NSArray<XGFEBoardBaseWindow *> *)windows change:(CGPoint)point {
    NSMutableArray<NSString *> *names = [NSMutableArray array];
    [self foundAllStickWindowsByName:window.nameOfStick toArray:names];
    for (XGFEBoardBaseWindow *w in windows) {
        if ([names containsObject:w.nameOfStick]) {
            [w setFrameOrigin:CGPointMake(w.frame.origin.x + point.x, w.frame.origin.y + point.y)];
        }
    }
}

当然这么做还有一些细节需要处理,特别在windowDidMove与mouseDragged最后一次事件结束的时候,最后一次frame移动由于屏幕切换等奇奇怪怪的原因并没有及时生效,有可能发生错位。需要delay一丢丢时间再补一次校验。

不过以上工作完成之后做出来效果是这样的:

點擊在新視窗中瀏覽此圖片

emmmmmm,这是因为,这两个事件其实不能60fps完全实时的描述windowframe移动的事件。要完整的做到这个,可以用CADisplayLink做timer并且及时去采集展示的frame信息...不过好像还是会有一定延迟。这个延迟可能是数据上不可避免的。

2. 对比各个方案后,想到了更投机取巧的方式,即:在开始拖动的时候,把foundAllStickWindowsByName得到的窗口绑定到自己的childWindow中去,并且在结束事件的时候解除绑定。这样就方便的实现的拖动的效果了。

储存与恢复

一般作为NSWindow可以自动储存当前程序结束的时候的位置和大小,并且在程序打开的时候恢复。那作为本app的纯代码生成窗口,并且可以随便拖动吸附和状态切换,这件事应该如何考虑呢。

- 自动位置储存,可以使用 - (void)saveFrameUsingName:(NSWindowFrameAutosaveName)name 储存当前的frame并且通过 - (BOOL)setFrameAutosaveName:(NSWindowFrameAutosaveName)name 给恢复回来。不过实际测试来看,这一套东西偶尔会有bug,因此需要手动维护。

- 结论上,通过NSUserDefaults直接进行储存,储存的内容包括 1. NSDictionary<NSString *, NSArray<NSNumber *> *> *posMap,记录当前窗口的name和位置映射;2. NSDictionary<NSString *, NSString *> *stickPairs即上文的stickPairs,记录窗口吸附关系的映射。

- 储存时机:1. 每次窗口移动结束后储存,即上文触发吸附的时机; 2. 窗口打开关闭的时候储存。其中窗口发生这样的变动时肯定要解除和它相关的所有关系再储存
- (void)changeWindow:(XGFEBoardBaseWindow *)window inWindows:(NSArray<XGFEBoardBaseWindow *> *)windows {
    NSString *name = window.nameOfStick;
    [_stickPairs removeObjectForKey:name];//移除自己吸附的关系
    for (NSString *key in _stickPairs) {//移除别人吸附自己的关系
        if ([_stickPairs[key] isEqualToString:name]) {
            [_stickPairs removeObjectForKey:key];
        }
    }
    [self storeWindows:windows];//储存
}

同时,3. 在窗口本身大小发生变化的时候,从方便的角度也要如上解除关系再储存,否则吸附边可能不对。4. 恢复的时机就很显然了,app初始化所有窗口之后立刻执行恢复即可。
关键词:oc , mac
logo