检测用户何时截屏

18

我正在寻找一种方法,让我的应用程序在用户使用 Command-Shift-3Command-Shift-4 截屏时接收通知。

像 Droplr 和 Cloud App 这样的应用程序就是一个例子,它们可以自动上传截屏。

我已经搜索了一些资料,发现这可能与 Darwin 通知有关,但我不确定从何处开始。


看看这个类似的问题:https://dev59.com/jk3Sa4cB1Zd3GeqPxrtv,它似乎有一个好的答案。 - Nick Moore
那是一个很好的答案,我会去看一下。谢谢。 - Joshua
刚刚发布了我的答案,链接在这里:https://dev59.com/RW855IYBdhLWcg3wIAoa#4519189。 - Joshua
3个回答

19

哈哈。哇,这比我做的简单多了,非常感谢你做这个! - Joshua
@Joshua 是的,实际上找到截图非常简单。至于UI,绑定使其完全无需编写代码。 :) 只需要几个简单的值转换器来处理图像视图和路径控件。 - Dave DeLong
这些绑定甚至更令人惊讶。我从未想过绑定可以如此强大,并且可以为您节省如此多的代码行! - Joshua
@DaveDeLong,感谢您与社区分享代码。使用您的示例项目,我正在尝试仅在创建截图图像时“仅”获得通知。目前,该代码还会检测是否删除了任何此类文件并发送通知,这有点破坏了我在我的Cocoa应用程序中想要的流程。有什么想法吗? - Shumais Ul Haq
我解决了 :).. 只是添加了以下通知 :[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(queryUpdated:) name:NSMetadataQueryDidUpdateNotification object:query];。然后在通知方法中做了这个:NSMetadataItem *item = [[notification.userInfo objectForKey:(NSString *)kMDQueryUpdateAddedItems] lastObject]; 如果有项目,那么: NSString *screenShotPath = [item valueForAttribute:NSMetadataItemPathKey]; NSLog(screenShotPath); [self uploadScreenshotAtPath:screenShotPath]; - Shumais Ul Haq
显示剩余5条评论

4

这是我完成它的方法,有点复杂,但我会尝试一步一步地带你完成:


在开始之前,在您的头文件中声明以下变量和方法:

BOOL shouldObserveDesktop;
NSDictionary *knownScreenshotsOnDesktop;
NSString *screenshotLocation;
NSString *screenshotFilenameSuffix;

- (void)startObservingDesktop;
- (void)stopObservingDesktop;
- (NSDictionary *)screenshotsOnDesktop;
- (NSDictionary *)screenshotsAtPath:(NSString *)dirpath modifiedAfterDate:(NSDate *)lmod;
- (void)checkForScreenshotsAtPath:(NSString *)dirpath;
- (NSDictionary *)findUnprocessedScreenshotsOnDesktop;

现在在您的实现文件中,首先添加以下代码:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    screenshotLocation = [[NSString stringWithString:@"~/Desktop"] retain];
    screenshotFilenameSuffix = [[NSString stringWithString:@".png"] retain];
    knownScreenshotsOnDesktop = [[self screenshotsOnDesktop] retain];
    [self startObservingDesktop];
}

这将为调用所有方法时设置变量。接下来添加:

- (void)onDirectoryNotification:(NSNotification *)n {
    id obj = [n object];
    if (obj && [obj isKindOfClass:[NSString class]]) {
        [self checkForScreenshotsAtPath:screenshotLocation];
    }
}

- (void)startObservingDesktop {
    if (shouldObserveDesktop)
        return;
    NSDistributedNotificationCenter *dnc = [NSDistributedNotificationCenter defaultCenter];
    [dnc addObserver:self selector:@selector(onDirectoryNotification:) name:@"com.apple.carbon.core.DirectoryNotification" object:nil suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
    shouldObserveDesktop = YES;
}

- (void)stopObservingDesktop {
    if (!shouldObserveDesktop)
        return;
    NSDistributedNotificationCenter *dnc = [NSDistributedNotificationCenter defaultCenter];
    [dnc removeObserver:self name:@"com.apple.carbon.core.DirectoryNotification" object:nil];
    shouldObserveDesktop = NO;
}

在这里,我们观察将在截屏时调用的通知,并传递要调用的方法(在本例中为onDirectoryNotification:)。还有一种方法可以停止观察桌面/通知。通知调用checkForScreenshotsAtPath:,它将检查桌面上的截图。以下是该方法及其调用的其他方法的代码:

-(void)checkForScreenshotsAtPath:(NSString *)dirpath {        
    NSDictionary *files;
    NSArray *paths;

    // find new screenshots
    if (!(files = [self findUnprocessedScreenshotsOnDesktop]))
        return;

    // sort on key (path)
    paths = [files keysSortedByValueUsingComparator:^(id a, id b) { return [b compare:a]; }];

    // process each file
    for (NSString *path in paths) {
        // Process the file at the path
    }
}

-(NSDictionary *)findUnprocessedScreenshotsOnDesktop {
    NSDictionary *currentFiles;
    NSMutableDictionary *files;
    NSMutableSet *newFilenames;

    currentFiles = [self screenshotsOnDesktop];
    files = nil;

    if ([currentFiles count]) {
        newFilenames = [NSMutableSet setWithArray:[currentFiles allKeys]];
        // filter: remove allready processed screenshots
        [newFilenames minusSet:[NSSet setWithArray:[knownScreenshotsOnDesktop allKeys]]];
        if ([newFilenames count]) {
            files = [NSMutableDictionary dictionaryWithCapacity:1];
            for (NSString *path in newFilenames) {
                [files setObject:[currentFiles objectForKey:path] forKey:path];
            }
        }
    }

    knownScreenshotsOnDesktop = currentFiles;
    return files;
}

-(NSDictionary *)screenshotsOnDesktop {
    NSDate *lmod = [NSDate dateWithTimeIntervalSinceNow:-5]; // max 5 sec old
    return [self screenshotsAtPath:screenshotLocation modifiedAfterDate:lmod];
}

以下是通知调用的前3种方法,最终的方法是screenshotsAtPath:modifiedAfterDate:,需要注意的是这个方法非常长,因为它必须确认文件确实是屏幕截图:

-(NSDictionary *)screenshotsAtPath:(NSString *)dirpath modifiedAfterDate:(NSDate *)lmod {
    NSFileManager *fm = [NSFileManager defaultManager];
    NSArray *direntries;
    NSMutableDictionary *files = [NSMutableDictionary dictionary];
    NSString *path;
    NSDate *mod;
    NSError *error;
    NSDictionary *attrs;

    dirpath = [dirpath stringByExpandingTildeInPath];

    direntries = [fm contentsOfDirectoryAtPath:dirpath error:&error];
    if (!direntries) {
        return nil;
    }

    for (NSString *fn in direntries) {

        // always skip dotfiles
        if ([fn hasPrefix:@"."]) {
            //[log debug:@"%s skipping: filename begins with a dot", _cmd];
            continue;
        }

        // skip any file not ending in screenshotFilenameSuffix (".png" by default)
        if (([fn length] < 10) ||
            // ".png" suffix is expected
            (![fn compare:screenshotFilenameSuffix options:NSCaseInsensitiveSearch range:NSMakeRange([fn length]-5, 4)] != NSOrderedSame)
            )
        {
            continue;
        }

        // build path
        path = [dirpath stringByAppendingPathComponent:fn];

        // Skip any file which name does not contain a space.
        // You want to avoid matching the filename against
        // all possible screenshot file name schemas (must be hundreds), we make the
        // assumption that all language formats have this in common: it contains at least one space.
        if ([fn rangeOfString:@" "].location == NSNotFound) {
            continue;
        }

        // query file attributes (rich stat)
        attrs = [fm attributesOfItemAtPath:path error:&error];
        if (!attrs) {
            continue;
        }

        // must be a regular file
        if ([attrs objectForKey:NSFileType] != NSFileTypeRegular) {
            continue;
        }

        // check last modified date
        mod = [attrs objectForKey:NSFileModificationDate];
        if (lmod && (!mod || [mod compare:lmod] == NSOrderedAscending)) {
            // file is too old
            continue;
        }

        // find key for NSFileExtendedAttributes
        NSString *xattrsKey = nil;
        for (NSString *k in [attrs keyEnumerator]) {
            if ([k isEqualToString:@"NSFileExtendedAttributes"]) {
                xattrsKey = k;
                break;
            }
        }
        if (!xattrsKey) {
            // no xattrs
            continue;
        }
        NSDictionary *xattrs = [attrs objectForKey:xattrsKey];
        if (!xattrs || ![xattrs objectForKey:@"com.apple.metadata:kMDItemIsScreenCapture"]) {
            continue;
        }

        // ok, let's use this file
        [files setObject:mod forKey:path];
    }

    return files;
}

好的,这就是我如何检测用户截屏的方法,可能有一些bug,但目前看起来运行良好。如果您想要全部代码,可以在pastebin.com上找到以下链接:

Header - http://pastebin.com/gBAbCBJB

Implementation - http://pastebin.com/VjQ6P3zQ


1
只需保持一个NSMetadataQuery / MDQuery运行以监视“kMDItemIsScreenCapture == 1”的新匹配,这样不是更容易吗? - Pierre Bernard
嗯...从没想过,我会看一下。谢谢! - Joshua
请注意,用户可以更改屏幕截图保存的位置。它不一定是桌面。 - Nathan Kinsinger
是的,我打算使用这段代码 http://pastebin.com/VQRgbjbc 来获取位置,但是似乎对我来说并没有起作用,因为com.apple.screencapture默认值不存在。 - Joshua
NSDictionary *xattrs = [attrs objectForKey:@"NSFileExtendedAttributes"]; - user102008
关于@Pierre的评论,我制作了一个小演示应用程序来运行NSMetadataQuery搜索kMDItemIsScreenCapture = 1文件:https://github.com/davedelong/Demos/tree/master/ScreenShot%20Detector - Dave DeLong

-4

当用户截屏时,您需要注册一个对象来接收系统通知。

因此:

[[NSNotificationCenter defaultCenter] addObserver: theObjectToRecieveTheNotification selector:@selector(theMethodToPerformWhenNotificationIsRecieved) name:@"theNameOftheScreenCapturedNotification" object: optionallyAnObjectOrArgumentThatIsPassedToTheMethodToBecalled];

不确定通知名称是什么,但可能已经存在。

在dealloc中不要忘记注销自己:

[[NSNotificationCenter defaultCenter] removeObserver:self];


3
我已经知道如何做了,但问题是要知道哪个通知需要观察。据我所知,没有一个关于用户截屏的通知。 - Joshua
我也找不到这样的通知。 - Nick Moore

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接