2

I have an application where, in one window, there is an NSImageView. The user should be able to drag and drop ANY FILE/FOLDER (not only images) into the image view, so I subclassed NSImageView class to add support for those types.

The reason why I chose an NSImageView instead of a normal view is because I also wanted to display an animation (say an arrow pointing downwards and going up and down) when the user hovers over with files ready to drop. My question is this: what would be the best way (most efficient, quickest, least CPU usage, etc) to do this?

In fact, I have already done it, but what made me ask this question is the fact that when I set the images to change at a rate below 0.02 sec it starts to lag. Here is how I did it:

In the NSImageView subclass:

  • have an ivar: NSTimer* animTimer;
  • override awakeFromNib, calling [super awakeFromNib] and loading the images into an array (about 45 images) using NSImage
  • whenever user enters with files, start animTimer with frequency = 0.025 (less and it lags), and a selector that sets the next image in the array (called drawNextImage)
  • whenever the user exits or ends the drag and drop, call [animTimer invalidate] to stop updating images

Here is how I set the image in the subclass:

- (void)drawNextImage
{
    currentImageIndex++; // ivar / kNumberDNDImages is a constant defined as 46
    if (currentImageIndex >= kNumberDNDImages) { currentImageIndex = 0;}
    [super setImage: [imagesArray objectAtIndex: currentImageIndex]]; // imagesArray is ivar
}

So, how would I do this quick enough? I'd like the frequency to be about 0.01 secs, but less than 0.025 lags, so that is what I have set for the moment. Oh, and my images are the correct size (+ or - one pixel or something) and they are in .png (I need the transparency - jpegs, for example, won't do it).

EDIT:

I have tried to follow NSResponder's suggestion, and have updated my method to this:

- (void)drawNextImage
{
    currentImageIndex++;
    if (currentImageIndex >= kNumberDNDImages) { currentImageIndex = 0;}
    NSRect smallImgRect;
    smallImgRect.origin = NSMakePoint(kSmallImageWidth * currentImageIndex, [self.bigDNDImage size].height); // Up left corner - ??
    smallImgRect.size = NSMakeSize(kSmallImageWidth, [self.bigDNDImage size].height);

    // Bottom left corner - ??
    NSPoint imgPoint = NSMakePoint(([self bounds].size.width - kSmallImageWidth) / 2, 0);

    [bigDNDImage drawAtPoint: imgPoint fromRect: smallImgRect operation: NSCompositeCopy fraction: 1];
}

I have also moved this method and the other drag and drop methods from the NSImageView subclass to an NSView subclass I already had. Everything is exactly the same, except for the superclass and this method. I also modified some constants.

In my early testing of this, I got some error/warning messages that didn't stop execution talking abou NSGraphicsContext or something. These have disappeared now, but just so you know. I have absolutely no idea why they were appearing and what they mean. If they ever appear again I'll worry about them, not now :)

EDIT 2:

This is what I'm doing now:

- (void)drawNextImage
{
    currentImageIndex++;
    if (currentImageIndex >= kNumberDNDImages) { currentImageIndex = 0;}
    [self drawCurrentImage];
}
- (void)drawCurrentImage
{
    NSRect smallImgRect;
    smallImgRect.origin = NSMakePoint(kSmallImageWidth * currentImageIndex, 0); // Bottom left, for sure
    smallImgRect.size = NSMakeSize(kSmallImageWidth, [self.bigDNDImage size].height);

    // Bottom left as well
    NSPoint imgPoint = NSMakePoint(([self bounds].size.width - kSmallImageWidth) / 2, 0);

    [bigDNDImage drawAtPoint: imgPoint fromRect: smallImgRect operation: NSCompositeCopy fraction: 1];
}

And the catch here is to call drawCurrentImage when drawRect is called (see, it actually was easier to solve than I thought).

Now, I must say I haven't tried this with the composite image, because I couldn't find a good and quick way to merge 40+ images the way I wanted (one next to the other). But for the ones ineterested, I modified this to do the same thing as my NSImageView subclass (reading 40+ images from an array and displaying them) and I found no speed bump: NSView is as laggy below 0.025 as NSImageView. Also I found some problems when using core animation (the image is drawn in weird places instead of the place I tell her to) and some warnings talking about NSGraphicsContext, which I don't know how to solve at all (I'm a complete noob when it comes to drawing and such with Objective-C's tools). So for the time being I'm using NSImageView, unless I find a way to merge all those images and try it with NSView.

Alex
  • 5,009
  • 3
  • 39
  • 73

2 Answers2

2

Core Animation would probably be quickest, since it'll do everything on the GPU. Create a layer for each image, setting each layer's contents to the CGImage you can make from each image, add them all as sublayers of a single top-level layer, host the top-level layer in a plain NSView, and then just toggle each image layer's hidden property in turn.

Peter Hosey
  • 95,783
  • 15
  • 211
  • 370
  • Could you provide an example of this? I lost you on the "add them all as sublayers of a single top-level layer" part. – Charlie Jun 04 '14 at 20:02
  • @Charlie: For each image, create a layer and set the layer's `contents` to that image as a CGImage, and add all of those layers as sublayers of one parent layer. Set that layer as the layer of an NSView. – Peter Hosey Jun 05 '14 at 04:14
  • Actually, I ended up getting it working... except not with `autoresizingMask`. I posted it to its own question, however. – Charlie Jun 07 '14 at 00:11
0

I'd probably draw all of the component images into one long image, and draw segments into a view using -drawAtPoint:fromRect:operation:fraction:. I'm sure you could make it faster than that by resorting to OpenGL, though.

NSResponder
  • 16,861
  • 7
  • 32
  • 46
  • The one thing I don't understand here is where the (0,0) points are. What is the default (0,0) point for NSImage: left-up, right-up, left-down or right-down? Same thing for the view's (0,0) point. Where is it? I need this in order to calculate the values for the drawAtPoint: and fromRect: I am assuming NSImage uses up-left and the view uses bottom-left, but it is not woring. I have updated my question to show what I am doing now. – Alex Jul 01 '11 at 12:23
  • @Alex Truppel: It's lower-left except when it's not (`isFlipped` is true). The one exception you don't set yourself is NSScreens' frames, which have the origin in the upper-left. – Peter Hosey Jul 02 '11 at 06:25
  • Another way to make this faster would be to make the composite image a CGLayer. These exist specifically to be drawn over and over again. – Peter Hosey Jul 02 '11 at 06:26
  • @peter I found the solution to my image not being displayed at all (which was my problem when I asked about the coordinates): I had forgotten to draw it on drawrect, and so I only drew it once and then the image disappeared. About CGLayer, I don't know, beacause I do draw an image multiple times, but the catch is that the image changes everytime. If I understood correctly the docs, that has no speed benefit. I might be wrong, though. – Alex Jul 03 '11 at 09:21
  • @Alex Truppel: Yeah, you need to do all view drawing in `drawRect:`, or in response to a message sent directly from there. As for CGLayer, I specifically said “the composite image”, referring to NSResponder's suggestion of compositing all of the images into a single image. – Peter Hosey Jul 03 '11 at 16:48
  • @Peter Hosey Yeah, you're completely right. I misunderstood that. So I have the option to draw the composite image myself, or using CGLayer. The only problem is that I can't find a way to merge 45 images (or am I just too lazy?) and so, at least for now, I will do this the easy, but slow, way and use NSImageView (the subclass I did). But please keep suggesting things, because I can always learn, and other people with the same question will benefit as well. – Alex Jul 03 '11 at 18:47