0

UPDATE: Relative to the questions and answers below, it seems I have a misunderstanding of the NSView class in relation to the custom classes I'm trying to draw and the wrapping NSScrollView. In the end, what I'm trying to figure out is how do I manage the dynamic drawing of custom data (not photos) in an NSView that has an area larger than what is viewable?

I am not looking for a handout, but I am a novice to Cocoa and I thought I was doing best-practice based on Apple's docs, but it seems I've gotten the fundamentals wrong. Apple's documentation is incredibly detailed, technical, centered entirely around working with photos, and thus useless to me. The related code examples provided by Apple (e.g. Sketch) get the document size from the printer paper sizes in their typically oblique fashion, and that's not what I need. I've scoured the web for tutorials, examples and the like, but I'm not finding much of anything (and I promise to write one when I get this sorted out).

I'm porting this code from REALbasic where I have this completely working, even with Undo commands, but the paradigms to do so are entirely different. This just isn't "clicking" for me. I appreciate the help given, I'm still missing something here, anything else folks have to offer is appreciated.

Thanks


I have a subclassed NSView where I'm creating a piano-roll MIDI interface. I am trying to resolve a few problems:

  • Drawing artifacts during and after scrolling
  • Lines not spanning across the visible area during and after scrolling
  • While scrolling and sometimes on mouseDown, the horizontal scroller jumps to the right 1 (one) pixel, but I don't have scrollToPoint implemented anywhere yet.

Symptoms that relate to the above:

  • Implementing adjustScroll makes everything worse.
  • mouseDown corrects all of the problems except sometimes the 1-pixel jump to the right.
  • If I uncomment the NSLog command the beginning of drawRect nothing draws.

Apple's documentation mentions pixel-accurate drawing, but (of course) offers up no examples on how this can be achieved. I've been using the floor() function to try to get consistent values, but once I start tacking on scrollToPoint or any other complexity, things go haywire.

Please see the linked image as an example. The screenshot, if you can believe it, actually cleans up what I see on screen. There are double lines almost everywhere at half opacity as well. The same is applied to any objects I draw as well.

Graphics Artifacts and inconsistencies in a subclassed NSView generated after scrolling http://www.oatmealandcoffee.com/external/NSViewArtifacts.png

Here is the code. I hate giving up so much publicly, but I've searched everywhere for clues, and if the Internet is any indication I'm the only person with this problem, and I really just want to get this sorted out and move forward. There is a lot, and there is more to come, but these is the core stuff I really need to get right, and, frankly, I am at a loss on how to correct it.

    - (void)drawRect:(NSRect)rect {
        //NSLog(@"OCEditorView:drawRect: START");

        [self setFrame:[[self EditorDocument] DocumentRect]];

        [[NSGraphicsContext currentContext] setShouldAntialias:NO];

        // CLEAR BACKGROUND

        [[[self EditorDocument] ColorWhiteKey] set];
        NSRectFill(rect);

        // BACKGROUND KEYS

        int firstRowLine = 0; //NSMinY(rect); //<- adding the function results in bad spacing on scrolling
        int currentRowLine = 0;
        int lastRowLine = NSMaxY(rect);

        //NSLog(@"lastRowLine:%d", lastRowLine);

        float currentZoomY = [self ZoomY];

        for (currentRowLine = firstRowLine; currentRowLine <= lastRowLine; currentRowLine += currentZoomY) {

            int currentTone = floor(currentRowLine / [self ZoomY]);
            BOOL isBlackKey = [[self MusicLib] IsBlackKey:currentTone];

            //NSLog(@"%d, tone:%d, black:%d", [self MusicLib], currentTone, isBlackKey);

            if (isBlackKey) {
                [[[self EditorDocument] ColorBlackKey] set];
            } else {
                [[NSColor whiteColor] set];
            }

            NSBezierPath *rowLine = [NSBezierPath bezierPath];

            NSPoint bottomLeftPoint = NSMakePoint(NSMinX(rect), currentRowLine);
            NSPoint bottomRightPoint = NSMakePoint(NSMaxX(rect), currentRowLine);
            NSPoint topRightPoint = NSMakePoint(NSMaxX(rect), currentRowLine + [self ZoomY]);
            NSPoint topLeftPoint = NSMakePoint(NSMinX(rect), currentRowLine + [self ZoomY]);

            [rowLine moveToPoint:bottomLeftPoint];
            [rowLine lineToPoint:bottomRightPoint];
            [rowLine lineToPoint:topRightPoint];
            [rowLine lineToPoint:topLeftPoint];

            [rowLine closePath];

            [rowLine fill];

            BOOL isOctave = [[self MusicLib] IsOctave:currentTone];
            if (isOctave) {
                [[[self EditorDocument] ColorXGrid] set];

                NSBezierPath *octaveLine = [NSBezierPath bezierPath];
                NSPoint leftPoint = NSMakePoint(NSMinX(rect), currentRowLine);
                NSPoint rightPoint = NSMakePoint(NSMaxX(rect), currentRowLine);
                [octaveLine moveToPoint:leftPoint];
                [octaveLine lineToPoint:rightPoint];
                [octaveLine stroke];
            }
        } 

        // BACKGROUND MEASURES

        //[[self EditorDocument].ColorYGrid setStroke];

        int firstColumnLine = 0;
        int currentColumnLine = 0;
        int lastColumnLine = NSMaxX(rect);

        int snapToValueInBeats = [[self EditorDocument] SnapToValue];
        int snapToValueInPixels = floor(snapToValueInBeats * [self ZoomX]);
        int measureUnitInBeats = floor([[self EditorDocument] TimeSignatureBeatsPerMeasure] * [[self EditorDocument] TimeSignatureBasicBeat]);
        int measureUnitInPixels = floor(measureUnitInBeats * [self ZoomX]);

        for (currentColumnLine = firstColumnLine; currentColumnLine <= lastColumnLine; currentColumnLine += snapToValueInPixels) {

            //int currentBeat = floor(currentColumnLine / [self ZoomX]);
            int isAMeasure = currentColumnLine % measureUnitInPixels;
            int isAtSnap = currentColumnLine % snapToValueInPixels;

            if ((isAMeasure == 0) || (isAtSnap == 0)) {

                if (isAtSnap == 0) { 
                    [[NSColor whiteColor] set];                 
                }

                if (isAMeasure == 0) { 
                    [[[self EditorDocument] ColorXGrid] set]; 
                }

                NSBezierPath *columnLine = [NSBezierPath bezierPath];

                NSPoint startPoint = NSMakePoint(currentColumnLine, NSMinY(rect));
                NSPoint endPoint = NSMakePoint(currentColumnLine, NSMaxY(rect));

                [columnLine moveToPoint:startPoint];
                [columnLine lineToPoint:endPoint];

                [columnLine setLineWidth:1.0];
                [columnLine stroke];

            } // isAMeasure or isAtSnap
         } // currentColumnLine

        // NOTES

        for (OCNoteObject *note in [[self EditorDocument] Notes]) {

            OCNoteObject *currentNote = note;

            NSRect noteBounds = [self GetRectFromNote:currentNote];
            //NSLog(@"noteBounds:%d", noteBounds);

            // set the color for the note fill
            // this will have to come from the parent Track

            NSMutableArray *trackColors = [self EditorDocument].TrackColors;

            if (note.Selected) {
                [[trackColors objectAtIndex:0] set];
            } else {
                [[trackColors objectAtIndex:1] set];
            }

            [NSBezierPath fillRect:noteBounds];

            // outline

            [[NSColor blackColor] set];
            [NSBezierPath strokeRect:noteBounds];

         } // for each note

        /*
        if (EditorController.startingUpApplication == YES) {
            [self setDefaultSettingForApplicationStartUp];
        }
         */
    //NSLog(@"OCEditorView:drawRect: END"); 
    }

- (void)mouseDown:(NSEvent *)theEvent {

    //NSLog(@"OCEditorObject:mouseDown: START");

    // This converts the click into coordinates
    MouseDownPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];

    // Calculate the beat and pitch clicked into...

    float startBeat = floor(MouseDownPoint.x / [self ZoomX]);
    float pitch = floor(MouseDownPoint.y / [self ZoomY]);
    float length = [[self EditorDocument] NewNoteLength];

    //NSLog(@"X:%f, Y:%f", MouseDownPoint.x, MouseDownPoint.y);
    //NSLog(@"beat:%f, pitch:%f", startBeat, pitch);

    LastDragPoint = MouseDownPoint; // save the point just in case.

    OCNoteObject *note = [self GetClickedNoteFromPoint:MouseDownPoint];

    if ([EditorController EditorMode] == AddObjectMode) {

        //NSLog(@"AddObjectMode)");

        float snapToX = [[self EditorDocument] SnapToValue];
        float snappedStartBeat = floor(startBeat / snapToX) * snapToX;

        //NSLog(@"%f = %f / %f * %f", snappedStartBeat, startBeat, snapToX, snapToX);

        OCNoteObject *newNote = [[self EditorDocument] CreateNote:snappedStartBeat Pitch:pitch Length:length];
        //NSLog(@"newNote:%d", newNote);

        [newNote Deselect];

    } else if ([EditorController EditorMode] == EditObjectMode) {

        //NSLog(@"EditObjectMode");

        // if nothing was clicked, then clear the selections
        // else if the shift key was pressed, add to the selection

        if (note == nil) {
            [self SelectNone];  
        } else {

            //NSLog(@"mouseDown note.pitch:%f, oldPitch:%f", note.Pitch, note.OldPitch);

            BOOL editingSelection = (([theEvent modifierFlags] & NSShiftKeyMask) ? YES : NO);
            if (editingSelection) {
                if (note.Selected) {
                    [self RemoveFromSelection:note];
                } else {
                    [self AddToSelection:note];
                }
            } else {
                if (note.Selected) {
                    // do nothing
                } else {
                    [self SelectNone];
                    [self AddToSelection:note];
                }
            }

            [self SetOldData];

        } // (note == nil)

    } else if ([EditorController EditorMode] == DeleteObjectMode) {

        if (note != nil) {
            [self RemoveFromSelection:note];
            [[self EditorDocument] DestroyNote:note];
        } // (note != nil)

    } // EditorMode

    [self setFrame:[[self EditorDocument] DocumentRect]];
    [self setNeedsDisplay:YES];
}

- (void)mouseDragged:(NSEvent *)theEvent {
    //NSLog(@"mouseDragged");

    NSPoint currentDragPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    // NSLog(@"currentDragPoint: %d", currentDragPoint)

    float snapToValueInBeats = [[self EditorDocument] SnapToValue];

    int deltaXinPixels = floor(currentDragPoint.x - MouseDownPoint.x);
    int deltaYinPixels = floor(currentDragPoint.y - MouseDownPoint.y);

    int deltaXinBeats = floor(deltaXinPixels / [self ZoomX]);
    int deltaY = floor(deltaYinPixels / [self ZoomY]);

    int deltaX = floor(deltaXinBeats / snapToValueInBeats) * snapToValueInBeats;

        for (OCNoteObject *note in [self Selection]) {
            [self MoveNote:note DeltaX:deltaX DeltaY:deltaY];       
        }

    LastDragPoint = currentDragPoint;

    [self autoscroll:theEvent];

    [self setNeedsDisplay:YES]; //artifacts are left if this is off.
}

- (void)mouseUp:(NSEvent *)theEvent {
    if ([EditorController EditorMode] == AddObjectMode) {

    } else if ([EditorController EditorMode] == EditObjectMode) {

    } else if ([EditorController EditorMode] == DeleteObjectMode) {

    }

    [self setNeedsDisplay:YES];
}

I could very well be missing something obvious, but I think I'm too close to the code to see the solution for what it is. Any help is greatly appreciated! Thanks!

Philip Regan
  • 5,005
  • 2
  • 25
  • 39
  • In Objective-C method names start with a lower case letter. See http://stackoverflow.com/questions/155964/what-are-best-practices-that-you-use-when-writing-objective-c-and-cocoa/158304#158304 – Nikolai Ruhe Oct 24 '09 at 17:16
  • In SO and markdown, code is indented with exactly **four** spaces. – Nikolai Ruhe Oct 24 '09 at 17:17
  • Sorry for sounding a little harsh; that was not intended. You are making people read a lot of code. It would definitely help if one could see your efforts to make it as readable as possible. That involves stripping irrelevant parts. – Nikolai Ruhe Oct 24 '09 at 18:34
  • I'm cool with harsh as long as the information is useful. I was afraid to strip for fear of removing something relevant. – Philip Regan Oct 24 '09 at 22:22

2 Answers2

6

I think you are misunderstanding the way drawRect: and its argument works:

The message drawRect: is sent by cocoa whenever your view or parts of it need to be redrawn. The CGRect argument is the bounding box of all updated areas for the current redraw. That means that you should not derive any positions of objects within your view from this rectangle. It is only passed to the method to allow for optimized drawing: if something is completely outside of this rectangle it does not need to be redrawn.

You should calculate all positions within your view from the views coordinate system: [self bounds]. This does not change each time drawRect: is performed and gives you an origin and size for the contents of the view.

There are a couple of other issues with your code (for instance, don't call setFrame: from within drawRect:) but I think you should first get the coordinates right and then look further into how to calculate pixel-aligned coordinates for your rectangles.

Nikolai Ruhe
  • 81,520
  • 17
  • 180
  • 200
  • +1 I think the use of the rect parameter is definitely causing a lot of confusion here. You're forcing things right at the boundary when they should actually be drawn off screen and clipped. – Rob Napier Oct 24 '09 at 18:04
  • Then, I guess I'm not understanding the relationship between the view and my model filled with custom classes. My model has an editing area larger than the viewable area. Objects can, and will, be everywhere and I need to support live scrolling. I called 'setFrame:' to the Model's Rect because it sorted out the scrollbars for me. I didn't see any other way of managing scrolling than to draw the complete set of data each time. (Response continued in next comment...) – Philip Regan Oct 24 '09 at 22:07
  • In my other IDE (REALbasic), I had *direct* access to the scrollbars and their properties, so it was a cinch to get the offset values and draw accordingly; I only drew what was viewable and within the context of the viewing area. If I wanted an object at pixel 0, then it simply got drawn there. So, how do I duplicate that here? I feel like I'm missing fundamental. Thanks for the responses! – Philip Regan Oct 24 '09 at 22:08
  • In Cocoa you draw everything in the view's `bounds`. The origin of this rect is usually at 0, 0, so you can draw everything like you used to do. The parts you need to draw (the visible region, or rather, the region that needs redisplay) can be determined from the argument you get passed in `drawRect:`. – Nikolai Ruhe Oct 25 '09 at 17:29
  • Philip Regan: Positioning your drawing within the visible area is the scroll view's job. Your view should not know or care whether it is in a scroll view, because the scroll view and clip view will reposition your drawing and clip to the visible area for you. All you need to do is draw, as if no scroll view exists. – Peter Hosey Oct 27 '09 at 10:37
0

This code of yours looks rather more elaborate than it needs to be. Check out NSCenterScanRect(), and NSRectFillListWithColors(). Also, it's rather wasteful to create and discard paths in -drawRect:.

NSResponder
  • 16,861
  • 7
  • 32
  • 46
  • Great! And the alternatives are what, exactly? – Philip Regan Oct 12 '09 at 10:53
  • So, I had some time to look into your answer, but I don't see how NSCenterScanRect nor NSRectFillListWithColors() is going to help me with the issues listed at the top of my (self-admittedly robust) question. – Philip Regan Oct 23 '09 at 13:55
  • I don't think you're going to see how anything is going to help you until you sit down in front of that documentation and really put some effort into learning what you need to do before you blindly try to do it. – Azeem.Butt Oct 26 '09 at 18:40
  • I'm not trying to "blindly" do it; I'm just new. I honestly thought I had this correct and I didn't. But, please don't make assumptions about my working methods. Thanks – Philip Regan Oct 28 '09 at 13:42