TListView in vsReport style is very popular control to show information in table-like manner. In vsReport style, you can group information into rows and categorize them into columns. Defining columns would be easy, including the column's title, minimum width, title alignment, and other stuff. The columns actually quite flexible, since user could resize them in runtime either to save space or to view more.
Unfortunately the current implementation of TListView did not provide event related with column resizing operation. For example, it did not provide event for when user starts resizing a colum, or for when resizing, and also not for when user finishe resizing. In a few of my projects, I found the need for such events.
But fortunately for us, there are ways to customize TListView to make those events available for us. The most efficient way is actually by handling WM_NOTIFY message sent by the TListView's header control to itself. In implementation, we have two options.
- Replace WindowProc property of "ordinary" TListView with new one, which filters and handles WM_NOTIFY message.
- Create a descendant of TListView which handles WM_NOTIFY message specifically.
1. Replacing TListView's WindowProc
A control which derived from TWinControl has WindowProc property which points to method that handles messages sent to the control. The most interesting point about this property is that it is writable. Meaning that you can change many behavior of a TWinControl by simply changing this single property.
However you must be careful in manipulating this property since it is responsible for nearly all behaviour of the control. Therefore use the following precaution.
- Before replacing it's very important to save previous WindowProc value
- In new WindowProc method it is wise to call the old WindowProc somewhere when your code did not actually handle all aspect of a message.
- Before finish with the application/module/form, it is good behaviour to return back old WindowProc value.
In this approach have the following advantages.
- Quick solution, since the code is placed exactly where needed.
- Easy maintenance if you only need this occasionally (one or two in a whole project).
However there are also disadvantages, all related with maintenance when you need the same feature more than just occasionally.
- Difficult to maintain once you need the same feature more than once or twice.
- Slower to code when you need the same feature more than twice. With descendant approach you don't have to copy the code anymore.
In the following example we will place a TListView in runtime, then replace its WindowProc with our own which allows us to intercept events when columns begin to resize, in resizing process, and after finish resizing.
- Create a new Delphi application. You will get a new form, usually named Form1. Leave it as is.
- Drop a TScrollBox to Form1. It will be named ScrollBox1. Leave it as is. Change its Align property to alClient.
- Drop a TSplitter to Form1. Make sure its Align property is alLeft.
- Drop a TPanel to Form1 and change its Align property to alClient. By default this TPanel will be named Panel1. Leave it as is.
- Drop a TMemo to Panel1. Adjust its position and dimension so it covers nearly all Panel1 (see the next image). Name this TMemo as mmLogs. Change its ReadOnly property to True and ScrollBars to ssVertical. We will show our log messages here.
- Drop a TListView on top area of ScrollBox1. This TListView by default will be given name of ListView1.
- Right click on ListView1 and click on Columns Editor... to bring up column editor.
- In the columns editor add 3 columns. Set the first's caption to ID, the second's to Code, and the third's to Name.
- First we have to code our new WindowProc replacement. Declare a method like the following in private section of TForm1.
TForm1 = class(TForm) ... private ... FOldListViewWindowProc: TWndMethod; procedure NewListViewWindowProc(var AMsg: TMessage); ... end;Note:
FOldListViewWindowProc will be used to store the old WindowProc value of ListView.
- And the implementation of NewListViewWindowProc would be:
procedure TForm1.NewListViewWindowProc(var AMsg: TMessage); var vColWidth: Integer; vMsgNotify: TWMNotify absolute AMsg; begin // call the old WindowProc of ListView1 FOldListViewWindowProc(AMsg); // Currently we care only with WM_NOTIFY message if AMsg.Msg = WM_NOTIFY then begin case PHDNotify(vMsgNotify.NMHdr)^.Hdr.Code of HDN_ENDTRACK: DoColumnResized(PHDNotify(vMsgNotify.NMHdr)^.Item); HDN_BEGINTRACK: DoColumnBeginResize(PHDNotify(vMsgNotify.NMHdr)^.Item); HDN_TRACK: begin vColWidth := -1; if (PHDNotify(vMsgNotify.NMHdr)^.PItem<>nil) and (PHDNotify(vMsgNotify.NMHdr)^.PItem^.Mask and HDI_WIDTH <> 0) then vColWidth := PHDNotify(vMsgNotify.NMHdr)^.PItem^.cxy; DoColumnResizing(PHDNotify(vMsgNotify.NMHdr)^.Item, vColWidth); end; end; end; end;
- Do the replacement in Form1's OnCreate event. Something like the following.
procedure TForm1.FormCreate(Sender: TObject); begin FOldListViewWindowProc := ListView1.WindowProc; ListView1.WindowProc := NewListViewWindowProc; SetupMyListView; end;
- And our event handlers are:
- DoColumnBeginResize method handles when ListView1's column begin resizing.
- DoColumnResizing method handles when ListView1's column is resizing.
- DoColumnResized method handles when ListView1's column is finished resizing.
procedure TForm1.DoColumnBeginResize(const AColIndex: Integer); begin Log('ListView1: Column ' + IntToStr(AColIndex) + ' starts resizing'); end; procedure TForm1.DoColumnResizing(const AColIndex, AWidth: Integer); begin Log('ListView1: Column ' + IntToStr(AColIndex) + ' is resizing. Width: ' + IntToStr(AWidth)); end; procedure TForm1.DoColumnResized(const AColIndex: Integer); begin Log('ListView1: Column ' + IntToStr(AColIndex) + ' is resized'); end;
- And the log method would be:
procedure TForm1.Log(const ALogMsg: string); begin mmLogs.Lines.Add(ALogMsg); end;
Run the Demo
Upon running and resizing a couple of columns in ListView1, you will get something like the following image. Messages logged in the TMemo proves that we have successfully forward and handle TListView's columns resizing events.
2. Create Descendant of TListView
In this approach we create a descendant of TListView and handle WM_NOTIFY message within it. The most interesting fact is that the message handling can be done very easily. Much more each than meddling with WindowProc property. Basically you can go simply like this.
type TMyListView=class(TListView) private procedure HandleWMNotify(var AMsg: TWMNotify); message WM_NOTIFY; end;
And HandleWMNotify will only be called when TMyListView receives WM_NOTIFY message. Yes, as simple as that!
However we of course need to add events as published properties to our TMyListView, in order to make it accessible to others especially in design time. Also it is wise to separate TMyListView codes to its own unit and add it to a package which later can be installed and make TMyListView available in component pallette.
- Create a new Package project. Save it as MyListView_.dpk.
- Add new unit to the package. Save the unit as MyListView.pas.
- Declare the following method types in the interface section:
type TColumnEvent = procedure (ASender: TObject; const AColIndex: Integer) of object; TColumnSizeEvent = procedure (ASender: TObject; const AColIndex, AColWidth: Integer) of object;They serve as a "template" of our column events.
- Declare our TListView descendant, in this case we name the class TMyListView. Also in interface section. Something like this:
TMyListView=class(TListView) private FOnColumnBeginResize: TColumnEvent; FOnColumnResizing: TColumnSizeEvent; FOnColumnResized: TColumnEvent; procedure HandleWMNotify(var AMsg: TWMNotify); message WM_NOTIFY; protected procedure DoColumnResized(const AColIndex: Integer); procedure DoColumnBeginResize(const AColIndex: Integer); procedure DoColumnResizing(const AColIndex, AWidth: Integer); published // event that will be raised when a column starts resizing. The column's index // will be indicated in AColIndex parameter property OnColumnBeginResize: TColumnEvent read FOnColumnBeginResize write FOnColumnBeginResize; // event that will be raised when a column is resizing. The column's index // will be indicated in AColIndex parameter, and the current width indicated // in AColWidth parameter property OnColumnResizing : TColumnSizeEvent read FOnColumnResizing write FOnColumnResizing; // event that will be raised when a column just finished resizing. The // column's index will be indicated in AColIndex parameter property OnColumnResized : TColumnEvent read FOnColumnResized write FOnColumnResized; end;
- Declare a Register procedure in the interface section without no parameter.
The core of TMyListView would be in HandleWMNotify method. And it goes like this:
procedure TMyListView.HandleWMNotify(var AMsg: TWMNotify); var vColWidth: Integer; begin inherited; case PHDNotify(AMsg.NMHdr)^.Hdr.Code of HDN_ENDTRACKA, HDN_ENDTRACKW: DoColumnResized(PHDNotify(AMsg.NMHdr)^.Item); HDN_BEGINTRACKA, HDN_BEGINTRACKW : DoColumnBeginResize(PHDNotify(AMsg.NMHdr)^.Item); HDN_TRACKA, HDN_TRACKW: begin vColWidth := -1; if (PHDNotify(AMsg.NMHdr)^.PItem<>nil) and (PHDNotify(AMsg.NMHdr)^.PItem^.Mask and HDI_WIDTH <> 0) then vColWidth := PHDNotify(AMsg.NMHdr)^.PItem^.cxy; DoColumnResizing(PHDNotify(AMsg.NMHdr)^.Item, vColWidth); end; end; end;
As you can see that the codes is not much different with those with WindowProc property approach. Because they actually the same.
DoColumnBeginResize, DoColumnResizing, and DoColumnResized methods are just to call respected event handler if assigned. Only for example, I only will show you implementation of DoColumnBeginResize. I.e.:
procedure TMyListView.DoColumnBeginResize(const AColIndex: Integer); begin if Assigned(FOnColumnBeginResize) then FOnColumnBeginResize(Self, AColIndex); end;
Register procedure will be used to register TMyListView to component pallete. This procedure will automatically called when you install a package. And in our case, Register was implemented like this:
procedure Register; begin RegisterComponents('CodeCall', [TMyListView]); end;
The above code will install TMyListView to component pallette in a tab titled CodeCall.
After TMyListView is completed, right click on MyListView_.dpk project in Project Manager and click on Install menu item. When successfully installed, you will get message something like this.
And you can find CodeCall tab in your component pallette and it will have new component there.
We are going to use the demo application we have created for the previous approach (using WindowProc replacement). We will extend it. Open that demo application and follow the following steps.
- Open Form1.
- Drop TMyListView from CodeCall tab in component pallete to Form1. Adjust its location and size to be immediately below ListView1 and about the same size. It will be automatically named MyListView1.
- Inspect MyListView in Object inspector. Open the Events tab. You will find three events that do not exist in ordinary TListView. They were OnColumnBeginResize, OnColumnResizing, and OnColumnResized.
- Double click on the cell next to OnColumnBeginResize to create skeleton code for MyListView1's OnColumnBeginResize event handler. And use the following code for it.
procedure TForm1.MyListView1ColumnBeginResize(ASender: TObject; const AColIndex: Integer); begin Log(ASender.ClassName + ': Column ' + IntToStr(AColIndex) + ' starts resizing'); end;
- Double click on the cell next to OnColumnResizing to create skeleton code for MyListView1's OnColumnResizing event handler. And use the following code for it.
procedure TForm1.MyListView1ColumnResizing(ASender: TObject; const AColIndex, AColWidth: Integer); begin Log(ASender.ClassName + ': Column ' + IntToStr(AColIndex) + ' is resizing. Width: ' + IntToStr(AColWidth)); end;
- Double click on the cell next to OnColumnResized to create skeleton code for MyListView1's OnColumnResized event handler. And use the following code for it.
procedure TForm1.MyListView1ColumnResized(ASender: TObject; const AColIndex: Integer); begin Log(ASender.ClassName + ': Column ' + IntToStr(AColIndex) + ' is resized'); end;
- When finished with the above steps, you might get Form1 in design something like shown below.
- Now it's time to run the demo application. After running and doing a couple column resizing in MyListView1, you might get something like this.
Full source code of TMyListView and the demo project is attached. Feel free to use or improve it.
Edited by LuthfiHakim, 10 February 2013 - 10:10 AM.