Checkbox Tip

This is a short post to help with questionnaire type applications, specifically:

  • how to align checkboxes to the top of the text?
  • how do you avoid accidentally selecting questions when scrolling?
  • how to increase touch sensitivity?

I find the built in list controls a little problematic in this regard, and the checkbox itself is not too flexible. FMX, however, is very flexible.

An Explanation of Method

Essentially, this is what we are trying to achieve:

There is a base layout, its hittest property is set to false so scrolling gestures pass through to the parent control. It is also aligned to top, so we can stack it vertically.

Next we create a second layout, aligned to the left, and we set its width to 50, and we add it to our base layout. This creates a column divide for us. We set its hittest to true so we can capture the tap and click events to toggle our checkbox.

The checkbox is added to the second layout, positioned to the top left as desired, and we set its hittest to true. This allows the checkbox to look more consistent with the underlying platform, and to participate in any related special effects. For example, the highlighting on hover in Windows 10:

Lastly we add the label to the base control, it too has its hittest property set to false.

This effectively gives us two columns, one for selecting the checkbox – a wide area for touch – and the much larger column containing the question text and used for scrolling. We can easily toggle the checkbox, and smoothly scroll the questions without risk of accidentally setting the checkbox.

One of the problems with using a multi-line label, is that it is difficult to avoid a line beginning with a space on a word wrap. I find setting the label alignment to client, after the layout size has been calculated, to help somewhat. The only real solution is to find a component that trims whitespace at the start of a row.

The Code

This is the complete function which does the layout:

function TQuizFrame.CreateQuestion(id: integer; text: string) : TCheckBox;
  // base layout - the row
  var baseLayout            := TLayout.Create(Scroller);
  baseLayout.Parent         := Scroller;
  baseLayout.Align          := TAlignLayout.Top;
  baseLayout.Margins.Left   := 4;
  baseLayout.Margins.Top    := 8;
  baseLayout.Margins.Right  := 16;
  baseLayout.Margins.Bottom := 8;
  baseLayout.Tag            := id;
  baseLayout.HitTest        := false;

  // layout to arrange the checkbox - left column (selection)
  var checkboxLayout         := TLayout.Create(baseLayout);
  checkboxLayout.Parent      := baseLayout;
  checkboxLayout.Align       := TAlignLayout.Left;
  checkboxLayout.Size.Width  := 50;
  checkboxLayout.HitTest     := true;
  checkboxLayout.OnTap       := LayoutOnTap;
  checkboxLayout.OnMouseDown := LayoutMouseDown;

  // the checkbox
  var box := TCheckBox.Create(checkboxLayout);
  box.Parent        := checkboxLayout;
  box.Width         := 20;
  box.Height        := 20;
  box.Position.Y    := 3;
  box.Position.X    := (checkboxLayout.Width - 20) / 2.0 - 3;
  box.HitTest       := true;

  checkboxLayout.TagObject := box;

  // the label - right column (scrolling)
  var lbl := TLabel.Create(baseLayout);
  lbl.Parent                 := baseLayout;
  lbl.Position.X             := checkboxLayout.Width;
  lbl.Position.Y             := 2;
  lbl.Width                  := Scroller.Width - checkboxLayout.Width - 16;
  lbl.TextSettings.VertAlign := TTextAlign.Leading;
  lbl.TextSettings.WordWrap  := true;
  lbl.AutoSize               := True;
  lbl.Text                   := text;
  lbl.HitTest                := false;


  baseLayout.TagObject := box;
  baseLayout.Height    := lbl.Height + 16;

  // align the label now the layout is set - slight improvement
  lbl.Align := TAlignLayout.Client;

  Result := box;

And the two wired up event handlers:

procedure TQuizFrame.LayoutOnTap(Sender: TObject; const PointF: TPointF);
  var checkbox := ((Sender as TLayout).TagObject) as TCheckBox;
  checkbox.IsChecked := not checkbox.IsChecked;

procedure TQuizFrame.LayoutMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Single);
  var checkbox := ((Sender as TLayout).TagObject) as TCheckBox;
  checkbox.IsChecked := not checkbox.IsChecked;

The Screenshots

The following screenshots are from a Pixel 4a, the left image is using a wide left column and places the checkbox is to the top left. The right image places the checkbox aligned to the top center.

Personally I prefer the smaller width for the first column. In order to avoid accidental selection we prevent scrolling in the first column, but we should minimize the impact of this privation. If the first column is too wide, the user will be frustrated that scrolling doesn’t work. But we need it wide enough to easily touch. I also prefer the checkbox aligned horizontally to the center.

I hope this tip helps.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s