Smalltalk по-русски
Advertisement

ВНИМАНИЕ: Это не окончательная версия документа (как и видно из текста)!

Эта версия документа взята из дистрибутива VisualWorks 7.4.1

(Перевод - Pollock - Создание GUI программными средствами (VisualWorks))


Using the GUI Painter you build a graphical user interface by positioning widgets directly on a canvas on the screen, and so build the canvas “visually,” making it look like you want it to look. Building a GUI programmatically is less intuitive, perhaps, but is often a more powerful and flexible an approach. For programmers who are used to building GUIs in this way, it is often the preferred approach. VisualWorks supports both modes of GUI building.

In this chapter we discuss the programmatic approach. The pattern described will be familiar to Smalltalk/V (or Visual Smalltalk) programmers and several other Smalltalk environments, but less familiar to VisualWorks programmers who have come to expect the GUI to be built from a window spec. Rather than a spec, this approach simply constructs one or more Smalltalk methods that create the GUI.

Overview of a GUI structure[]

The GUI is defined in a subclass of UserInterface or one of its subclasses. UserInterface provides the support for drawing a GUI from a spec, but provides services that are useful when building a GUI programmatically as well, providing a framework within which to build the GUI and connect it to the underlying application model.

The general structure of a window is a Window object (either an ApplicationWindow or a DialogWindow) containing a collection of widgets. Each widget is a kind of a pane, implemented as a subclass of ComponentPane, and so can contain additional panes. The result is a containment hierarchy of components making up the window.

For example, a very simple window with a single button with a label is built like this:

| window  button label |

window := Pollock.ApplicationWindow new.

button := Pollock.Button new.
button frame: (OriginExtentFrame origin: (5 @ 5) extent: (75 @ 20)).

label := DisplayLabel new.
label label: (Pollock.Label string: 'MyButton').

button addComponent: label.
window addComponent: button.

window open

In this minimal example, an ApplicationWindow, a Button, and a DisplayLabel are defined, then the label is added to the button and the button is added to the window by sending addComponent: messages. The frame: message that is sent to the button specifies its location relative to the window bounds and its size (see “#Positioning Widgets” below for more information). Finally, the window is opened, causing it to display on the screen.

Clearly, there is a lot more to building a GUI than this, and a complex GUI requires a great deal of work. Each widget type has specific options and requirements that must be specified, as described in the reference section for widgets. Layout frames control the positioning and sizing of widgets, which can become quite complex. A GUI might have a menu bar and/or a tool bar. And, there is a great deal of control you can exercise over the GUI, such as the enabling/disabling of or hiding and displaying of widgets. These topics and more are covered in the following sections and chapters, but this is the basic structure.

The GUI Framework[]

The Pollock framework provides a simple structure for building a GUI. When you build a new user interface, you simply implement a few methods that specify the features you want to include in the GUI and connect them to your application model.

To use the framework features, you create your GUI class as a subclass of an appropriate UserInterface subclass. Without doing anything else, this alone provides the logic for creating and opening a window. For example, create a new class as a subclass of UserInterface, called MyClass. (Make sure to include Pollock.* in the imports.) Then evaluate in a workspace:

MyClass open

A new, though empty, application window, an instance of ApplicationWindow, will open.

To make the GUI useful, however, it needs some widgets. The framework expects the createInterface method to define these. In this method you simply define an instance of a desired widget, providing any appropriate information such as its frame, and add it by sending an addComponent: message to the user interface instance (i.e., self). For example, to recreate the simple example shown above (under “#Overview of a GUI structure”) in MyClass, implement createInterface as follows:

createInterface
    | button label |

    button := Pollock.Button new.
    button frame: (OriginExtentFrame origin: (5 @ 5) extent: (75 @ 20)).

    label := DisplayLabel new.
    label label: (Pollock.Label string: 'MyButton').

    button addComponent: label.
    self addComponent: button

Similarly, getMainMenu is the method in which you would define a menu bar, and getToolInventory is the method in which you would define a tool bar. In addition, there is a method for configuring the window (hookupWindow) and for configuring events to trigger (hookupInterface).

To summarize, the framework methods used to configure an interface using methods are:

createInterface

This method defines the widgets in a user interface and their properties.

getMainMenu

This method returns a Pollock.Menu instance, which is used as the window menu bar.

getToolInventory

This method returns a Pollock.ToolInventory instance, which is used as the window tool bar.

hookupInterface

This method specifies trigger events.

hookupWindow

This method specifies properties of the user interface window, such as its opening size, colors, opening style, and so on.

The receiver (self) for each of these methods is the application class, the subclass of UserInterface. Accordingly, to supply the needed GUI attributes, you send messages appropriate for a UserInterface to self. This will be illustrated in the following sections.

The order in which these methods are invoked is defined in the openWithoutSpec method. How to use these methods in creating a user interface is described in subsequent sections.

UI Look and Feel[]

TBD

Windows[]

A standard GUI is presented as an application windows containing individual widgets. An application might open additional windows as appropriate for interacting with the user.

Window Types[]

Windows come in three types:

  • Application windows, having full decorations and border widgets. These are the windows primarily used to present the GUI. An application might open additional application windows. Application windows are (typically) non-modal, allowing the user to freely switch between any of these windows.
  • Dialog windows, having a border but (depending on the window manager) typically fewer border widgets. Dialogs are also (typically) modal, meaning that they do not relinquish focus to any other window in the application until the dialog is closed.
  • Pop-up windows display only temporarily, until focus switches away. They are used for menus, but also for things like fly-by help. Window decoration is generally minimal, and does not provide for user interaction.

In this chapter we discuss application windows and dialog windows. Pop-up windows are discussed *** somewhere else ***.

Class Hierarchy[]

The crucial window class hierarchy is:

Object
  GraphicsMedium
    DisplaySurface
      Window
        ScheduledWindow
          UserInterfaceWindow
            ApplicationWindow
            DialogWindow
        TransientWindow

Window is an abstract class, and is regarded as private. It provides the common functionality for all windows. ScheduledWindow provides the top level connection with the host windowing system. Accordingly, it provides support for such items as common window controls, labels and icons. While it is a concrete class, for most purposes you can regard it as abstract. It cannot have a menubar or a toolbar, and does not interact with UserInterface objects.

UserInterfaceWindow is the abstract superclass for windows that interact with UserInterface objects. It provides common behavior for ApplicationWindow and DialogWindow.

The window classes you need to be aware of, because you will create subclasses of them, are ApplicationWindow, DialogWindow, and (less frequently) TransientWindow. These are discussed in subsequent sections.

Building an Application Window[]

This section discusses creating and formatting an application window within the Pollock GUI framework.

There are a variety of properties you can set for a window, such as its opening type, position and size, its label and icon. Some of these are set by sending messages to the window itself, and others by sending messages to the user interface.

Using the GUI framework, window properties should be set in the hookupWindow method.

The following sections describe how to set several properties.

Creating the Window[]

When you create a subclass of UserInterface, as suggested above, to build your GUI, there is nothing more you need to do to create the window itself. For example, create a subclass of UserInterface and call it MyClass. Then, open it by evaluating in a workspace:

MyClass open

A new, though empty and rather small, ApplicationWindow opens.

To make the window more useful as a GUI, you need to set several properties and add widgets. The rest of this section addresses setting its various properties.

Set the Window Label[]

Set a window’s label by sending a label: message to the window’s user interface, in the hookupWindow method, with a label String as argument.

hookupWindow
    ...
    self mainWindow label: 'My Window Label'.
    ...

Set the Window ID[]

In a variety of situations it is necessary or useful to set the window ID (for example, see “#Saving a Window Size and Position” below). To set the ID, send a specID: message to the user interface, with a symbol as argument. Because the symbol is used to identify the window, pick a symbol that will be unique among windows.

hookupWindow|
    ...
    self specID: #my Window.
    ...

Assigning a Window Icon[]

For window managers that support iconified windows, VisualWorks provides a default icon to represent a collapsed window. You can assign a different icon to better represent the window. The icon must be an image (refer to “Graphical Objects” in the Application Developer’s Guide).

An icon is an instance of Icon, and consists of an image (figure) and a mask (shape), both of which are instances of CachedImage. The icon may also be registered in the Icon class IconConstants dictionary.

To create an icon with a mask, send a figure:shape: message to an instance of Icon, specifying the image and mask. The resulting Icon is set for the window by sending an icon: message to the window’s artist.

For example, suppose we have an image and a mask named “scribble” created using the Image Editor and stored in our interface class as a class method (as is typical for such resource methods). Then we assign the icon as follows:

hookupWindow
    ...
    self mainWindow artist icon: 
        ( Pollock.DisplayImage
            image: ( self class scribble ) 
            mask: ( self class scribbleMask ) ).
    ....

Remember that self in the hookupWindow method is the UserInterface subclass instance. To get the window from the user interface, we send the mainWindow message. And, to get the artist from the window, we send the artist message. Then, we can assign the icon to the artist.

If either the image or mask is an instance of Image, not CachedImage, send asCachedImage to the image when assigning it to figure or shape.

Window Opening Size and Position[]

The default window size is too small for most GUIs. This becomes apparent when you start placing and arranging widgets in the window, as described later. To accommodate

Window Opening Size[]

The size of a window is specified by setting its frame size. WindowFrame is a special frame class for sizing and positioning windows, and is the default frame for ScheduledWindow, TransientWindow, and their subclasses.

To set the openning size, send a specifiedSize: message to the frame, with a point specifying the size (as if setting the extent from 0@0):

hookupWindow
    ...
    self mainWindow frame specifiedSize: (450 @ 300).
    ...

You can also set the window to open at a saved size or its last size when closed. See “#Saving a Window Size and Position” below.

Maximum and Minimum Sizes[]

By default, there is no restriction on how large or small a user can stretch or shrink a window by moving its corners. For some GUI layouts this presents a problem, because shrinking the screen too small can cause widgets to overlap, and stretching too large simply looks bad. In these cases, it is desirable to impose sizing restrictions.

You limit the resizing by sending maximumSize: and minimumSize: messages to the window frame, for example:

hookupWindow
    ...
    self mainWindow frame
        specifiedSize: (450 @ 300);
        minimumSize: (200 @ 150); 
        maximumSize: (900 @ 700).
    ...

The actual size limits you specify will depend on the GUI composition.

Window Opening Position[]

By initial system default, new windows open centered on the screen. This behavior can be changed either system-wide, or for individual windows.

Setting the Default Window Opening Position[]

The system-wide default is held in the class variable Pollock.ScheduledWindow.SystemOpenStyle. There are three system-wide position settings: screen centered, cascaded, and user placed. To set the system default, send one of these messages to Pollock.ScheduledWindow:

centeredOpen

Set the system default to open new windows screen centered.

cascadeOpen

Set the system default to open new windows cascaded.

promptForOpen

Set the system default to display a window frame, prompting the user for the window placement.

For example, to change the system default to user placement, evaluate:

Pollock.ScheduledWindow promptForOpen

The defaults determine window placement unless specified differently for a specific window.

Setting Individual Window Opening Position[]

For individual windows, you have more flexibility in how to specify the opening position than you do at the system-wide level.

The following messages, which are sent to a window’s userInterface, are used to set opening position options:

openType: aSymbol

Sets the general opening type. Options are:
  • #systemDefault – Window opening style is taken from the system default setting.
  • #cascade – Window opens in the next cascading position.
  • #screenCenter – Window opens centered on the screen.
  • #userPlacement – A frame indicates window location before opening, allowing the user to place it on the screen.
  • #custom – Allows setting additional positionType and sizeType values.

positionType: aSymbol

If openType: is set to #custom, this message allows you to set additional position options. These options allow you to set the sizeType in addition to the positionType. Options are:
  • #systemDefault – Window opens in the system default position.
  • #screenCenter – Window opens in the center of the screen.
  • #cascade – Window opens in the next cascade position.
  • #userPlacement – A frame indicates window location before opening, allowing the user to place it on the screen.
  • #mouseCenter – Window opens centered on the current mouse position.
  • #specifiedPosition – Window opens in the position specified by the window’s frame (WindowFrame) openPosition.
  • #lastPosition – Window opens in the position where last saved (see “#Saving a Window Size and Position” below).
  • #lastPositionAutoSave – Window opens in the location where it was last closed (see “#Saving a Window Size and Position” below).

For example, to set the window to open in cascading style, you can set the #cascade openType:

hookupWindow
    ....
    self openType: #cascade.
    ...

For additional opening positions, set the openType to #custom, and then set the postionType:

hookupWindow
    ....
    self openType: #custom.
    self positionType: #mouseCenter.
    ...

To open the window in the same, specified position each time, set the positionType as #specifiedPosition, and set the position for the window frame:

hookupWindow
    ...
    self openType: #custom.
    self positionType: #specifiedPosition.
    self mainWindow frame openingPosition: (10 @ 10).
    ...

For opening in a saved position, refer to “#Saving a Window Size and Position”.

Saving a Window Size and Position[]

As described in the previous section, setting the openType to #custom enables options to set the window to open in the size and/or position that it was last saved or the last closed. This is frequently useful for auxilliary windows or dialogs, such as tool or formatting windows, so the user can position them conveniently and expect them to open in the same location when needed.

positionType: aSymbol

If openType: is set to #custom, this message allows you to set additional position options. These options allow you to set the sizeType in addition to the positionType. Options are:

sizeType: aSymbol

If openType: is set to #custom, this message allows you to set the save option for getting the window size from a previous size. The options are:

The window size and position, when saved, are stored in a dictionary with the window ID as key, so you must set the specID for the window (see “#Set the Window ID” above).

Then, to have the window size and/or position saved when the window is closed, send a sizeType: or positionType: message with the appropriate symbol:

hookupWindow
    ... 
    self mainWindow specID: #my Window.
    self 
        openType: #custom;
        sizeType: #lastSizeAutoSave;
        positionType: #lastPositionAutoSave.
    ...

In this case, because the auto-save options are used, the window size and position are saved when the window is closed. Next time the window is opened (or a window with the same specID), it will open in the saved location.

Instead of auto-save, you can set the types to open at the last saved size and/or position as follows:

hookupWindow 
    ...
    self mainWindow specID: #my Window.
    self
        openType: #custom;
        sizeType: #lastSize;
        positionType: #lastPosition.
    ...

To effect a save, your code must explicitly save the size and/or position, send a saveLastSize or saveLastPostion message. If the window is an ApplicationWindow, you can send these messages directly to the window; otherwise, send the messages to the window’s userInterface.

window saveLastSize.
window saveLastPosition.

Usually, a menu item would be provided to invoke these messages. For example, we can create a menu and two items in the getMainMenu method so it is added to our window as a menubar:

getMainMenu
    ^Menu menuItems: (Array 
        with: ( ( MenuItem label: 'Window' ) 
            submenu: (Menu
                labels: #('Save position' 'Save size')
                actions: #(#saveLastPosition #saveLastSize) )))

See “#Menubar” for further information on setting up a menubar.

When you open the window again, still specifying the specID, openType, sizeType, and positionType, the saved size and/or position will be used.

Widgets[]

A window is a container for widgets, the individual GUI controls that provide for users to input and review information and to invoke actions.

Pollock provides a collection of widgets most commonly used in an application GUI. It also has a framework that makes it easy to develop your own custom widgets.

The widget set is described in *** another chapter ***, which describes the specific configuration requirements for each widget. In this section we describe the general configuration requirements, such as placement and positioning.

Adding Widgets to a Window[]

As explained *** someplace ***, windows and widgets are kinds of panes, and panes can contain other panes. Widgets are added to panes either directly, or as components of other widgets to which they have been added. Widgets are added to panes by sending an addComponent: message to the containing pane, whether a window or a containing widget, as illustrated earlier in this chapter.

Within the Pollock GUI framework, widgets are assembled in the createInterface method, which the framework then adds to the window. For example, to add a Button with a DisplayLabel to an application window, create this method in the application’s UserInterface subclass:

createInterface
    | button label |
    button := Pollock.Button new.
    button frame: (OriginExtentFrame origin: 10 @ 200 extent: 70 @ 25).
    label := Pollock.DisplayLabel string: 'Push'.
    button addComponent: label.
    self addComponent: button

This is minimal and requires refining to contribute to a good GUI, but illustrates the basic pattern. Two widgets are created, a button and a label. The label is added to the button, accepting its default placement within the button. The button, however, needs to be sized and positioned in the window, so a frame is defined. The button is then added to self, which is the application user interface, which then handles adding it to the window.

The following sections and additional examples throughout this document will provide elaborations on this basic pattern.

Widget ID[]

Every widget placed in a window has an ID, whether explicitly or implicitly assigned. The ID is used to identify the widget when it needs to be accessed for programmatic control.

The default ID consists of the widget class name with a integer appended, based on the order in which the widget is created. For widgets that do need to be accessed, whether to change their state, or to set or read their current state and values, it is recommended to explicitly set a meaningful ID.

To set the ID, send an id: message to the widget with a symbol as argument:

createInterface
| button |
    ...
    button := Pollock.Button new.
    button id: #acceptButton.
    ...

The ID will be used in later examples.

Widget Models[]

Most widgets have a model, an object the value of which is reflected by the widget, and the value of which is affected by changes to the widget. An input field, for example, typically both displays the current value of its model, and changes the value of its model if the user edits its displayed text.

Widgets that take models have specific requirements for the kind of object that can be the model. Refer to the descriptions of the individual widgets for specific requirements.

To assign a model, send a model: message to the widget. For example, an InputField widget expects an ObservedValue holding an AdvancedText (or a Text or a String) as its model. Generally, the model will be held in an instance variable, and you will provide an accessor method for the variable, such as the following with lazy initialization:

textValue
    textValue isNil ifTrue:
        [textValue := 'testing' asAdvancedText asObservedValue ].
    ^textValue

and then invoke this method when assigning the model:

createInterface
    ...
    editor := Pollock.InputField new.
    editor model: self textValue.
    ...

Then, when the user edits the contents of the input field, the value of the textValue variable is updated as well. This is managed by the GUI model framework, and is described in detail in ( *** some other chapter *** ).

Help Text[]

Widgets often have fly-by help ...

Positioning Widgets[]

When you add a widget to a user interface, you need to specify its framing, which orients the widget on the GUI and determines its size. In a few examples above we used the OriginExtentFrame because it is easy to understand. However, for purposes of GUI design, it is also limited. This section describes the framing classes and their use in building a GUI.

The layout frames are:

OriginExtentFrame Specifies a fixed size and position for the widget.
AlignmentFrame The extent is fixed, but the origin varies to maintain an alignment.
FractionalFrame Computes a frame relative to the widget's enclosing pane.
RelationalFrame Like FractionalFrame, but can set a size relative to a sibling widget.

There are a few other special-purpose frames as well, but the above are the frames you are likely to use to build your GUI.

OriginExtentFrame[]

OriginExtentFrame sets a specific origin (top left corner) and extent (bottom right corner relative to the origin) to a widget. The origin and extent are set as point values. This frame is useful for widgets that have a fixed size and location in a pane.

Very simply, create an OriginExtentFrame by sending an origin:extent: message to the class:

createInterface
    | button |
    ...
    button frame:
        (OriginExtentFrame origin: (5 @ 5) extent: (75 @ 20))
    ...

The origin sets the top left corner relative to the containing pane (or window), and the point dimensions specify the number of pixels inset from the left and top of the containing pane, respectively.

The extent, which is given as a point value, sets the width and height sizes, respectively, in pixels. The point value can be thought of as specifying a position relative to the origin.

When the widget is a DisplayLabel, the frame adjusts its extent to accommodate the changing text size.

AlignmentFrame[]

AlignmentFrame is primarily used to align a DisplayLabel or a DisplayImage within another widget, such as a Button or Frame, and provides alignment options for the item within the displayable area in the widget.

Typically, the alignment frame takes its origin and extent from the displayable area of the containing pane, and so is created only specififying its alignment. By default, an alignment frame is created centered both vertically and horizontally, so for example, within a button:

createInterface
    | button label |
    ...
    button := Pollock.Button new.
    button frame: (OriginExtentFrame origin: (5@5) extent: (50@50)).
    label := Pollock.DisplayLabel string: 'test'.
    label frame: AlignmentFrame new.
    button addComponent: label.
    self addComponent: button.
    ...

This creates a button in a window with a centered label. To align the label, for example, in the bottom right corner, use instead:

label frame: AlignmentFrame bottomRight.

There is an instance creation method for each origin alignment option:

bottomCentered

Centers the widget along the bottom border of the containing pane.

bottomLeft

Aligns to the bottom left corner of the containing widget.

bottomRight

Aligns to the bottom right corner of the containing widget.

centerCentered

Center aligns the widget vertically and horizontally in the containing widget.

centerLeft

Left aligns the widget vertically centered in the containing widget.

centerRight

Right aligns the widget vertically centered in the containing widget.

topCentered

Centers the widget along the top border of the containing widget.

topLeft

Aligns to the top left corner of the containing widget.

topRight

Aligns to the top right corner of the containing widget.

Additional instance creation methods allow specifying an origin and extent as well as the alignment, but are seldom useful.

Unlike OriginExtentFrame, AlignmentFrame does not adjust its extent to accommodate a changing text size; the frame size must be changed explicitly.

FractionalFrame[]

FractionalFrame is the usual frame to use to size and position a widget that needs to change size as the window size changes. You specify the location and size as a proportion of the containing pane or window. Then, as the user changes the window dimension by dragging the corner, the widgets change size or positon to maintain proportions.

Dimensions are given as a fraction of the display area and/or an offset. The fraction may be specified either as a numeric value between 0 and 1, indicating the location left to right or top to bottom. The offset specifies the number of pixels, also left to right or top to bottom, from the fractional position. Between the fractional and offset dimentions, widget layouts can be very precisely specified.

Two instance creation methods provide specifying the frame in terms of fractional proportions or offsets:

fractionLeft: leftFraction top: topFraction right: rightFraction bottom: bottomFraction

Create a new instance with edge proportions as specified. Offsets are initialized to 0 (zero).

offsetLeft: leftOffset top: topOffset right: rightOffset bottom: bottomOffset

Create a new instance with edge offsets as specified. Fractions are initialized to 0 (zero).

Alternatively, create the frame by sending new to the class and then set each fraction and/or offset individually.

To set the proportions and offsets, use these instance methods:

bottomFraction: aNumber

Set the bottom edge to aNumber fractional part of the containing pane or window, measured from the top.

bottomFraction: aNumber offset: anInteger

Set both the fraction and the offset.

bottomOffset: anInteger

Set the bottom edge offset as anInteger pixels from the bottomFraction.

leftFraction: aNumber

Set the left edge to aNumber fractional part of the containing pane or window, measured from the left.

leftFraction: aNumber offset: anInteger

Set both the fraction and the offset.

leftOffset: anInteger

Set the left edge offset as anInteger pixels from the leftFraction.

rightFraction: aNumber

Set the right edge to aNumber fractional part of the containing pane or window, measured from the left.

rightFraction: aNumber offset: anInteger

Set both the fraction and the offset.

rightOffset: anInteger

Set the right edge offset as anInteger pixels from the rightFraction.

topFraction: aNumber

Set the bottom edge to aNumber fractional part of the containing pane or window, measured from the top.

topFraction: aNumber offset: anInteger

Set both the fraction and the offset.

topOffset: anInteger

Set the top edge offset as anInteger pixels from the topFraction.

For example, to fill the entire window with a single widget, such as a TextEdit pane, leave the offsets at 0, set the left and top fractions to 0, and set the right and bottom fractions to 1:

createInterface
    | editor |
    ...
    editor := Pollock.TextEdit new.
    editor frame:
        (FractionalFrame fractionLeft: 0 top: 0 right: 1 bottom: 1 ).
    self addComponent: editor.
    ...

The fractional properties specify the fractional part of the edges of the widget away from the top and left edges of the containing pane. So, setting the top and left fractions to 0 put them at the top and left edges. At the other extreme, setting the bottom and right fractions to 1 maximizes their distance from the top and left, so puts them at the bottom and right edges. In this way, as the window is stretched or compressed, the TextEdit pane stretches and compresses with it, continuing to completely fill the window.

Frequently, however, it is visually appealing to include a frame around a widget like this. This is even more important when there are more widgets. To inset the TextEdit pane on all four sides by 5 pixels, add these offsets:

createInterface
    | editor |
    ....
    editor := Pollock.TextEdit new.
    editor frame:
        (FractionalFrame fractionLeft: 0 top: 0 right: 1 bottom: 1 ).
    editor frame topOffset: 5; 
        leftOffset: 5; 
        rightOffset: -5;
        bottomOffset: -5.
    self addComponent: editor.
    ...

These provide a constant inset for the widget that does not change as the window size changes; all the size changing is done in the widget itself. Attempting to provide a border by setting small fractional positions to the borders would not give the same effect, since the border size would also change as the window size changes.

Note that the offset sizes are all specified in pixels measured from left to right and top to bottom. Negative values actually specify the number of pixels from right to left and bottom to top, to bring the borders in from the right and bottom.

Given these fraction and offset values, you can construct quite complex GUI layouts. A couple more examples at this point will illustrate a few options.

You may want to add a Button to the layout, below the text editor. Buttons are often a fixed size, but might either be in a fixed position or allowed to drift as the window size changes. First, for a fixed location, you can do the following:

createInterface
    | editor button |
    ...
    editor := Pollock.TextEdit new.
    editor frame:
        (FractionalFrame fractionLeft: 0 top: 0 right: 1 bottom: 1 left: 0).
    editor frame topOffset: 5; 
        rightOffset: -5; 
        bottomOffset: -30;
        lefOffset: 5.
    button := Pollock.Button new.
    button frame: 
        (FractionalFrame fractionLeft: 0 top: 1 right: 0 bottom: 1 ).
    button frame topOffset: -30; 
        bottomOffset: -5; 
        leftOffset: 5; 
        rightOffset: 75.
    self addComponent: editor; addComponent: button.
    ...

The text editor bottom offset was changed to make room for the button. Then, to hold the button in a constant position in the window, in the lower left corner, fractional values were set to position it there. The offset values are then used to move it slightly to the right and to set its width, and to position and size it up from the bottom.

As a variation, the following adds two buttons that retain a constant size but drift from left to right as the window width changes.

createInterface
    | editor button1 button2 |
    ...
    button1 := Pollock.Button new.
    button1 frame:
        (FractionalFrame fractionLeft: 2/5 top: 1 right: 2/5 bottom: 1 ).
    button1 frame topOffset: -30; 
        bottomOffset: -5; 
        leftOffset: -70; 
        rightOffset: 0.
    button2 := Pollock.Button new.
    button2 frame: 
        (FractionalFrame fractionLeft: 3/5 top: 1 right: 3/5 bottom: 1 ).
    button2 frame topOffset: -30; 
        bottomOffset: -5; 
        leftOffset: 0; 
        rightOffset: 70.
    self addComponent: button1; addComponent: button2.
    ...

This example emphasizes that the offset is relative to the fractional position. For button1, the right offset is 0, placing the right edge of the button 2/5 of the way across the window, and the left offset sets the left edge 70 pixels to the left (negative value). In contrast, button2 has its left offset set to 0, placing its left edge 3/5 of the way across the window, and right offset sets the right edge 70 pixels to the right. In this way, the width of the widgets is set, but their positions are allowed to adjust.

The danger with this positioning occurs at the extremes of window sizing. If the window becomes too narrow, the buttons will run out of bounds. In other arrangements, the buttons could overlap. These conditions can usually be accounted for by setting minimum sizes for the window (see “#Maximum and Minimum Sizes”).

RelationalFrame[]

RelationalFrame allows you to specify the edges of a widget relative to the edges of another widget or the containing pane, or relative to a fractional location within the pane.

To create a RelationalFrame instance, send either a new or offsetLeft:top:right:bottom: message to the class. Because RelationalFrame is a subclass of FractionalFrame, you can also use the fractionLeft:top:right:bottom: creation message, but this is less usual for a RelationalFrame.

To set the location and size of a widget using a RelationalFrame involves three properties for each edge: the relation, the attachment, and the offset. The relation identifies the reference widget, relative to which a frame’s edge will be aligned. The attachment identifies which edge of the reference widget is to be used for the alignment. The offset sets the number of pixels of offset for the alignment.

To set a frame’s edge relative to a reference widget, you send a message to the frame identifying which edge is being aligned and the ID of the reference widget. There is a separate message for each of the four edges to be set:

bottomRelation: aSymbolOrNil

Aligns the bottom edge of the frame with the edge of the widget identified by aSymbol.

leftRelation: aSymbolOrNil

Aligns the left edge of the frame with the edge of the widget identified by aSymbol.

rightRelation: aSymbolOrNil

Aligns the right edge of the frame with the edge of the widget identified by aSymbol.

topRelation: aSymbolOrNil

Aligns the top edge of the frame with the edge of the widget identified by aSymbol.

By default, the specified edge is aligned with the opposite edge of the reference widget, so bottomRelation: aligns with the top edge of the reference widget, leftRelation: aligns with the right edge, rightRelation: with the left edge, and topRelation: with the bottom edge. In this way it is simple to align adjacent widgets. For example, to align the left edge of button2 to the right edge of button1, send leftRelation: to the frame for button2, with the ID for Button1 as argument:

button2 := Pollock.Button new.
button2 frame: (RelationalFrame new).
button2 frame leftRelation: #Button1.

(In this and following examples, default ID names are used, consisting of the widget class name and an integer, as described in “#Widget ID”.)

However, in some circumstances you might want to align edges right to right, top to top, left to left, or bottom to bottom. To do this, specify the attachment of relation with one of these messages:

bottomAttachment: aSymbolOrNil

With #bottom as argument, aligns the bottom of the frame with the bottom edge of the reference widget. With #top or nil as argument (the default) aligns the bottom of the frame with the top edge of the reference widget.

leftAttachment: aSymbolOrNil

With #left as argument, aligns the left of the frame with the left edge of the reference widget. With #right or nil as argument (the default) aligns the left of the frame with the right edge of the reference widget.

rightAttachment: aSymbolOrNil

With #right as argument, aligns the right of the frame with the right edge of the reference widget. With #left or nil as argument (the default) aligns the right of the frame with the left edge of the reference widget.

topAttachment: aSymbolOrNil

With #top as argument, aligns the top of the frame with the top edge of the reference widget. With #bottom or nil as argument (the default) aligns the top of the frame with the bottom edge of the reference widget.

For example, you might want to stack two buttons, aligning the left edge of button2 to the left edge of button1. To do this, use the leftAttachment: message as follows:

button2 := Pollock.Button new.
button2 frame: RelationalFrame new.
button2 frame leftRelation: #button1;
    leftAttachment: #left;
    topRelation: #button1.

In this code fragment, explicitly setting the left attachment for button2 is necessary to override the default that would align it with button1’s right edge. The default to align its top with button1’s bottom edge, however, is the desired behavior, and so is left unchanged.

Typically, you also need to provide spacing between the edges of widgets. This is done by setting an offset. The effect of the offset varies with context. If two widgets are adjacent, for example, one is set with its leftRelation to the right edge of another, the offset sets the number of pixels between the widgets. If an edge is not related to a widget, then the offset is figured relative to the corresponding edge of the containing pane. As for FractionalFrame, the offset can be positive or negative.

Again, four messages set the offset:

bottomOffset: anInteger

Sets the number of pixels to offset the bottom edge.

leftOffset: anInteger

Sets the number of pixels to offset the left edge.

rightOffset: anInteger

Sets the number of pixels to offset the right edge.

topOffset: anInteger

Set the number of pixels to offset the top edge.

The following example places a Button below a TextEdit pane, with the top of the button aligned to the bottom of the text edit:

createInterface
    | editor button1 |
    ...
    editor := Pollock.TextEdit new.
    editor frame: (FractionalFrame fractionLeft: 0 top: 0 right: 1 bottom: 1).
    editor frame topOffset: 5; leftOffset: 5; rightOffset: -5; bottomOffset: -35.
    button1 := Pollock.Button new.
    button1 frame:
        (RelationalFrame offsetLeft: 0 top: 5 right: 70 bottom: -5 ).
    button1 frame topRelation: #TextEdit1;
        leftRelation: #TextEdit1; 
        leftAttachment: #left;
        rightRelation: #TextEdit1.
    self addComponent: editor; addComponent: button1.
    ...

The frame is created using the offset setting creation method because several of the offsets need to be specified. This is usually the case, and so is an efficient message to use to create the frame. Since the Button will be aligned on the left with the TextEdit pane, it needs no offset. The top offset is set to space the Button 5 pixels down from the TextEdit and the bottom offset is set for 5 pixels up (-5) from the bottom of the window.

To align the Button with the TextEdit, the leftRelation: message is sent to the frame with the TextEdit’s ID. To align with the TextEdit’s left edge rather than the right, the leftAttachment: message is sent with #left as argument.

Setting the Button to a constant width is done by also relating it to the left edge of the TextEdit. This time, because it’s an opposite edge, the attachment does not need to be changed. The offset gives the Button its width of 70 pixels.

To add a second button to the right of the first button, insert this code before opening the window (and adding a temporary variable):

createInterface
    | editor button1 button2 |
    ...
    button2 := Pollock.Button new.
    button2 frame:
        (RelationalFrame offsetLeft: 5 top: 0 right: 75 bottom: 0 ).
    button2 frame leftRelation: #Button1;
        topRelation: #Button1; topAttachment: #top;
        bottomRelation: #Button1; bottomAttachment: #bottom;
        rightRelation: #Button1; rightAttachment: #right.
    self addComponent: button2.
    ...

The left relation to Button1 with a left offset of 5 positions the new button a little to the right of the original. Setting the relations and attachments for both the top and bottom of the new button, with offsets of 0, set the alignments for those edges. Setting the width is set relative to Button1’s right edge, with the offset large enough for the width of the button plus the offset between the two buttons.

Note that no reference to the TextEdit in doing this, which simplifies modifying the layout. Any change affecting Button1 will affect Button2 as well.

In the above example, the two buttons retain a constant size and position relative the the TextEdit and each other. As a variation, and to illustrate using a fractional positioning for a RelationalFrame, change the configuration for Button2 as follows:

createInterface
    ...
    button2 := Pollock.Button new.
    button2 frame: (RelationalFrame offsetLeft: 5 top: 0 right: 75 bottom: 0 ).
    button2 frame leftFraction: 0.5; rightFraction: 0.5.
    button2 frame topRelation: #Button1; topAttachment: #top;
        bottomRelation: #Button1; bottomAttachment: #bottom.
    self addComponent: button2.
    ...

In this example, the left and right edges are aligned with the horizontal center of the window by sending the leftFraction: message. Everything else remains the same, including the offsets to move the button slightly off center and to set the button width. The real difference is that, as the window is widened, the second button moves to follow the centerline.

Forms[]

A Form in Pollock holds a collection of widgets, that can be installed as a unit into a user interface. Forms provide many of the functions that subcanvases did in the original VisualWorks GUI framework.

Forms provide a simple mechanism for exchanging part of a user interface, together with its programmatic interface, in a window during runtime. They also can have scrollbars, and so provide a way to include a scrollable subform within your GUI, possibly as an alternative to opening another window to display a complex interface.

A form can be either built directly in your application’s UserInterface subclass, or created as a separate UserInterface and installed as part of building the user interface. Both approaches are described below.

Constructing on-the-fly[]

A Form can be added directly to the application GUI by defining it in the user interface createInterface method. This involves little more than creating the Form, setting its frame, adding widgets, then adding the Form to the user interface.

createInterface
    | form textPane... |
    ...
    form := Pollock.Form new.
    form frame: 
        (FractionalFrame fractionTop: 0 right: 0.5 bottom: 0.5 left: 0).
    textPane := Pollock.TextEdit new.
    textPane frame: 
        (FractionalFrame fractionTop: 0 right: 1 bottom: 1 left: 0).
    textPane frame bottomOffset: -35.
    button := Pollock.Button new.
    button frame: 
        (FractionalFrame fractionTop: 1 right: 0 bottom: 1 left: 0).
    button frame topOffset: -30; rightOffset: 70 ;
        bottomOffset: -5; leftOffset: 5.
    form addComponent: textPane; addComponent: button.
    self addComponent: form.
    ...

In this configuration, the form is scaled relative to the window, but its components are scaled relative to the form.

If the components extend beyond the form, you can add scrollbars by adding:

form horizontalScrollbar: true; verticalScrollbar: true.

Loading from a UserInterface[]

For ease of reuse, you might define a form’s contents as a UserInterface itself. The GUI framework provides a simple mechanism for loading that user interface into the form.

To do so, create a UserInterface subclass. In its createInterface method, add components to the user interface as usual. For example, we can create a TestForm class as a subclass of UserInterface and create this method:

createInterface
    | textPane button |
    textPane := Pollock.TextEdit new.
    textPane frame: 
        (FractionalFrame fractionLeft: 0 top: 0 right: 1 bottom: 1 ).
    textPane frame bottomOffset: -35.
    button := Pollock.Button new.
    button frame: (FractionalFrame fractionLeft: 0 top: 1 right: 0 bottom: 1 ).
    button frame topOffset: -30; 
        rightOffset: 70 ; 
        bottomOffset: -5; 
        leftOffset: 5.
    self addComponent: textPane; addComponent: button.

This defines a new user interface with a text pane and a button. Now, to add this to our application’s user interface, create a Form with appropriate framing, and add the components from the reusable interface:

createInterface
    | form ... |
    ...
    form := Pollock.Form new.
    form frame:
        (FractionalFrame fractionLeft: 0 top: 0 right: 0.5 bottom: 0.5).
    form frame topOffset: 5;
        rightOffset: -5;
        bottomOffset: -5;
        leftOffset: 5.
    form addComponentsFromClass: TestForm.
    ... 

This has the effect of loading the user interface defined in TestForm into the upper left quarter of the application’s user interface.

Menu and Tool Bars[]

Two common, and useful, additions to a windows are menubars and toolbars. These are added to the top of a window for giving quick mouse access to operations supported by the application.

The Pollock GUI framework provides simple mechanisms for adding these items to a window: the getMainMenu and getToolInventory methods.

Menubar[]

A menubar displays a collection of menus, each of which contains items, and possibly submenus of items, that provide direct access to operations supported by an application. A user accesses the menu items using a mouse or similar pointing device, or by key sequences (if configured).

A window’s menubar is created from the Menu returned by the getMainMenu method in the UserInterface subclass. In this section we illustrate some basic methods of defining the menubar in this method. For more on menus in general, including modifying them during runtime, refer to (*** someplace else ***) .

A menubar starts with one or more menu labels, which are displayed on the bar itself. To create these, simply add these as instances of MenuItem. Create the Menu instance by sending a menuItems: message to the class, with an Array of MenuItem instances as the argument. Each MenuItem instance is created with a label: message to provide the label on the menubar. For example,

getMainMenu
    ^Menu menuItems: (Array
        with: ( MenuItem label: 'File' )
        with: ( MenuItem label: 'Edit' ) ) .

All this does is create the menubar and add two menu labels to it. Each item should then have a menu of additional items, which is added by providing each item with a submenu. To add a submenu, send a MenuItem a submenu: message, with another Menu as argument:

getMainMenu
    ^Menu menuItems: (Array
        with: ( ( MenuItem label: 'File' )
            submenu: ( Menu labels: #( 'New' 'Open' 'Close' ) ) )
        with: ( ( MenuItem label: 'Edit' )
            submenu: ( Menu labels: #( 'Copy' 'Cut' 'Paste') ) ) ).

Finally, those submenu items need actions. The simplest situation is to provide arrays of message selectors for the labels. The message selectors must invoke methods defined in the UserInterface subclass, or inherited from a superclass.

getMainMenu
    ^Menu menuItems: (Array
        with: ( ( MenuItem label: 'File' )
            submenu: ( Menu labels: #( 'New' 'Open' 'Close' ) 
                actions: #( #newFile #openFileDialog #closeFile ) ) )
        with: ( ( MenuItem label: 'Edit' )
            submenu: ( Menu labels: #( 'Copy' 'Cut' 'Paste') 
                actions: #( #copyText #cutText #pasteText ) ) ) ).

This is a simple menubar. The Menu defining the menubar can be more complex, including addition features, such as icons and groups, shortcut keys, further submenus, and enabled/disabled items, as well as using UserMessage instances for the labels instead of strings. For implementing these features, refer to (*** that other place ***).

Toolbars[]

A toolbar provides easy access to an application’s most features. A toolbar commonly contains buttons, each with an icon indicating the function it invokes, but can also contain drop-down lists and input fields. By convention, a toolbar is placed within a window’s borders, just below the manu bar, if there is one.

Creating a Toolbar with Buttons[]

To create a toolbar and add it to your GUI, add a getToolInventory method to the user interface class. The method must return an instance of ToolInventory, which simply contains a collection, or inventory, of ToolItem instances. Similar to the simple method of creating a Menu, you create a ToolInventory by sending it a toolItems: message with a collection of ToolItem instances as argument:

getToolInventory
    ^ToolInventory toolItems:
        ( Array with: ( ToolItem image: self cutIcon
                action: #cutText )
            with: ( ToolItem image: self pastIcon
                action: #pasteText ) ) )

This example adds two buttons with icons to the toolbar. Each ToolItem is created by specifying an icon to display on the button and an action to perform when the button is pressed. In this case, the images are returned by messages sent to the user interface itself and the actions are identified as message selectors to be sent to the user interface.

Adding toolbar items to the Array as shown above works for up to four items. There are various alternatives when the toolbar get larger, including alternative Array creation methods. However, ToolInventory instances respond to the addToolItem: message. Modifying the above example, we can build the toolbar as follows:

getToolInventory
    | toolInventory |
    toolInventory := ToolInventory new.
    toolInventory addToolItem: ( ToolItem image: self cutIcon
            action: #cutText );
        addToolItem: ( ToolItem image: self pastIcon
            action: #pasteText ).
    ^toolInventory. 

Grouping Items[]

When you have several toolbar buttons, it becomes useful to group those items. Usually the groups will correspond to the menus on which the equivalent item occurs. Groups are indicated by additional space and, depending on the platform, a seperator line.

For example, above we created a menu bar with two menus, each with three items: New, Open, and Close under the File menu, and Copy, Cut, and Paste under the Edit menu. It is common to have toolbar items for these actions as well, but instead of a contiguous line of six buttons, we might prefer to group them, and to do so in the same way we did in the menus.

The simplest way is to add the tool items and then specify the size of the groups, in order. To do this, send a setGroupSizes: to the ToolInventory with a collection of integers specifying the sizes. In the case described, there are two groups of three items each:

getToolInventory
    | toolInventory |
    toolInventory := ToolInventory new. 
    toolInventory 
        addToolItem: (ToolItem image: self newIcon action: #newFile);
        addToolItem: (ToolItem image: self openIcon action: #openFile);
        addToolItem: (ToolItem image: self closeIcon action: #closeFile);
        addToolItem: (ToolItem image: self cutIcon action: #cutText );
        addToolItem: (ToolItem image: self pasteIcon action: #pasteText);
        addToolItem: (ToolItem image: self copyIcon action: #copyText).
    toolInventory setGroupSizes: #(3 3).
    ^toolInventory

This is not a great way to set group sizes. From an object-oriented perspective, it assumes a great deal of knowledge of the structural details of the toolbar (which, of course we have). But, during design, every time you add, remove, or rearrange buttons, you also have to change the group size settings.

A better way is to allow the ToolInventory to determine the group sizes, by building the toolbar from subordinate tool inventories. To do this, create the top tool inventory, and add groups as additional tool inventories with the addToolInventory: message:

getToolInventory
    | toolInventory |
    toolInventory := ToolInventory new. 
    toolInventory 
        addToolInventory: ( ToolInventory toolItems: 
            (Array with: (ToolItem image: self newIcon action: #newFile)
                with: (ToolItem image: self openIcon action: #openFile)
                with: (ToolItem image: self closeIcon action: #closeFile)));
        addToolInventory: (ToolInventory toolItems:
            (Array with: (ToolItem image: self  cutIcon action: #cutText )
                with: (ToolItem image: self pasteIcon action: #pasteText)
                with: (ToolItem image: self copyIcon action: #copyText))).
    ^toolInventory

This example produces the same result as the previous, but with the added advantage that the groups are themselves specified as ToolInventory instances. These sub-inventories could easily be defined in separate methods and invoked in getToolInventory to assemble the toolbar.

Assigning an Identifier[]

You will need to access some items on the toolbar during runtime. This is more common for input field and drop-down lists, described below, than for buttons, but may be necessary for buttons as well. For example, you may need to enable or disable a button depending on the application state.

To access the item, it needs to have an ID. In the toolbar, you do this by assigning it a nameKey, which identifies it in the toolbar inventory. The nameKey is a Symbol, unique in that toolbar. To assign the nameKey, send a nameKey: message to the ToolItem with a Symbol:

For example, to assign a nameKey to the New File button in the example above, modify the code adding the following:

getToolInventory
    | toolInventory |
    toolInventory := ToolInventory new. 
    toolInventory 
        addToolInventory: ( ToolInventory toolItems: 
            ( Array with: ( ( ToolItem image: self newIcon action: #newFile ) 
                    nameKey: #newButton; yourself )
    ...

Concluding this addition with yourself simply ensures that it is the receiver of the nameKey: message, the ToolItem instance, that is added to the tool inventory.

Toolbar DropDown List[]

A drop-down list is occassionally useful on a toolbar. There are several ToolItem instance creation methods for adding a drop-down list, with varying attributes specified for the item. As a ToolItem, the instance can be added to the toolbar in the same way as are buttons.

To create a drop-down list toolbar item, send one of the variants of dropDownListWith: to ToolItem. Browse the instance creation methods for the specific messages. The keywords and arguments are:

dropDownListWith: aSymbolOrObservedListOrValuable

Specifies the list representing choices for the drop-down list. A Symbol is sent to the user interface as a unary message selector. An ObservedList is an instance of ObservedList. A Valuable can be an evaluable expression. If the evaluable expression is a one-argument block, the user interface is sent as the argument value. In all cases, the return value is expected to be an ObservedList.

initialValue: aSymbolOrStringOrTextOrValuable

Specifies the initially displayed text or item. A Symbol is sent to the user interface as a unary message selector. A String or Text (or AdvancedText) sets the display text (such as ‘-choose one-’). A Valuable is any evaluable expression that returns a text or a selection in the ObservedList.

label: aBlockOrStringOrUserMessageOrTextOrNil

Specifies a label to be shown to the immediate left of the input field.

width: anIntegerOrNil

Specifies the width of the list display part of the dropdown box, in pixels. If set to nil, the default width is 30.

action: anEvaluableAction

This keyword specifies an action to perform when the user performs an action on the dropdown list (enter, delete, or select). anEvaluableAction may be a method selector or a block closure.
    getToolInventory
        | toolInventory |
        toolInventory := ToolInventory new. 
        ...
        toolInventory addToolItem: 
            ( ( ToolItem dropDownListWith: #list
                initialValue: ( self list at: 1 )
                label: 'Choose' 
                width: 75
                action: #updateSelection ) nameKey: #dropList; yourself ). 
        ...
        ^toolInventory

The choices for the list can also be returned by the choices: message (*** need to test and document this alternative *** )

The drop-down list is assigned its model by the list method, which returns an ObservedList. The initial value can be a String or Text or, as illustrated here, one of the list items. Again, an method selector, updateSelection, is assigned to be sent to the user interface when an action is performed by the user in the drop-down list.

In other respects, the toolbar drop-down list behaves as does the DropDownList widget. Refer to that description for additional examples.

Toolbar Input Field[]

An input field is occassionally useful on a toolbar. There are several ToolItem instance creation methods for adding an input field, with varying attributes specified for the item. As a ToolItem, the instance can be added to the toolbar in the same way as are buttons.

To create an input field toolbar item, send one of the variants of inputFieldWith: to ToolItem. Browse the instance creation methods for the specific messages. The keywords and arguments are:

inputFieldWith: aSymbolOrStringOrTextOrValuable

Specifies the display value of the input field. A String or Text value simply provides the text to display. A Symbol is sent to the user interface as a unary message selector. A Valuable can be any evaluable expression. If the evaluable expression is a one-argument block, the user interface is sent as the argument value. In all cases, the return value is expected to be a String, Text, AdvancedText or UserMessage.

label: aBlockOrStringOrUserMessageOrTextOrNil

Specifies a label to be shown to the immediate left of the input field.

width: anIntegerOrNil

Specifies the width of the input field, in pixels. If set to nil, the default is 30.

action: anEvaluableAction

Specifies an action to perform when the user performs an action in the input field (enter or delete). anEvaluableAction may be a method selector or a block closure.

For example, this code adds both an input field and a drop-down list, and sets each of the parameters:

getToolInventory
    | toolInventory |
    toolInventory := ToolInventory new. 
    ...
    toolInventory addToolItem: 
        ( ( ToolItem inputFieldWith: #field
                label: 'Find:' 
                width: 100 
                action: #updateInput ) nameKey: #inputField; yourself ).
    ...
    ^toolInventory

The input field is assigned a model returned by the field message, which returns a String or Text. It also sends the message updateInput when some action is performed by the user in the input field. The nameKey provides the id for this widget, which will be used to retrieve the model’s value.

In other respects, the toolbar input field behaves as does the InputField widget. Refer to that description for additional examples.

Setting Visual Options[]

Miscellaneous Things[]

Icons[]

Icons are useful decorations in a variety of contexts. We have used them, for instance, to decorate a window and identify it as belonging to our application. They are also useful for labels on buttons and other widgets, and for list items. This section describes a bit more about how to set them up.

(See the window section for how to create one, and copy it here, with more detail. This had been missing in some earlier GUI doc.)

Creating an Icon[]

You can create an icon using the Image Editor.

For icons with a transparent background, set the background color of the image to black.

Window icons should be 32 by 32 pixels.

For toolbar icons, the size depents on the platform which determines the toolbar height. In general, a size of 20 x 20 works for all platforms.

Registering an Icon[]

Icons that are reused frequently can be registered with the Icon class, which stores icons in a dictionary.

To register an icon, send constantNamed:put: to the Icon class, with a symbol for its name and the icon as arguments:

| myIcon |
    myIcon := Icon new 
        figure: VisualLauncher CGHelp24shape: VisualLauncher CGHelp24Mask.
    myIcon cleanFigure.
    Icon constantNamed: #HelpIcon put: myIcon

To later retrieve the icon, send constantNamed: to the Icon class:

| win |
    win := ScheduledWindow new.
    win icon: ( Icon constantNamed: #HelpIcon).
    win open

Installing an Icon[]

Instances of ScheduledWindow and its subclasses store an icon in their icon instance variable. To set an icon for a window, send an icon: message to the window with the icon to use:

| win |
win := ScheduledWindow new.
win icon: ( Icon constantNamed: #HelpIcon).
win open

Windows other than instances of ScheduledWindow or its subclasses do not hold onto the icon, and must ensure that the icon persists as long as the window does. Registering the icon is sufficient.

Advertisement