SAS/AF: Maintaining Aspect Ratio of a
Resized Component
In many applications using the SAS/AF attachment system is sufficient for changing the size of a component in a FRAME when the FRAME is resized. However, there are cases when the size of the component should maintain a fixed aspect ratio. The case for this is most often when using the SAS Graph Output component showing some chart or graph as a preview prior to printing.
Example
This sample shows screenshots of a container box with a fixed aspect ratio of 3:1. Note that the component size is maximized to fit in available space regardless of frame shape.
Rules
The frame coordinate system is thus; (0,0) is the upper left corner, (x,y) is x units right and y units down. (x, y) is also known as (horizontalPosition, verticalPosition). The component is
resized by adjusting its lower right corner. The upper left corner is static. A minimum gap to each frame edge can be indicated, if the edge gap is not specified, it will be assumed to be whatever
the gap is when the frame is first displayed. By specifying edge gaps, the frame designer does not need to size the component exactly at build-time. The desired aspect ratio, R
, is determined from build-time size if it is not provided.
Formulas
In this article, aspect ratio1 is defined as Horizontal Width / Vertical Height
, or simply W / H
.
Thus for any new frame size W* x H*
find the largest w' x h'
within such that
w' / h' = W / H
Frame:
UL = (0,0)
LR = (Wframe,Hframe)
Component:
UL = (gapleft, gaptop)
LR = (Wframe-gapright,Hframe-gapbottom)
w'max = Wframe - gapleft - gapright
h'max = Hframe - gaptop - gapbottom
R' = w'max / h'max
if R' > R then h' = h'max and w' = h' * R
if R'<= R then w' = w'max and h' = w' / R
How it works
The frame being developed installs a _resize() method override and enables the AF executor mechanism that invokes the _resize() method when a resize event occurs. The _resize() override performs the calculations needed to determine the new region size of the component that is having it's aspect ratio maintained.
Frame SCL
Any frame developed to contain a resizeable component having a fixed aspect ratio must enable resize notification and communicate necessary parameters to the _resize() override.
Enable resize notification
Place this code in the INIT: section
_frame_._enableResizeNotify(); * v8;
or
call send (_frame_, '_enable_resize_notify_'); * V6 or V8;
Install frame _resize() method override
(Version 8)
In the property editor, find the Methods node under the _FRAME_ component.
Locate the _resize method in the Inherited methods list, right-click on it and select
override.
Accept or change the suggested Source Entry.
Change the Enabled metadata to Yes
or place this code in the INIT: section (Version 6 or 8)
* presuming methods.scl resides in same catalog as frame; this_cat = scan (screenname(),1,'.') || '.' || scan (screenname(),2,'.'); call send (_frame_, '_set_instance_method_' , '_resize_' , this_cat || '.methods.scl', 'RESIZE');
Specify component that is to have a fixed aspect ratio
An SCL list will be used to communicate information to _resize(). The SCL list will have named items and sublists that _resize() will parse for parameters related to the formulas discussed above. Since all good designs plan for the future, the list will actually be a list of lists. The list is attached to the _frame_ so it is available to _resize()
List construct
The list is attached to _frame_ as a named item of type list. The name is aspectedResizeInfo
* * aspectedResizeInfo is a list of lists. * For now, only the first sublist will be utilized (thus only * one component is managed for aspected resize) * Each sublist should contain named items. * Named items that can/should be present. * * widgetName (C) * - name of component that should be resized * while maintaining some aspect ratio * * edgeGap (L) * - list of gaps, should numeric items named * . left * . top * . right * . bottom * * any gap not specified is calculated from * the initial component placement and size * * aspectRatio (N) * - aspectRatio that should be maintained * or * aspectRatio (L) * - list should contain numeric items * . width * . height * or * if not present, initial aspect ratio will be maintained *;
Frame SCL example
This code would be placed in the INIT: section (Version 6 list population is left as an exercise to the reader)
declare list aspectedResizeInfo = { { widgetName = 'ContainerBox1' , edgeGap = { top=20, right=20, bottom=30 } , aspectRatio = { width=3, height=1 } } }; rc = setNitemL ( _frame_, aspectedResizeInfo, 'aspectedResizeInfo' );
Method SCL
This catalog entry can be located anywhere you want and named anything you want, the method name performing the resize can be anything you want. The only restriction is that the frame reference the proper catalog entry and method name when overriding the frame _resize() method.
Based on the FRAME SCL example, presume the entry METHODS in the same catalog as the frame contains a method named RESIZE. Almost 2/3 of the code is for deciphering the aspectedResizeInfo list and checking for errors.
RESIZE: method; infoLists = getNitemL (_self_, 'aspectedResizeInfo', 1,1, 0); if infoLists = 0 then do; put 'WARNING: No list named [aspectedResizeInfo]' ' was found on the _frame_'; return; end; * * infoLists is a list of lists. * For now, only the first sublist will be utilized (thus only * one component is managed for aspected resize) * Each sublist should contain named items. * Named items that can/should be present. * * widgetName (C) * - name of component that should be resized * while maintaining some aspect ratio * * edgeGap (L) * - list of gaps, should numeric items named * . left * . top * . right * . bottom * * any gap not specified is calculated from * the initial component placement and size * * aspectRatio (N) * - aspectRatio that should be maintained * or * aspectRatio (L) * - list should contain numeric items * . width * . height * or * if not present, initial aspect ratio will be maintained *; call send (_self_, '_WINFO_', 'NUMXPIXEL', fW); call send (_self_, '_WINFO_', 'NUMYPIXEL', fH); do i = 1 to 1; infoList = getItemL (infoLists, i); widgetName = getNitemC (infoList, 'widgetName',1,1,''); if widgetName ne '' then call send (_self_, '_GET_WIDGET_', widgetName, widgetId); * do this loop since aspectRatio can present * as a number item or a list item; aspectRatio = .; j = 1; do until (aspectRatio > 0 or p = 0); p = namedItem (infoList, 'aspectRatio', j); j ++ 1; if p then if itemType (infoList, p) = 'N' then aspectRatio = getItemN (infoList, p); end; gapLeft = getNitemN (infoList, 'gapLeft',1,1,.); gapTop = getNitemN (infoList, 'gapTop',1,1,.); gapRight = getNitemN (infoList, 'gapRight',1,1,.); gapBottom = getNitemN (infoList, 'gapBottom',1,1,.); if not namedItem (infoList, 'READY') then do; if not widgetId then do; put "WARNING: " widgetName= "not found in frame"; return; end; r = makelist (); call send (widgetId, '_get_Region_', r, 'PIXELS'); W = getNitemN (r, 'LRX') - getNitemN (r, 'ULX'); H = getNitemN (r, 'LRY') - getNitemN (r, 'ULY'); AR = W / H; if aspectRatio = . then do; * do this loop since aspectRatio can present * as a number item or a list item; j = 1; do until (aspectRatio > 0 or p = 0); p = namedItem (infoList, 'aspectRatio', j); j ++ 1; if p then if itemType (infoList, p) = 'L' then do; L = getItemL (infoList, p); W = getNitemN (L, 'width', 1,1,0); H = getNitemN (L, 'height', 1,1,0); if W>0 and H>0 then aspectRatio = W/H; end; end; end; if aspectRatio <= 0 and R > 0 then aspectRatio = AR; if aspectRatio <= 0 then put 'WARNING: aspectRatio could not be determined'; L = getNitemL (infoList, 'edgeGap'); gapLeft = getNitemN (L, 'Left',1,1,.); gapTop = getNitemN (L, 'Top',1,1,.); gapRight = getNitemN (L, 'Right',1,1,.); gapBottom = getNitemN (L, 'Bottom',1,1,.); if gapLeft = . then gapLeft = getNitemN (R, 'ULX'); if gapTop = . then gapTop = getNitemN (R, 'ULY'); if gapRight = . then gapRight = fW - getNitemN (R, 'LRX'); if gapBottom = . then gapBottom = fH - getNitemN (R, 'LRY'); rc = dellist (r, 'Y'); rc = insertN (infoList, aspectRatio, 1, 'aspectRatio'); rc = insertN (infoList, gapLeft, 1, 'gapLeft'); rc = insertN (infoList, gapTop, 1, 'gapTop'); rc = insertN (infoList, gapRight, 1, 'gapRight'); rc = insertN (infoList, gapBottom, 1, 'gapBottom'); rc = setNitemN (infoList, 1, 'READY'); end; if widgetId <= 0 then return; if aspectRatio <= 0 then return; w_max = fw - gapLeft - gapRight; h_max = fh - gapTop - gapBottom; if not (w_max > 0 and h_max > 0) then do; put 'WARNING: Component will not be resized due to small frame'; return; end; r_hat = w_max / h_max; r = makelist (); call send (widgetId, '_get_Region_', r, 'PIXELS'); ulx = getNitemN (r, 'ULX'); uly = getNitemN (r, 'ULY'); lrx = getNitemN (r, 'LRX'); lry = getNitemN (r, 'LRY'); new_ulx = gapLeft; new_uly = gapTop; if r_hat > aspectRatio then do; new_lrx = ulx + h_max * aspectRatio; new_lry = uly + h_max; end; else do; new_lrx = ulx + w_max ; new_lry = uly + w_max / aspectRatio; end; if new_ulx ne ulx or new_uly ne uly or new_lrx ne lrx or new_lry ne lry then do; rc = setNitemN (r, new_ulx, 'ULX'); rc = setNitemN (r, new_uly, 'ULY'); rc = setNitemN (r, new_lrx, 'LRX'); rc = setNitemN (r, new_lry, 'LRY'); call send (widgetId, '_set_region_', r); end; rc = dellist (r, 'Y'); end; endmethod; rc = rc; _self_ = _self_;
1 The purist might complain that aspect ratio is really longer dimension / shorter dimension
, but that definition is not helpful in this application.