Here is my first cut at implementing semi-lightweight labeled controls.
Bill Schwab had made a comment sometime recently about labeled text edits and that got me thinking about it, or rethinking, because this is something that I've run into quite a bit over the years. That is, the tedium of creating forms where you're repeatedly adding, resizing, and repositioning controls (of whatever kind) and their associated labels (static text). Having two windows controls for each entry field also seems excessive, not just in terms of Windows resources, but in view complexity for the developer. If you have a window with eight fields on it, it's easier to look at and work with it if you see eight controls, rather than sixteen. My initial approach to this problem is to create a subclass under TextEdit, called LabeledTextEdit, give it label and alignment aspects and have the control itself draw its label. The label is just a simple string, drawn in the font of its associated control. The alignment can be to the left of the control (centered vertically or at the top) or on top of the control (at the left or centered in the middle). This gives #leftCenter, #leftTop, #topLeft, and #topCenter, though I suppose one could easily argue that each and every end and midpoint of the control's rectangle, both inside and outside, should be included. Since we'll need to draw, erase and redraw the label, we need to track its location and size, which means a private instance variable that will hold its bounding rectangle. Here's the class definition and comment. TextEdit subclass: #LabeledTextEdit instanceVariableNames: 'label labelRect labelAlignment' classVariableNames: '' poolDictionaries: '' classInstanceVariableNames: '' This class adds a string label to its Windows control. The label has a bounding rectangle and it may be aligned in various ways, relative to its Windows control. Instance Variables: label <string> label to be displayed with the receiver. labelRect <rectangle> the bounding rectangle of the label, relative to its parent view. labelAlignment <symbol> e.g., #topLeft (above the control, on the left side), #topCenter (above the control, in the middle), #leftTop (to the left of the control, at the top), and #leftCenter (to the left of the control, in the middle). On the class side, add the following so that label and labelAlignment aspects show up in the ViewComposer: labelAlignmentAspects "Answer the set of symbols that an instance of the receiver can align its label to." ^#(#leftTop #leftCenter #topLeft #topCenter) publishedAspectsOfInstances "Answer a Set of the aspects published by instances of the receiver" ^super publishedAspectsOfInstances add: (Aspect string: #label); add: (Aspect choice: #labelAlignment from: self labelAlignmentAspects); yourself Initially I had an #initialize method for the instance variables but I decided to go with lazy initialization because I might want to generalize the class and incorporate it directly in TextEdit (or even ControlView), rather than as a subclass. This also meant adding default methods for the label name and alignment. Note that labelRect is a private instance variable so there are no accessors. defaultLabel "Answer the default label text of the receiver." ^'Label:' defaultLabelAlignment "Answer the default alignment of the receiver's label." ^#leftCenter label "Answer the label of the receiver." ^label notNil ifTrue: [label] ifFalse: [self defaultLabel] label: aString "Set the label of the receiver to aString." self eraseLabel. label := aString. self drawLabel labelAlignment "Answer the alignment of the label." ^labelAlignment notNil ifTrue: [labelAlignment] ifFalse: [self defaultLabelAlignment] labelAlignment: aSymbol "Set the alignment of the label." self eraseLabel. labelAlignment := aSymbol. self drawLabel The drawing methods are as follows: eraseLabel "Private - erase (invalidate) the rectangle containing the receiver's label." labelRect notNil ifTrue: [ self parentView invalidateRect: labelRect. self invalidate] "This line seems needed to force the above redraw." drawLabel "Private - Draw the receiver's label." | canvas | (canvas := self parentView canvas) isNil ifTrue: [^nil]. canvas setBkMode: TRANSPARENT; font: self labelFont. (labelRect := Rectangle origin: self position extent: (canvas textExtent: self label)) moveBy: (self perform: (self labelAlignment, 'LabelOffset') asSymbol). canvas text: self label at: labelRect origin. canvas free Notes on the above: labelRect holds the current bounding rectangle of the label, relative to the parent. Since alignment is always outside the bounds of the Windows control (above or to the left), to erase the label we need to invalidate the portion of the parent's view that labelRect occupies. To draw the label we recalculate labelRect and then draw the text within the new labelRect bounds. Recalculating labelRect is done by first positioning labelRect at the control's origin, giving it an extent that's the label's computed extent, and then moving it to the correct alignment position. Helper methods that #drawLabel uses are: leftCenterLabelOffset "Private - return a point that is the distance from the Windows control's origin to the label's origin." ^labelRect width negated @ ((self rectangle height - labelRect height) // 2) leftTopLabelOffset "Private - return a point that is the distance from the Windows control's origin to the label's origin." ^labelRect width negated @ 0 topCenterLabelOffset "Private - return a point that is the distance from the Windows control's origin to the label's origin." ^((self rectangle width - labelRect width) // 2) @ labelRect height negated topLeftLabelOffset "Private - return a point that is the distance from the Windows control's origin to the label's origin." ^0@labelRect height negated labelFont "Answer the label's font." ^self font notNil ifTrue: [self font] ifFalse: [self parentView font notNil ifTrue: [self parentView font] ifFalse: [SmalltalkSystemShell defaultFont]] The label font is derived from the Windows control's font. I'm not sure if #labelFont above is entirely correct and/or could be simplified, but it seems to return reasonable results (on my screen at least). Finally, the event methods. WmPaint: is used to draw the label when it needs updating (e.g., another window that covered it is gone or has been moved). OnPositionChanged: is used to redraw the label (erase the old and draw the new) when the Windows control is moved or resized. wmPaint: message wParam: wParam lParam: lParam "Private - Controls do their own painting so only allow the default." self drawLabel. ^super wmPaint: message wParam: wParam lParam: lParam onPositionChanged: aPositionEvent "Handle a window position change event (move or resize). Implementation Note: Labeled controls are partially transparent and do not redraw some of their area, so when they are moved they leave behind traces of their former label. We therefore erase the label now." self eraseLabel. ^super onPositionChanged: aPositionEvent To add a resource for the new class, evalute: LabeledTextEdit makeResource: 'Labeled text' inClass: TextPresenter. To try it out, bring up ViewComposer on a new or existing shell or dialog and then add a TextPresenter.Labeled text resource to it (or mutate a TextEdit to a LabeledTextEdit). You should see 'Label:' appear on the left side of the textedit portion and you can change the label and labelAlignment aspects directly in the VC. Move or resize the control and the label moves with it. You can also subclass other controls using the exact code above (except for the class names of course). I did this with ListView, ListBox, ComboBox, DateTimePicker, and MultilineTextEdit. The only changes I made were to defaultLabel and defaultLabelAlignment in some of the classes. (LabeledListView and LabeledListBox's default alignment is #topCenter, default label is 'List'; LabeledDateTimePicker's defaultLabel is 'Date:'; and LabeledMultilineTextEdit's default alignment is #leftTop.) One added bonus to all of this is that when I look at a form in the VC, on the lower left side (View Hierarchy) I see a list of my controls without the "extraneous" static textedits cluttering up the list. See http://www.sirius.com/~lsumberg/Dolphin/LabeledTransDlg.jpg for an example (7 controls instead of 12). There are a couple of drawbacks that I know of. For one thing, there are no accelerator keys for the labels. For example, with a static textedit whose text is '&Name', you can go directly to the control next to it by typing Alt-N. You can't do that with LabeledTextEdit and I have no idea if it can be implemented in this design. For some people this could be a killer requirement. (Hmm, on second thought, it might not be too hard to do this -- e.g., trap keystrokes to the control and send them to some accelerator object that says change focus to that other control or go on with that keystroke.) Is this important? Any ideas on how to implement it? The other thing is that these labeled controls do not display well with most layouts since the label is outside the known boundary of the control. For example, a LabeledTextEdit that has a #north arrangement and #leftCenter labelAlignment will have the text edit portion filling the upper part of the parent view, but its label will be off the view, all the way to the left. Similarly, if its arrangement is #east, the label will print on whatever view is arranged in the center. For me this has not been much of a problem since for most forms that I use static labels on, I use a FramingLayout and these labeled controls seem to work fine with that. Still, if there were a way to work around this, where the containers or layouts recognize the labeled offsets, other layouts could be used. (Hmm again ... I've tinkered a bit with one approach that works. See http://www.sirius.com/~lsumberg/Dolphin/LabeledAcme.jpg for an example -- notice there are no container views.) Again, this could simplify views by reducing the number of controls needed in each window. Some thoughts on bells and whistles include adding a labelFont aspect (real easy to do) and adding wordwrap/multiline capability. Note that the code above is for Dolphin Version 4 -- a few small changes are needed to make it run in earlier versions. As always, comments, discussion, suggestions and such are verrrry welcome! If I'm reinventing the wheel (it's hard to believe no one has done this before) or offbase, please let me know. -- Louis P.S. You can download a package of labeled controls from http://www.sirius.com/~lsumberg/Dolphin/LabeledControlsV4.pac and then install them through the package browser. If you have trouble adding the LabeledListView resource to a view, do the following: Bring up a new VC, open a ListPresenter.Enhanced list view, mutate it to a LabeledListView, evaluate the following in the Workspace at the lower right: self primaryColumn parent: self and Save As ListPresenter.LabeledListView. You can also just edit an existing view that you have and wherever you have a static text, mutate the associated windows control to its labeled equivalent (and delete the static text *s*). |
Free forum by Nabble | Edit this page |