[ Bevel Buttons ]

Bevel Button Widget


Writing Widgets
Bevel Button / Styled Text Editor
Exercises

With the introduction of System 8, a number of additional standard controls were introduced. One of these new controls was the beveled button; they behave like regular buttons, but have a rectangular 3D appearance, rather than the usual rounded rectangle. They come in three types with varying levels of apparent relief. We'll subclass the W.Button class to display these new controls, instead of the standard push button.

Firstly we need to import the W module to gain access to W.Button class, the Ctl module to access the Control Manager toolbox calls, and the Controls module to access the Control Manager constants. We also need Qd for drawing the default ring, and a utility routine from Wbase which helps with the beveled appearance.

import W
import Ctl
import Controls
import Qd

from Wbase import _darkencolor

The first class we define will be a standard bevel button, which subclasses the Button class:

class NormalBevelButton(W.Button):

Each of the the bevel buttons is created by a different control definition procedure. To make it easier to create the other types of bevel button, we'll define the control procedure as a class variable:

    procID = Controls.kControlBevelButtonNormalBevelProc

Most of Button's methods will work just fine with our new button class, but Button.__init__ assumes that the control definition procedure is pushButProc. We need to override that to use the correct control definition:

    def __init__(self, possize, title = "BevelButton", callback = None):
        W.ControlWidget.__init__(self, possize, title, self.procID, callback, 0, 0, 1)
        self._isdefault = 0

The change of shape means that we also need to change what happens if the button is the default button of a window. Normally the Appearance Manager would take care of this automatically, but the interface is not yet implemented in the Ctl module. Instead we will have to draw the default ring by hand around the button.

The method which takes care of this is drawfatframe(). Again, the change is fairly straightforward (although it looks complex because of the drawing commands):

    def drawfatframe(self, onoff):
        # draw a platinum appearance default box
        color = (0xe000, 0xe000, 0xe000)
        (l, t, r, b) = Qd.InsetRect(self._bounds, -4, -4)
        if onoff:
            # Draw the frame
            Qd.FrameRect((l, t, r, b))
            Qd.RGBForeColor(color)
            Qd.PaintRect((l+1, t+1, r-1, b-1))
            Qd.RGBForeColor(_darkencolor(color))
            Qd.MoveTo(l+2, b-2)
            Qd.LineTo(r-2, b-2)
            Qd.LineTo(r-2, t+2)
            Qd.RGBForeColor(_darkencolor(color))
            (l, t, r, b) = Qd.InsetRect(self._bounds, -1, -1)
            Qd.PaintRect((l, t, r, b))
            Qd.RGBForeColor((0, 0, 0))
            Qd.RGBForeColor(_darkencolor(color))
            Qd.MoveTo(l, b-1)
            Qd.LineTo(r-1, b-1)
            Qd.LineTo(r-1, t)
        else:
            # Erase the frame
            Qd.RGBForeColor((0xffff, 0xffff, 0xffff))
            Qd.PaintRect((l, t, r, b))
        Qd.RGBForeColor((0, 0, 0))

However, because we were lazy and used Qd.PaintRect rather than Qd.FrameRect to draw the fat frame, the frame draws over the top of the button. Fixing this is simple, but requires rewriting a few routines to make sure the button control is drawn after the default ring.

    def enable(self, onoff):
        if self._control and self._enabled != onoff:
            self._enabled = onoff
            if self._isdefault and self._visible:
                self.SetPort()
                self.drawfatframe(onoff)
            self._control.HiliteControl((not onoff) and 255)
    
    def activate(self, onoff):
        self._activated = onoff
        if self._enabled:
            if self._isdefault and self._visible:
                self.SetPort()
                self.drawfatframe(onoff)
            self._control.HiliteControl((not onoff) and 255)

    def draw(self, visRgn = None):
        if self._visible:
            if self._isdefault and self._activated:
                self.drawfatframe(self._enabled)
            self._control.Draw1Control()
    
    def _setdefault(self, onoff):
        self._isdefault = onoff
        if self._control and self._enabled:
            self.SetPort()
            self.drawfatframe(onoff)
            self.draw()

We've now done all the hard work. Defining classes for the other two types of bevel button is as simple as:

class SmallBevelButton(NormalBevelButton):
	ProcID = Controls.kControlBevelButtonSmallBevelProc

class LargeBevelButton(NormalBevelButton):
	ProcID = Controls.kControlBevelButtonLargeBevelProc

You can view the code put together into a complete module. Run it as main in the IDE to see it in action.

These classes could be improved in a number of ways - to take the overall theme into account when drawing the default frame, for example - but they do illustrate the process involved in writing your own widget classes.

(XXX SmallBevelButton and LargeBevelButton look the same as NormalBevelButton, despite the correct ProcID being passed, on my computer. Is Appearance Manager somehow not set up correctly?)

Exercises

  1. By changing the parameter minvalue passed to ControlWidget.__init__() to kControlBehaviorToggles or kControlBehaviorSticky, a bevel button can act like a check box or a radio button, respectively. Subclass the RadioButton and CheckBox widgets to use bevel buttons.
  2. Fix the drawfatframe() routine, so that the other routines do not need to be changed.