How to detect when an MDIClient window has been scrolled - c#

I need to update the position of a child window inside my System.Windows.Forms.MDIClient container when the user scrolls it by dragging the MDIClient's scrollbar thumb.
However I can't find an event that triggers when this happens.
Am I simply missing it, or do I need a workaround, possibly by talking direct to the scrollbar?
I've already tried handling MDIClient.Layout events, but they aren't being triggered by scrolling.
EDIT: I actually only need to know when the scrolling has stopped, in order to change my child window's position.
EDIT2: As a temporary workaround, I'm resetting the child window position on a timer every second, obviously not ideal, but better than nothing. Looks terrible though!

That's possible although a bit awkward. Winforms doesn't make it very easy to find the MdiClient window back and the class itself doesn't expose the Scroll event. That can be worked around, as always in Winforms, you have to sub-class the native MDI client window of your parent window so you can capture the WM_VSCROLL message. This code worked well, paste it into your parent form class:
void MdiClient_Scroll(object sender, ScrollEventArgs e) {
if (e.Type == ScrollEventType.EndScroll) {
// Do your stuff
//...
}
}
private MdiClientWrapper wrapper;
protected override void OnHandleCreated(EventArgs e) {
// Find the MdiClient and sub-class it so we can get the Scroll event
base.OnHandleCreated(e);
if (wrapper != null) wrapper.Scroll -= MdiClient_Scroll;
var client = this.Controls.OfType<MdiClient>().First();
wrapper = new MdiClientWrapper();
wrapper.AssignHandle(client.Handle);
wrapper.Scroll += MdiClient_Scroll;
}
private class MdiClientWrapper : NativeWindow {
public event ScrollEventHandler Scroll;
private int oldPos;
protected override void WndProc(ref Message m) {
if (m.Msg == 0x115) { // Trap WM_VSCROLL
var type = (ScrollEventType)(m.WParam.ToInt32() & 0xffff);
var pos = m.WParam.ToInt32() >> 16;
Scroll(this, new ScrollEventArgs(type, oldPos, pos));
oldPos = pos;
}
base.WndProc(ref m);
}
}

Related

Detecting click anywhere on form (including controls) in WinForms

A WinForms application, in which a certain UI control has to disappear 5 seconds after the user interacts with the window (clicking anywhere on the form including any control, or key presses).
Assuming an event is fired when this happens, the following is the event handler:
private async void userInteract(object sender, System.EventArgs e)
{
if (progressBarFinished)
{
await Task.Delay(5000);
statusIdle(); // this method hides a progress bar after 5 seconds. It is working.
}
}
In the form constructor, the event is subscribed to as follows:
// at the moment, it is not working for when a user clicks anywhere on the form
// it is working when a specific control click event occurs, like this one
progressBar.Click += userInteract;
Subscribing to the MouseClick and KeyPress events of the Form itself have been tried, but the event does not seem to fire then. It's only when specific control events are used, that it works, as stated above.
A form’s WndProc method processes messages sent to the form by the Windows operating system. This is an extremely important method that allows forms to move, resize, repaint, and perform other critical operations.
// Constants for decoding the Win32 message.
protected const int WM_MOUSEACTIVATE = 0x0021;
protected const int WM_LBUTTONDOWN = 0x201;
protected const int WM_RBUTTONDOWN = 0x204;
protected override void WndProc(ref Message m)
{
// Check the Message parameter to see if the message is WM_MOUSEACTIVATE indicating that a control was clicked.
if (m.Msg == WM_MOUSEACTIVATE)
{
int wparam = m.WParam.ToInt32();
if (wparam == WM_LBUTTONDOWN || wparam == WM_RBUTTONDOWN)
{
// TODO: Do something with the mouse event.
Console.WriteLine(m);
return;
}
}
base.WndProc(ref m);
}
Ref 1
Ref 2

How to display/hide a control on mouse hover event

I have been developing an application lately, and found myself stuck on a simple but annoying problem.
I would like to make a specific control visible/not visible when I enter its parent, and being able to perform events (e.g.: click) on this control. The problem is, the mouse hover even does not work on the parent when I enter the very control I want to display. This result in a flickering of the control I want to display (mouse hover works -> control is displayed -> mouse hover does not work anymore -> control is hidden -> mouse hover works -> etc).
I have found this "solution" to help me have something "stable".
// Timer to make the control appearing properly.
private void Timer_Elapsed(object o, ElapsedEventArgs e)
{
try
{
ItemToHideDisplay.Visible = true;
var mousePoint = this.PointToClient(Cursor.Position);
if (mousePoint.X > this.Width ||
mousePoint.X < 0 ||
mousePoint.Y > this.Height ||
mousePoint.Y < 0)
{
HideDisplayTimer.Stop();
ItemToHideDisplay.Visible = false;
base.OnMouseLeave(e);
}
}
catch
{
// We don't want the application to crash...
}
}
protected override void OnMouseEnter(EventArgs e)
{
HideDisplayTimer.Start();
base.OnMouseEnter(e);
}
Basically, when I enter the object, a timer starts and checks every 50ms if the mouse is in the parent. If so, the control is displayed. If not, the timer is stopped and the control hidden.
This works. Yay. But I find this solution very ugly.
So my question is: is there another approach, another solution more beautiful than this one?
Tell me if I am not clear enough :)
Thanks in advance!
EDIT: Hey I think I have found it myself!
The trick is to override OnMouseLeave of the parent control with this:
protected override void OnMouseLeave(EventArgs e)
{
var mousePoint = this.PointToClient(Cursor.Position);
if (mousePoint.X > this.Width ||
mousePoint.X < 0 ||
mousePoint.Y > this.Height ||
mousePoint.Y < 0)
{
base.OnMouseLeave(e);
}
}
This way, when entering the control I have displayed (entering the parent control), the mouse leave event is not triggered!
It works!
Thanks for your answers. You can continue to post your ideas I guess, because I don't see a lot of solutions out there on the internet :)
You can make a control "transparent" to mouse events. So mouse events will just pass through it.
You have to write your own class inheriting your desired control. If, for example, a Label is your specific control, then create a new class inheriting Label - you get the point. :-)
In your new class you then make use of the window messages to make your control ignore mouse events:
protected override void WndProc(ref Message m)
{
const int WM_NCHITTEST = 0x0084;
const int HTTRANSPARENT = -1;
switch(m.Msg)
{
case WM_NCHITTEST:
m.Result = (IntPtr)HTTRANSPARENT;
break;
default:
base.WndProc(ref m);
}
}
You can read more about WndProc at MSDN.
You could register a message filter for your form and pre-process the mouse move events of your form. Thanks to this, you don't have to override your child controls etc.
The message filter, once registered in the parent form, will work for the child forms too, so even when a part of your form is covered by a child form, your target control should still appear/dissapear depending on the mouse position.
In the following example, there is a panel on a form, and that panel has a button inside.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
internal void CaptureMouseMove(Point location)
{
if (panel1.RectangleToScreen(panel1.ClientRectangle).Contains(location))
{
button1.Visible = true;
Console.WriteLine(location + "in " + panel1.RectangleToScreen(panel1.ClientRectangle));
}
else
{
button1.Visible = false;
Console.WriteLine(location + "out " + panel1.RectangleToScreen(panel1.ClientRectangle));
}
}
internal bool Form1_ProcessMouseMove(Message m)
{
Point pos = new Point(m.LParam.ToInt32() & 0xffff, m.LParam.ToInt32() >> 16);
Control ctr = Control.FromHandle(m.HWnd);
if (ctr != null)
{
pos = ctr.PointToScreen(pos);
}
else
{
pos = this.PointToScreen(pos);
}
this.CaptureMouseMove(pos);
return false;
}
private MouseMoveMessageFilter mouseMessageFilter;
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
// add filter here
this.mouseMessageFilter = new MouseMoveMessageFilter();
this.mouseMessageFilter.TargetForm = this;
this.mouseMessageFilter.ProcessMouseMove = this.Form1_ProcessMouseMove;
Application.AddMessageFilter(this.mouseMessageFilter);
}
protected override void OnClosed(EventArgs e)
{
// remove filter here
Application.RemoveMessageFilter(this.mouseMessageFilter);
base.OnClosed(e);
}
private class MouseMoveMessageFilter : IMessageFilter
{
public Form TargetForm { get; set; }
public Func<Message, bool> ProcessMouseMove;
public bool PreFilterMessage(ref Message m)
{
if (TargetForm.IsDisposed) return false;
//WM_MOUSEMOVE
if (m.Msg == 0x0200)
{
if (ProcessMouseMove != null)
return ProcessMouseMove(m);
}
return false;
}
}
}
I would do a trick here :) I would wrap the control into a new control :) check this out.
XAML:
<UserControl MouseEnter="Border_MouseEnter" MouseLeave="UserControl_MouseLeave" Margin="100" Background="Transparent">
<UserControl x:Name="ControlToHide" Background="Red">
<Button Content="hi" Width="100" Height="100"/>
</UserControl>
</UserControl>
Code behind:
private void Border_MouseEnter(object sender, MouseEventArgs e)
{
this.ControlToHide.Visibility = System.Windows.Visibility.Hidden;
}
private void UserControl_MouseLeave(object sender, MouseEventArgs e)
{
this.ControlToHide.Visibility = System.Windows.Visibility.Visible;
}
it's light, easy and working . Enjoy

Focus on scroll

I have a user control with a scrollbar (scrollbar appears as a contained user control, which inherits from Panel, is too large). When using the mouse to scroll all is well, but trying to scroll with the mousewheel dont work.
My solution here is to set focus to my child-control in an eventhandler for Scroll. This works. Now the question; Will this result in a lot of unecessary calls to childControl.Focus()? Is there a more neat way of doing this?
Edit: I think I was a bit unclear with my question so Rephrasing the question:
is
private void ChildControl_OnScroll(object sender, ScrollEventArgs scrollEventArgs)
{
this.childControl.Focus();
}
a bad way of setting the focus? I.e. will the focus be set mutliple times each time I scroll? or rather, will this cause (tiny) performance issues.
Here's another approach that gives focus when the scrollbar area of panel1 inside SomeUserControl is clicked. It uses NativeWindow so you don't have to change the panel in your UserControl. This way Focus() will only be called once, when the mouse goes down in the scrollbar area:
public partial class SomeUserControl : UserControl
{
private TrapMouseDownOnScrollArea trapScroll = null;
public SomeUserControl()
{
InitializeComponent();
this.VisibleChanged += new EventHandler(SomeUserControl_VisibleChanged);
}
void SomeUserControl_VisibleChanged(object sender, EventArgs e)
{
if (this.Visible && trapScroll == null)
{
trapScroll = new TrapMouseDownOnScrollArea(this.panel1);
}
}
private class TrapMouseDownOnScrollArea : NativeWindow
{
private Control control = null;
private const int WM_NCLBUTTONDOWN = 0xA1;
public TrapMouseDownOnScrollArea(Control ctl)
{
if (ctl != null && ctl.IsHandleCreated)
{
this.control = ctl;
this.AssignHandle(ctl.Handle);
}
}
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case WM_NCLBUTTONDOWN:
if (this.control != null)
{
Rectangle screenBounds = control.RectangleToScreen(new Rectangle(0, 0, control.Width, control.Height));
if (screenBounds.Contains(Cursor.Position))
{
control.Focus();
}
}
break;
}
base.WndProc(ref m);
}
}
}
This might be overkill for your scenario, but it demonstrates one way to trap lower level messages. As said before, you could also derive from Panel to achieve the same affect. You could also trap messages at the application level with IMessageFilter.
The MouseWheel event is an event that "bubbles". Windows sends it to the control that has the focus, regardless of where the mouse cursor is located. The most typical problem is that you have a control that cannot receive the focus. A Panel for example.
This changes when you put a control on the panel. Now that control can get the focus and gets the MouseWheel message. It won't have any use for it so the message passes to its parent. Which does have a use for it, the panel scrolls as expected.
You can get a focusable panel control from this answer. A generic "make it work like a browser or Office program" solution from this question
If childControl has a MouseEnter() event then use that instead:
private void childControl_MouseEnter(object sender, EventArgs e)
{
childControl.Focus();
}
Then the mouse wheel events should be direct to childControl.

Forwarding child control MouseMove events to the parent seamlessly

I have a custom UserControl that I am drawing using the GDI+. It is a transparent control which draws small shapes on top of the parent control.
All the parent window does is creates the control, gives it a Rectangle in which to draw itself, and then it receives events if the user clicks on the non-transparent areas.
The drawing part works perfectly, but right now what I need to do is to as seamlessly as possible forward all MouseMove, MouseClick, etc. events to the parent control IF those events occurred outside the shapes.
The shapes are drawn using GraphicsPath, and I am already able to detect if the mouse position is over a shape using GraphicsPath.IsVisible().
I want to do this in a way that requires zero or minimal extra code on the parent. The parent should not necessarily know whether the MouseMove event was forwarded from the child control or not, it should treat them all equally.
Do I have to pinvoke/SendMessage() to do this? Or is there an easier way using the .NET framework?
This is possible in the winapi, the WM_NCHITTEST message is sent by the window manager to ask what part of the control the mouse is on top of. What you can do is return HTTRANSPARENT and it will ask the parent window. Here's a sample UserControl that implements this. Catching the message requires overriding WndProc():
public partial class UserControl1 : UserControl {
public UserControl1() {
InitializeComponent();
paths = new List<GraphicsPath>();
GraphicsPath example = new GraphicsPath();
example.AddEllipse(new Rectangle(10, 10, 50, 30));
paths.Add(example);
}
List<GraphicsPath> paths;
protected override void OnPaint(PaintEventArgs e) {
foreach (var path in paths) e.Graphics.FillPath(Brushes.Blue, path);
base.OnPaint(e);
}
protected override void WndProc(ref Message m) {
base.WndProc(ref m);
// Trap WM_NCHITTEST on the client area
if (m.Msg == 0x84 && m.Result == (IntPtr)1) {
Point pos = new Point(m.LParam.ToInt32());
pos = this.PointToClient(pos);
bool oncurve = false;
foreach (var path in paths)
if (path.IsVisible(pos)) oncurve = true;
if (!oncurve) m.Result = (IntPtr)(-1); // HTTRANSPARENT
}
}
}
Test code in the form:
private void userControl11_MouseMove(object sender, MouseEventArgs e) {
Console.WriteLine("On shape {0}", e.Location);
}
private void Form1_MouseMove(object sender, MouseEventArgs e) {
Console.WriteLine("On form {0}", e.Location);
}

How to handle Form caption right click

I'd like a context menu on the caption bar right click
any tips/samples pref in c# ?
UPDATE - for various reasons, right click on the form won't work because the form is not empty and the form is composited dynamically so....
You can do this by trapping the WM_NCRBUTTONDOWN notification that Windows sends when the user right-clicks the title bar. The control class does not have an event for it, you'll need to override WndProc(). Here's an example form, you'll need to add a ContextMenuStrip:
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
protected void OnTitlebarClick(Point pos) {
contextMenuStrip1.Show(pos);
}
protected override void WndProc(ref Message m) {
const int WM_NCRBUTTONDOWN = 0xa4;
if (m.Msg == WM_NCRBUTTONDOWN) {
var pos = new Point(m.LParam.ToInt32());
OnTitlebarClick(pos);
return;
}
base.WndProc(ref m);
}
}
MSDN explains how to handle right-clicks on Windows Forms controls. Controls, including Forms, inherit the MouseClick event.
MouseEventArgs will tell you what button was clicked through the Button property. Have a look at the MouseButtons Enumeration.
if you handle the form mouse-click, you can then use the following code:
private void Dialog_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
this.Text = "new caption text";
}
}
But you'll have to make sure that you generate this event for the top-level control on a form. For instance if you have a group box on the form, it will receive the mouse-click events rather than the form itself, for the areas of the form that are under the group box.
There is already a menu managed by Windows when you right-click the titlebar.
Do you want to replace it completely?
If you want to add to it you will have to use the Win32 API and interop and you will have to subclass the form.
See the AppendMenu() function.
Basically you need to use p-invoke to do this. There is a really great example at Here
You can see from the example you will need to manually mimic the event handlers, but this is pretty straight forward.
You can override WndProc of the form and capture the WM_NCRBUTTONDOWN message:
protected override void WndProc(ref Message m)
{
const int WM_NCRBUTTONDOWN = 0xA4;
if (m.Msg == WM_NCRBUTTONDOWN)
{
MessageBox.Show("Caption right clicked!");
}
else
{
base.WndProc(ref m);
}
}
This code will suppress the window's context menu, however. You may not wish this. The WM_NCRBUTTONDOWN message will also be sent if you right click the window borders as well. You may not desire this either.

Categories