Search code examples
listviewdelphimulti-selectownerdrawnvirtuallistview

Multiple selection using shift arrow broken after programmatically selecting a line in Delphi ListView


I am using a both owner draw and data listview in a Delphi and I noticed a weird problem if I select using shift arrow immediately after having first programmatically changed the selected line the selection.

Consider the following window where I have tried to display the problem with minimal code:

Shift Down selection gets messed up

And here is the minimal Delphi code that replicates the problem:

unit Main;

//--------------------------------------------------------------------------------------------------
//    I N T E R F A C E
//--------------------------------------------------------------------------------------------------

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

//--------------------------------------------------------------------------------------------------
//    T Y P E      D E F I N I T I O N S
//--------------------------------------------------------------------------------------------------

type TMainForm = class(TForm)

    listView             : TListView;

    bottomPanel          : TPanel;

        position10Button : TButton;

    procedure FormCreate(
                  sender : TObject);

    //----------------------------------------------------------------------------------------------
    //    LIST VIEW EVENT HANDLERS
    //----------------------------------------------------------------------------------------------

    procedure ListViewData(
                  sender : TObject;
                  item   : TListItem);

    procedure ListViewDrawItem(
                  sender : TCustomListView;
                  item   : TListItem;
                  rect   : TRect;
                  state  : TOwnerDrawState);

    //----------------------------------------------------------------------------------------------
    //    POSITION BUTTON HANDLER
    //----------------------------------------------------------------------------------------------

    procedure Position10ButtonClick(
                  sender : TObject);

private

    //----------------------------------------------------------------------------------------------
    //    WINDOWS MESSAGE HANDLERS
    //----------------------------------------------------------------------------------------------

    procedure WMMeasureItem(
                  var msg : TWMMeasureItem);    message WM_MEASUREITEM;

private

    //----------------------------------------------------------------------------------------------
    //    DRAWING
    //----------------------------------------------------------------------------------------------

    procedure DrawHighlightRect(
                  canvas : TCanvas;
                  rect   : TRect;
                  color  : TColor);

end;

//--------------------------------------------------------------------------------------------------
//    G L O B A L     V A R I A B L E S
//--------------------------------------------------------------------------------------------------

var MainForm : TMainForm;

//--------------------------------------------------------------------------------------------------
//    I M P L E M E N T A T I O N
//--------------------------------------------------------------------------------------------------

implementation uses CommCtrl;

{$R *.dfm}

//--------------------------------------------------------------------------------------------------
//    F O R M     C R E A T E
//--------------------------------------------------------------------------------------------------

procedure TMainForm.FormCreate(
                        sender : TObject);
begin
  //  Set double buffering for listview.

  listView.doubleBuffered := TRUE;

  //  Set listview count: 20 lines.

  listView.items.count := 20;

  //  Set focus on listview.

  WINDOWS.SetFocus(
              listView.handle);
end;

//--------------------------------------------------------------------------------------------------
//    FORM CONTROLS EVENT HANDLERS
//--------------------------------------------------------------------------------------------------

//--------------------------------------------------------------------------------------------------
//    LIST VIEW EVENT HANDLERS
//--------------------------------------------------------------------------------------------------

//--------------------------------------------------------------------------------------------------
//    L I S T V I E W     D A T A
//--------------------------------------------------------------------------------------------------

procedure TMainForm.ListViewData(
                        sender : TObject;
                        item   : TListItem);

begin
  if item = NIL then EXIT;

  item.caption := SYSUTILS.IntToStr(item.index);
end;

//--------------------------------------------------------------------------------------------------
//    L I S T V I E W     D R A W     I T E M
//--------------------------------------------------------------------------------------------------

procedure TMainForm.ListViewDrawItem(
                        sender : TCustomListView;
                        item   : TListItem;
                        rect   : TRect;
                        state  : TOwnerDrawState);

  const TEXT_MARGIN = 7;

  var drawRect : TRect;

begin
  //  Draw focus rectangle for selected item.

  if item.selected then
    begin
      drawRect := rect;

      Inc( drawRect.top,   1);
      Dec( drawRect.bottom,1);

      DrawHighlightRect(
          sender.canvas,
          drawRect,
          clBlack);
    end;

  //  Prepare brush to draw text.

  sender.canvas.brush.style := bsClear;

  //  Draw text.

  drawRect       := rect;
  drawRect.left  := TEXT_MARGIN;

  WINDOWS.DrawText(
              sender.canvas.handle,
              PCHAR(item.caption),
              Length( item.caption),
              drawRect,
              DT_SINGLELINE or
              DT_LEFT       or
              DT_VCENTER);
end;

//--------------------------------------------------------------------------------------------------
//    P O S I T I O N     1 0     B U T T O N     C L I C K
//--------------------------------------------------------------------------------------------------

procedure TMainForm.Position10ButtonClick(
                        sender : TObject);
begin
  WINDOWS.SetFocus(
              listView.handle);

  //  Unselect all.

  listView.ClearSelection;

  //  Select and focus line 10.

  listview.items[10].selected := TRUE;
  listview.items[10].focused  := TRUE;
end;

//--------------------------------------------------------------------------------------------------
//    WINDOWS MESSAGE HANDLERS
//--------------------------------------------------------------------------------------------------

//--------------------------------------------------------------------------------------------------
//    W M     M E A S U R E     I T E M
//--------------------------------------------------------------------------------------------------

procedure TMainForm.WMMeasureItem(
                        var msg : TWMMeasureItem);
begin
  inherited;

  //  Set height of list view items.

  if msg.IDCtl = listView.handle then msg.measureItemStruct^.itemHeight := 25;
end;

//--------------------------------------------------------------------------------------------------
//    D R A W     H I G H L I G H T     R E C T
//--------------------------------------------------------------------------------------------------

procedure TMainForm.DrawHighlightRect(
                        canvas : TCanvas;
                        rect   : TRect;
                        color  : TColor);

  var topLeft              : TPoint;
  var topRight             : TPoint;
  var bottomRight          : TPoint;
  var bottomLeft           : TPoint;

begin
  //  Prepare pen.

  canvas.pen.style := psSolid;
  canvas.pen.width := 1;
  canvas.pen.mode  := pmCopy;

  //  Compute outer rectangle points.

  topLeft.x     := rect.left;
  topLeft.y     := rect.top;

  topRight.x    := rect.right;
  topRight.y    := rect.top;

  bottomRight.x := rect.right;
  bottomRight.y := rect.bottom;

  bottomLeft.x  := rect.left;
  bottomLeft.y  := rect.bottom;

  //  Draw rectangle.

  canvas.pen.color := color;

  canvas.PolyLine( [ topLeft, topRight, bottomRight, bottomLeft, topLeft]);

  //  Compute inner rectangle points.

  topLeft.x     := rect.left   + 1;
  topLeft.y     := rect.top    + 1;

  topRight.x    := rect.right  - 1;
  topRight.y    := rect.top    + 1;

  bottomRight.x := rect.right  - 1;
  bottomRight.y := rect.bottom - 1;

  bottomLeft.x  := rect.left   + 1;
  bottomLeft.y  := rect.bottom - 1;

  //  Draw rectangle.

  canvas.pen.color := color;

  canvas.PolyLine( [ topLeft, topRight, bottomRight, bottomLeft, topLeft]);
end;

//--------------------------------------------------------------------------------------------------

end.

[Edit] As pointed out by Andreas Rejbrand, the problem also exists with a non-ownerdraw non-ownerdata listview.

unit Main;

//--------------------------------------------------------------------------------------------------
//    I N T E R F A C E
//--------------------------------------------------------------------------------------------------


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

//--------------------------------------------------------------------------------------------------
//    T Y P E      D E F I N I T I O N S
//--------------------------------------------------------------------------------------------------

type TMainForm = class(TForm)

    listView             : TListView;

    bottomPanel          : TPanel;

        position10Button : TButton;

    procedure FormCreate(
                  sender : TObject);

    //----------------------------------------------------------------------------------------------
    //    POSITION BUTTON HANDLER
    //----------------------------------------------------------------------------------------------

    procedure Position10ButtonClick(
                  sender : TObject);

end;

//--------------------------------------------------------------------------------------------------
//    G L O B A L     V A R I A B L E S
//--------------------------------------------------------------------------------------------------

var MainForm : TMainForm;

//--------------------------------------------------------------------------------------------------
//    I M P L E M E N T A T I O N
//--------------------------------------------------------------------------------------------------

implementation uses CommCtrl;

{$R *.dfm}

//--------------------------------------------------------------------------------------------------
//    F O R M     C R E A T E
//--------------------------------------------------------------------------------------------------

procedure TMainForm.FormCreate(
                        sender : TObject);

  var index   : integer;
  var newItem : TListItem;

begin
  //  Set double buffering for listview.

  listView.doubleBuffered := TRUE;

  for index := 0 to 19 do
    begin
      newItem := listview.items.Add;
      newItem.caption := SYSUTILS.IntToStr( index);
    end;

  //  Set focus on listview.

  WINDOWS.SetFocus(
              listView.handle);
end;

//--------------------------------------------------------------------------------------------------
//    FORM CONTROLS EVENT HANDLERS
//--------------------------------------------------------------------------------------------------

//--------------------------------------------------------------------------------------------------
//    P O S I T I O N     1 0     B U T T O N     C L I C K
//--------------------------------------------------------------------------------------------------

procedure TMainForm.Position10ButtonClick(
                        sender : TObject);
begin
  WINDOWS.SetFocus(
              listView.handle);

  //  Unselect all.

  listView.ClearSelection;

  //  Select and focus line 10.

  listview.items[10].selected := TRUE;
  listview.items[10].focused  := TRUE;
end;

//--------------------------------------------------------------------------------------------------

end.

Solution

  • Notice that the "minimal" example in your Q contains a lot of unnecessary code. You can reproduce this issue without both owner drawing and owner data. Just drop a new TListView control on a form, add a few items in the IDE, and set MultiSelect to True. (*)

    Now, the trick is to use the LVM_SETSELECTIONMARK message, or the ListView_SetSelectionMark function (in Delphi):

    ListView1.ClearSelection;
    ListView1.ItemIndex := 10;
    ListView_SetSelectionMark(ListView1.Handle, 10)
    

    (*) And, of course, you need to enable double buffering to avoid all the horrible visual glitches you get otherwise.