I have a winforms ToolStripComboBox with a ComboBox property. By default, it seems to auto-scroll to the selected index. See screenshot below:
On form load, I'm setting SelectedIndex to 1, which is what needs to happen. But I want the first item in the list (SelectedIndex 0) to be visible, or in other words auto-scroll to the very top. I can't find any way to force the combobox to scroll to the top by default, or to do so programmatically. There is an AutoScrollOffset property on ComboBox which I have experimented with, but it seems to do nothing, no matter what I set it to.
As seen in my screenshot above, I want to force the combobox (either via property or method call) to appear like the 2nd pic in which the top item (All - All Categories) is visible, while still leaving index 1 selected.
How can this be done?
When you open the dropdown, a LB_SETTOPINDEX message will be sent to the list which is in the dropdown menu. This message is responsible to setting the top index in the list.
You can handle this message and change its WParam to Intptr.Zero to always use 0 as top index.
Native Methods
Here is a class which contains native methods, structures and constants to manipulate the combo box for this purpose:
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public class NativeMethods
{
[StructLayout(LayoutKind.Sequential)]
public struct COMBOBOXINFO
{
public int cbSize;
public RECT rcItem;
public RECT rcButton;
public int stateButton;
public IntPtr hwndCombo;
public IntPtr hwndEdit;
public IntPtr hwndList;
}
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left; public int Top; public int Right; public int Bottom;
}
[DllImport("user32.dll")]
public static extern bool GetComboBoxInfo(IntPtr hWnd, ref COMBOBOXINFO pcbi);
public class ListBoxHelper : NativeWindow
{
private const int LB_SETTOPINDEX = 0x0197;
public ListBoxHelper(IntPtr hwnd) { this.AssignHandle(hwnd); }
protected override void WndProc(ref Message m)
{
if (m.Msg == LB_SETTOPINDEX)
m.WParam = IntPtr.Zero;
base.WndProc(ref m);
}
}
}
ComboBox
Here is a ComboBox which its dropdown always opens showing item 0 as top item:
public class MyComboBox : ComboBox
{
NativeMethods.ListBoxHelper listBoxHelper;
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
var info = new NativeMethods.COMBOBOXINFO();
info.cbSize = Marshal.SizeOf(info);
NativeMethods.GetComboBoxInfo(this.Handle, ref info);
listBoxHelper = new NativeMethods.ListBoxHelper(info.hwndList);
}
}
ToolStripComboBox
ToolStripComboBox hosts a ComboBox inside. So the solution is similar:
public class MyToolStripComboBox : ToolStripComboBox
{
public MyToolStripComboBox()
{
this.Control.HandleCreated += Control_HandleCreated;
}
NativeMethods.ListBoxHelper listBoxHelper;
private void Control_HandleCreated(object sender, EventArgs e)
{
base.OnVisibleChanged(e);
var info = new NativeMethods.COMBOBOXINFO();
info.cbSize = Marshal.SizeOf(info);
NativeMethods.GetComboBoxInfo(this.Control.Handle, ref info);
listBoxHelper = new NativeMethods.ListBoxHelper(info.hwndList);
}
}
Related
I'm not able to find an event to fire when I hover my ComboBox Items.
I'm using windows form to build an application.
I found a something similar for WPF:
how to change label text when I hover mouse over a combobox item?.
How can I do it the similar way in Windows Forms, or is there an alternate way?
Class ComboBoxListEx:
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows.Forms;
[DesignerCategory("Code")]
public class ComboBoxListEx : ComboBox
{
private int listItem = -1;
private const int CB_GETCURSEL = 0x0147;
public event EventHandler<ListItemSelectionChangedEventArgs> ListItemSelectionChanged;
protected virtual void OnListItemSelectionChanged(ListItemSelectionChangedEventArgs e)
=> this.ListItemSelectionChanged?.Invoke(this, e);
public ComboBoxListEx() { }
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
switch (m.Msg)
{
case CB_GETCURSEL:
int selItem = m.Result.ToInt32();
if (listItem != selItem)
{
listItem = selItem;
OnListItemSelectionChanged(new ListItemSelectionChangedEventArgs(
listItem, listItem < 0 ? string.Empty : this.GetItemText(this.Items[listItem]))
);
}
break;
default:
// Add Case switches to handle other events
break;
}
}
public class ListItemSelectionChangedEventArgs : EventArgs
{
public ListItemSelectionChangedEventArgs(int idx, string text)
{
this.ItemIndex = idx;
this.ItemText = text;
}
public int ItemIndex { get; private set; }
public string ItemText { get; private set; }
}
}
private void comboBoxListEx1_ListItemSelectionChanged(object sender, ComboBoxListEx.ListItemSelectionChangedEventArgs e)
{
label15.Text = e.ItemText;
}
You can create a Custom Control, derived from ComboBox, override its WndProc method to intercept the CB_GETCURSEL message.
Call base.WndProc(ref m) first. When the message is processed, the Message object's m.Result property is set to a value (as IntPtr) that represents the Item currently tracked in the ListBox (the Item highlighted when the Mouse Pointer hovers it).
► Note: prior to .Net Framework 4.8, the CB_GETCURSEL message result is not bubbled up automatically: we must send LB_GETCURSEL to the child ListBox to get the index of the Item currently highlighted.
The ListBox handle is retrieved using GetComboBoxInfo: it could be also accessed using reflection (the private ChildListAutomationObject property returns the ListBox AutomationElement, which provides the handle), or sending a CB_GETCOMBOBOXINFO message (but it's the same as calling GetComboBoxInfo()).
This custom ComboBox raises an Event, ListItemSelectionChanged, with a custom EventArgs object, ListItemSelectionChangedEventArgs, which exposes two public properties: ItemIndex and ItemText, set to the Index and Text of the hovered item.
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
[DesignerCategory("Code")]
public class ComboBoxListEx : ComboBox
{
private const int CB_GETCURSEL = 0x0147;
private int listItem = -1;
IntPtr listBoxHandle = IntPtr.Zero;
public event EventHandler<ListItemSelectionChangedEventArgs> ListItemSelectionChanged;
protected virtual void OnListItemSelectionChanged(ListItemSelectionChangedEventArgs e)
=> this.ListItemSelectionChanged?.Invoke(this, e);
public ComboBoxListEx() { }
// .Net Framework prior to 4.8 - get the handle of the ListBox
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
listBoxHandle = GetComboBoxListInternal(this.Handle);
}
protected override void WndProc(ref Message m)
{
int selItem = -1;
base.WndProc(ref m);
switch (m.Msg) {
case CB_GETCURSEL:
selItem = m.Result.ToInt32();
break;
// .Net Framework prior to 4.8
// case CB_GETCURSEL can be left there or removed: it's always -1
case 0x0134:
selItem = SendMessage(listBoxHandle, LB_GETCURSEL, 0, 0);
break;
default:
// Add Case switches to handle other events
break;
}
if (listItem != selItem) {
listItem = selItem;
OnListItemSelectionChanged(new ListItemSelectionChangedEventArgs(
listItem, listItem < 0 ? string.Empty : GetItemText(Items[listItem]))
);
}
}
public class ListItemSelectionChangedEventArgs : EventArgs
{
public ListItemSelectionChangedEventArgs(int idx, string text) {
ItemIndex = idx;
ItemText = text;
}
public int ItemIndex { get; private set; }
public string ItemText { get; private set; }
}
// -------------------------------------------------------------
// .Net Framework prior to 4.8
[DllImport("user32.dll", CharSet = CharSet.Auto)]
internal static extern bool GetComboBoxInfo(IntPtr hWnd, ref COMBOBOXINFO pcbi);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern int SendMessage(IntPtr hWnd, uint uMsg, int wParam, int lParam);
private const int LB_GETCURSEL = 0x0188;
[StructLayout(LayoutKind.Sequential)]
internal struct COMBOBOXINFO
{
public int cbSize;
public Rectangle rcItem;
public Rectangle rcButton;
public int buttonState;
public IntPtr hwndCombo;
public IntPtr hwndEdit;
public IntPtr hwndList;
public void Init() => this.cbSize = Marshal.SizeOf<COMBOBOXINFO>();
}
internal static IntPtr GetComboBoxListInternal(IntPtr cboHandle)
{
var cbInfo = new COMBOBOXINFO();
cbInfo.Init();
GetComboBoxInfo(cboHandle, ref cbInfo);
return cbInfo.hwndList;
}
}
Works like this:
I’m trying to catch TVN_SELCHANGING message from a TreeView. I know there is also the BeforeSelect event but I’d like to understand why I’m not able to catch the message…
I’ve read on msdn the TVN_SELCHANG(ED)(ING) LParam is a pointer to a NMTREEVIEW structure. Also that the code is sent in the form of a WM_NOTIFY message.
So I’ve tried to implement it:
(this helped me)
public partial class TreeviewEx : TreeView
{
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential)]
private struct TVITEM
{
public uint mask;
public IntPtr hItem;
public uint state;
public uint stateMask;
public IntPtr pszText;
public int cchTextMax;
public int iImage;
public int iSelectedImage;
public int cChildren;
public IntPtr lParam;
}
[StructLayout(LayoutKind.Sequential)]
private struct NMHDR
{
public IntPtr hwndFrom;
public IntPtr idFrom;
public int code;
}
[StructLayout(LayoutKind.Sequential)]
private struct NMTREEVIEW
{
public NMHDR hdr;
public int action;
public TVITEM itemOld;
public TVITEM itemNew;
public POINT ptDrag;
}
private const int TVN_FIRST = -400;
private const int TVN_SELCHANGINGA = (TVN_FIRST - 1);
private const int TVN_SELCHANGINGW = (TVN_FIRST - 50);
private const int TVN_SELCHANGEDA = (TVN_FIRST - 2);
private const int TVN_SELCHANGEDW = (TVN_FIRST - 51);
private const int WM_NOTIFY = 0x004e;
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_NOTIFY)
{
var notify = (NMTREEVIEW)Marshal.PtrToStructure(m.LParam, typeof(NMTREEVIEW));
if (notify.action == TVN_SELCHANGINGA)
{
MessageBox.Show("changing");
}
}
base.WndProc(ref m);
}
I've tried all actions, but none of them seem to work. What am I doing wrong?
Right, this doesn't work. Lots of history behind it, the native Windows controls were designed to be used in C programs. Using Petzold's "Programming Windows" style coding where you put the custom logic for a window in the window procedure. And just used a control like TreeView as-is. Accordingly, these controls send their notification messages to the parent window. Because that's where you put your code.
That's not very compatible with the way modern GUI code is written. Particularly the notion of inheriting a control to give it new behavior. Like you did with your TreeViewEx class. You really want to get these notifications in your own class first. So you can do interesting things with OnBeforeSelect() to customize the behavior of the control. Now having this message sent to the parent is rather a big problem, a control should never be aware of its parent's implementation.
Winforms fixes this problem, it reflects the message from the parent window back to the original window. Altering the message, necessary so it is completely clear that it is a reflected message. Which it does by adding a constant to the message number, WM_REFLECT, a value that you can hardcode to 0x2000. So fix it like this:
private const int WM_REFLECT = 0x2000;
protected override void WndProc(ref Message m) {
if (m.Msg == WM_REFLECT + WM_NOTIFY) {
var nmhdr = (NMHDR)Marshal.PtrToStructure(m.LParam, typeof(NMHDR));
if (nmhdr.code == TVN_SELCHANGINGW) {
var notify = (NMTREEVIEW)Marshal.PtrToStructure(m.LParam, typeof(NMTREEVIEW));
// etc..
}
}
base.WndProc(ref m);
}
How do I achieve this in a WinForms container control when the scroll bars are visible?
Highlighted here (Google Chrome browser):
EDIT: This cursor is the only one that is visible on a screenshot. I hope it's clear what i mean.
EDIT:
Tried this on my control. Does not work.
const int WM_MBUTTONDOWN = 0x207;
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_MBUTTONDOWN)
DefWndProc(ref m);
else
base.WndProc(ref m);
}
Here's what I have so far. It exits "reader mode" if I release the middle button, and I haven't implemented scrolling within the control (I used a textbox), but it may give you something to start with.
[DllImport("comctl32.dll", SetLastError=true, EntryPoint="#383")]
static extern void DoReaderMode(ref READERMODEINFO prmi);
public delegate bool TranslateDispatchCallbackDelegate(ref MSG lpmsg);
public delegate bool ReaderScrollCallbackDelegate(ref READERMODEINFO prmi, int dx, int dy);
[StructLayout(LayoutKind.Sequential)]
public struct READERMODEINFO
{
public int cbSize;
public IntPtr hwnd;
public int fFlags;
public IntPtr prc;
public ReaderScrollCallbackDelegate pfnScroll;
public TranslateDispatchCallbackDelegate fFlags2;
public IntPtr lParam;
}
[StructLayout(LayoutKind.Sequential)]
public struct MSG
{
public IntPtr hwnd;
public UInt32 message;
public IntPtr wParam;
public IntPtr lParam;
public UInt32 time;
public POINT pt;
}
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int x;
public int y;
}
[StructLayout(LayoutKind.Sequential)]
struct RECT
{
public int left, top, right, bottom;
}
private bool TranslateDispatchCallback(ref MSG lpMsg)
{
return false;
}
private bool ReaderScrollCallback(ref READERMODEINFO prmi, int dx, int dy)
{
// TODO: Scroll around within your control here
return false;
}
private void EnterReaderMode()
{
READERMODEINFO readerInfo = new READERMODEINFO
{
hwnd = this.textBox1.Handle,
fFlags = 0x00,
prc = IntPtr.Zero,
lParam = IntPtr.Zero,
fFlags2 = new TranslateDispatchCallbackDelegate(this.TranslateDispatchCallback),
pfnScroll = new ReaderScrollCallbackDelegate(this.ReaderScrollCallback)
};
readerInfo.cbSize = Marshal.SizeOf(readerInfo);
DoReaderMode(ref readerInfo);
}
private void textBox1_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == System.Windows.Forms.MouseButtons.Middle)
{
EnterReaderMode();
}
}
The RichTextBox control does it by default when you press the mouse wheel button.
Edit: Sorry I misunderstood and thought you were asking about doing it within a textbox not a container control
I have a WPF window that enables glass on itself during its SourceInitialized event. This works perfectly. I'll use the simplest example possible (just one window object) to demonstrate where the issue is.
public partial class MainWindow : Window
{
public bool lolz = false;
public MainWindow()
{
InitializeComponent();
this.SourceInitialized += (x, y) =>
{
AeroExtend(this);
};
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
if (!lolz)
{
MainWindow mw = new MainWindow();
mw.lolz = true;
mw.ShowDialog();
}
}
}
This creates two MainWindows. When I debug this in Visual Studio everything works as expected.
When I run without debugging, not so much.
The child window has an odd, incorrectly applied glass frame... but only when running it directly without Visual Studio debugging. Same code ran twice but with different results. It doesn't matter when I create the second window, I've tied it to a button click with the same output.
Any ideas?
EDIT: Here is an excerpt of the code I'm using for AeroExtend
[DllImport("dwmapi.dll")]
private static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS pMargins);
[DllImport("dwmapi.dll", PreserveSig = false)]
private static extern bool DwmIsCompositionEnabled();
[StructLayout(LayoutKind.Sequential)]
private class MARGINS
{
public MARGINS(Thickness t)
{
cxLeftWidth = (int)t.Left;
cxRightWidth = (int)t.Right;
cyTopHeight = (int)t.Top;
cyBottomHeight = (int)t.Bottom;
}
public int cxLeftWidth, cxRightWidth,
cyTopHeight, cyBottomHeight;
}
...
static public bool AeroExtend(this Window window)
{
if (Environment.OSVersion.Version.Major >= 6 && DwmIsCompositionEnabled())
{
IntPtr mainWindowPtr = new WindowInteropHelper(window).Handle;
HwndSource mainWindowSrc = HwndSource.FromHwnd(mainWindowPtr);
mainWindowSrc.CompositionTarget.BackgroundColor = Colors.Transparent;
window.Background = System.Windows.Media.Brushes.Transparent;
MARGINS margins = new MARGINS(new Thickness(-1));
int result = DwmExtendFrameIntoClientArea(mainWindowSrc.Handle, ref margins);
if (result < 0)
{
return false;
}
return true;
}
return false;
}
The problem is that you have MARGINS defined as a class. You'll notice that if you tried using a different set of values for the margin (e.g. 10 pixels on each edge) that it will still try to fill the entire area. Also as I mentioned in my comment the other day, you will notice that you have an artifact in the lower right corner even in the original window that wasn't shown modally. If you simply change the MARGINS from a class to a struct then the problem does not occur. e.g.
[StructLayout(LayoutKind.Sequential)]
private struct MARGINS
Alternatively you could leave MARGINS a class but then you should change the way the DwmExtendFrameIntoClientArea is defined. e.g.
[DllImport("dwmapi.dll")]
private static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, [MarshalAs(UnmanagedType.LPStruct)] MARGINS pMargins);
I'm working on a control to tie together the view from one ListView to another so that when the master ListView is scrolled, the child ListView view is updated to match.
So far I've been able to get the child ListViews to update their view when the master scrollbar buttons are clicked. The problem is that when clicking and dragging the ScrollBar itself, the child ListViews are not updated. I've looked at the messages being sent using Spy++ and the correct messages are getting sent.
Here is my current code:
public partial class LinkedListViewControl : ListView
{
[DllImport("User32.dll")]
private static extern bool SendMessage(IntPtr hwnd, UInt32 msg, IntPtr wParam, IntPtr lParam);
[DllImport("User32.dll")]
private static extern bool ShowScrollBar(IntPtr hwnd, int wBar, bool bShow);
[DllImport("user32.dll")]
private static extern int SetScrollPos(IntPtr hWnd, int wBar, int nPos, bool bRedraw);
private const int WM_HSCROLL = 0x114;
private const int SB_HORZ = 0;
private const int SB_VERT = 1;
private const int SB_CTL = 2;
private const int SB_BOTH = 3;
private const int SB_THUMBPOSITION = 4;
private const int SB_THUMBTRACK = 5;
private const int SB_ENDSCROLL = 8;
public LinkedListViewControl()
{
InitializeComponent();
}
private readonly List<ListView> _linkedListViews = new List<ListView>();
public void AddLinkedView(ListView listView)
{
if (!_linkedListViews.Contains(listView))
{
_linkedListViews.Add(listView);
HideScrollBar(listView);
}
}
public bool RemoveLinkedView(ListView listView)
{
return _linkedListViews.Remove(listView);
}
private void HideScrollBar(ListView listView)
{
//Make sure the list view is scrollable
listView.Scrollable = true;
//Then hide the scroll bar
ShowScrollBar(listView.Handle, SB_BOTH, false);
}
protected override void WndProc(ref Message msg)
{
if (_linkedListViews.Count > 0)
{
//Look for WM_HSCROLL messages
if (msg.Msg == WM_HSCROLL)
{
foreach (ListView view in _linkedListViews)
{
SendMessage(view.Handle, WM_HSCROLL, msg.WParam, IntPtr.Zero);
}
}
}
}
}
Based on this post on the MS Tech Forums I tried to capture and process the SB_THUMBTRACK event:
protected override void WndProc(ref Message msg)
{
if (_linkedListViews.Count > 0)
{
//Look for WM_HSCROLL messages
if (msg.Msg == WM_HSCROLL)
{
Int16 hi = (Int16)((int)msg.WParam >> 16);
Int16 lo = (Int16)msg.WParam;
foreach (ListView view in _linkedListViews)
{
if (lo == SB_THUMBTRACK)
{
SetScrollPos(view.Handle, SB_HORZ, hi, true);
int wParam = 4 + 0x10000 * hi;
SendMessage(view.Handle, WM_HSCROLL, (IntPtr)(wParam), IntPtr.Zero);
}
else
{
SendMessage(view.Handle, WM_HSCROLL, msg.WParam, IntPtr.Zero);
}
}
}
}
// Pass message to default handler.
base.WndProc(ref msg);
}
This will update the location of the child ListView ScrollBar but does not change the actual view in the child.
So my questions are:
Is it possible to update the child ListViews when the master ListView ScrollBar is dragged?
If so, how?
I wanted to do the same thing, and after searching around I found your code here, which helped, but of course didn't solve the problem. But after playing around with it, I have found a solution.
The key came when I realized that since the scroll buttons work, that you can use that to make the slider work. In other words, when the SB_THUMBTRACK event comes in, I issue repeated SB_LINELEFT and SB_LINERIGHT events until my child ListView gets close to where the master is. Yes, this isn't perfect, but it works close enough.
In my case, my master ListView is called "reportView", while my child ListView is called "summaryView". Here's my pertinent code:
public class MyListView : ListView
{
public event ScrollEventHandler HScrollEvent;
protected override void WndProc(ref System.Windows.Forms.Message msg)
{
if (msg.Msg==WM_HSCROLL && HScrollEvent != null)
HScrollEvent(this,new ScrollEventArgs(ScrollEventType.ThumbTrack, (int)msg.WParam));
base.WndProc(ref msg);
}
}
And then the event handler itself:
reportView.HScrollEvent += new ScrollEventHandler((sender,e) => {
if ((ushort) e.NewValue != SB_THUMBTRACK)
SendMessage(summaryView.Handle, WM_HSCROLL, (IntPtr) e.NewValue, IntPtr.Zero);
else {
int newPos = e.NewValue >> 16;
int oldPos = GetScrollPos(reportView .Handle, SB_HORZ);
int pos = GetScrollPos(summaryView.Handle, SB_HORZ);
int lst;
if (pos != newPos)
if (pos<newPos && oldPos<newPos) do { lst=pos; SendMessage(summaryView.Handle,WM_HSCROLL,(IntPtr)SB_LINERIGHT,IntPtr.Zero); } while ((pos=GetScrollPos(summaryView.Handle,SB_HORZ)) < newPos && pos!=lst);
else if (pos>newPos && oldPos>newPos) do { lst=pos; SendMessage(summaryView.Handle,WM_HSCROLL,(IntPtr)SB_LINELEFT, IntPtr.Zero); } while ((pos=GetScrollPos(summaryView.Handle,SB_HORZ)) > newPos && pos!=lst);
}
});
Sorry about the odd formatting of the while loops there, but that's how I prefer to code things like that.
The next problem was getting rid of the scroll bars in the child ListView. I noticed you had a method called HideScrollBar. This didn't really work for me. I found a better solution in my case was leaving the scroll bar there, but "covering" it up instead. I do this with the column header as well. I just slide my child control up under the master control to cover the column header. And then I stretch the child to fall out of the panel that contains it. And then to provide a bit of a border along the edge of my containing panel, I throw in a control to cover the visible bottom edge of my child ListView. It ends up looking rather nice.
I also added an event handler to sync changing column widths, as in:
reportView.ColumnWidthChanging += new ColumnWidthChangingEventHandler((sender,e) => {
summaryView.Columns[e.ColumnIndex].Width = e.NewWidth;
});
While this all seems a bit of a kludge, it works for me.
This is conjecture just to get the mental juices flowing so take it as you will:
In the scroll handler for the master list, can you call the scroll handler for the child list (passing the sender and eventargs from the master)?
Add this to your Form load:
masterList.Scroll += new ScrollEventHandler(this.masterList_scroll);
Which references this:
private void masterList_scroll(Object sender, System.ScrollEventArgs e)
{
childList_scroll(sender, e);
}
private void childList_scroll(Object sender, System.ScrollEventArgs e)
{
childList.value = e.NewValue
}
I would create my own class, inheriting from ListView to expose the Vertical and Horizontal scroll events.
Then I would do create scroll handlers in my form to synchronize the two controls
This is sample code which should allow a listview to publish scroll events:
public class MyListView : System.Windows.Forms.ListView
{
const int WM_HSCROLL = 0x0114;
const int WM_VSCROLL = 0x0115;
private ScrollEventHandler evtHScroll_m;
private ScrollEventHandler evtVScroll_m;
public event ScrollEventHandler OnHScroll
{
add
{
evtHScroll_m += value;
}
remove
{
evtHScroll_m -= value;
}
}
public event ScrollEventHandler OnHVcroll
{
add
{
evtVScroll_m += value;
}
remove
{
evtVScroll_m -= value;
}
}
protected override void WndProc(ref System.Windows.Forms.Message msg)
{
if (msg.Msg == WM_HSCROLL && evtHScroll_m != null)
{
evtHScroll_m(this,new ScrollEventArgs(ScrollEventType.ThumbTrack, msg.WParam.ToInt32()));
}
if (msg.Msg == WM_VSCROLL && evtVScroll_m != null)
{
evtVScroll_m(this, new ScrollEventArgs(ScrollEventType.ThumbTrack, msg.WParam.ToInt32()));
}
base.WndProc(ref msg);
}
Now handle the scroll events in your form:
Set up a PInvoke method to be able to send a windows message to a control:
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int SendMessage(IntPtr hWnd, [MarshalAs(UnmanagedType.U4)] int iMsg, int iWParam, int iLParam);
Set up your event handlers (lstMaster and lstChild are two listboxes):
lstMaster.OnVScroll += new ScrollEventHandler(this.lstMaster_OnVScroll);
lstMaster.OnHScroll += new ScrollEventHandler(this.lstMaster_OnHScroll);
const int WM_HSCROLL = 0x0114;
const int WM_VSCROLL = 0x0115;
private void lstMaster_OnVScroll(Object sender, System.ScrollEventArgs e)
{
SendMessage(lstChild.Handle,WM_VSCROLL,(IntPtr)e.NewValue, IntPtr.Zero);
}
private void lstMaster_OnHScroll(Object sender, System.ScrollEventArgs e)
{
SendMessage(lstChild.Handle,WM_HSCROLL,(IntPtr)e.NewValue, IntPtr.Zero);
}
A naive solution to your problem can be handling the paint message in the parent list view and checking if the linked list views are displaying the correct data. If they don't, then update them to display the correct data by calling the EnsureVisible method.