drag to pan on an UserControl - winforms

I'm trying to build my own "PictureBox like" control adding some functionalities. For example, I want to be able to pan over a big image by simply clicking and dragging with the mouse.
The problem seems to be on my OnMouseMove method. If I use the following code I get the drag speed and precision I want, but of course, when I release the mouse button and try to drag again the image is restored to its original position.
using System.Drawing;
using System.Windows.Forms;
namespace Testing
{
public partial class ScrollablePictureBox : UserControl
{
private Image image;
private bool centerImage;
public Image Image
{
get { return image; }
set { image = value; Invalidate(); }
}
public bool CenterImage
{
get { return centerImage; }
set { centerImage = value; Invalidate(); }
}
public ScrollablePictureBox()
{
InitializeComponent();
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
Image = null;
AutoScroll = true;
AutoScrollMinSize = new Size(0, 0);
}
private Point clickPosition;
private Point scrollPosition;
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
clickPosition.X = e.X;
clickPosition.Y = e.Y;
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.Button == MouseButtons.Left)
{
scrollPosition.X = clickPosition.X - e.X;
scrollPosition.Y = clickPosition.Y - e.Y;
AutoScrollPosition = scrollPosition;
}
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
e.Graphics.FillRectangle(new Pen(BackColor).Brush, 0, 0, e.ClipRectangle.Width, e.ClipRectangle.Height);
if (Image == null)
return;
int centeredX = AutoScrollPosition.X;
int centeredY = AutoScrollPosition.Y;
if (CenterImage)
{
//Something not relevant
}
AutoScrollMinSize = new Size(Image.Width, Image.Height);
e.Graphics.DrawImage(Image, new RectangleF(centeredX, centeredY, Image.Width, Image.Height));
}
}
}
But if I modify my OnMouseMove method to look like this:
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.Button == MouseButtons.Left)
{
scrollPosition.X += clickPosition.X - e.X;
scrollPosition.Y += clickPosition.Y - e.Y;
AutoScrollPosition = scrollPosition;
}
}
... you will see that the dragging is not smooth as before, and sometimes behaves weird (like with lag or something).
What am I doing wrong?
I've also tried removing all "base" calls on a desperate movement to solve this issue, haha, but again, it didn't work.
Thanks for your time.

Finally, I managed to find a solution:
protected Point clickPosition;
protected Point scrollPosition;
protected Point lastPosition;
protected override void OnMouseDown(MouseEventArgs e)
{
clickPosition.X = e.X;
clickPosition.Y = e.Y;
}
protected override void OnMouseUp(MouseEventArgs e)
{
Cursor = Cursors.Default;
lastPosition.X = AutoScrollPosition.X;
lastPosition.Y = AutoScrollPosition.Y;
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
Cursor = Cursors.Hand;
scrollPosition.X = clickPosition.X - e.X - lastPosition.X;
scrollPosition.Y = clickPosition.Y - e.Y - lastPosition.Y;
AutoScrollPosition = scrollPosition;
}
}

This is always confusing every time I have to do it.
For the benefit of those newly arriving to this thread I came up with a slightly simpler solution that gives smooth panning.
Private GrabPoint As Point
Private Sub OnMouseDown(MouseEventArgs e)
GrabPoint = e.Location
End Sub
Private Sub OnMouseMove(MouseEventArgs e)
If e.Button = System.Windows.Forms.MouseButtons.Left Then
AutoScrollPosition = GrabPoint - e.Location - AutoScrollPosition
End If
End Sub
Private Sub OnMouseUp(MouseEventArgs e)
GrabPoint = Point.Empty
End Sub
By the way, the grab and grabbing hand cursors can be downloaded from:
http://theburningmonk.com/2010/03/wpf-loading-grab-and-grabbing-cursors-from-resource/
You can add them to your project as embedded resources and set them with:
Cursor = New Cursor(System.Reflection.Assembly.GetExecutingAssembly.GetManifestResourceStream(String.Format("{0}.{1}.cur", Me.GetType.Namespace, "grabbing")))

Related

wpf how to get mouse position in container when dragging controls inside?

I want to get mouse position of container while dragging controls inside so I can add auto-scroll logic to container. However, MouseMove does not fired at all when dragging, DragOver fired only when dragging over controls inside.
test example
Draggable gizmo:
public class Gizmo : TextBlock
{
public Gizmo()
{
this.AllowDrop = true;
this.Background = Brushes.Gray;
this.Margin = new System.Windows.Thickness(6);
}
public Gizmo(string content) : this()
{
this.Text = content;
}
private bool isDragging;
private Point lastPressedLocation;
protected override void OnPreviewMouseMove(System.Windows.Input.MouseEventArgs e)
{
if (e.LeftButton == System.Windows.Input.MouseButtonState.Pressed)
{
if (!this.isDragging)
{
Point newLocation = e.GetPosition(this);
Vector offset = this.lastPressedLocation - newLocation;
if (offset.LengthSquared > 36)
{
this.lastPressedLocation = newLocation;
this.isDragging = true;
System.Windows.DragDrop.DoDragDrop(this, DateTime.Now, DragDropEffects.Move);
}
else
{
this.isDragging = false;
}
}
}
}
private bool canDrop;
protected override void OnPreviewDragEnter(DragEventArgs e)
{
Console.WriteLine("drag enter inside");
if (this.Text == "gizmo 1")
{
e.Effects = DragDropEffects.Move;
this.canDrop = true;
}
else
{
e.Effects = DragDropEffects.None;
this.canDrop = false;
}
e.Handled = true;
base.OnPreviewDragEnter(e);
}
protected override void OnPreviewDragOver(DragEventArgs e)
{
Console.WriteLine("drag over inside");
if (this.canDrop)
{
e.Effects = DragDropEffects.Move;
}
else
{
e.Effects = DragDropEffects.None;
e.Handled = true;
}
base.OnPreviewDragOver(e);
}
}
container:
public class Container : WrapPanel
{
protected override void OnInitialized(EventArgs e)
{
for (int i = 1; i <= 16; i++)
this.Children.Add(new Gizmo(string.Format("gizmo {0}", i)));
base.OnInitialized(e);
}
protected override void OnPreviewDragEnter(System.Windows.DragEventArgs e)
{
Console.WriteLine("drag enter outside");
base.OnPreviewDragEnter(e);
}
protected override void OnPreviewDragOver(System.Windows.DragEventArgs e)
{
//I want to get mouse postion here, but this will be called only when dragging over gizmo inside
Console.WriteLine("drag over outside");
base.OnPreviewDragOver(e);
}
}
running result and question
or it's just impossible?
The last function in your code should work. Alternatively (since there should be no other elements handling the event before you) you can use the OnDragOver method instead of the Preview.
protected override void OnDragOver(DragEventArgs e)
{
Point position = e.GetPosition(this);
}
If it doesn't work, that usually means that specific area of your control is not hit-test visible. Make sure IsHitTestVisible is true (has to be, otherwise child elements wouldn't work either) and that the Background of your control is not null. If you want no background and still be able to be hit-test visible, use Transparent for the background.

How do I hittest for a TabControl tab?

Given a point relative to a Page, how do I hittest for a TabControl's tab? VisualTreeHelper.HitTest gives me the contents, but when I go up the visual tree I see nothing that would tell me that I have actually hit a tab. I don't even see the tab control itself.
public class ViewManipulationAgent : IDisposable
{
private const int _limit = 125;
private INavigationService _navigationService;
private FrameworkElement _container;
private FrameworkElement _element;
private TranslateTransform _translate;
private IInputElement _touchTarget;
// When I use this object,
// a_container is the main Frame control in my application.
// a_element is a page within that frame.
public ViewManipulationAgent(FrameworkElement a_container, FrameworkElement a_element)
{
_navigationService = a_navigationService;
_container = a_container;
_element = a_element;
// Since I set IsManipulationEnabled to true all touch commands are suspended
// for all commands on the page (a_element) unless I specifically cancel (see below)
_element.IsManipulationEnabled = true;
_element.PreviewTouchDown += OnElementPreviewTouchDown;
_element.ManipulationStarting += OnElementManipulationStarting;
_element.ManipulationDelta += OnElementManipulationDelta;
_element.ManipulationCompleted += OnElementManipulationCompleted;
_translate = new TranslateTransform(0.0, 0.0);
_element.RenderTransform = _translate;
}
// Since the ManipulationStarting doesn't provide position I capture the position
// here and then hit test elements to find any controls for which I want to bypass
// manipulation.
private void OnElementPreviewTouchDown(object sender, TouchEventArgs e)
{
var position = e.GetTouchPoint(_element).Position;
_touchTarget = null;
HitTestResult result = VisualTreeHelper.HitTest(_element, position);
if (result.VisualHit == null)
return;
var button = VisualTreeHelperEx.FindAncestorByType<ButtonBase>(result.VisualHit) as ButtonBase;
if (button != null)
{
_touchTarget = button;
return;
}
var slider = VisualTreeHelperEx.FindAncestorByType<Slider>(result.VisualHit) as Slider;
if (slider != null)
{
_touchTarget = slider;
return;
}
}
// Here is where I cancel manipulation if a specific touch target was found in the
// above event.
private void OnElementManipulationStarting(object sender, ManipulationStartingEventArgs e)
{
if (_touchTarget != null)
{
e.Cancel(); // <- I have to cancel manipulation or the buttons and other
// controls cannot be manipulated by the touch interface.
return;
}
e.ManipulationContainer = _container;
}
private void OnElementManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
var element = e.Source as FrameworkElement;
if (element == null)
return;
var translate = _translate.X + e.DeltaManipulation.Translation.X;
if (translate > _limit)
{
GoBack();
translate = _limit;
}
if (translate < -_limit)
{
GoForward();
translate = -_limit;
}
_translate.X = translate;
}
private void GoForward()
{
var navigationService = ServiceLocator.Current.GetInstance<INavigationService>();
navigationService.GoForward();
}
private void GoBack()
{
var navigationService = ServiceLocator.Current.GetInstance<INavigationService>();
navigationService.GoBack();
}
private void OnElementManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
{
_touchTarget = null;
_translate.X = 0;
}
public void Dispose()
{
_element.PreviewTouchDown -= OnElementPreviewTouchDown;
_element.ManipulationStarting -= OnElementManipulationStarting;
_element.ManipulationDelta -= OnElementManipulationDelta;
_element.ManipulationCompleted -= OnElementManipulationCompleted;
}
}

Simulating a Drag/Drop event in WPF

I want to simulate a drag/drop event in WPF.
For this I'll need to gain access to the data stored in the "Drag/Drop buffer" and also I'll need to create a DragEventArgs.
I noticed that the DragEventArgs is sealed and has no public ctor.
So my questions are:
1. how can I create an instance of DragEventArgs?
2. How can I gain access to the drag/drop buffer?
i recently do this! i simulated drag/drop with MouseDown, MouseMove and MouseUp events. for example for my application, i have some canvases that i want to drag and drop them. every canvas has an id. in MouseDown event, i buffer its id and use it in MouseMove and MouseUp event. Desktop_Canvas is my main Canvas that contains some canvases. these canvases are in my dictionary (dic).
here is my code:
private Dictionary<int, Win> dic = new Dictionary<int, Win>();
private Point downPoint_Drag = new Point(-1, -1);
private int id_Drag = -1;
private bool flag_Drag = false;
public class Win
{
public Canvas canvas = new Canvas();
public Point downpoint = new Point();
public Win()
{
canvas.Background = new SolidColorBrush(Colors.Gray);
}
}
private void Desktop_Canvas_MouseMove(object sender, MouseEventArgs e)
{
try
{
Point movePoint = e.GetPosition(Desktop_Canvas);
if (flag_Drag && downPoint_Drag != new Point(-1, -1))
{
double dy1 = movePoint.Y - downPoint_Drag.Y, x = -1, dx1 = movePoint.X - downPoint_Drag.X, y = -1;
downPoint_Drag = movePoint;
if (x == -1)
x = Canvas.GetLeft(dic[id_Drag].canvas) + dx1;
if (y == -1)
y = Canvas.GetTop(dic[id_Drag].canvas) + dy1;
Canvas.SetLeft(dic[id_Drag].canvas, x);
Canvas.SetTop(dic[id_Drag].canvas, y);
}
}
catch
{
MouseEventArgs ee = new MouseEventArgs((MouseDevice)e.Device, 10);
Desktop_Canvas_MouseLeave(null, ee);
}
}
private void Desktop_Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
try
{
downPoint_Drag = new Point(-1, -1);
id_Drag =-1;
flag_Drag = false;
}
catch
{
MouseEventArgs ee = new MouseEventArgs((MouseDevice)e.Device, 10);
Desktop_Canvas_MouseLeave(null, ee);
}
}
private void Desktop_Canvas_MouseLeave(object sender, MouseEventArgs e)
{
MouseButtonEventArgs ee = new MouseButtonEventArgs((MouseDevice)e.Device, 10, MouseButton.Left);
Desktop_Canvas_MouseLeftButtonUp(null, ee);
}
void canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
downPoint_Drag = e.GetPosition(Desktop_Canvas);
int hoverId = HoverWin(downPoint_Drag);
flag_Drag = true;
id_Drag = hoverId;
dic[id_Drag].downpoint = new Point(downPoint_Drag.X, downPoint_Drag.Y);
}
private int HoverWin(Point p)
{
foreach (int i in dic.Keys)
{
if (dic[i].canvas.IsMouseOver)
return i;
}
return -1;
}

How to create and use WebBrowser in background thread?

How can I create System.Windows.Forms.WebBrowser in background STA thread? I try use some code like this:
var tr = new Thread(wbThread);
tr.SetApartmentState(ApartmentState.STA);
tr.Start();
private void wbThread()
{
CWebBrowser browser = new CWebBrowser();
var text = browser.Navigate("http://site.com", CWebBrowser.EventType.loadCompleted).Body.InnerHtml;
}
CWebBrowser - custom class, wich delegate System.Windows.Forms.WebBrowser object Navigate method and wait until page completed loads. The problem is LoadCompleted event on System.Windows.Forms.WebBrowser object never raises. I found some solution here, but it does not work (can't find method Application.Run() on my WPF app).
public class CWebBrowser : ContentControl
{
public readonly System.Windows.Forms.WebBrowser innerWebBrowser;
private readonly AutoResetEvent loadCompletedEvent;
private readonly AutoResetEvent navigatedEvent;
public enum EventType
{
navigated, loadCompleted
}
public CWebBrowser()
{
innerWebBrowser = new System.Windows.Forms.WebBrowser();
loadCompletedEvent = new AutoResetEvent(false);
navigatedEvent = new AutoResetEvent(false);
System.Windows.Forms.Integration.WindowsFormsHost host = new System.Windows.Forms.Integration.WindowsFormsHost();
host.Child = innerWebBrowser;
Content = host;
innerWebBrowser.DocumentCompleted +=new System.Windows.Forms.WebBrowserDocumentCompletedEventHandler(innerWebBrowser_DocumentCompleted);
innerWebBrowser.Navigated += new System.Windows.Forms.WebBrowserNavigatedEventHandler(innerWebBrowser_Navigated);
}
void innerWebBrowser_Navigated(object sender, System.Windows.Forms.WebBrowserNavigatedEventArgs e)
{
navigatedEvent.Set();
}
void innerWebBrowser_DocumentCompleted(object sender, System.Windows.Forms.WebBrowserDocumentCompletedEventArgs e)
{
if (((sender as System.Windows.Forms.WebBrowser).ReadyState != System.Windows.Forms.WebBrowserReadyState.Complete) || innerWebBrowser.IsBusy)
return;
var doc = innerWebBrowser.Document;
loadCompletedEvent.Set();
}
public System.Windows.Forms.HtmlDocument Navigate(string url, EventType etype)
{
if (etype == EventType.loadCompleted)
loadCompletedEvent.Reset();
else if (etype == EventType.navigated)
navigatedEvent.Reset();
innerWebBrowser.Navigate(url);
if (etype == EventType.loadCompleted)
loadCompletedEvent.WaitOne();
else if (etype == EventType.navigated)
navigatedEvent.WaitOne();
System.Windows.Forms.HtmlDocument doc = null;
Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Background, new Action(
delegate
{
doc = innerWebBrowser.Document;
}));
return doc;
}
}
Thansk for all advices and sorry for my bad english :o(
Why don't you use the default WebBrowser control like this?
public MainPage()
{
InitializeComponent();
System.Windows.Deployment.Current.Dispatcher.BeginInvoke(startNavigate);
}
void startNavigate()
{
WebBrowser wb = new WebBrowser();
wb.LoadCompleted += new LoadCompletedEventHandler(wb_LoadCompleted);
wb.Navigated += new EventHandler<System.Windows.Navigation.NavigationEventArgs>(wb_Navigated);
wb.Navigate(new Uri("http://www.google.com"));
}
void wb_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e)
{
// e.Content
}
void wb_LoadCompleted(object sender, NavigationEventArgs e)
{
// e.Content when the document finished loading.
}
Edit: You are using old System.Windows.Forms.WebBrowser control, instead System.Windows.Controls.WebBrowser which is part of WPF.

WPF mediaelement

I have a MediaElement, but how can I call a function when the property "position" of MediaElement changes?
Position is not a DependencyProperty.
You can use a DispatchTimer. This article provides some good insight on how to get this working. MediaElement and More with WPF.
Here is some sample code that I took from a project I'm working on. It shows the position of the video using a slider control and allows the user to change the position.
I'm a bit of a newbie too, so it is possible that some of it is wrong (feel free to comment on problems in the comments section :).
private DispatcherTimer mTimer;
private bool mIsDragging = false;
private bool mTick = false;
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
medPlayer.Play();
medPlayer.Stop();
mTimer = new DispatcherTimer();
mTimer.Interval = TimeSpan.FromMilliseconds(100);
mTimer.Tick += new EventHandler(mTimer_Tick);
mTimer.Start();
}
void mTimer_Tick(object sender, EventArgs e)
{
if (!mIsDragging)
{
try
{
mTick = true;
sldPosition.Value = medPlayer.Position.TotalMilliseconds;
}
finally
{
mTick = false;
}
}
}
private void sldPosition_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e)
{
mIsDragging = true;
medPlayer.Pause();
}
private void sldPosition_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
{
mIsDragging = false;
if (chkPlay.IsChecked.Value)
medPlayer.Play();
}
private void sldPosition_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
var pos = TimeSpan.FromMilliseconds(e.NewValue);
lblPosition.Content = string.Format("{0:00}:{1:00}", pos.Minutes, pos.Seconds);
if (!mTick)
{
medPlayer.Position = TimeSpan.FromMilliseconds(sldPosition.Value);
if (medPlayer.Position == medPlayer.NaturalDuration.TimeSpan)
{
chkPlay.IsChecked = false;
medPlayer.Stop();
}
}
}

Resources