Множественный выбор с использованием стрелки сдвига, сломанной после программного выбора строки в Delphi ListView

Я использую как рисование владельца, так и список данных в Delphi, и я заметил странную проблему, если я выбираю с помощью стрелки сдвига сразу после того, как сначала программно изменил выбранную строку выбора.

Рассмотрим следующее окно, в котором я попытался отобразить проблему с минимальным кодом:

Выбор Shift Down перепутался

А вот минимальный код Delphi, воспроизводящий проблему:

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.

[Редактировать] Как указал Андреас Рейбранд, проблема также существует с представлением списка без владельца и без данных владельца.

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.

person Emmanuel Ichbiah    schedule 21.05.2020    source источник
comment
Возможно, это немного придирка, но ваш минимальный пример, конечно, не минимален, и все же он не полон. Например, вы можете воспроизвести проблему, даже если вы отключите данные владельца и отрисовку владельца!   -  person Andreas Rejbrand    schedule 21.05.2020


Ответы (2)


Обратите внимание, что «минимальный» пример в вашем Q содержит много ненужного кода. Вы можете воспроизвести эту проблему без чертежа владельца и без данных владельца. Просто поместите новый элемент управления TListView в форму, добавьте несколько элементов в IDE и установите для MultiSelect значение True. (*)

Теперь хитрость заключается в использовании сообщения LVM_SETSELECTIONMARK. , или функцию ListView_SetSelectionMark ( в Делфи):

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

(*) И, конечно же, вам нужно включить двойную буферизацию, чтобы избежать всех ужасных визуальных сбоев, которые вы получите в противном случае.

person Andreas Rejbrand    schedule 21.05.2020
comment
Андреас, вы правы, проблема возникает и без ownerdata и ownerdraw. Спасибо за трюк с SetSelectionMark, но мне пришлось заменить вашу строку ListView1.itemIndex := 10 на ListView1.items[10].selected := TRUE; ListView1.items[10].focused := ИСТИНА; чтобы заставить его работать. - person Emmanuel Ichbiah; 21.05.2020
comment
Андреас, если не доказано обратное, установка элемента в фокус важна. Если вы подтвердите и отредактируете свое решение, я буду рад принять его. И еще раз спасибо за знакомство с API ListView_SetSelectionMark. :-) - person Emmanuel Ichbiah; 21.05.2020
comment
@EmmanuelIchbiah: я не могу подтвердить это в более простом контексте, описанном выше, в Delphi 10.3.2 и Windows 7 (64-разрядная версия). Я не пробовал это в вашем контексте. - person Andreas Rejbrand; 21.05.2020
comment
Я использую Windows 10 (64-разрядную версию), и мне необходимо настроить состояние фокусировки. - person Emmanuel Ichbiah; 21.05.2020
comment
@EmmanuelIchbiah: В простом контексте (новое приложение VCL, представление раскрывающегося списка, установка MultiSelect, добавление 20 элементов с помощью IDE)? Вы также используете Delphi 10.3.2? Что именно произойдет, если вы используете только мой код выше? - person Andreas Rejbrand; 21.05.2020
comment
Интересно, что ваше решение работает с Delphi 10.2.3, но не с Delphi 2007. Должен сказать, я удивлен, увидев, что установка состояния фокуса имеет значение, потому что в документации говорится, что когда выбран только один элемент, он также автоматически фокусируется. - person Emmanuel Ichbiah; 21.05.2020

В вашем обработчике ListViewDrawItem() if item.selected then должно быть if odSelected in state then

Но кроме того, в вашем обработчике Position10ButtonClick() вы устанавливаете свойства Selected и Focused определенного элемента, но вместо этого вам нужно выполнять эти назначения в событии OnData, чего вы сейчас не делаете. Вам нужно сохранить детали выбора где-нибудь в стороне, а затем применить эту информацию в событии OnData. Вам также потребуется обработать событие OnSelectItem и сохранить сведения, которые оно предоставляет вам, когда пользователь вносит изменения в текущий выбор.

Попробуйте что-то вроде этого:

type
  MyListItemInfo = record
    Caption: String;
    Selected: Boolean;
    Focused: Boolean;
  end;

private
  MyListItems: array of MyListItemInfo;

procedure TMainForm.FormCreate(
                        Sender : TObject);
var
  I: Integer;
begin
  //  Set double buffering for listview.

  ListView.DoubleBuffered := True;

  //  Set listview count: 20 lines.

  SetLength(MyListItems, 20);

  for I := Low(MyListItems) to High(MyListItems) do
  begin
    MyListItems[I].Caption := SysUtils.IntToStr(I);
    MyListItems[I].Selected := False;
    MyListItems[I].Focused := False;
  end;

  ListView.Items.Count := Length(MyListItems);

  //  Set focus on listview.

  ListView.SetFocus;
end;

procedure TMainForm.ListViewSelectItem(
                        Sender  : TObject;
                        Item    : TListItem;
                        Selected: Boolean);
var
  I: Integer;
begin
  if Item <> nil then
  begin
    MyListItems[Item.Index].Selected := Selected;
    ListView.UpdateItems(Item.Index, Item.Index);
  end else
  begin
    for I := 0 to listView.Items.Count-1 do
      MyListItems[I].Selected := Selected;
    ListView.Invalidate;
  end;
end;

procedure TMainForm.ListViewData(
                        Sender : TObject;
                        Item   : TListItem);

begin
  Item.Caption := MyListItems[Item.Index].Caption;
  Item.Selected := MyListItems[Item.Index].Selected;
  item.Focused := MyListItems[Item.Index].Focused;
end;

procedure TMainForm.Position10ButtonClick(
                        Sender : TObject);
var
  I: Integer;
begin
  ListView.SetFocus;

  //  Unselect all.

  ListView.ClearSelection;

  for I := Low(MyListItems) to High(MyListItems) do
  begin
    MyListItems[I].Selected := False;
    MyListItems[I].Focused := False;
  end;

  //  Select and focus line 10.

  MyListItems[10].Selected := True;
  MyListItems[10].Focused := True;

  ListView.Invalidate;
end;
person Remy Lebeau    schedule 21.05.2020
comment
Привет Реми, большое спасибо за вашу помощь. Я всегда предполагал, что OnData должен был установить заголовок и подэлементы, если они есть. Что мне нравится в решении с использованием SetSelectionMark, так это то, что оно работает для любого списка, независимо от его статуса данных владельца. Как отметил Андреас, проблема, с которой я столкнулся, не была специфична для списков владельцев данных. - person Emmanuel Ichbiah; 21.05.2020
comment
Я всегда предполагал, что OnData должен был установить заголовок и подэлементы, если они есть — это для предоставления любых из данных для каждого элемента, которые вам нужно, а не только заголовок. Например, индексы состояний и изображений. Именно это делает ListView данных владельца виртуальным — данные не хранятся в ListView, вы предоставляете данные по запросу. - person Remy Lebeau; 21.05.2020
comment
В любом случае у меня есть выбранный атрибут в моих данных. В чем я более скептичен, так это в сохранении сфокусированного атрибута. Когда вы щелкаете строку (без удержания Shift или Ctrl), список внутренне знает, что щелкнутая строка становится сфокусированной записью. В вашем OnSelectItem вы действительно не знаете, нужно ли вам устанавливать сфокусированное состояние или нет. Я думаю, что, вероятно, разумнее полагаться на сам ListView, чтобы поддерживать информацию о сфокусированном состоянии. Вы так не думаете. - person Emmanuel Ichbiah; 21.05.2020
comment
TListView предоставляет события только для изменения выбора и изменения состояния флажка, но не для изменения фокуса. Однако базовые уведомления LVN_ITEMCHANGING и LVN_ITEMCHANGED предоставляют эту информацию. - person Remy Lebeau; 22.05.2020
comment
OK. Я вижу некоторую свободу действий. Я не уверен, что проблема, с которой я столкнулся, связана с отсутствием информации (выбранной и сфокусированной), указанной в обработчике OnData, тем более, что проблема возникает также с ListView без данных владельца. - person Emmanuel Ichbiah; 22.05.2020
comment
Я забыл добавить, что вам также нужно поддерживать «выбрано» в событии OnDataStateChange. Интересно, что startIndex и endIndex содержат неправильный диапазон, объясняющий беспорядок выбора. Поэтому я думаю, что это ошибка ListView, и использование ListView_SetSelectionMark — это просто обходной путь, чтобы обойти ее. - person Emmanuel Ichbiah; 22.05.2020
comment
@EmmanuelIchbiah У меня никогда не было таких проблем с OnData... Мероприятия. Но с другой стороны, мне никогда не приходилось использовать MultiSelect=true с OwnerData=true. - person Remy Lebeau; 22.05.2020