Иногда функционала компонентов, предлагаемых Windows Forms, начинает не хватать. Основной метод расширения их функциональности — получение своих собственных компонентов, с помощью наследования от готового набора. В данном посте я хочу рассказать о переопределении метода WndProc(), который, как известно, получает все сообщения, адресованные компоненту.

В качестве примера, я рассмотрю переопределение этого метода для заголовка столбца компонента ListView. Сам по себе ListView весьма удобен и многофункционален, но для меня стало проблемой то, что ширину его колонок (используются в режиме Details) невозможно зафиксировать. То есть пользователь всегда будет волен изменять их, как его душе угодно, что, на мой взгляд, часто может приводить GUI в неудовлетворительный вид:
image


Для разминки сначала переопределим WndProc() для самого ListView, а не для его ColumnHeader'ов. Будем ловить щелчки левой кнопкой мыши:
class FixedColumnsListView : ListView<br>{<br>    protected override void WndProc(ref Message message)<br>    {<br>        const int WM_LBUTTONDOWN = 0x0201;<br>        switch (message.Msg)<br>        {<br>            case WM_LBUTTONDOWN:<br>                //делаем вид, что обработали сообщение:<br>                message.Result = (IntPtr)1; //записываем true в Result<br>                             //(не обязательно, работает и без этого)<br><br>                //заканчиваем работу WndProc() <br>                //без вызова его для базового класса:<br>                return; <br>        }<br>        //так как мы не поймали ничего нас интересующего, <br>        //то спокойно отдаем сообщение на обработку WndProc() базового класса<br>        base.WndProc(ref message);<br>    }<br>}<br><br>* This source code was highlighted with Source Code Highlighter.
Магические константы взять можно
отсюда.
Получаем ListView, который не реагирует на щелчки левой кнопкой мыши по своей рабочей области. Однако, любопытно то, что щелчки по заголовкам его столбцов все еще обрабатываются без всяких проблем.

Перейдем к основной части, переопределению WndProc() для ColumnHeader'ов. Прежде всего, нам нужно получить их дескриптор (handle). Сделать это можно несколькими способами. Например, мы можем воспользоваться WinAPI-функцией SendMessage():
[DllImport("User32.dll")]<br>public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);<br><br>  ...<br>  //в каком-нибудь методе нашего класса:<br>  const int LVM_GETHEADER = 0x1000 + 31;<br><br>  IntPtr columnHeadersHandle = SendMessage(this.Handle, LVM_GETHEADER, IntPtr.Zero, IntPtr.Zero);<br><br>* This source code was highlighted with Source Code Highlighter.
Константы, на этот раз, можно посмотреть
тут.
SendMessage() в данном случае вернет нам желанный дескриптор. Другой способ получить его — использовать GetWindow(). В связи с тем, что у ListView дочерний элемент только один, можно поступить так:
[DllImport("User32.dll")]<br>public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);<br><br>  ...<br>  //в каком-нибудь методе нашего класса:<br>  const uint GW_CHILD = 5;<br><br>  IntPtr columnHeadersHandle = GetWindow(this.Handle, GW_CHILD);<br><br>* This source code was highlighted with Source Code Highlighter.
Теперь нам нужно добраться до WndProc() этого элемента. На помощь приходит готовый .NET-овский класс NativeWindow, который нужно унаследовать и переопределить в нем нужный метод. Опишем свой класс для работы с заголовками столбцов, реализуя в нем все нам необходимое:
private class ListViewColumnHeaders : NativeWindow<br>{<br>    public ListViewColumnHeaders(ListView parentListView)<br>    {<br>        const uint GW_CHILD = 5;<br><br>        IntPtr columnHeadersHandle = GetWindow(parentListView.Handle, GW_CHILD);<br><br>        //заменим полученный дескриптор на дескриптор экземпляра нашего класса<br>        this.AssignHandle(columnHeadersHandle);<br>        //теперь все сообщения, направленные к заголовкам таблицы, будут обрабатываться нами<br>    }<br><br>    protected override void WndProc(ref Message message)<br>    {<br>        const int WM_LBUTTONDOWN = 0x0201;<br>        const int WM_SETCURSOR = 0x0020; //событие изменения внешнего вида курсора<br><br>        switch (message.Msg)<br>        {<br>            case WM_SETCURSOR:<br>            case WM_LBUTTONDOWN:<br>                message.Result = (IntPtr)1;<br>                return;<br>        }<br><br>        base.WndProc(ref message);<br>    }<br>}<br><br>* This source code was highlighted with Source Code Highlighter.
Отлавливая WM_SETCURSOR мы блокируем изменение курсора на растягивающий при наведении его на границы заголовков.

Теперь нам осталось лишь использовать полученный класс по назначению. Инициализировать его будем внутри OnCreateControl() родительского ListView. Единственным оставшимся уязвимым местом получаемого компонента FixedColumnsListView будет то, что юзер сможет сделать AutoResize всех колонок нажав Ctrl-Num+, так что предусмотрим и это. Итоговый вариант:
class FixedColumnsListView : ListView<br>{<br>    [DllImport("User32.dll")]<br>    public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);<br><br>    private ListViewColumnHeaders ColumnHeaders<br>    {<br>        get;<br>        set;<br>    }<br><br>    protected override void OnCreateControl()<br>    {<br>        base.OnCreateControl();<br><br>        ColumnHeaders = new ListViewColumnHeaders(this);<br>    }<br><br>    protected override void OnKeyDown(KeyEventArgs e)<br>    {<br>        if (e.KeyValue == 107 && e.Modifiers == Keys.Control)<br>        {<br>            e.Handled = true;<br>        }<br>        else<br>        {<br>            base.OnKeyDown(e);<br>        }<br>    }<br>}<br><br>* This source code was highlighted with Source Code Highlighter.

Желаемый эффект достигнут, заголовки стоят намертво, курсор не меняется раздражающе на растягивающий:
image

Справедливости ради, стоит отметить, что отчасти такого же эффекта можно было добиться используя дефолтный ListView, добавив такой обработчик:
private void listView_ColumnWidthChanging(object sender, ColumnWidthChangingEventArgs e)<br>{<br>    e.Cancel = true;<br>    e.NewWidth = listView.Columns[e.ColumnIndex].Width;<br>}<br><br>* This source code was highlighted with Source Code Highlighter.
И еще один костыль для горячих клавиш. Однако, курсор все также обманывающе менялся бы, что, на мой взгляд, несолидно. Ну и кроме того, переопределением WndProc() можно добиться еще множества разных преимуществ. Например, реализовать нормальную работу со скролл-баром компонента.