I'm using C# with WPF. I have a grid of buttons and I need to do the following: If the user presses one button, moves the cursor and releases it on another button, the content of the first button is moved to the other one, something like dragging.
I tried using The previewmousedown and the previewmouseup button events to know which button the mouse is pressed on and which button it is released on but the previewmouseup event is fired also on the button the mouse is pressed on (not on the one the mouse is released on).
Any ideas about how to implement this in other ways, please? Thanks a lot in advance.
The easiest way to do drag & drop is with the built-in DragDrop API. Here's a proof of concept for you, where buttons can be clicked normally *and* dragged to swap their content.
If you want to change the behavior so the content is copied or moved (instead of swapped), just change the lines under the comment in OnButtonDrop.
ButtonDragging.xaml:
<Window x:Class="WpfTest2.ButtonDragging"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DockPanel LastChildFill="True">
<Label x:Name="_statusLabel" DockPanel.Dock="Bottom" Content=" " />
<Grid x:Name="_grid" />
</DockPanel>
</Window>
ButtonDragging.xaml.cs:
public partial class ButtonDragging
{
private Button _mouseDownButton;
private Point _mouseDownLocation;
public ButtonDragging()
{
InitializeComponent();
BuildButtonGrid();
}
private void BuildButtonGrid()
{
const int rows = 5;
const int columns = 5;
var starLength = new GridLength(1d, GridUnitType.Star);
for (var i = 0; i < rows; i++)
_grid.RowDefinitions.Add(new RowDefinition { Height = starLength });
for (var i = 0; i < columns; i++)
_grid.ColumnDefinitions.Add(new ColumnDefinition { Width = starLength });
for (var i = 0; i < rows; i++)
{
for (var j = 0; j < columns; j++)
{
var button = new Button { Content = $#"({i}, {j})", AllowDrop = true };
Grid.SetColumn(button, i);
Grid.SetRow(button, j);
button.PreviewMouseMove += OnButtonMouseMove;
button.PreviewMouseLeftButtonDown += OnButtonLeftButtonDown;
button.PreviewMouseLeftButtonUp += OnButtonLeftButtonUp;
button.Drop += OnButtonDrop;
button.Click += OnButtonClick;
button.LostMouseCapture += OnButtonLostMouseCapture;
_grid.Children.Add(button);
}
}
}
private void OnButtonClick(object sender, RoutedEventArgs e)
{
_statusLabel.Content = $#"You clicked {(sender as Button)?.Content}!";
}
private void ClearPendingDrag()
{
_mouseDownButton = null;
_mouseDownLocation = default(Point);
}
private void OnButtonDrop(object sender, DragEventArgs e)
{
ClearPendingDrag();
var source = e.Data.GetData(typeof(object)) as Button;
if (source == null)
return;
var target = (Button)sender;
if (target == source)
return;
var sourceContent = source.Content;
var targetContent = target.Content;
// As a proof of concept, this swaps the content of the source and target.
// Change as necessary to get the behavior you want.
target.Content = sourceContent;
source.Content = targetContent;
_statusLabel.Content = $#"You swapped {sourceContent} with {targetContent}!";
}
private void OnButtonLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var button = (Button)sender;
_mouseDownButton = button;
_mouseDownLocation = e.GetPosition(button);
if (!Mouse.Capture(button, CaptureMode.SubTree))
ClearPendingDrag();
}
private void OnButtonLeftButtonUp(object sender, MouseButtonEventArgs e)
{
ClearPendingDrag();
}
private void OnButtonMouseMove(object sender, MouseEventArgs e)
{
if (_mouseDownButton == null)
return;
var position = e.GetPosition(_mouseDownButton);
var distance = position - _mouseDownLocation;
if (Math.Abs(distance.X) > SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(distance.Y) > SystemParameters.MinimumVerticalDragDistance)
{
var button = (Button)sender;
var data = new DataObject(typeof(object), button);
data.SetData("Source", sender);
DragDrop.DoDragDrop(button, data, DragDropEffects.Move);
ClearPendingDrag();
}
}
private void OnButtonLostMouseCapture(object sender, MouseEventArgs e)
{
ClearPendingDrag();
}
}
Note that there are probably some third-party (and open source) drag and drop solutions out there that are more MVVM-friendly. It's worth checking out, but this should get you a minimum viable deliverable.
Take a look at this blog post. This is the best drag and drop article I read online.
From the link below here are the series of events for a drag and drop operation:
Dragging is initiated by calling the DoDragDrop method for the source control.
The DoDragDrop method takes two parameters:
data, specifying the data to pass
allowedEffects, specifying which operations (copying and/or moving) are allowed
A new DataObject object is automatically created.
This in turn raises the GiveFeedback event. In most cases you do not need to worry about the GiveFeedback event, but if you wanted to display a custom mouse pointer during the drag, this is where you would add your code.
Any control with its AllowDrop property set to True is a potential drop target. The AllowDrop property can be set in the Properties window at design time, or programmatically in the Form_Load event.
As the mouse passes over each control, the DragEnter event for that control is raised. The GetDataPresent method is used to make sure that the format of the data is appropriate to the target control, and the Effect property is used to display the appropriate mouse pointer.
If the user releases the mouse button over a valid drop target, the DragDrop event is raised. Code in the DragDrop event handler extracts the data from the DataObject object and displays it in the target control.
To detect a mouse Drag operation on the source, source control needs to subscribe to MouseLeftButtonDown and MouseMove.
void Window1_Loaded(object sender, RoutedEventArgs e)
{
this.DragSource.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(DragSource_PreviewMouseLeftButtonDown);
this.DragSource.PreviewMouseMove += new MouseEventHandler(DragSource_PreviewMouseMove);
}
To prevent from starting a false drag & drop operation where the user accidentally drags, you can use SystemParameters.MinimumHorizontalDragDistance and SystemParameters.MinimumVerticalDragDistance.
void DragSource_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed && !IsDragging)
{
Point position = e.GetPosition(null);
if (Math.Abs(position.X - _startPoint.X) > SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(position.Y - _startPoint.Y) > SystemParameters.MinimumVerticalDragDistance)
{
StartDrag(e);
}
}
}
void DragSource_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_startPoint = e.GetPosition(null);
}
Now you detect a drag operation, all you need to know is dropping into.
A simple scenario without any effect could be done like this.
private void StartDrag(MouseEventArgs e)
{
IsDragging = true;
DataObject data = new DataObject(System.Windows.DataFormats.Text.ToString(), "abcd");
DragDropEffects de = DragDrop.DoDragDrop(this.DragSource, data, DragDropEffects.Move);
IsDragging = false;
}
I think this can get you started. I really recommend to read the complete post from the link.
https://blogs.msdn.microsoft.com/jaimer/2007/07/12/drag-amp-drop-in-wpf-explained-end-to-end/
Related
I'm trying to understand what is executing before the MouseWheel event.
What I've done:
I have a form which has AutoScroll property set to true. There is a control (ZEDGRAPH) at the top and the bottom of this form.
To overcome the issue of scrolling and zooming at the same time I captured the mousewheel += new MouseEvenHandler(mymethod) for the form.Then using a bool variable I keep track of when the control (ZEDGRAPH) has focus and when it does not.
When it has focus I make verticalscroll.value = (int)mydesiredposition;
This works in accomplishing what I wanted which is to ignore the mousewheel event in the form and focus on the control.
What I am struggling with is the fact that when I scroll the form flickers every time and scrolls down before coming to the set scrollbar value.
So what I am wondering is what is getting triggered before this mouseeventhandler that causes it to flicker and is there a relatively simple workaround this?
My code snapshot:
public Form(Form1 f)
{
InitializeComponent();
this.MouseWheel += new MouseEventHandler(mousewheel);
}//end of constructor
//
//
bool mousehoverZedGraph1 = false;
bool mousehoverZedGraph2 = false;
//
//
private void zedGraphControl1_MouseHover(object sender, EventArgs e)
{
mousehoverZedGraph1 = true;
return;
}
private void mousewheel(object sender, MouseEventArgs e)
{
if (mousehoverZedGraph1 == true)
{
VerticalScroll.Enabled = false;
VerticalScroll.Value = 0;
return;
}
else if (mousehoverZedGraph2 == true)
{
VerticalScroll.Value = 429;
VerticalScroll.Enabled = false;
}
else
{
//VerticalScroll.Value += e.Delta;
}
}
private void Form_MouseEnter(object sender, EventArgs e)
{
mousehoverZedGraph1 = mousehoverZedGraph2 = false;
VerticalScroll.Enabled = true;
}
A small video highlighting the flicker:
I have an application that scans an image to display on the application. After the image is scanned, I have the option to zoom the image. There's a combo box on the application that display the zoom percentage as well.
I can zoom fine using my mouse wheel and the combo box % changes accordingly which is fine. The problem happens if I manually select the combo box and select a zoom percentage, say 50%, then there's no changes at all.
Code:
private void ImageBox_ZoomLevelsChanged(object sender, EventArgs e)
{
this.FillZoomLevels();
}
private void ZoomComboBox_Click(object sender, EventArgs e)
{
}
private void FillZoomLevels()
{
ZoomComboBox.Items.Clear();
foreach (int zoom in ImageBox.ZoomLevels)
ZoomComboBox.Items.Add(string.Format("{0}%", zoom));
}
Am I doing anything wrong? Appreciate any help.
When there is more than one control on the Panel, and the ScrollBars are shown, the MouseWheel event of the Panel would be raised, but if there's only a PictureBox on the Panel whose Image large enough to make the panel's ScrollBars visible, then the MouseWheel event of the Panel won't be raised, instead, the Form's MouseWheel event fires.
In the following sample, I just add a PictureBox onto the Panel without any other controls added, set a large Image to the PictureBox which make the Panel's ScrollBars invisible, since there's only one control on the panel, the MouseWheel event of the Panel won't be raised, but the Form's MouseWheel event be raised, we handle this event instead, handle the Form's KeyDown and KeyUp events as well.
Furthermore, set the SizeMode of the PictureBox to StretchImage instead of AutoSize, thus when we change the size of the PictureBox, the Image in the PictureBox will resize to fit the PictureBox.
The PictureBox.Scale() method won't help you in this screnario, change the size of the PictureBox instead.
public partial class Form4 : Form
{
public Form4()
{
InitializeComponent();
}
bool ctrlKeyDown;
bool shiftKeyDown;
private void Form4_Load(object sender, EventArgs e)
{
this.ctrlKeyDown = false;
this.shiftKeyDown = false;
//If there's only PictureBox control on the panel, the MouseWheel event of the form raised
//instead of the MouseWheel event of the Panel.
this.MouseWheel += new MouseEventHandler(Form4_MouseWheel);
this.KeyDown += new KeyEventHandler(Form4_KeyDown);
this.KeyUp += new KeyEventHandler(Form4_KeyUp);
//this is important for zooming the image
this.pictureBox2.SizeMode = PictureBoxSizeMode.StretchImage;
}
void Form4_KeyUp(object sender, KeyEventArgs e)
{
this.ctrlKeyDown = e.Control;
this.shiftKeyDown = e.Shift;
}
void Form4_KeyDown(object sender, KeyEventArgs e)
{
this.ctrlKeyDown = e.Control;
this.shiftKeyDown = e.Shift;
}
void Form4_MouseWheel(object sender, MouseEventArgs e)
{
bool IsGoUp = e.Delta > 0 ? true : false;
if (this.ctrlKeyDown)
{
if (IsGoUp && this.panel1.HorizontalScroll.Value > 5)
{
this.panel1.HorizontalScroll.Value -= 5;
}
if (!IsGoUp && this.panel1.HorizontalScroll.Value < this.panel1.HorizontalScroll.Maximum - 5)
{
this.panel1.HorizontalScroll.Value += 5;
}
}
else if (this.shiftKeyDown)
{
int hStep = (int)(this.pictureBox2.Image.Width * 0.02);
int vStep = (int)(this.pictureBox2.Image.Height * 0.02);
if (IsGoUp)
{
this.pictureBox2.Width += hStep;
this.pictureBox2.Height += vStep;
}
else
{
this.pictureBox2.Width -= hStep;
this.pictureBox2.Height -= vStep;
}
}
else
{
if (IsGoUp && this.panel1.VerticalScroll.Value > 5)
{
this.panel1.VerticalScroll.Value -= 5;
}
if (!IsGoUp && this.panel1.VerticalScroll.Value < this.panel1.VerticalScroll.Maximum - 5)
{
this.panel1.VerticalScroll.Value += 5;
}
}
}
}
I use a standard WPF ComboBox control. When popup is opened and user clicks somewhere outside, popup is closed. But if there is button on the window and user clicks on it (with popup still opened), button's click handler is not executed. Popup is closed, but user has to click one more time on the button to raise click event on it.
I know that is standard behavior for this control. Have you any ideas how to bypass this behavior? Thanks!
I fixed some bugs with #Eng. M.Hamdy very good approach and did it in C#, also applying it to all comboboxes application wide.
Application hook:
EventManager.RegisterClassHandler(typeof(ComboBox), UIElement.PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(FixComboBoxOutClick));
Handler code:
private void FixComboBoxOutClick(object sender, MouseButtonEventArgs e) {
if (sender is ComboBox combo) {
Point comboRelativePoint = Mouse.GetPosition(combo);
if (comboRelativePoint.X < 0 || comboRelativePoint.Y < 0 || comboRelativePoint.X > combo.ActualWidth || comboRelativePoint.Y > combo.ActualHeight) {
UIElement popupContent = combo.FindChild<Popup>(null).Child;
Point popupRelativePoint = Mouse.GetPosition(popupContent);
if (popupRelativePoint.X < 0 || popupRelativePoint.Y < 0 || popupRelativePoint.X > popupContent.RenderSize.Width || popupRelativePoint.Y > popupContent.RenderSize.Height) {
combo.IsDropDownOpen = false;
}
}
}
}
You can look for FindChild<T>() implementations here.
You can create an event for ComboBox DropDownClosed and with the hittestfunction, find the other control that the user has clicked.
private void ComboBox_DropDownClosed(object sender, EventArgs e)
{
Point m = Mouse.GetPosition(this);
VisualTreeHelper.HitTest(this, this.FilterCallback, this.ResultCallback, new PointHitTestParameters(m));
}
private HitTestFilterBehavior FilterCallback(DependencyObject o)
{
var c = o as Control;
if ((c != null) && !(o is MainWindow))
{
if (c.Focusable)
{
if (c is ComboBox)
{
(c as ComboBox).IsDropDownOpen = true;
}
else
{
var mouseDevice = Mouse.PrimaryDevice;
var mouseButtonEventArgs = new MouseButtonEventArgs(mouseDevice, 0, MouseButton.Left)
{
RoutedEvent = Mouse.MouseDownEvent,
Source = c
};
c.RaiseEvent(mouseButtonEventArgs);
}
return HitTestFilterBehavior.Stop;
}
}
return HitTestFilterBehavior.Continue;
}
private HitTestResultBehavior ResultCallback(HitTestResult r)
{
return HitTestResultBehavior.Continue;
}
Then in the FilterCallback function after finding that control, raise the mouse down event on that control.
I found the raise event, does not work on comboboxes so for clicking that, I simply set the IsDropDownOpen to true.
I found the code in here and modified it a little.
I used an easy solution:
In the PreviewMouseLeftButtonDown event, if the mouse pos is outside the combobox, close the dropdown. This will allow other control to get the mouse click:
Dim p = Mouse.GetPosition(combo)
If p.X < 0 OrElse p.Y < 0 OrElse p.X > combo.Width OrElse p.Y > combo.Height Then
cmb.IsDropDownOpen = False
End If
You can try to release the mouse capture right after the ComboBox gets one:
In your's ComboBox properties in XAML:
GotMouseCapture="ComboBox_OnGotMouseCapture"
And in code-behind:
private void ComboBox_OnGotMouseCapture(object sender, MouseEventArgs e)
{
ComboBox.ReleaseMouseCapture();
}
I have a tableLayoutPanel with 16 cells. 15 of the cells have controls. I want to be able to move the controls from one cell to another at runtime.
I have used
private void button15_Click(object sender, EventArgs e)
{
tableLayoutPanel1.Controls.Remove(button15);
tableLayoutPanel1.Controls.Add(button15, 3, 3);
}
This works well but i want to know if there is any better way to do this???
In Winforms, you can only move a control inside its parent (of course there are some exceptions to some controls which in fact don't have any Parent). So the idea here is if you want to move a control of your TableLayoutPanel, you have to set its Parent to your Form of another container when mouse is held down, when moving, the position of the control is in the new parent, after mouse is released, we have to set the Parent of the control to the TableLayoutPanel back, of course we have to find the drop-down cell position and use SetCellPosition method to position the control on the TableLayoutPanel, here is the demo code for you (works great), I use 2 Buttons in this demo, you can replace them with any control you want:
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
//This will prevent flicker
typeof(TableLayoutPanel).GetProperty("DoubleBuffered", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).SetValue(tableLayoutPanel1, true, null);
}
Point downPoint;
bool moved;
//This is used to store the CellBounds together with the Cell position
//so that we can find the Cell position later (after releasing mouse).
Dictionary<TableLayoutPanelCellPosition, Rectangle> dict = new Dictionary<TableLayoutPanelCellPosition, Rectangle>();
//MouseDown event handler for all your controls (on the tableLayoutPanel1)
private void Buttons_MouseDown(object sender, MouseEventArgs e) {
Control button = sender as Control;
button.Parent = this;
button.BringToFront();
downPoint = e.Location;
}
//MouseMove event handler for all your controls (on the tableLayoutPanel1)
private void Buttons_MouseMove(object sender, MouseEventArgs e) {
Control button = sender as Control;
if (e.Button == MouseButtons.Left) {
button.Left += e.X - downPoint.X;
button.Top += e.Y - downPoint.Y;
moved = true;
tableLayoutPanel1.Invalidate();
}
}
//MouseUp event handler for all your controls (on the tableLayoutPanel1)
private void Buttons_MouseUp(object sender, MouseEventArgs e) {
Control button = sender as Control;
if (moved) {
SetControl(button, e.Location);
button.Parent = tableLayoutPanel1;
moved = false;
}
}
//This is used to set the control on the tableLayoutPanel after releasing mouse
private void SetControl(Control c, Point position) {
Point localPoint = tableLayoutPanel1.PointToClient(c.PointToScreen(position));
var keyValue = dict.FirstOrDefault(e => e.Value.Contains(localPoint));
if (!keyValue.Equals(default(KeyValuePair<TableLayoutPanelCellPosition, Rectangle>))) {
tableLayoutPanel1.SetCellPosition(c, keyValue.Key);
}
}
//CellPaint event handler for your tableLayoutPanel1
private void tableLayoutPanel1_CellPaint(object sender, TableLayoutCellPaintEventArgs e) {
dict[new TableLayoutPanelCellPosition(e.Column, e.Row)] = e.CellBounds;
if (moved) {
if (e.CellBounds.Contains(tableLayoutPanel1.PointToClient(MousePosition))) {
e.Graphics.FillRectangle(Brushes.Yellow, e.CellBounds);
}
}
}
}
Remove Lock & set dock to none and move!
I have create a field of small panels on a form and I want the user to be able to color these panels. To color them you have to simpely click the panel, this works. Now I want to be able to let the use click a panel and drag over other panels to color more panels at once.
I have added the mousedown and mouseup event to all panels to set a boolean. The I use the mousemove event to color the panels. Yet only the first panel gets collored.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
namespace Application
{
public partial class Form1 : Form
{
private const int dim = 25;
private int cols;
private int rows;
private bool mouse_down = false;
private sbyte fill = -1;
private Panel pnlTmp;
public Form1()
{
InitializeComponent();
this.buildGrid();
}
/// <summary>
/// Rebuild the grid to fit the screen.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void toolStripMenuItem2_Click(object sender, EventArgs e)
{
this.buildGrid();
}
/// <summary>
/// Build the grid to fit the screen.
/// </summary>
private void buildGrid()
{
panel1.Controls.Clear();
cols = int.Parse(Math.Floor((double)panel1.Width / dim).ToString());
rows = int.Parse(Math.Floor((double)panel1.Height / dim).ToString());
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
Panel pnl = new Panel();
pnl.BorderStyle = BorderStyle.FixedSingle;
pnl.Width = dim;
pnl.Height = dim;
pnl.Location = new Point(j * 25 + 1, i * 25 + 1);
pnl.Name = string.Format("pnl-{0}-{1}", j, i);
pnl.MouseDown += new MouseEventHandler(pnl_MouseDown);
pnl.MouseUp += new MouseEventHandler(pnl_MouseUp);
pnl.MouseMove += new MouseEventHandler(pnl_MouseHover);
panel1.Controls.Add(pnl);
}
}
}
void pnl_MouseHover(object sender, EventArgs e)
{
if (mouse_down)
{
Panel p = (Panel)sender;
if (p != pnlTmp)
{
if (fill == -1)
fill = (p.BackColor == SystemColors.Control) ? (sbyte)1 : (sbyte)0;
if (fill == 1)
p.BackColor = Color.Blue;
else
p.BackColor = SystemColors.Control;
Debug.WriteLine(p.Name);
}
pnlTmp = p;
}
}
void pnl_MouseDown(object sender, MouseEventArgs e)
{
Debug.WriteLine("true");
mouse_down = true;
}
void pnl_MouseUp(object sender, MouseEventArgs e)
{
Debug.WriteLine("false");
mouse_down = false;
fill = -1;
}
private void panel1_MouseLeave(object sender, EventArgs e)
{
//mouse_down = false;
//fill = -1;
}
}
}
I have tried debugging the application and the result was that only the first panel keeps firing the event, even if I am moving over other panels.
Could someone tell me why this is?
Your problem is caused by a feature called "mouse capture". It is controlled by the Control.Capture property. The default behavior is that it is turned on automatically in the OnMouseDown() method, before it fires the MouseDown event.
Mouse capture forces all mouse events to be directed to the window that you clicked, even if you move the mouse outside of the window. Which is why you only ever get the MouseMove and MouseUp events for the panel that you clicked on. It is important in a number of scenarios, particularly to reliably generate the Click and MouseUp events.
The workaround is to simply turn the capture back off in your MouseDown event handler:
void pnl_MouseDown(object sender, MouseEventArgs e) {
((Control)sender).Capture = false;
}
Do note that you now have a new problem, your "mouse_down" variable is no longer reliable. If you move the mouse outside of any panel, or outside of the form, and release the mouse then the MouseUp event is sent to the wrong window and your mouse_down variable remains set to true even though the mouse is no longer down. You lost the guarantee provided by the capture feature. You solve that problem by checking the button state in your MouseMove event handler, like this:
private void pnl_MouseMove(object sender, MouseEventArgs e) {
if (e.Button == MouseButtons.Left) {
// etc...
}
}
Beware that I fixed your incorrect pnl_MouseHover event handler and renamed it to pnl_MouseMove. I can see how you ended up making this mistake, but it possibly did prevent you from discovering this solution yourself. Careful with that axe Eugene ;)