How to pass C# ListView scroll to parent container - c#

In my C# Windows Forms application, I have a form which contains a System.Windows.Forms.TabPage that contains multiple System.Windows.Forms.ListViews. The TabPage has a vertical scrollbar.
While scrolling through this TabPage, when the mouse is hovering one of the ListViews, the scrolling stops.
What would be the best way to solve this issue and allow to continue scrolling when the mouse is hovering one of the ListViews?

After searching for a bit, this was my solution:
I created a new control with the following code:
using System;
using System.Windows.Forms;
namespace MyApplication.Controls
{
public class ScrollableListView : ListView
{
[System.Runtime.InteropServices.DllImport("User32.dll")]
public static extern IntPtr PostMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
protected override void WndProc(ref Message m)
{
const int WM_MOUSEWHEEL = 0x020A;
switch (m.Msg)
{
case WM_MOUSEWHEEL:
if (m.HWnd == Handle)
{
if (Parent is TabPage)
PostMessage((Parent as TabPage).Handle, m.Msg, m.WParam, m.LParam);
}
break;
default:
base.WndProc(ref m);
break;
}
}
}
}
When the mousewheel is used on this control, the event will be sent to the parent control, if the parent control is a System.Windows.Forms.TabPage.

Related

Make MouseWheel event pass from child to parent

I have a Panel where AutoScroll is true. This panel contains many smaller panels that fill all available space like tiles. When there are too many sub-panels to display I get the vertical scroll bar as expected.
Each of these "tiles" have some event handlers tied to them to handle MouseDown / MouseUp / MouseMove as they can be dragged around.
The problem I'm having is that mouse wheel scroll doesn't work on the parent panel as it won't have focus. I can't give it focus because in all probability I'll be scrolling while moving a sub-panel which will have the focus instead, and even then that would require workarounds since panels don't like focus.
I've been trying (and failing) to find a way to propagate only mousewheel events from the children to the parent.
I've read that in Winforms if a control cannot handle a mouse event it will bubble it up to the parent of that control, and then to the parent of that control and so on until it finds a suitable handler.
With this in mind I figured the best solution would be to use WndProc to override all scroll related events on the sub-panel and pass them to the parent while leaving all other events intact but admittedly this isn't my strong suit and I'm lost.
I've tried a few other solutions such as making the sub-panels invisible to all mouse events but as you might have guessed this was bad. I've read about implementing a message filter but didn't understand it.
Here's the code that will give you a very basic example of the panel and its children:
private void Form1_Load(object sender, EventArgs e)
{
Height = 600;
Width = 300;
Color[] colors = new Color[]{ Color.PowderBlue, Color.PeachPuff };
Panel panel = new Panel()
{
Height = this.ClientSize.Height - 20,
Width = 200,
Top = 10,
Left = 10,
BackColor = Color.White,
BorderStyle = BorderStyle.FixedSingle,
AutoScroll = true
};
for (int i = 0; i < 10; i++)
{
Panel subPanel = new Panel()
{
Name = #"SubPanel " + i.ToString(),
Height = 100,
Width = panel.Width - System.Windows.Forms.SystemInformation.VerticalScrollBarWidth - 2,
BackColor = colors[i % 2],
Top = i * 100
};
subPanel.MouseClick += subPanel_MouseClick;
panel.Controls.Add(subPanel);
}
Controls.Add(panel);
}
void subPanel_MouseClick(object sender, MouseEventArgs e)
{
Panel panel = sender as Panel;
Text = panel.Name;
}
Here's my attempt at overriding WndProc in a custom panel:
class NoScrollPanel : Panel
{
private const int WM_HSCROLL = 0x114;
private const int WM_VSCROLL = 0x115;
private const int MOUSEWHEEL = 0x020A;
private const int KEYDOWN = 0x0100;
protected override void WndProc(ref Message m)
{
if ((m.HWnd == Handle) && (m.Msg == MOUSEWHEEL || m.Msg == WM_VSCROLL || (m.Msg == KEYDOWN && (m.WParam == (IntPtr)40 || m.WParam == (IntPtr)35))))
{
PostMessage(Parent.Handle, m.Msg, m.WParam, m.LParam);
}
else
{
base.WndProc(ref m);
}
}
[DllImport("User32.dll")]
private static extern IntPtr PostMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
}
Any help or alternative approaches are most welcome. Thanks!
At my side #a-clymer's solution does not work, perhaps different environment. Currently there is no direct, clear answer for my problem so I try to combine several thoughts of other professionals and succeed.
In my current project, I have a few input controls contained in a Panel. I managed to make the mouse wheel scroll the panel instead of the ComboBox items by creating a child class of ComboBox and override its WndProc:
public class ComboBoxWithParentMouseWheel : ComboBox
{
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
const int WM_MOUSEWHEEL = 0x020A;
//thanks to a-clymer's solution
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_MOUSEWHEEL)
{
//directly send the message to parent without processing it
//according to https://stackoverflow.com/a/19618100
SendMessage(this.Parent.Handle, m.Msg, m.WParam, m.LParam);
m.Result = IntPtr.Zero;
}else base.WndProc(ref m);
}
}
All credit goes to Hans Passant (again), taken from the thread he suggested: https://stackoverflow.com/a/3562449/17034
Allowing the the containing panel to take focus worked fine. For the demo code above the class needs no changes, just use it for only the containing panel. I had to make some tweaks to my project to call focus when necessary, but it was far from rocket science.
Thanks again.
I could not get any of these solutions to work. I am overriding the WndProc fxn just like you. Finally found the solution! I noticed that if the container holding my TextBox was in focus, then the scroll worked, just not when the TextBox was in focus.
I didn't need to change the transparency of the event, I needed to send the event to a different Control Handle! (It seems so simple now, but I've trying to figure this out for days!)
internal class myTextBox : TextBox
{
const int WM_MOUSEWHEEL = 0x020A;
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_MOUSEWHEEL)
m.HWnd = this.Parent.Handle; // Change the Handle of the message
base.WndProc(ref m);
}
}
I haven't seen any mention of this method in all my Google searches, if there is some reason NOT to do this I hope someone will reply.
An improved version of #a-clymer version that works better.
const int WM_MOUSEWHEEL = 0x020A;
if (m.Msg == WM_MOUSEWHEEL)
{
// find the first scrollable parent control
Control p = this;
do
{
p = p.Parent;
} while (p != null && !(p is ScrollableControl));
// rewrite the destination handle of the message
if (p != null)
m.HWnd = p.Handle;
}

Execute application when paste is clicked in Windows (explorer)

I have an application, which basically is a copier, but it can do much more other stuff. What I can't get past is this:
I want to open the application when the user selected a few files (in explorer, desktop, or anywhere in Windows), and all those selected files, should be in the cache or something like that so that it is in a list or something.
This is done by Windows, so I don't have to do that. When the user selected all of the files he wanted to select, and copied it, how do I execute the application when the user pastes that files somewhere else? So that it automatically opens?
I have this:
[DllImport("User32.dll")]
protected static extern int
SetClipboardViewer(int hWndNewViewer);
[DllImport("User32.dll", CharSet = CharSet.Auto)]
public static extern bool
ChangeClipboardChain(IntPtr hWndRemove,
IntPtr hWndNewNext);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int SendMessage(IntPtr hwnd, int wMsg,
IntPtr wParam,
IntPtr lParam);
IntPtr nextClipboardViewer;
protected override void WndProc(ref System.Windows.Forms.Message m)
{
// Defined in winuser.h
const int WM_DRAWCLIPBOARD = 0x308;
const int WM_CHANGECBCHAIN = 0x030D;
switch (m.Msg)
{
case WM_DRAWCLIPBOARD:
DisplayClipboardData();
SendMessage(nextClipboardViewer, m.Msg, m.WParam, m.LParam);
break;
case WM_CHANGECBCHAIN:
if (m.WParam == nextClipboardViewer)
{
nextClipboardViewer = m.LParam;
}
else
{
SendMessage(nextClipboardViewer, m.Msg, m.WParam,
m.LParam);
}
break;
default:
base.WndProc(ref m);
break;
}
}
void DisplayClipboardData()
{
try
{
IDataObject iData = new DataObject();
iData = Clipboard.GetDataObject();
if (iData.GetDataPresent(DataFormats.Rtf))
{
richTextBox1.Rtf = (string)iData.GetData(DataFormats.Rtf);
}
else if (iData.GetDataPresent(DataFormats.Text))
{
richTextBox1.Text = (string)iData.GetData(DataFormats.Text);
}
else
{
richTextBox1.Text = "[Clipboard data is not RTF or ASCII Text]";
}
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
}
}
But this only works when text is selected. How can I display the file directory of each file?
If you want to react on the copy (Ctrl+C) or cut (Ctrl+V) operation rather than the paste (Ctrl+V) operation, it is actually relatively simple.
All you have to do is monitor the clipboard, because that's where this information goes. The data format you have to listen for is FileDrop.
Unfortunatelly, you will have to use the WinAPI to listen for clipboard changes.
When you received such a change, you can retrieve the list of copied files like this:
void DisplayClipboardData()
{
if(!Clipboard.ContainsFileDropList())
return;
var fileList = Clipboard.GetFileDropList();
// Do something with the file list.
}
I think you have to do 2 things:
1) Hook up to the windows keyboard & mouse events using the SetWindowsHookEx, here is an example in a winform application
2) check the clipboard's contents and do what you need with them
The main thing for this approach is that the hook which you would listen for keyboard events must be associated with a thread, so your application has to be already loaded up and remain resident in memory for you receive the events. You can create a limited gui and have an icon in the system trey maybe.
Edit: MSDN overview on windows hooks

How to handle WM_MOUSEHWHEEL the way Explorer does it?

I have Logitech M705 mouse with a scroll wheel that allows for horizontal scrolling. I have successfully implemented a handler for this button event in my C# program (implemented as described here), but so far I can only get it to scroll once. In Explorer, when I press the wheel to the right, it scrolls to the right continuously until I release the wheel. In my program, it scrolls only one step. The WM_MOUSEHWHEEL message is not seen until I release and press the wheel again!
Q: How do you implement continuous horizontal scrolling for the WM_MOUSEHWHEEL message?
Add this to all controls (form, children etc):
protected override void WndProc(ref System.Windows.Forms.Message m)
{
base.WndProc(ref m);
const int WM_MOUSEHWHEEL = 0x020E;
if (m.Msg == WM_MOUSEHWHEEL)
{
m.Result = new IntPtr(HIWORD(m.WParam) / WHEEL_DELTA);
}
}
The key is to return a nonzero value for all controls that might process the message!
Use IMessageFilter
public partial class MyForm: Form, IMessageFilter
...
public ImageForm(Image initialImage)
{
InitializeComponent();
Application.AddMessageFilter(this);
}
/// <summary>
/// Filters out a message before it is dispatched.
/// </summary>
/// <returns>
/// true to filter the message and stop it from being dispatched; false to allow the message to continue to the next filter or control.
/// </returns>
/// <param name="m">The message to be dispatched. You cannot modify this message. </param><filterpriority>1</filterpriority>
public bool PreFilterMessage( ref Message m )
{
if (m.Msg.IsWindowMessage(WindowsMessages.MOUSEWHEEL)) //if (m.Msg == 0x20a)
{ // WM_MOUSEWHEEL, find the control at screen position m.LParam
var pos = new Point(m.LParam.ToInt32() & 0xffff, m.LParam.ToInt32() >> 16);
var hWnd = WindowFromPoint(pos);
if (hWnd != IntPtr.Zero && hWnd != m.HWnd && FromHandle(hWnd) != null)
{
SendMessage(hWnd, m.Msg, m.WParam, m.LParam);
return true;
}
}
return false;
}
[DllImport("user32.dll")]
private static extern IntPtr WindowFromPoint(Point pt);
[DllImport("user32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
also in form closing add:
Application.RemoveMessageFilter(this);
This will pick up an all windows messages (although only mousewheel is trapped here) - by using the mouseposition to find the control its over, you can then force windows to send the message to that control even if it has no focus.
NOTE: I have used WindowsMessages.MOUSEWHEEL which is from a class I have that enumerates the messages, just replace
if (m.Msg.IsWindowMessage(WindowsMessages.MOUSEWHEEL))
with the commented bit at the end
if (m.Msg == 0x20a)

Detect keyup on winform without any control

As stated in title, I have a form that doesn't have any control on itself (so I can't focus it!!! damn).
I keep it controlless because I need to show images on background and I need to move it by keeping mouse clicked.
Are there any way to detect the keyup event when this is the foreground window?should I use a global hook (and check which is the foreground image obviusly)?
Any simplier workaround?I tested with an hidden control but it's not working.
The problem of putting a control with opacity = 0 brings the possibility to "miss" the MouseDown and MouseUp events (because they could happen over the control instead of the form, but I can still redirect them)
Any suggestion?
Here is the question where I picked some resources:
Fire Form KeyPress event
Can't you just set the Form's KeyPreview to true and use the Form's KeyUp Event? (or am i missing something?)
I would override OnKeyUp as it seems to be exactly what you are asking for. Here is an example of popping up a Message Box when the Escape key is released.
protected override void OnKeyUp(KeyEventArgs e)
{
if (e.KeyCode == Keys.Escape)
{
MessageBox.Show("Escape was pressed");
e.Handled = true;
}
base.OnKeyUp(e);
}
It looks that you are seeking for GlobalHook. Please have a look at SetWindowsHookEx Native Api. You can easily write your Pinvoke statements.
Here is an example from pinvoke.net
using System.Windows.Forms;
public class MyClass
{
private HookProc myCallbackDelegate = null;
public MyClass()
{
// initialize our delegate
this.myCallbackDelegate = new HookProc(this.MyCallbackFunction);
// setup a keyboard hook
SetWindowsHookEx(HookType.WH_KEYBOARD, this.myCallbackDelegate, IntPtr.Zero, AppDomain.GetCurrentThreadId());
}
[DllImport("user32.dll")]
protected static extern IntPtr SetWindowsHookEx(HookType code, HookProc func, IntPtr hInstance, int threadID);
[DllImport("user32.dll")]
static extern int CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
private int MyCallbackFunction(int code, IntPtr wParam, IntPtr lParam)
{
if (code < 0) {
//you need to call CallNextHookEx without further processing
//and return the value returned by CallNextHookEx
return CallNextHookEx(IntPtr.Zero, code, wParam, lParam);
}
// we can convert the 2nd parameter (the key code) to a System.Windows.Forms.Keys enum constant
Keys keyPressed = (Keys)wParam.ToInt32();
Console.WriteLine(keyPressed);
//return the value returned by CallNextHookEx
return CallNextHookEx(IntPtr.Zero, code, wParam, lParam);
}
}

C# ListView mouse wheel scroll without focus

I'm making a WinForms app with a ListView set to detail so that several columns can be displayed.
I'd like for this list to scroll when the mouse is over the control and the user uses the mouse scroll wheel. Right now, scrolling only happens when the ListView has focus.
How can I make the ListView scroll even when it doesn't have focus?
"Simple" and working solution:
public class FormContainingListView : Form, IMessageFilter
{
public FormContainingListView()
{
// ...
Application.AddMessageFilter(this);
}
#region mouse wheel without focus
// P/Invoke declarations
[DllImport("user32.dll")]
private static extern IntPtr WindowFromPoint(Point pt);
[DllImport("user32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
public bool PreFilterMessage(ref Message m)
{
if (m.Msg == 0x20a)
{
// WM_MOUSEWHEEL, find the control at screen position m.LParam
Point pos = new Point(m.LParam.ToInt32() & 0xffff, m.LParam.ToInt32() >> 16);
IntPtr hWnd = WindowFromPoint(pos);
if (hWnd != IntPtr.Zero && hWnd != m.HWnd && System.Windows.Forms.Control.FromHandle(hWnd) != null)
{
SendMessage(hWnd, m.Msg, m.WParam, m.LParam);
return true;
}
}
return false;
}
#endregion
}
You'll normally only get mouse/keyboard events to a window or control when it has focus. If you want to see them without focus then you're going to have to put in place a lower-level hook.
Here is an example low level mouse hook

Categories

Resources