Retired Document
Important: This sample code may not represent best practices for current development. The project may use deprecated symbols and illustrate technologies and techniques that are no longer recommended.
ScrollAndZoom.c
/* |
* File: ScrollAndZoom.c of ScrollAndZoom |
* |
* Contains: An illustration of the use of the Context Transformation Matrix (CTM) with HIViews. |
* |
* Version: 1.0 |
* |
* Created: October 7th, 2004 |
* |
* Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Computer, Inc. |
* ("Apple") in consideration of your agreement to the following terms, and your |
* use, installation, modification or redistribution of this Apple software |
* constitutes acceptance of these terms. If you do not agree with these terms, |
* please do not use, install, modify or redistribute this Apple software. |
* |
* In consideration of your agreement to abide by the following terms, and subject |
* to these terms, Apple grants you a personal, non-exclusive license, under AppleÕs |
* copyrights in this original Apple software (the "Apple Software"), to use, |
* reproduce, modify and redistribute the Apple Software, with or without |
* modifications, in source and/or binary forms; provided that if you redistribute |
* the Apple Software in its entirety and without modifications, you must retain |
* this notice and the following text and disclaimers in all such redistributions of |
* the Apple Software. Neither the name, trademarks, service marks or logos of |
* Apple Computer, Inc. may be used to endorse or promote products derived from the |
* Apple Software without specific prior written permission from Apple. Except as |
* expressly stated in this notice, no other rights or licenses, express or implied, |
* are granted by Apple herein, including but not limited to any patent rights that |
* may be infringed by your derivative works or by other works in which the Apple |
* Software may be incorporated. |
* |
* The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO |
* WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED |
* WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
* PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN |
* COMBINATION WITH YOUR PRODUCTS. |
* |
* IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR |
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE |
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
* ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION |
* OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT |
* (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN |
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
* |
* Copyright: Copyright © 2004 Apple Computer, Inc, All Rights Reserved |
*/ |
//**************************************************** |
#pragma mark * compilation directives * |
//**************************************************** |
#pragma mark - |
#pragma mark * includes & imports * |
#include "ScrollAndZoom.h" |
//**************************************************** |
#pragma mark - |
#pragma mark * typedef's, struct's, enums, defines, etc. * |
//**************************************************** |
#pragma mark - |
#pragma mark * local (static) function prototypes * |
//**************************************************** |
#pragma mark - |
#pragma mark * exported globals * |
//**************************************************** |
#pragma mark - |
#pragma mark * local (static) globals * |
//**************************************************** |
#pragma mark - |
#pragma mark * exported function implementations * |
/***************************************************** |
* |
* myLiveSliderAction(theControl, partCode) |
* |
* Purpose: Necessary action proc to have a "live" slider, even if it does nothing |
* |
* Inputs: theControl - the slider control |
* partCode - the part being actionned |
* |
* Returns: none |
*/ |
void pascal myLiveSliderAction(ControlRef theControl, ControlPartCode partCode) |
{ |
} |
/***************************************************** |
* |
* SanityCheck(where, myData) |
* |
* Purpose: Makes sure that we always scroll in a position such that we don't display out-of-bounds content |
* |
* Inputs: where - the point where we are being asked to scroll to |
* myData - all the data necessary to check the bounds, the origin, and the scale factor |
* |
* Returns: the point where it is safe to scroll to |
*/ |
HIPoint SanityCheck(HIPoint where, ScrollAndZoomData* myData) |
{ |
HIRect bounds; |
HIViewGetBounds(myData->view, &bounds); |
HISize imageSize = myData->imageSize; |
bounds.size.height /= myData->zoomFactor; |
bounds.size.width /= myData->zoomFactor; |
if (where.y + bounds.size.height > imageSize.height) |
where.y = imageSize.height - bounds.size.height; |
if (where.y < 0) where.y = 0; |
if (where.x + bounds.size.width > imageSize.width) |
where.x = imageSize.width - bounds.size.width; |
if (where.x < 0) where.x = 0; |
return where; |
} |
/***************************************************** |
* |
* ScrollAndZoomHandler(inHandlerCallRef, inEvent, inUserData) |
* |
* Purpose: Event handler that implements our HIScrollAndZoom custom view |
* |
* Inputs: inHandlerCallRef - reference to the current handler call chain |
* inEvent - the event |
* inUserData - app-specified data you passed in the call to InstallEventHandler |
* |
* Returns: OSStatus - error code (0 == no error) |
*/ |
pascal OSStatus ScrollAndZoomHandler(EventHandlerCallRef inHandlerCallRef, EventRef inEvent, void *inUserData) |
{ |
OSStatus result = eventNotHandledErr; |
ScrollAndZoomData* myData = (ScrollAndZoomData*)inUserData; |
switch (GetEventClass(inEvent)) |
{ |
case kEventClassHIObject: |
switch (GetEventKind(inEvent)) |
{ |
case kEventHIObjectConstruct: |
{ |
// allocate some instance data |
myData = (ScrollAndZoomData*) calloc(1, sizeof(ScrollAndZoomData)); |
// get our superclass instance |
HIViewRef epView; |
result = GetEventParameter(inEvent, kEventParamHIObjectInstance, typeHIObjectRef, NULL, sizeof(epView), NULL, &epView); |
// remember our superclass in our instance data |
myData->view = epView; |
// store our instance data into the event |
result = SetEventParameter(inEvent, kEventParamHIObjectInstance, typeVoidPtr, sizeof(myData), &myData); |
break; |
} |
case kEventHIObjectInitialize: |
{ |
// always begin kEventHIObjectInitialize by calling through to the previous handler |
result = CallNextEventHandler(inHandlerCallRef, inEvent); |
// if that succeeded, do our own initialization |
if (result == noErr) |
{ |
myData->originPoint.x = 0; |
myData->originPoint.y = 0; |
myData->imageSize.width = 3000; |
myData->imageSize.height = 2000; |
myData->zoomFactor = 1; |
// Make ourselves opaque to optimize the composite drawing |
HIViewChangeFeatures(myData->view, kHIViewIsOpaque, 0); |
} |
break; |
} |
case kEventHIObjectDestruct: |
{ |
if (myData != NULL) |
free(myData); |
result = noErr; |
break; |
} |
default: |
break; |
} |
break; |
case kEventClassScrollable: |
switch (GetEventKind(inEvent)) |
{ |
case kEventScrollableGetInfo: |
{ |
// we're being asked to return information about the scrolled view that we set as Event Parameters |
HIRect bounds; |
HIViewGetBounds(myData->view, &bounds); |
HISize imageSize = myData->imageSize; |
HISize lineSize = { kLineSize, kLineSize }; |
// From our scrollable view perspective, our bounds are reduced when the zoom factor increases |
bounds.size.height /= myData->zoomFactor; |
bounds.size.width /= myData->zoomFactor; |
SetEventParameter(inEvent, kEventParamViewSize, typeHISize, sizeof(bounds.size), &bounds.size); |
SetEventParameter(inEvent, kEventParamImageSize, typeHISize, sizeof(imageSize), &imageSize); |
SetEventParameter(inEvent, kEventParamLineSize, typeHISize, sizeof(lineSize), &lineSize); |
SetEventParameter(inEvent, kEventParamOrigin, typeHIPoint, sizeof(myData->originPoint), &myData->originPoint); |
result = noErr; |
break; |
} |
case kEventScrollableScrollTo: |
{ |
// we're being asked to scroll, we just do a sanity check and ask for a redraw if the location is different |
HIPoint where; |
GetEventParameter(inEvent, kEventParamOrigin, typeHIPoint, NULL, sizeof(where), NULL, &where); |
where = SanityCheck(where, myData); |
if ((myData->originPoint.y != where.y) || (myData->originPoint.x != where.x)) |
{ |
myData->originPoint = where; |
HIViewSetNeedsDisplay(myData->view, true); |
} |
break; |
} |
default: |
break; |
} |
break; |
case kEventClassControl: |
switch (GetEventKind(inEvent)) |
{ |
case kEventControlDraw: |
{ |
CGContextRef context; |
result = GetEventParameter(inEvent, kEventParamCGContextRef, typeCGContextRef, NULL, sizeof(context), NULL, &context); |
HIRect bounds; |
HIViewGetBounds(myData->view, &bounds); |
// Setting our background color to light blue |
CGContextSetRGBFillColor(context, 0.8, 1.0, 1.0, 1.0); |
CGContextFillRect(context, bounds); |
// Adjust the transform so the text doesn't draw upside down |
// Look at the HIView documentation for more details but basically: |
// The HIView coordinate system is set up to match QuickDraw's coordinate system, |
// origin at the top left, and positive Y going down. |
// But the Core Graphics operations assume that the origin is botton left |
// and Positive Y going up. |
// To let developers draw the "QuickDraw" way, to simplify the porting of their |
// legacy code, the CGContext we're being passed has already been transformed so that |
// the Y axis is reversed. |
// But the Core Graphics Text operations assume that the origin is botton left |
// and Positive Y going up. If we don't reverse the Text transform (thus cancelling |
// the previous transformation), then we would get our text upside down. |
CGContextSaveGState(context); |
CGAffineTransform transform = CGAffineTransformIdentity; |
transform = CGAffineTransformScale(transform, 1, -1); |
CGContextSetTextMatrix(context, transform); |
// Furthermore, adjust the Context Transformation Matrix to scale and translate |
// to reflect the zooming and scrolling values of the slider and scroll bar controls |
// OK, this is geometry... |
// As far as our drawing data is concerned, its size or position never change, its the |
// "visible" rectangle manipulated by the User which moves and changes size. |
// Thus, when the User moves this viewable rectangle to, let's say, position 100, 200, |
// in the coordinate system of our data, in order to draw the "viewable" portion of our data, |
// we would have to "move" our data to a position of -100, -200, to draw the correct data |
// in the coordinate system of the custom view. |
// |
// Instead of applying the translation to each and every CG drawing operation |
// which would give us code like: |
// CGContextMoveToPoint(context, i - myData->originPoint.x, -myData->originPoint.y); |
// CGContextAddLineToPoint(context, myData->imageSize.height + i - myData->originPoint.x, myData->imageSize.height - myData->originPoint.y); |
// CGContextMoveToPoint(context, i - myData->originPoint.x, -myData->originPoint.y); |
// CGContextAddLineToPoint(context, -myData->originPoint.x, i - myData->originPoint.y); |
// We apply instead once and for all the translation to the Transformation Matrix |
// with CGContextTranslateCTM in order to get code like: |
// CGContextMoveToPoint(context, i, 0); |
// CGContextAddLineToPoint(context, myData->imageSize.height + i, myData->imageSize.height); |
// CGContextMoveToPoint(context, i, 0); |
// CGContextAddLineToPoint(context, 0, i); |
// And we also apply the scaling once and for all with CGContextScaleCTM for the same reason. |
CGContextScaleCTM(context, myData->zoomFactor, myData->zoomFactor); |
CGContextTranslateCTM(context, -myData->originPoint.x, -myData->originPoint.y); |
// Drawing the entire grid, letting the CGClip do the magic |
CGContextBeginPath(context); |
UInt32 i, j; |
for (i = 0; i < myData->imageSize.width; i += 50) |
{ |
CGContextMoveToPoint(context, i, 0); |
CGContextAddLineToPoint(context, myData->imageSize.height + i, myData->imageSize.height); |
CGContextMoveToPoint(context, i, 0); |
CGContextAddLineToPoint(context, 0, i); |
} |
for (i = 0; i < myData->imageSize.height; i += 50) |
{ |
CGContextMoveToPoint(context, 0, i); |
CGContextAddLineToPoint(context, myData->imageSize.height - i, myData->imageSize.height); |
CGContextMoveToPoint(context, myData->imageSize.width, i); |
CGContextAddLineToPoint(context, myData->imageSize.width - myData->imageSize.height + i, myData->imageSize.height); |
} |
CGContextClosePath(context); |
CGContextSetRGBStrokeColor(context, 0.0, 0.0, 0.0, 1.0); |
CGContextStrokePath(context); |
// Drawing __ONLY__ the items which bounding box intersect our bounds (translated and scaled) |
// to optimize the drawing |
// In order to know when our items intersect the bounds of our "viewable" rectangle, we need |
// to apply a reverse transformation on those bounds. |
// The best way to explain this is with an example: |
// When we zoom in, it's as if the size of our drawing data increases |
// But in the coordinate system of our drawing data, it's as if the "viewable" rectangle |
// gets smaller. |
bounds = CGRectOffset(bounds, myData->originPoint.x, myData->originPoint.y); |
bounds.size.width /= myData->zoomFactor; |
bounds.size.height /= myData->zoomFactor; |
CGContextSetRGBFillColor(context, 0.0, 0.0, 0.0, 1.0); |
CGContextSelectFont(context, "Helvetica-Bold", 72.0, kCGEncodingMacRoman); |
for (i = 0; i < myData->imageSize.width; i += 100) |
for (j = 0; j < myData->imageSize.height; j += 100) |
{ |
HIRect charRect = { {i, j}, {100, 100} }; |
if (CGRectIntersectsRect(charRect, bounds)) |
{ |
char s[] = { 'A' + i / 100 + j / 100 }; |
CGContextShowTextAtPoint(context, charRect.origin.x + 10, charRect.origin.y + 90, s, 1); |
} |
} |
// restoring our context the way we found it when we started |
CGContextRestoreGState(context); |
result = noErr; |
break; |
} |
case kEventControlValueFieldChanged: |
{ |
// The user moved the zoom slider! |
ControlRef theControl; |
GetEventParameter(inEvent, kEventParamDirectObject, typeControlRef, NULL, sizeof(theControl), NULL, &theControl); |
// Checking the new value |
if (myData->zoomFactor != GetControl32BitValue(theControl)) |
{ |
myData->zoomFactor = GetControl32BitValue(theControl); |
// Let's make sure that we still display valid content |
myData->originPoint = SanityCheck(myData->originPoint, myData); |
// Sending an event to the HIScrollView to let it know its scrollable view has changed |
EventRef theEvent; |
CreateEvent(NULL, kEventClassScrollable, kEventScrollableInfoChanged, GetCurrentEventTime(), kEventAttributeUserEvent, &theEvent); |
SendEventToEventTarget(theEvent, GetControlEventTarget(HIViewGetSuperview(myData->view))); |
ReleaseEvent(theEvent); |
HIViewSetNeedsDisplay(myData->view, true); |
} |
result = eventNotHandledErr; |
break; |
} |
default: |
break; |
} |
break; |
default: |
break; |
} |
return result; |
} |
/***************************************************** |
* |
* GetScrollAndZoomClass(where, myData) |
* |
* Purpose: Registers our custom HIScrollAndZoom view class and installs the appropriate handlers |
* |
* Inputs: none |
* |
* Returns: our class ID as a CFStringRef |
*/ |
CFStringRef GetScrollAndZoomClass() |
{ |
// following code is pretty much boiler plate. |
static HIObjectClassRef theClass; |
if (theClass == NULL) |
{ |
static EventTypeSpec kFactoryEvents[] = |
{ |
{ kEventClassHIObject, kEventHIObjectConstruct }, |
{ kEventClassHIObject, kEventHIObjectInitialize }, |
{ kEventClassHIObject, kEventHIObjectDestruct }, |
{ kEventClassScrollable, kEventScrollableGetInfo }, |
{ kEventClassScrollable, kEventScrollableScrollTo }, |
{ kEventClassControl, kEventControlValueFieldChanged }, |
{ kEventClassControl, kEventControlDraw } |
}; |
HIObjectRegisterSubclass(kHIScrollAndZoomClass, kHIViewClassID, 0, ScrollAndZoomHandler, |
GetEventTypeCount(kFactoryEvents), kFactoryEvents, 0, &theClass); |
} |
return kHIScrollAndZoomClass; |
} |
/***************************************************** |
* |
* Do_NewWindow(outWindow) |
* |
* Purpose: called to create a new window, each other window will be created from APIs and the other one from Interface Builder |
* |
* Notes: called by Handle_CommandProcess() ("File/New" menu item), Handle_OpenApplication(). Handle_ReopenApplication() |
* |
* Inputs: outWindow - if not NULL, the address where to return the WindowRef |
* - if not NULL, the callee will have to ShowWindow |
* |
* Returns: OSErr - error code (0 == no error) |
*/ |
OSStatus Do_NewWindow(WindowRef * outWindow) |
{ |
WindowRef aWindowRef = NULL; |
OSStatus status; |
// Create a window, title and position it |
Rect bounds = {0, 0, 360, 480}; |
status = CreateNewWindow(kDocumentWindowClass, kWindowStandardDocumentAttributes | kWindowLiveResizeAttribute | kWindowStandardHandlerAttribute | kWindowCompositingAttribute, &bounds, &aWindowRef); |
require_noerr(status, CantCreateWindow); |
require(NULL != aWindowRef, CantCreateWindow); |
status = SetWindowTitleWithCFString(aWindowRef, CFSTR("Scroll And Zoom")); |
require_noerr(status, CantCreateWindow); |
status = RepositionWindow(aWindowRef, NULL, kWindowCascadeOnMainScreen); |
require_noerr(status, CantCreateWindow); |
// Get the Content view |
HIViewRef contentView; |
status = HIViewFindByID(HIViewGetRoot(aWindowRef), kHIViewWindowContentID, &contentView); |
require_noerr(status, CantCreateWindow); |
// Create the HIScrollView, the scrollable view (our HIScrollAndZoom custom view), and the zoom slider control |
HIViewRef scrollView; |
status = HIScrollViewCreate(kHIScrollViewValidOptions, &scrollView); |
require_noerr(status, CantCreateScrollView); |
HIViewRef scrollAndZoomView; |
status = HIObjectCreate(GetScrollAndZoomClass(), NULL, (HIObjectRef *)&scrollAndZoomView); |
require_noerr(status, CantCreateScrollAndZoomView); |
Rect sliderRect = {0, 0, 20, 100}; |
HIViewRef slider; |
status = CreateSliderControl(NULL, &sliderRect, 1, 1, 100, kControlSliderDoesNotPoint, 0, true, myLiveSliderAction, &slider); |
require_noerr(status, CantCreateSlider); |
// Embed each control in its appropriate parent and make it visible |
status = HIViewAddSubview(scrollView, scrollAndZoomView); |
require_noerr(status, CantSetUpViews); |
status = HIViewSetVisible(scrollAndZoomView, true); |
require_noerr(status, CantSetUpViews); |
status = HIViewAddSubview(contentView, scrollView); |
require_noerr(status, CantSetUpViews); |
status = HIViewSetVisible(scrollView, true); |
require_noerr(status, CantSetUpViews); |
status = HIViewAddSubview(contentView, slider); |
require_noerr(status, CantSetUpViews); |
status = HIViewSetVisible(slider, true); |
require_noerr(status, CantSetUpViews); |
// Set the frame of each view |
HIRect hiFrame; |
status = HIViewGetBounds(contentView, &hiFrame); |
require_noerr(status, CantSetUpViews); |
hiFrame = CGRectInset(hiFrame, 16, 16); |
status = HIViewSetFrame(scrollView, &hiFrame); |
require_noerr(status, CantSetUpViews); |
hiFrame.origin.x += hiFrame.size.width - 130; |
hiFrame.origin.y += hiFrame.size.height - 40; |
hiFrame.size.width = 100; |
hiFrame.size.height = 20; |
status = HIViewSetFrame(slider, &hiFrame); |
require_noerr(status, CantSetUpViews); |
// Set the layout info of each view for automated frame adjustment when resized |
HILayoutInfo layout = { |
kHILayoutInfoVersionZero, |
{ |
{ NULL, kHILayoutBindTop }, |
{ NULL, kHILayoutBindLeft }, |
{ NULL, kHILayoutBindBottom }, |
{ NULL, kHILayoutBindRight } |
}, |
{ |
{ NULL, 0.0 }, |
{ NULL, 0.0 } |
}, |
{ |
{ NULL, kHILayoutPositionNone }, |
{ NULL, kHILayoutPositionNone } |
} |
}; |
status = HIViewSetLayoutInfo(scrollView, &layout); |
require_noerr(status, CantSetUpViews); |
HILayoutInfo layout2 = { |
kHILayoutInfoVersionZero, |
{ |
{ NULL, kHILayoutBindNone }, |
{ NULL, kHILayoutBindNone }, |
{ NULL, kHILayoutBindBottom }, |
{ NULL, kHILayoutBindRight } |
}, |
{ |
{ NULL, 0.0 }, |
{ NULL, 0.0 } |
}, |
{ |
{ NULL, kHILayoutPositionNone }, |
{ NULL, kHILayoutPositionNone } |
} |
}; |
status = HIViewSetLayoutInfo(slider, &layout2); |
require_noerr(status, CantSetUpViews); |
// Install the kEventControlValueFieldChanged handler on the zoom slider control to handle the "live" zooming |
// We pass (ScrollAndZoomData*)HIObjectDynamicCast((HIObjectRef)scrollAndZoomView, kHIScrollAndZoomClass) as inUserData |
// so that the ScrollAndZoomHandler receives the same inUserData as it receives when the other events are received. |
EventTypeSpec eventTypeVC = {kEventClassControl, kEventControlValueFieldChanged}; |
InstallEventHandler(GetControlEventTarget(slider), ScrollAndZoomHandler, 1, &eventTypeVC, (ScrollAndZoomData*)HIObjectDynamicCast((HIObjectRef)scrollAndZoomView, kHIScrollAndZoomClass), NULL); |
// The window was created hidden so show it if the outWindow parameter is NULL, |
// if it's not, it will be the responsibility of the caller to show it. |
if (NULL == outWindow) |
ShowWindow(aWindowRef); |
SetWindowModified(aWindowRef, false); |
CantInstallEventHandler: |
CantSetTitle: |
CantSetUpViews: |
CantCreateSlider: |
CantCreateScrollAndZoomView: |
CantCreateScrollView: |
CantCreateWindow: |
if (NULL != outWindow) |
*outWindow = aWindowRef; |
return status; |
} // Do_NewWindow |
Copyright © 2005 Apple Computer, Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2005-08-10