Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cocoa (Mac Os X) Printing Multiple Pages, Why is Preview Window Displaying 2 pages instead of 1?

I have a cocoa app I'm working on in which I created a customView that I want to send to the printer. In the subclassed NSView, I set some frame options as well, the code is below. I have 2 global variables to hold printing information declared outside main() function.

- (id)initWithFrame:(NSRect)frame
{
    extern NSPrintInfo *globalPrintInfo;
    extern NSPrintOperation *globalPrintOperation;

    //Modify the frame before it's sent to it's super method.  Also set the global variables to there default values.
    globalPrintOperation = [NSPrintOperation printOperationWithView:self];
    globalPrintInfo = [globalPrintOperation printInfo];//Get the print information from it.

    [globalPrintInfo setBottomMargin:0.0];
    [globalPrintInfo setLeftMargin:0.0];
    [globalPrintInfo setTopMargin:0.0];
    [globalPrintInfo setRightMargin:0.0];

    [globalPrintOperation setPrintInfo:globalPrintInfo];//save the printInfo changes.

    //modify the frame to reflect the correct height & width of the paper.
    frame.size.height = globalPrintInfo.paperSize.height-globalPrintInfo.topMargin-globalPrintInfo.bottomMargin;
    frame.size.width = globalPrintInfo.paperSize.width-globalPrintInfo.leftMargin-globalPrintInfo.rightMargin;
    frame.origin.x=0;
    frame.origin.y=0;

    NSLog(@"Printer Name=%@, Printer Type=%@",globalPrintInfo.printer.name,globalPrintInfo.printer.type);

    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.

    }

    return self;
}

For the subclassed NSView so that I can see it's boundaries I added the following code below to it's drawRect method.

- (void)drawRect:(NSRect)dirtyRect
{
    if ( [NSGraphicsContext currentContextDrawingToScreen] ) {
        NSLog(@"Drawing To Screen");
    } else {
        NSLog(@"Drawing To Printer");
    }

    // Draw common elements here

    CGContextRef myContext = [[NSGraphicsContext currentContext] graphicsPort];

    //Set color of drawing to green, and fill the rectangle green, so we can see it's boundaries.
    [[NSColor greenColor] setFill];
    NSRectFill(dirtyRect);

    CGContextSelectFont(myContext, "Helvetica-Bold", 18, kCGEncodingMacRoman);
    CGContextSetCharacterSpacing(myContext, 10);
    CGContextSetTextDrawingMode(myContext, kCGTextFillStroke);

    CGContextSetRGBFillColor(myContext, 0, 0, 0, 1);//black
    CGContextSetRGBStrokeColor (myContext, 0, 0, 1, 1);//blue stroke
    CGContextShowTextAtPoint(myContext, 40, 0, "Here is some text!", 18);

}

When I go to run the print operation using the global variable, like so...

- (IBAction)print:(id)sender {
    NSLog(@"Testing Print");

    extern NSPrintOperation *globalPrintOperation;

    [globalPrintOperation runOperation];
}

I get the print window to appear and I see my 'green background' for my view, but for some reason it's split onto 2 pages. I'm not sure exactly what is going on as I set the width and height of the frame to the pagesize.width & height, any help is appreciated. Some images of what I see are below.

My guess is the pagesize width and height are in different units than the pixels unit type used for defining the frame of the view.

My ultimate goal is to make a program where the user selects what they want and it prints particular pages based on the options they selected, but first I got to figure out how to even get what 'content' I expect onto '1' page instead of '2'. I could manually figure out the width and height by experimentation but that would not be very dynamic for different paper sizes I'm assuming.

Thanks in advance.

Image1

Image2

EDIT ***

I just edited my code to the following below for the subclassed NSVIEW

//METHOD OVERIDES
- (id)initWithFrame:(NSRect)frame
{
    extern NSPrintInfo *globalPrintInfo;
    extern NSPrintOperation *globalPrintOperation;

    //Modify the frame before it's sent to it's super method.  Also set the global variables to there default values.
    globalPrintOperation = [NSPrintOperation printOperationWithView:self];//use whatever is currently there as the default print operation.
    globalPrintInfo = [globalPrintOperation printInfo];//Get the print information from it.

    [globalPrintInfo setBottomMargin:0.0];
    [globalPrintInfo setLeftMargin:0.0];
    [globalPrintInfo setTopMargin:0.0];
    [globalPrintInfo setRightMargin:0.0];

    [globalPrintOperation setPrintInfo:globalPrintInfo];//save the printInfo changes.

    //modify the frame to reflect the correct height & width of the paper.
    frame.size.height = (globalPrintInfo.paperSize.height-globalPrintInfo.topMargin-globalPrintInfo.bottomMargin);
    frame.size.width = globalPrintInfo.paperSize.width-globalPrintInfo.leftMargin-globalPrintInfo.rightMargin;
    frame.origin.x=0;
    frame.origin.y=0;

    NSLog(@"Printer Name=%@, Printer Type=%@",globalPrintInfo.printer.name,globalPrintInfo.printer.type);

    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.

    }

    return self;
}

- (void)drawRect:(NSRect)dirtyRect
{
    if ( [NSGraphicsContext currentContextDrawingToScreen] ) {
        NSLog(@"Drawing To Screen");
    } else {
        NSLog(@"Drawing To Printer");
    }

    // Draw common elements here

    CGContextRef myContext = [[NSGraphicsContext currentContext] graphicsPort];

    //Set color of drawing to green, and fill the rectangle green, so we can see it's boundaries.
    [[NSColor greenColor] setFill];
    NSRectFill(dirtyRect);

    CGContextSelectFont(myContext, "Helvetica-Bold", 18, kCGEncodingMacRoman);
    CGContextSetCharacterSpacing(myContext, 10);
    CGContextSetTextDrawingMode(myContext, kCGTextFillStroke);

    CGContextSetRGBFillColor(myContext, 0, 0, 0, 1);//black
    CGContextSetRGBStrokeColor (myContext, 0, 0, 1, 1);//blue stroke
    CGContextShowTextAtPoint(myContext, 40, 0, "Here is some text!", 18);

}


- (BOOL)knowsPageRange:(NSRangePointer)range {
    NSRect bounds = [self bounds];
    float printHeight = [self calculatePrintHeight];

    range->location = 1;
    range->length = NSHeight(bounds) / printHeight + 1;

    NSLog(@"Calculated Page Range");
    return YES;
}

// Return the drawing rectangle for a particular page number
- (NSRect)rectForPage:(int)page {
    NSRect bounds = [self bounds];
    float pageHeight = [self calculatePrintHeight];
    NSLog(@"Created Rect For View");
    return NSMakeRect( NSMinX(bounds), NSMaxY(bounds) - page * pageHeight,
                      NSWidth(bounds), pageHeight );
}

//CUSTOM METHODS

// Calculate the vertical size of the view that fits on a single page
- (float)calculatePrintHeight {

    extern NSPrintInfo *globalPrintInfo;
    extern NSPrintOperation *globalPrintOperation;

    // Obtain the print info object for the current operation

    // Calculate the page height in points
    NSSize paperSize = [globalPrintInfo paperSize];
    float pageHeight = paperSize.height - [globalPrintInfo topMargin] - [globalPrintInfo bottomMargin];

    // Convert height to the scaled view
    float scale = [[[globalPrintInfo dictionary] objectForKey:NSPrintScalingFactor]
                   floatValue];

    NSLog(@"Calculated Print Height:%f",(pageHeight/scale));
    return (pageHeight / scale);
}

@end

I was able to get what I want now, accept when I go to print preview it still thinks there is a second page for some reason? not sure why about that now. I'll upload what I see...

Notice how it says 1 of 2? 2nd page is just blank white though.

enter image description here

like image 217
Joseph Astrahan Avatar asked Oct 06 '22 13:10

Joseph Astrahan


1 Answers

So I improved my printing class a bit to be more flexible for many pages and wanted to share the code. I still have that annoying white border at the bottom that I can't quite figure out but when I go to print it doesn't appear to be there? So I will need some help figuring that out, but otherwise, I designed a class that you simply send it an array of views and it will print the views in the order you receive them.

To do this I created 2 classes, PSPrint & PSPrintView. Both are subclasses of NSView

Here is the code for PSPrint.h & PSPrint.m

#import <Foundation/Foundation.h>
#import "PSPrintView.h"

@interface PSPrint : NSView
@property NSMutableArray *printViews;
@property (strong) NSPrintOperation *printOperation;
- (void)printTheViews;
@end

#import "PSPrint.h"
#import "PSPrintView.h"

@implementation PSPrint

- (id)initWithFrame:(NSRect)frame
{
    NSLog(@"Initializing Main PSPrintView");
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.
        _printViews = [[NSMutableArray alloc]initWithCapacity:1];//start it with capacity of 1
    }

    return self;
}

- (void)drawRect:(NSRect)dirtyRect{

}

- (BOOL)knowsPageRange:(NSRangePointer)range {
    NSRect bounds = [self bounds];
    float printHeight = [self calculatePrintHeight];

    range->location = 1;
    range->length = NSHeight(bounds) / printHeight + 0;

    NSLog(@"Calculated Page Range");
    return YES;
}

- (void)printTheViews{
    NSLog(@"Starting printTheViews Function of PSPrint");

    NSPrintInfo *sharedPrintInfo = [NSPrintInfo sharedPrintInfo];

    NSUInteger numOfViews = _printViews.count;

     NSLog(@"Creating %ld SubViews",numOfViews);

    NSUInteger totalHeight = 0;//if not initialized to 0 weird problems occur after '3' clicks to print, TODO: Find out why? Maybe because address space in memory not guaranteed to be 0 again?
    NSUInteger heightOfView = 0;
    PSPrintView *tempView;

    for (NSUInteger i=0; i<numOfViews; i++) {
        tempView = [_printViews objectAtIndex:i];
        heightOfView = tempView.frame.size.height;
        totalHeight = totalHeight + heightOfView;
    }

    //Change the frame size to reflect the amount of pages.
    NSSize newsize;
    newsize.width = sharedPrintInfo.paperSize.width-sharedPrintInfo.leftMargin-sharedPrintInfo.rightMargin;
    newsize.height = totalHeight;
    [self setFrameSize:newsize];

    NSLog(@"Total Height Of Main Print View Is %f",_frame.size.height);

    NSInteger incrementor=-1;//default the incrementor for the loop below.  This controls what page a 'view' will appear on.

    //Add the views in reverse, because the Y position is bottom not top.  So Page 3 will have y coordinate of 0.  Doing this so order views is placed in array reflects what is printed.
    for (NSInteger i=numOfViews-1; i>=0; i--) {
        incrementor++;
        tempView = [_printViews objectAtIndex:i];//starts with the last item added to the array, in this case rectangles, and then does circle and square.
        heightOfView = tempView.frame.size.height;

        NSPoint origin;
        origin.x = 0;
        origin.y = heightOfView*incrementor;//So for the rectangle it's placed at position '0', or the very last page.

        [tempView setFrameOrigin:origin];

        [self addSubview:tempView];
    }


    _printOperation = [NSPrintOperation printOperationWithView:self printInfo:sharedPrintInfo];
    [_printOperation runOperation];
}

// Return the drawing rectangle for a particular page number
- (NSRect)rectForPage:(int)page {
    NSRect bounds = [self bounds];
    float pageHeight = [self calculatePrintHeight];
    NSLog(@"Created Rect For View");
    return NSMakeRect( NSMinX(bounds), NSMaxY(bounds) - page * pageHeight,
                      NSWidth(bounds), pageHeight );
}

// Calculate the vertical size of the view that fits on a single page
- (float)calculatePrintHeight {

    NSPrintInfo *sharedPrintInfo = [NSPrintInfo sharedPrintInfo];

    // Obtain the print info object for the current operation

    // Calculate the page height in points
    NSSize paperSize = [sharedPrintInfo paperSize];
    float pageHeight = paperSize.height - [sharedPrintInfo topMargin] - [sharedPrintInfo bottomMargin];

    // Convert height to the scaled view
    float scale = [[[sharedPrintInfo dictionary] objectForKey:NSPrintScalingFactor]
                   floatValue];

    NSLog(@"Calculated Print Height:%f",(pageHeight/scale));
    return (pageHeight / scale);
}

@end

The code for PSPrintView.h & PSPrintView.m is below

#import <Cocoa/Cocoa.h>

@interface PSPrintView : NSView
enum optionsForView{drawRectangle,drawCircle,drawSquare};
@property enum optionsForView myOptions;
- (void)drawSquare;
- (void)drawCircle;
- (void)drawRectangle;
@end


#import "PSPrintView.h"

@implementation PSPrintView

- (id)initWithFrame:(NSRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.
    }

    return self;
}

- (void)drawRect:(NSRect)dirtyRect
{
    NSLog(@"Drawing Green View Boundaries");
    NSPrintInfo *sharedPrintInfo = [NSPrintInfo sharedPrintInfo];

    //So we know the boundaries for the page. Remove in actual application.
    [[NSColor greenColor] setFill];
    NSRectFill(dirtyRect);

    NSString *drawType;
    const char *cString;

    if(sharedPrintInfo.orientation == NSPortraitOrientation){
        drawType = @"Portrait";
    }else{
        drawType = @"Landscape";
    }

    cString = [drawType cStringUsingEncoding:NSASCIIStringEncoding];

    //Draw the print mode for reference.
    CGContextRef myContext = [[NSGraphicsContext currentContext] graphicsPort];
    CGContextSelectFont(myContext, "Helvetica-Bold", 30, kCGEncodingMacRoman);
    CGContextSetCharacterSpacing(myContext, 10);
    CGContextSetTextDrawingMode(myContext, kCGTextFillStroke);
    CGContextSetRGBFillColor(myContext, 0, 0, 0, 1);//black
    CGContextSetRGBStrokeColor (myContext, 1, 1, 1, 1);//white stroke
    CGContextShowTextAtPoint(myContext, 0, 0, cString, drawType.length);

    //Control what type of page is drawn.
    switch (_myOptions){
        case drawSquare:
            [self drawSquare];
            break;
        case drawCircle:
            [self drawCircle];
            break;
        case drawRectangle:
            [self drawRectangle];
            break;
    }

}

- (void)drawSquare{
    NSLog(@"Drawing Square");

    //Because this function can only be called from drawRect we are guaranteed with the function below to be in the correct graphics context
    CGContextRef myContext = [[NSGraphicsContext currentContext] graphicsPort];

    NSRect redSquare = CGRectMake (0, 0, 200, 200 );
    redSquare.origin.y = self.bounds.size.height-redSquare.size.height;//move it to the top of the page.

    CGContextSetRGBFillColor (myContext, 1, 0, 0, 1);//set to red color
    CGContextFillRect (myContext,redSquare);//Y coordinate set to height to put it in upper left, 200 is total height of the view.

}

-(void)drawCircle{
    NSLog(@"Drawing Circle");

    //Because this function can only be called from drawRect we are guaranteed with the function below to be in the correct graphics context
    CGContextRef myContext = [[NSGraphicsContext currentContext] graphicsPort];

    CGContextSetRGBFillColor (myContext, 1, 0, 1, 1);//set to red color
    NSRect ovalFrame = CGRectMake (0, 0, 200, 200 );

    ovalFrame.origin.x=0;//from within the view it's self.
    ovalFrame.origin.y=0;//at the top of the page.

    CGContextFillEllipseInRect(myContext,ovalFrame);
}

-(void)drawRectangle{
    NSLog(@"Drawing Rectangle");

    //Because this function can only be called from drawRect we are guaranteed with the function below to be in the correct graphics context
    CGContextRef myContext = [[NSGraphicsContext currentContext] graphicsPort];

    NSRect redRectangle = CGRectMake (0, 0, 300, 100 );
    redRectangle.origin.y = self.bounds.size.height-redRectangle.size.height-(self.bounds.size.height/2);//move it to the top of the page.

    CGContextSetRGBFillColor (myContext, 1, 1, 0, 1);//set to red color
    CGContextFillRect (myContext,redRectangle);//Y coordinate set to height to put it in upper left, 200 is total height of the view.
}

@end

An example using these classes is below in my AppController. I simply created a push button and had 3 checkboxes on the screen for a rectangle, circle and square.

Here is appController.h and appController.m

#import <Foundation/Foundation.h>
#import "PSPrint.h"
#import "PSPrintView.h"

@interface AppController : NSObject
- (IBAction)printResultsButton:(id)sender;
@property (weak) IBOutlet NSButton *squareCheckBox;
@property (weak) IBOutlet NSButton *circleCheckBox;
@property (weak) IBOutlet NSButton *rectangleCheckBox;
@property (strong) PSPrint *PSPrintObject;
- (IBAction)pageSetup:(id)sender;
@end

#import "AppController.h"
#import "PSPrint.h"
#import "PSPrintView.h"

@implementation AppController

- (IBAction)printResultsButton:(id)sender {
    NSLog(@"Print Button Pressed");

    //First get the shared print info object so we know page sizes.  The shared print info object acts like a global variable.
    NSPrintInfo *sharedPrintInfo = [NSPrintInfo sharedPrintInfo];

    //initialize it's base values.
    sharedPrintInfo.leftMargin = 0;
    sharedPrintInfo.rightMargin = 0;
    sharedPrintInfo.topMargin = 0;
    sharedPrintInfo.bottomMargin = 0;

    PSPrintView *printPageView;
    NSRect frame;
    frame.size.height = sharedPrintInfo.paperSize.height-sharedPrintInfo.topMargin-sharedPrintInfo.bottomMargin;
    frame.size.width = sharedPrintInfo.paperSize.width-sharedPrintInfo.leftMargin-sharedPrintInfo.rightMargin;

    //Initiate the printObject without a frame, it's frame will be decided later.
    _PSPrintObject = [[PSPrint alloc]init];

    //[_PSPrintObject.printViews initWithCapacity:1];//start it off with a capacity of '1'
    if(_squareCheckBox.state == NSOnState){

        //Allocate a new instance of NSView into the variable printPageView
        printPageView =[[PSPrintView alloc] initWithFrame:frame];

        //Set the option for the printView for what it should draw.
        printPageView.myOptions=drawSquare;

        //Finally append the view to the PSPrint Object.
        [_PSPrintObject.printViews addObject:printPageView];

        NSLog(@"Added Square Print View To Mutable Array");
    }

    if(_circleCheckBox.state == NSOnState){
        //Allocate a new instance of NSView into the variable printPageView
        printPageView =[[PSPrintView alloc] initWithFrame:frame];

        //Set the option for the printView for what it should draw.
        printPageView.myOptions=drawCircle;

        //Finally append the view to the PSPrint Object.
        [_PSPrintObject.printViews addObject:printPageView];

        NSLog(@"Added Circle Print View To Mutable Array");
    }

    if(_rectangleCheckBox.state == NSOnState){
        //Allocate a new instance of NSView into the variable printPageView
        printPageView =[[PSPrintView alloc] initWithFrame:frame];

        //Set the option for the printView for what it should draw.
        printPageView.myOptions=drawRectangle;

        //Finally append the view to the PSPrint Object.
        [_PSPrintObject.printViews addObject:printPageView];

        NSLog(@"Added Rectangle Print View To Mutable Array");
    }

    NSLog(@"Attempting to print all views...");
    [_PSPrintObject printTheViews];//print all the views, each view being a 'page'.

}

- (IBAction)pageSetup:(id)sender {
    NSPageLayout *pageLayout = [NSPageLayout pageLayout];

    [pageLayout runModal];//runs the model for the page layout UI.  It saves the global copy of printInfo in printOperation, which can be used to make decisions

}
@end

I hope this helps some of you guys in your struggles with printing, took me a while to do this, but these classes make printing a lot easier.

like image 152
Joseph Astrahan Avatar answered Oct 10 '22 04:10

Joseph Astrahan