check box in TListView header column - How to prevent it from stealing focus?
Asked Answered
N

1

8

This is related to the question How to show a check box in TListView header column?.

I want to use the code from this answer by @Sertac Akyuz. (I need this to work in WinXP also)

But I want to make the header CheckBox to not steal focus from the ListView or other active controls.

A fast workaround is to set the focus always to the ListView in the ListHeaderWndProc:

...
FListHeaderChk.Checked := not FListHeaderChk.Checked;
ListView1.SetFocus;
// code that checks/clears all items

But this is kinda ugly. Because the CheckBox is first focused and then the focus goes back to the ListView. also If I click the CheckBox and drag the mouse outside the CheckBox it fails to receive the BN_CLICKED message.

I have also tried:

TCheckBox = class(StdCtrls.TCheckBox)
  private
    procedure WMMouseActivate(var Message: TWMMouseActivate); message WM_MOUSEACTIVATE;
  protected
    procedure CreateParams(var Params: TCreateParams); override;
  public
    procedure DefaultHandler(var Message); override;
  end;

procedure TCheckBox.WMMouseActivate(var Message: TWMMouseActivate);
begin
  Message.Result := MA_NOACTIVATE; // no effect!
end;

procedure TCheckBox.CreateParams(var Params: TCreateParams);
const
  WS_EX_NOACTIVATE = $08000000;
begin
  inherited;
  Params.ExStyle := Params.ExStyle or WS_EX_NOACTIVATE; // no effect!
end;

procedure TCheckBox.DefaultHandler(var Message);
begin
  case TMessage(Message).Msg of
    WM_SETFOCUS:
    begin
      if IsWindow(TWMSetFocus(Message).FocusedWnd) then
      begin
        TMessage(Message).Result := 1; // ???
        // inherited // ??? 
        Windows.SetFocus(TWMSetFocus(Message).FocusedWnd);
        Exit;
        // Checkbox fails to receive `BN_CLICKED` message
      end;
    end;
  end;
  inherited;
end;

Nothing works. What am I missing?

Nez answered 4/2, 2013 at 14:4 Comment(3)
I guess the clean way of doing this would be to not to use a checkbox at all and try and see if the effect could be achieved with an owner-drawn header item in the list view. The -click and drag outside- should not generate a BN_CLICKED for a standard checkbox, this behavior could also be changed if the checkbox is drawn. BTW, there should be a TabStop:=False at some place for that checkbox...Utoaztecan
@SertacAkyuz, There is a TabStop := False. That was obvious. Using owner-drawn header is a good alternative, but this wont answer the question, and what is the fun about that? ;)Nez
Well, then see the answer I posted. :)Utoaztecan
U
7

Do not process the WM_COMMAND message for a BN_CLICKED notification, the behavior you want about having a click while pressing the button and then draggging outside after the click won't work otherwise. A click consists of pressing and releasing the button on the control.

Instead you can look for if the mouse is clicked inside the control and then switch the checked state if it is. You can eat the mouse message after that, so the control won't receive the focus. But this should be checked in the window procedure of the checkbox, not the list view. Modified code:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ComCtrls, StdCtrls;

type
  TForm1 = class(TForm)
    ListView1: TListView;
    CheckBox1: TCheckBox;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    FListHeaderChk: TCheckBox;
    FSaveListHeaderChkWndProc: TWndMethod;
    FListHeaderWnd: HWND;
    procedure ListHeaderChkWndProc(var Msg: TMessage);
  end;

var
  Form1: TForm1;

implementation

uses
  commctrl;

{$R *.dfm}

function GetCheckSize: TPoint;     // from checklst.pas
begin
  with TBitmap.Create do
    try
      Handle := LoadBitmap(0, PChar(OBM_CHECKBOXES));
      Result.X := Width div 4;
      Result.Y := Height div 3;
    finally
      Free;
    end;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  CheckSize: TPoint;
  HeaderSize: TRect;
begin
  ListView1.HandleNeeded;
  FListHeaderWnd := ListView_GetHeader(ListView1.Handle);

  FListHeaderChk := TCheckBox.Create(nil);
  CheckSize := GetCheckSize;
  FListHeaderChk.Height := CheckSize.X;
  FListHeaderChk.Width := CheckSize.Y;

  ShowWindow(ListView1.Handle, SW_SHOWNORMAL);
  windows.GetClientRect(FListHeaderWnd, HeaderSize);
  FListHeaderChk.Top := (HeaderSize.Bottom - FListHeaderChk.Height) div 2;
  FListHeaderChk.Left := FListHeaderChk.Top;

  FListHeaderChk.Parent := Self;
  FListHeaderChk.TabStop := False;
  windows.SetParent(FListHeaderChk.Handle, FListHeaderWnd);
  FSaveListHeaderChkWndProc := FListHeaderChk.WindowProc;
  FListHeaderChk.WindowProc := ListHeaderChkWndProc;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FListHeaderChk.Free;
end;

procedure TForm1.ListHeaderChkWndProc(var Msg: TMessage);
begin
  if (Msg.Msg = WM_MOUSEACTIVATE) and (Msg.LParamLo = HTCLIENT) then begin
    Msg.Result := MA_NOACTIVATEANDEAT;
    FListHeaderChk.Checked := not FListHeaderChk.Checked;
    Exit;
  end;

  FSaveListHeaderChkWndProc(Msg);
end;

end.
Utoaztecan answered 4/2, 2013 at 19:28 Comment(3)
Well Done! just 2 small Q if you may: (1) No need to restore FListHeaderChk.WindowProc on destruction? (2) Any reason why not create FListHeaderChk := TCheckBox.Create(Self);?Nez
@Nez - Thanks! (2) - Not at all, since the form already owns the listview, I don't see any problem with it also owning the checkbox. (1) Not really. The potential problem with subclassing is that more than one piece of code doing this and restoring (or not restoring) with the wrong order. If you're in control of the whole process I don't see a problem. I've seen in VCL code leaving it at that, but don't remember now where and if it was using WindowProc. Best would be to use a specialized checkbox descendant with no subclassing.Utoaztecan
I'm for using a check descendant. So overriding WndProc sounds even better solution to me. Thanks!Nez

© 2022 - 2024 — McMap. All rights reserved.