【问题标题】:Detect when a User takes a Screenshot检测用户何时截屏
【发布时间】:2010-12-23 08:01:51
【问题描述】:

我正在为我的应用寻找在用户使用Command-Shift-3Command-Shift-4 截屏时接收通知的方法。

这方面的一个例子是像 Droplr 和 Cloud App 这样的应用程序,它们会自动上传截取的屏幕截图。

我一直在四处寻找,发现它可能与达尔文通知有关,但我不确定从哪里开始。

【问题讨论】:

标签: objective-c cocoa macos notifications


【解决方案1】:

这在较早的 cmets 之一中已提及,但您可以使用 NSMetadataQuery 搜索 kMDItemIsScreenCapture = 1 所在的文件。这是添加到屏幕截图文件中的特殊属性。

我刚刚制作了一个小演示来展示如何做到这一点并将其发布在 github 上:

https://github.com/davedelong/Demos/blob/master/ScreenShot%20Detector

【讨论】:

  • 哈哈。哇,这比我做的要简单得多,非常感谢您这样做!
  • @Joshua 是的,实际上找到屏幕截图非常简单。至于 ui,绑定使其完全无代码。 :) 只需几个简单的值转换器即可与图像视图和路径控制一起使用。
  • 绑定更加惊人。我从没想过绑定可以如此强大,并为您节省这么多行代码!
  • @Joshua 谨慎使用它们。它们很难调试,而且由于它们是在 xib 中指定的,它们很容易断开连接并丢失。但他们很酷。 :)
  • @DaveDeLong 在启动时启动应用程序时,NSMetadataQueryDidUpdateNotification 在启动后一分钟内不会触发。知道为什么吗? (手动启动时完美运行)
【解决方案2】:

这就是我的做法,有点复杂,但我会尝试一步一步地告诉你:


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

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;
}

嗯,你有它。这就是我能够检测到用户何时截屏的方式,它可能有一些错误,但目前似乎工作正常。如果您想将所有代码合二为一,请查看 pastebin.com 上的链接:

标题 - http://pastebin.com/gBAbCBJB

实施 - http://pastebin.com/VjQ6P3zQ

【讨论】:

  • 保持 NSMetadataQuery / MDQuery 运行以观察“kMDItemIsScreenCapture == 1”的新匹配不是更容易吗?
  • 嗯……没想到,我去看看。谢谢!
  • 另外请记住,用户可以更改保存屏幕截图的位置。它并不总是桌面。
  • 是的,我计划使用此代码 pastebin.com/VQRgbjbc 来获取位置,但它似乎对我不起作用,因为 com.apple.screencapture 默认值不存在。
  • NSDictionary *xattrs = [attrs objectForKey:@"NSFileExtendedAttributes"];
【解决方案3】:

您必须注册一个对象才能在用户截屏时接收系统通知

所以:

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

不确定通知名称是什么,但它可能在外面。

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

[[NSNotificationCenter defaultCenter] removeObserver:self];

【讨论】:

  • 我已经知道该怎么做,但问题是知道要观察什么通知。据我所知,用户截屏时没有一个。
  • 我也找不到这样的通知。
猜你喜欢
  • 1970-01-01
  • 2019-12-02
  • 1970-01-01
  • 1970-01-01
  • 2012-11-09
  • 2015-06-14
  • 1970-01-01
  • 2018-08-29
相关资源
最近更新 更多