Delphi: StringGrid, position and Context Menu

2019-07-18 14:00发布

I'm having a problem with using a TStringGrid and Popup menu

I want to know the Row / Column of the cell that was last active when select an item from my Popup menu. However when I click on the popup menu, the StringGrid.Row is returned as -1.

I've tried using MouseToCell as part of OnClick, but even after setting SG.Row it still returns as -1 in the PopUp menus routines... I suspect that the problem is the Grid losing the focus.

Are there any solutions to this that don't require OnClick setting a global variable?

I'm using an Action List linked to the items on the Popup Menu to make sure that the actions are consistent between the toolbar and the Popup Menu

5条回答
走好不送
2楼-- · 2019-07-18 14:32

Hmm... I'm unable to duplicate the problem in my D2010.

A quick thought is that perhaps the problem occurs because you did not have any rows selected? Would presetting the StringGrid's Row to, say, 0 first in your Form's OnCreate help?

查看更多
趁早两清
3楼-- · 2019-07-18 14:33

Another way to know the row of a selection on a TStringGrid (it is really the only one):

YourstringGrid.Selection.Top;
YourstringGrid.Selection.Bottom;

If there is only one row selected they must match.

I never see .Selection.··· failing, while i saw YourstringGrid.Row seem to fail for getting the selection row, a lot of times it return -1 when you think it must return other values (see 4 points at the end to understand why seems to fail but it is really not a fail when it return -1, ... it is a concept bas understod).

Selection and cell with foucs are not the same thing... .Selection is for selection, .Row and .Col are for the cell with the focus and has nothing related to the selection, it can be a cell with focus while selection be a total different range of cells (both are different concepts).

Also more, i have detected that YourstringGrid.Row<>YourstringGrid.Selection.Top can be True. When the cell that has the focus is not on the top row of the selection.

Some hacks, tricks, code, etc. shown on Internet are only for when goRowSelect=False if it is set to True such routines do not work well, use them with care.

Hint: On a TStringGrid that has goRowSelect=True it is very buggy to select by code more than a row, changing .Selection sometimes do not update .Row (they do not change the actual cell that has the focus), so if anyone want to select just one row it is better to assign the row value directly to .Row.

Remember: On a TStringGrid with goRowSelect=True talking about what cell has the focus has no sence, so when coding it they do not have in mind such thing at all (.Row and .Col must not be readed when goRowSelect=True). In other words: if you allways have a full row selected what sence has to check the cell that has the focus, there is not such cell, it is the full row, etc... think as that to not get mad by BUGs on internal implementation when mixing focused cell on a goRowSelect=True TStringGrid.

Also worst: Whith goRowSelect=True and some keyboard combinations (Shift+Cursors) and mouse clicks, you can make rare selections, like two or three cells in a column, but not the full rows; remember it has goRowSelect=True and the grid shows only some cells of that rows selected, and if you read .Selection it tells you not all cells on the row are selected (True=(TheGrid.FixedCols+#<TheGrid.Selection.Left)) where # can be more than 1. Again, beware of such BUGs... i can only say... i allways trap selection changes and force full rows to be selectect (i put code on OnSelectCell to ensure all selection is allways full row/s), see a simple code (note i do not care what cell is being selected, by concept the selection must go to a full row or full rows, not a cell; again a rare concept, internal implementation do not think you want to do something when selected cell changes, so this event is supposed to not have code, i put this code to FIX the BUG of selection not being full row/s):

procedure TYourForm.YourGridSelectCell(Sender: TObject; ACol, ARow: Integer;  var CanSelect: Boolean);
begin
     YourGrid.Selection:=TGridRect(Rect(YourGrid.FixedCols,YourGrid.Selection.Top,YourGrid.FixedCols,YourGrid.Selection.Bottom));
end;

Simplifing: The TStringGrid is too much buggy, i have catch it telling '.Row=13' while at same time .Selection.Top=2 and .Selection.Bottom=5; how can be the active row one outside the selection? etc. It is because '.Row' (and also .Col) do not talk about row selected, talk about what cell has the focus, so seeing .Row to know what row is selected is wrong in concept... you will be seen the row of the cell that has the focus, nothing related with acutal selection cells.

I never see failing this things:

  • .Selection.··· allways tells you the selected area (the area shown as selected)
  • .Row if assigned a value (and grid has goRowSelect=True), that whole row gets selected (i had never tried that without having goRowSelect=True), but only one row.

Not to mention if you want to hack TStringGrid it a lot and make it a multi-row select with more than one contiguos selection at the same time (like ListBox multi select); that makes things very chazy because of all bugs TStringGrid has on .Row and .Selection properties managment.

For such multi slections grids, i recomend allways using a non oficial VCL component instead of the TStringGrid, if i do not remember bad it is called TMultiSelectStringGrid, on it you have a .Selected property for each cell, row and column that is Read/Write able. It really works great when you want a multi-select with Ctrl key pressed, also works great with multi-row selection and multi-column selectiong... just do a loop over the rows, columns or cells to check witch ones are selected and witch ones not. It also has a property .RightMouseSelect (if i do not remember bad) that makes right click to select, and it does well.

Warning, not all code out there to select on right click is correct... a lot of them do not care if you have multi-selection or not... never set .Row if you want to mantain more than one row selected, just check if mouse is outside the selection prior to change the .Selection else right click can make selection to change... so how one could popup menu for more that one row (example of use: popup menu entry called delete for more than one entry at the same time).

In other words (code i really use for VCL standard TStringGrid):

procedure TYourForm.YourGridMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
   ACol,ARow:Integer;
begin
     YourGrid.MouseToCell(X,Y,ACol,ARow);
     if goRowSelect in YourGrid.Options
     then begin // TStingGrig with full row selected, no individual cell must be selected
               if  (ARow<YourGrid.Selection.Top)
                 or
                   (YourGrid.Selection.Bottom<ARow)
               then begin // Where clicked is outside the actual rows that are selected
                         YourGrid.Row:=ARow;
                    end;
          end
     else begin // TStingGrig where individual cells can be selected
               if  (ARow<YourGrid.Selection.Top)
                 or
                   (YourGrid.Selection.Bottom<ARow)
                 or
                   (ACol<YourGrid.Selection.Left)
                 or
                   (YourGrid.Selection.Right<ACol)
               then begin // Where clicked is outside the actual selection
                         YourGrid.Selection:=TGridRect(Rect(ACol,ARow,ACol,ARow)); // Select the clicked cell 
                    end;
          end;
end;

Note: I really have that code on a procedure on a unit and call that procedure passing the Grid reference and X, Y coordinates; well to tell the really truth that unit i use is a full hack of TStringGrid with a declaration as type TStringGrid=class(Grids.TStringGrid), so i can use visual design and have sush extra funcions on all of them; just ensure for hack to work to add such unit on the interface uses section at end of the units list (or at least after Grids, never before Grids).

Special hints for controlling PopUp menu been or not been shown and what Popup menu to show:

  • Do it on event OnMouseDown, never on OnMouseUp, neither OnClick, etc.; or popup menu will be shown before selection changes... and sometines when the selection changes (by code) popup will hide inmediatly.
  • If done OnMouseDown there is no need to force the popup menu to be shown by code, it will do it normally; also more, you can cancel the popup to be shown, for example if clicked outside the cells data, or also can have different popups for FixedCols, FixedRows, also for each cell you can have a different popup (i talk about design time popups, you also can dinamically create popup entries prior to be shown, etc), put code for what you want to do allways on event OnMouseDown, talking about opoup menus created on design time; if popup is created on runtime, think the same: show or not show on OnMouseDown, construction of the menu on the own OnPopup menu event.

The basic trick is to do the selection chages on the event OnMouseDown, it is fired before popup menu is shown.

Ah, on my code i do not mind what button was clicked, since if left clicked inside mutiple rows selected, it will also act as normal (my code do not make any change to selection, nor to row, etc. it really does nothing, see the ifs), but you will see the selection changes to only one row.

Beware, selection also can be changed to a multi selection by a mouse left down, sustained, then move mouse and lift left button, that will select more than one cell/row. All this ways that the user has to make selections, makes internal implementation of standard component so buggy, not all combinations of actions had been taken in mind while it was internally coded.

Try to press Ctrl and or Shift while left mouse is pressed and you are moving the mouse on a standard grid with no code at all, ecetp code on OnMouseMove to show .Row, .Col and .Selection.···, will see thing you will not ever think would be possible. I see one time telling .Col value was some millons (an imposible value, since grid has only a few cols), same for .Row (different value than when fail on '.Col').

So do not believe values returned with .Row and .Col if you think on getting what is the selection (wrong concept, they express what cell has the focus, nothing related with what selection is); but you can use them to select one and only one Row or Column (column only if use hacked grid that allow it, or use a grid with goRowSelect=False and simulate a column selection by your own).

And please, allways have in mind this (please do it allways):

  • The cell that has the focus (.Row and .Col may tell you which one is) can be outside the actual selection, yes, it can be out (among it is very hard to force that fail, it occurs, no need to code anything, just using mouse move click and combinations with Alt, Ctrl, Shift, Cursors and Spacebar can happen). So do not trust .Row nor .Col to know anything about the selection (that is wrong on concept), allways use .Selection.···.

Hope this helps not getting headage as i was having till i understand this:

  1. .Selection.··· representes only one rectangular area of cells selected (no mind if goRowSelect is True or False).
  2. .Selection:=TGrigRect(Rect(Left,Top,Right,Bottom)); is the best way to change the actual selection to another one (ensure by your self Left<=Right and Top<=Bottom or else thing can go really bad); think on code as this huge: .Selection:=TGrigRect(Rect(Min(Left,Right),Min(Top,Bottom),Max(Right,Left),Max(Bottom,Top)); (see the Min and Max, they are in Maths unit).
  3. .Row gives the row number of that special cell that has a dotted rectangular drawn on it, no matter if it is inside the selection or outside the selection, also can be -1 if no cell has focus. It has nothing related with selection.
  4. .Col gives the col number of that special cell that has a dotted rectangular drawn on it, no matter if it is inside the selection or outside the selection, also can be -1 if no cell has focus. It has nothing related with selection.

Understanding that four things, i can get headages be something on the past. It took me too much to understand that two different concepts: Cell that has focus ('.Row' and '.Col') and cells selected (.Selection.···).

P.D.: Be free to share this info!

查看更多
▲ chillily
4楼-- · 2019-07-18 14:39

If you want / need Right clicking a cell move focus to it (as normally done with left click) you can use the code i use for that:

type TStringGridHacked=class(Grids.TStringGrid); // This is to have access to hidden (and very usefull) methods, like MoveCol, MoveRow, FocusCell, etc
procedure TMyForm.TheStringGridMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
   ACol,ARow:Integer;
begin
     if mbRight=Button
     then begin // Right mouse button clicked
               TheStringGrid.MouseToCell(X,Y,ACol,ARow); // Convert X,Y coordinates of mouse to cell Col & Row
               if (FixedCols<=ACol)and(FixedRows<=ARow)
               then begin // Cell is not a header one
                         TStringGridHacked(TheStringGrid).FocusCell(ACol,ARow,True); // Send focus to such cell doing only one move, so triggering SelectCell only once
                    end;
          end;
end;

I have been using that Hack type TStringGridHacked=class(Grids.TStringGrid) for a long, long time, since i found it on Internet.

I put such hack (type declaration) just after the implementation uses (if i need it more than once on same unit), or as is in code just before the procedure (if i only need it once); both ways works fine and make code more clear.

查看更多
我欲成王,谁敢阻挡
5楼-- · 2019-07-18 14:42

In one of my TStringGrid-based controls I am using MouseDown/MouseUp event to handle that pop-up menu because I have two different contextual menus, depending on which area of TStringGrid you've clicked. Works like a charm. Just make sure you call inherited BEFORE your code.

--
Please note that there is something strange about the order in which the events are called when you pop-up the contextual menu. More exactly, when you press the RMB and the pop-up menu pops the MouseUp event is not called immediately. It is called next time you press a mouse button (any button).

See this also: TStringGrid - OnMouseUp is not called!

查看更多
做个烂人
6楼-- · 2019-07-18 14:53

I am afraid that I do not fully understand what you mean. When I left-click a cell in a string grid, it gets selected, but not when I right-click it. When I right-click it, the popup menu is shown (if assigned), and on MenuItemClick I can easily read the row and col currently selected. See example video.

I guess that you actually want this: you want right-clicks to change the active cell as well as left-clicks. This is easily done:

procedure TForm1.StringGrid1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  if Button = mbRight then
    StringGrid1.Perform(WM_LBUTTONDOWN, 0, MakeLParam(Word(X), Word(Y)));
end;
查看更多
登录 后发表回答