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);
}
Related
I have an old WinForms app written in C# with custom drag and drop capabilities. And this app has an issue when it is run on a touch screen device, such as the Surface Pro 3.
Basically there is a treeview control that allows the items to be dragged to a different area of the app and have some calculations done. If I use the mouse or the stylus the custom drag image is drawn on the screen. If I drag an item using touch the image is not displayed but the code is executed properly, including the drawing of a custom cursor.
It seems that the custom drag image is not displayed because the mouse cursor is hidden by the O.S. during a touch drag operation. How do I get the drag image to display?
UPDATE
Here is some code to demonstrate what I am trying to fix. Create a new WinForms app, add a treeview to it and wire up the events. You'll notice that if you use a stylus or the mouse the drag operation will show an icon. If you touch-drag an item, nothing shows.
public Form1()
{
InitializeComponent();
TreeNode node = new TreeNode("welp");
treeView1.Nodes.Add(node);
treeView1.Nodes.Add(node);
treeView1.Nodes.Add(node);
treeView1.Nodes.Add(node);
treeView1.Nodes.Add(node);
treeView1.Nodes.Add(node);
treeView1.Nodes.Add(node);
treeView1.Nodes.Add(node);
treeView1.Nodes.Add(node);
treeView1.Nodes.Add(node);
}
private void treeView1_DragEnter(object sender, DragEventArgs e)
{
e.Effect = DragDropEffects.Move;
}
private void treeView1_GiveFeedback(object sender, GiveFeedbackEventArgs e)
{
e.UseDefaultCursors = true;
}
private void treeView1_ItemDrag(object sender, ItemDragEventArgs e)
{
((TreeView)sender).DoDragDrop(e.Item, DragDropEffects.Move);
}
Ok, So I got this working exactly the way that I want it to. Wasn't easy and it's probably not the preferred way to go about it but it does what I want and I was on a time crunch to get this finished.
First, I found this article Shell Style Drag and Drop. It worked, for the most part, but it was written about 7 years ago and doesn't take into consideration how the O.S. has changed. Too much to go into the details about that and why it didn't work for me.
The main issue I was trying to fix was that my original drag and drop code used the old school way of drawing a new cursor in the GiveFeedback event of a drag and drop operation. Worked great except on touch devices because the Windows O.S. hid the cursor during a touch-drag operation so my custom drawn cursor would never be displayed. A few (used lightly) Google searches revealed that Windows won't allow two pointer devices to be active at the same time, even with calling the native ShowCursor method. Fair enough.
The biggest issue with trying to draw on the screen while in a drag and drop operation is that most of the messages sent to a window, such as WM_MOUSEMOVE and WM_PAINT are suspended. The only way I could draw anything was in the GiveFeedback event.
So, I found this article that covers drawing using a transparent overlay. Again, it didn't do exactly what I wanted but with a little tweaking it worked perfectly. I was able to grab the HDC of the overlay and the HDC of my form and do a nice BitBlt operation to display my custom bitmap on the screen and erase the old drawing by grabbing the underlying image of the form. I also changed the method that draws a custom cursor to a method that draws a custom bitmap.
Here is what I changed in regards to the article above
this.BackColor = Color.White;
this.Opacity = 1; // Tweak as desired
this.TransparencyKey = Color.White;
And what I added to allow the message pump to go through
protected override CreateParams CreateParams
{
get
{
CreateParams createParams = base.CreateParams;
createParams.ExStyle |= Win32.WS_EX_TRANSPARENT;
return createParams;
}
}
The details of the rest are not overly complex but require a nice blending of the drag and drop operations, the overlay, drawing and erasing and double buffering. And there are situations, like in my case, where I have a TreeView control updating while I am dragging and I have to expand the rectangle of the draw operation so the bitblt image covers the areas outside of my drawing.
If you are interested in this article and want to see what I did and how I accomplished it, let me know in the comments and I will post code.
try this code
hope is helped you
public Form1()
{
InitializeComponent();
TreeNode node;
for (int x = 0; x < 3; ++x)
{
node = treeView1.Nodes.Add(String.Format("Node{0}", x * 4));
for (int y = 1; y < 4; ++y)
{
node = node.Nodes.Add(String.Format("Node{0}", x * 4 + y));
}
}
treeView1.AllowDrop = true;
treeView1.Dock = DockStyle.Fill;
treeView1.ItemDrag += new ItemDragEventHandler(treeView1_ItemDrag);
treeView1.DragEnter += new DragEventHandler(treeView1_DragEnter);
treeView1.DragOver += new DragEventHandler(treeView1_DragOver);
treeView1.DragDrop += new DragEventHandler(treeView1_DragDrop);
}
private void treeView1_ItemDrag(object sender, ItemDragEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
DoDragDrop(e.Item, DragDropEffects.Move);
}
else if (e.Button == MouseButtons.Right)
{
DoDragDrop(e.Item, DragDropEffects.Copy);
}
}
private void treeView1_DragEnter(object sender, DragEventArgs e)
{
e.Effect = e.AllowedEffect;
}
private void treeView1_DragOver(object sender, DragEventArgs e)
{
Point targetPoint = treeView1.PointToClient(new Point(e.X, e.Y));
treeView1.SelectedNode = treeView1.GetNodeAt(targetPoint);
}
private void treeView1_DragDrop(object sender, DragEventArgs e)
{
Point targetPoint = treeView1.PointToClient(new Point(e.X, e.Y));
TreeNode targetNode = treeView1.GetNodeAt(targetPoint);
TreeNode draggedNode = (TreeNode)e.Data.GetData(typeof(TreeNode));
if (!draggedNode.Equals(targetNode) && !ContainsNode(draggedNode, targetNode))
{
if (e.Effect == DragDropEffects.Move)
{
draggedNode.Remove();
targetNode.Nodes.Add(draggedNode);
}
else if (e.Effect == DragDropEffects.Copy)
{
targetNode.Nodes.Add((TreeNode)draggedNode.Clone());
}
targetNode.Expand();
}
}
private bool ContainsNode(TreeNode node1, TreeNode node2)
{
if (node2.Parent == null) return false;
if (node2.Parent.Equals(node1)) return true;
return ContainsNode(node1, node2.Parent);
}
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
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);
}
}
I'm trying to draw borders around words and paragraphs in RichTextBox but when I turn on UserPaint it doesn't draw text anymore while my custom painting seems to work. May be I just forgot to turn on something else? Here is what I have
public partial class RichTextBoxEx : RichTextBox
{
public RichTextBoxEx()
{
InitializeComponent();
SetStyle(ControlStyles.UserPaint, true);
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
//Do some painting here
}
}
Using info from this question didn't help me
This worked ok for me:
class RichBox : RichTextBox {
private const int WM_PAINT = 15;
protected override void WndProc(ref Message m) {
if (m.Msg == WM_PAINT) {
this.Invalidate();
base.WndProc(ref m);
using (Graphics g = Graphics.FromHwnd(this.Handle)) {
g.DrawLine(Pens.Red, Point.Empty,
new Point(this.ClientSize.Width - 1,
this.ClientSize.Height - 1));
}
} else {
base.WndProc(ref m);
}
}
}
The events in rich textbox are a pain in the back since they don't fire the way you think they should fire. Here's a post where someone posted the minimum required code to have a new control that exposes the proper paint events for you by hosting a rich textbox and intercepting the windows paint requests. It is in VB.Net but should be easy to translate it for your use.
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.