0

I have a Checkbox1 that I would like to customize that when user clicks on Caption (text) of checkbox it doesn't change it's state (checked/unchecked), but only it does when clicked on the actual checkbox square.

Here is current code with 2 global variables, one that says when to Skip changing state and one that remembers current state and makes sure it stay the same after OnClick - because the state is already changed when flow is in OnClick:

var
  gSkipClick:boolean = false;
  gPrevState:boolean;

procedure TForm1.CheckBox1Click(Sender: TObject);
begin
  // Make sure previous state is assigned when Skip
  if gSkipClick then
    Checkbox1.Checked := gPrevState;
end;

procedure TForm1.CheckBox1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  if (x > 12) then
  begin
    // Click outside checkbox square
    gSkipClick := True; // skip Click
    gPrevState := CheckBox1.Checked; // save current state
  end
  else
    gSkipClick := False;  // enable Click
end;

Now I wanted to do this with 2 new properties in TCheckBox that would replace global variables:

TCheckBox = class(Vcl.StdCtrls.TCustomCheckBox)
    private
      FSkipStateChange:Boolean;
      FPrevState:Boolean;
    protected
    published
      property SkipStateChange:Boolean read FSkipStateChange write FSkipStateChange;
      property PrevState:Boolean read FPrevState write FPrevState;
  end;

procedure TForm1.CheckBox1Click(Sender: TObject);
begin
  // Click outside checkbox square
  If TCheckBox(Checkbox1).SkipStateChange Then
    Checkbox1.Checked := TCheckBox(Checkbox1).PrevState;
end;

procedure TForm1.CheckBox1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  if (x > 12) then
  begin
    // Click outside checkbox square
    TCheckBox(Checkbox1).SkipStateChange := True;
    TCheckBox(Checkbox1).PrevState := Checkbox1.Checked;
  end
  else
    TCheckBox(Checkbox1).SkipStateChange := False;
end;

And it works, but if I click on Caption and then close form, this error occurs:

Project Project1.exe raised exception class $C0000005 with message 'access violation at 0x004080c5: read of address 0x0000000d'.

The error occurs in procedure TMonitor.Destroy; in System unit:

procedure TMonitor.Destroy;
begin
  if (MonitorSupport <> nil) and (FLockEvent <> nil) then { <-- ERROR ON THIS LINE}
    MonitorSupport.FreeSyncObject(FLockEvent);
  FreeMem(@Self);
end;

What am I doing wrong, why does error occur?

Mike Torrettinni
  • 1,816
  • 2
  • 17
  • 47
  • 1
    A checkbox isn't always 12 px wide. Your code will work very poorly on systems where the checkbox width is different. In addition, I think you should add the logic to your checkbox class. – Andreas Rejbrand Aug 17 '16 at 23:57
  • I did not consider this, yet. i assume you are talking about different Windows versions (XP...10) right? – Mike Torrettinni Aug 18 '16 at 00:03
  • Indeed. And different themes. And, possibly, different DPI values. Anyhow, if I were you, I would create a new custom control with a checkbox (without caption) and a static text next to it. If I'd do this at all -- users expect to be able to click the caption, and not to be forced to hit a tiny square with the mouse. – Andreas Rejbrand Aug 18 '16 at 00:03
  • The reason is that when they click on caption they will get explanation, details of a report that is run when is checked. Right now when they want to see the explanation it changes state every time they click on it. This is how TCheckListBox works - click on caption it doesn't automatically change the state. – Mike Torrettinni Aug 18 '16 at 00:13
  • @AndreasRejbrand "I *think* you should add the logic to your checkbox class." No doubt about it, this must be done. It makes no sense at all to partially implement something in a custom class and the rest of it outside of the class. – Jerry Dodge Aug 18 '16 at 00:59
  • @Mike "The reason is that when they click on caption they will get explanation..." Then that's all the more reason to consider two separate controls. Then, you don't even need to create your own class or custom functionality. Just drop a checkbox with the same width as the height (and no caption), and then drop a label right next to it. Done. – Jerry Dodge Aug 18 '16 at 01:01
  • 1
    Other programs that want to show additional information about a control will typically include an *extra* UI element beside the control, such as an "info" image. Please don't reduce usability of the check boxes as you propose. Web sites do that by default, and it's awful. – Rob Kennedy Aug 18 '16 at 01:03
  • Am just trying to customize checkbox to behave similar as is default behavior of TCheckListBox, maybe not perfect, but this is default Delphi control. And also would like to avoid now having 2 controls instead of 1 checkbox. Well, if this is possible. – Mike Torrettinni Aug 18 '16 at 01:07
  • 1
    You wouldn't have two controls. You'd have one that combined the two. And your logic for doing this is flawed - you're changing the behavior of a standard UI component to make it behave in a way other than what users expect. My users would be really upset, because they know how Windows work and don't expect to jump through hoops to work with their apps. If you need to do something different than what a checkbox does normally, don't use a checkbox - use a more appropriate control. – Ken White Aug 18 '16 at 02:50
  • 1
    Don't cripple the checkbox itself! Make a new composite control, consisting of the no-caption checkbox and an associated TLabel. Bring them into one by TFrame or semi-official Custom Containers Pack – Arioch 'The Aug 18 '16 at 09:54

1 Answers1

5

First the answer to the question asked about the runtime error. Your interposer class needs to be declared before your form class. You have not done that as can be guessed by your need to cast. You write

TCheckBox(Checkbox1)

but if the interposer was declared correctly you could write

CheckBox1

with no cast.

Look again at every code example for the interposer pattern. The interposer class is declared before any class that uses it.

Because the interposer is declared after the form in your code, the streaming mechanism is instantiating the original class and not the interposed class. Hence your casts are invalid and lead to runtime memory corruption.

It is not enough to define a type and cast to it. You must ensure that this type is instantiated. An object's type is determined when it is instantiated.

As a rule you should always check your casts with is or as. Had you done so here the system could have told you what was wrong. Although you should have been asking yourself why you had to cast in the first place. That should have been the warning signal. Remember that you can always suppress compiler errors by casting but doing so does not solve the problem. Casting something to be something it is not does not actually make it so. It just tells a big fat lie to the compiler and eventually it gets its revenge!


For an interposer you should derive from Vcl.StdCtrls.TCheckBox rather than Vcl.StdCtrls.TCustomCheckBox.


Mixing the code between a derived interposer class and the form is very poor design. When you want to do something similar on a different form are you really going to copy the code again? Code like this needs to be self-contained in the control.

On the other hand maybe the logic is specific to the form. In which case it should live there and you would use the standard check box. It fills me with woe that you refer to using global variables. If you need variables associated with a form, add them to the form class.


The commenters who criticise your UI design are correct in my view. Users like predictability. Nobody will ever be upset when a control behaves in the way they expect, in the way that every other similar control in the system behaves. Stop tinkering with standard controls, use them, let the users be able to predict your program's behaviour.

Tom Brunberg
  • 20,312
  • 8
  • 37
  • 54
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Now it works, when I put the class before form's class! This was my first attempt to customize control on my own, I guess I have more learning to do. – Mike Torrettinni Aug 18 '16 at 10:35