views:

1891

answers:

4

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:

  1. Is it possible to update the child ListViews when the master ListView ScrollBar is dragged?
  2. If so, how?
A: 

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.

Yuval Peled
The EnsureVisible method only seems to scroll vertically (when in Detail mode) as there is no way to specify a SubItem. Unfortunately, my problem is specifically related to scroll horizontally.
akmad
Oh... Sorry. I didn't notice you were talking about the horizontal scroll. So here's a tweak to the above suggestion: Maybe you can get the scroll position by P-Invoking GetScrollPos on the parent list and update the linked lists if needed.
Yuval Peled
+1  A: 

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
}
Rob Allen
This is a good idea; however, the ListView control does not have a Scroll event (at least not in .Net 2.0/3.5).
akmad
you can apparently (http://groups.google.com/group/microsoft.public.dotnet.framework.compactframework/browse_frm/thread/f71fb66d7c258a1d/d771fcfeb1ece63d) overlay a vScrollBar (and hScroll I would imagine) in your custom control to get that effect.
Rob Allen
Yet another good idea... but one that won't work in my specific scenario. I need to be able to scroll horizontally and the EnsureVisible method (used by the solution in that link) only seems to support scrolling vertically (there is no way to specify a SubItem).
akmad
In that case, you'll either need to build a new control entirely from scratch or use a GridView or other control which does support .scroll
Rob Allen
I think that creating a custom control is a better solution than pinvoking and trying to hack together the behaviors you want.
Will
+1  A: 

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); 
}
Jeremy
This does not fix the issue. The problem I am having has to do with dragging the scrollbar, not clicking on the directional buttons.
akmad
+2  A: 

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.

AZDean