// -------------------------------------------------------------------------------------------------------------------- // // The MIT License (MIT) // // Copyright (c) 2012 Oystein Bjorke // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // // // Represents a control that displays a plot. // // -------------------------------------------------------------------------------------------------------------------- namespace OxyPlot.WindowsForms { using System; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms; using OxyPlot.Axes; using OxyPlot.Series; /// /// Represents a control that displays a plot. /// [Serializable] public class Plot : Control, IPlotControl { /// /// The category for the properties of this control. /// private const string OxyPlotCategory = "OxyPlot"; /// /// The invalidate lock. /// private readonly object invalidateLock = new object(); /// /// The model lock. /// private readonly object modelLock = new object(); /// /// The rendering lock. /// private readonly object renderingLock = new object(); /// /// The current model (holding a reference to this plot control). /// [NonSerialized] private PlotModel currentModel; /// /// The is model invalidated. /// private bool isModelInvalidated; /// /// The model. /// private PlotModel model; /// /// The mouse manipulator. /// [NonSerialized] private ManipulatorBase mouseManipulator; /// /// The update data flag. /// private bool updateDataFlag = true; /// /// The zoom rectangle. /// private Rectangle zoomRectangle; /// /// The render context. /// private GraphicsRenderContext renderContext; /// /// Initializes a new instance of the class. /// public Plot() { this.renderContext = new GraphicsRenderContext(); // ReSharper disable DoNotCallOverridableMethodsInConstructor this.DoubleBuffered = true; // ReSharper restore DoNotCallOverridableMethodsInConstructor this.KeyboardPanHorizontalStep = 0.1; this.KeyboardPanVerticalStep = 0.1; this.PanCursor = Cursors.Hand; this.ZoomRectangleCursor = Cursors.SizeNWSE; this.ZoomHorizontalCursor = Cursors.SizeWE; this.ZoomVerticalCursor = Cursors.SizeNS; } /// /// Gets the actual model. /// /// The actual model. public PlotModel ActualModel { get { return this.Model; } } /// /// Gets or sets the keyboard pan horizontal step. /// /// The keyboard pan horizontal step. [Category(OxyPlotCategory)] public double KeyboardPanHorizontalStep { get; set; } /// /// Gets or sets the keyboard pan vertical step. /// /// The keyboard pan vertical step. [Category(OxyPlotCategory)] public double KeyboardPanVerticalStep { get; set; } /// /// Gets or sets the model. /// [Browsable(false)] [DefaultValue(null)] [Category(OxyPlotCategory)] public PlotModel Model { get { return this.model; } set { if (this.model != value) { this.model = value; this.OnModelChanged(); } } } /// /// Gets or sets the pan cursor. /// [Category(OxyPlotCategory)] public Cursor PanCursor { get; set; } /// /// Gets or sets the horizontal zoom cursor. /// [Category(OxyPlotCategory)] public Cursor ZoomHorizontalCursor { get; set; } /// /// Gets or sets the rectangle zoom cursor. /// [Category(OxyPlotCategory)] public Cursor ZoomRectangleCursor { get; set; } /// /// Gets or sets vertical zoom cursor. /// [Category(OxyPlotCategory)] public Cursor ZoomVerticalCursor { get; set; } /// /// Get the axes from a point. /// /// /// The point. /// /// /// The x axis. /// /// /// The y axis. /// public void GetAxesFromPoint(ScreenPoint pt, out Axis xaxis, out Axis yaxis) { if (this.Model == null) { xaxis = null; yaxis = null; return; } this.Model.GetAxesFromPoint(pt, out xaxis, out yaxis); } /// /// Get the series from a point. /// /// /// The point (screen coordinates). /// /// /// The limit. /// /// /// The series. /// public Series GetSeriesFromPoint(ScreenPoint pt, double limit) { if (this.Model == null) { return null; } return this.Model.GetSeriesFromPoint(pt, limit); } /// /// The hide tracker. /// public void HideTracker() { } /// /// The hide zoom rectangle. /// public void HideZoomRectangle() { this.zoomRectangle = Rectangle.Empty; this.Invalidate(); } /// /// The invalidate plot. /// /// /// The update data. /// public void InvalidatePlot(bool updateData) { lock (this.invalidateLock) { this.isModelInvalidated = true; this.updateDataFlag = this.updateDataFlag || updateData; } this.Invalidate(); } /// /// Called when the Model property has been changed. /// public void OnModelChanged() { lock (this.modelLock) { if (this.currentModel != null) { this.currentModel.AttachPlotControl(null); } if (this.Model != null) { if (this.Model.PlotControl != null) { throw new InvalidOperationException( "This PlotModel is already in use by some other plot control."); } this.Model.AttachPlotControl(this); this.currentModel = this.Model; } } this.InvalidatePlot(true); } /// /// The pan. /// /// /// The axis. /// /// /// The x 0. /// /// /// The x 1. /// public void Pan(Axis axis, ScreenPoint x0, ScreenPoint x1) { axis.Pan(x0, x1); this.InvalidatePlot(false); } /// /// Pans all axes. /// /// /// The horizontal delta. /// /// /// The vertical delta. /// public void PanAll(double deltax, double deltay) { foreach (var a in this.ActualModel.Axes) { a.Pan(a.IsHorizontal() ? deltax : deltay); } this.InvalidatePlot(false); } /// /// The refresh plot. /// /// /// The update data. /// public void RefreshPlot(bool updateData) { lock (this.invalidateLock) { this.isModelInvalidated = true; this.updateDataFlag = this.updateDataFlag || updateData; } this.Refresh(); } /// /// The reset. /// /// /// The axis. /// public void Reset(Axis axis) { axis.Reset(); this.InvalidatePlot(false); } /// /// Sets the cursor type. /// /// /// The cursor type. /// public void SetCursorType(CursorType cursorType) { switch (cursorType) { case CursorType.Pan: this.Cursor = this.PanCursor; break; case CursorType.ZoomRectangle: this.Cursor = this.ZoomRectangleCursor; break; case CursorType.ZoomHorizontal: this.Cursor = this.ZoomHorizontalCursor; break; case CursorType.ZoomVertical: this.Cursor = this.ZoomVerticalCursor; break; default: this.Cursor = Cursors.Arrow; break; } } /// /// The show tracker. /// /// /// The data. /// public void ShowTracker(TrackerHitResult data) { // not implemented for WindowsForms } /// /// The show zoom rectangle. /// /// /// The r. /// public void ShowZoomRectangle(OxyRect r) { this.zoomRectangle = new Rectangle((int)r.Left, (int)r.Top, (int)r.Width, (int)r.Height); this.Invalidate(); } /// /// The zoom. /// /// /// The axis. /// /// /// The p 1. /// /// /// The p 2. /// public void Zoom(Axis axis, double p1, double p2) { axis.Zoom(p1, p2); this.InvalidatePlot(false); } /// /// The zoom all. /// public void ZoomAll() { foreach (var a in this.Model.Axes) { a.Reset(); } this.InvalidatePlot(false); } /// /// Zooms all axes. /// /// /// The delta. /// public void ZoomAllAxes(double delta) { foreach (var a in this.ActualModel.Axes) { this.ZoomAt(a, delta); } this.RefreshPlot(false); } /// /// The zoom at. /// /// /// The axis. /// /// /// The factor. /// /// /// The x. /// public void ZoomAt(Axis axis, double factor, double x = double.NaN) { if (double.IsNaN(x)) { double sx = (axis.Transform(axis.ActualMaximum) + axis.Transform(axis.ActualMinimum)) * 0.5; x = axis.InverseTransform(sx); } axis.ZoomAt(factor, x); this.InvalidatePlot(false); } /// /// The on mouse down. /// /// /// The e. /// protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); if (this.mouseManipulator != null) { return; } this.Focus(); this.Capture = true; if (this.ActualModel != null) { var args = this.CreateMouseEventArgs(e); this.ActualModel.HandleMouseDown(this, args); if (args.Handled) { return; } } this.mouseManipulator = this.GetManipulator(e); if (this.mouseManipulator != null) { this.mouseManipulator.Started(this.CreateManipulationEventArgs(e)); } } /// /// The on mouse move. /// /// /// The e. /// protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (this.ActualModel != null) { var args = this.CreateMouseEventArgs(e); this.ActualModel.HandleMouseMove(this, args); if (args.Handled) { return; } } if (this.mouseManipulator != null) { this.mouseManipulator.Delta(this.CreateManipulationEventArgs(e)); } } /// /// Raises the event. /// /// /// A that contains the event data. /// protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); this.Capture = false; if (this.ActualModel != null) { var args = this.CreateMouseEventArgs(e); this.ActualModel.HandleMouseUp(this, args); if (args.Handled) { return; } } if (this.mouseManipulator != null) { this.mouseManipulator.Completed(this.CreateManipulationEventArgs(e)); } this.mouseManipulator = null; } /// /// Raises the event. /// /// /// A that contains the event data. /// protected override void OnMouseWheel(MouseEventArgs e) { base.OnMouseWheel(e); bool isControlDown = ModifierKeys == Keys.Control; var m = new ZoomStepManipulator(this, e.Delta * 0.001, isControlDown); m.Started(new ManipulationEventArgs(e.Location.ToScreenPoint())); } /// /// Raises the event. /// /// /// A that contains the event data. /// protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); try { lock (this.invalidateLock) { if (this.isModelInvalidated) { if (this.model != null) { this.model.Update(this.updateDataFlag); this.updateDataFlag = false; } this.isModelInvalidated = false; } } lock (this.renderingLock) { this.renderContext.SetGraphicsTarget(e.Graphics); if (this.model != null) { this.model.Render(this.renderContext, this.Width, this.Height); } if (this.zoomRectangle != Rectangle.Empty) { using (var zoomBrush = new SolidBrush(Color.FromArgb(0x40, 0xFF, 0xFF, 0x00))) using (var zoomPen = new Pen(Color.Black)) { zoomPen.DashPattern = new float[] { 3, 1 }; e.Graphics.FillRectangle(zoomBrush, this.zoomRectangle); e.Graphics.DrawRectangle(zoomPen, this.zoomRectangle); } } } } catch (Exception paintException) { var trace = new StackTrace(paintException); Debug.WriteLine(paintException); Debug.WriteLine(trace); using (var font = new Font("Arial", 10)) { e.Graphics.DrawString( "OxyPlot paint exception: " + paintException.Message, font, Brushes.Red, 10, 10); } } } /// /// Raises the event. /// /// /// A that contains the event data. /// protected override void OnPreviewKeyDown(PreviewKeyDownEventArgs e) { base.OnPreviewKeyDown(e); if (e.KeyCode == Keys.A) { this.ZoomAll(); } bool control = (e.Modifiers & Keys.Control) == Keys.Control; bool alt = (e.Modifiers & Keys.Alt) == Keys.Alt; double deltax = 0; double deltay = 0; double zoom = 0; switch (e.KeyCode) { case Keys.Up: deltay = -1; break; case Keys.Down: deltay = 1; break; case Keys.Left: deltax = -1; break; case Keys.Right: deltax = 1; break; case Keys.Add: case Keys.Oemplus: case Keys.PageUp: zoom = 1; break; case Keys.Subtract: case Keys.OemMinus: case Keys.PageDown: zoom = -1; break; } if ((deltax * deltax) + (deltay * deltay) > 0) { deltax = deltax * this.ActualModel.PlotArea.Width * this.KeyboardPanHorizontalStep; deltay = deltay * this.ActualModel.PlotArea.Height * this.KeyboardPanVerticalStep; // small steps if the user is pressing control if (control) { deltax *= 0.2; deltay *= 0.2; } this.PanAll(deltax, deltay); // e.Handled = true; } if (Math.Abs(zoom) > 1e-8) { if (control) { zoom *= 0.2; } this.ZoomAllAxes(1 + (zoom * 0.12)); // e.Handled = true; } if (control && alt && this.ActualModel != null) { switch (e.KeyCode) { case Keys.R: this.SetClipboardText(this.ActualModel.CreateTextReport()); break; case Keys.C: this.SetClipboardText(this.ActualModel.ToCode()); break; case Keys.X: // this.SetClipboardText(this.ActualModel.ToXml()); break; } } } /// /// Raises the event. /// /// /// An that contains the event data. /// protected override void OnResize(EventArgs e) { base.OnResize(e); this.InvalidatePlot(false); } /// /// Converts the changed button. /// /// /// The instance containing the event data. /// /// /// The mouse button. /// private static OxyMouseButton ConvertChangedButton(MouseEventArgs e) { switch (e.Button) { case MouseButtons.Left: return OxyMouseButton.Left; case MouseButtons.Middle: return OxyMouseButton.Middle; case MouseButtons.Right: return OxyMouseButton.Right; case MouseButtons.XButton1: return OxyMouseButton.XButton1; case MouseButtons.XButton2: return OxyMouseButton.XButton2; } return OxyMouseButton.Left; } /// /// Creates the mouse event arguments. /// /// /// The instance containing the event data. /// /// /// Mouse event arguments. /// private OxyMouseEventArgs CreateMouseEventArgs(MouseEventArgs e) { return new OxyMouseEventArgs { ChangedButton = ConvertChangedButton(e), Position = new ScreenPoint(e.Location.X, e.Location.Y), IsShiftDown = (ModifierKeys & Keys.Shift) == Keys.Shift, IsControlDown = (ModifierKeys & Keys.Control) == Keys.Control, IsAltDown = (ModifierKeys & Keys.Alt) == Keys.Alt, }; } /// /// Creates the manipulation event args. /// /// /// The MouseEventArgs instance containing the event data. /// /// /// A manipulation event args object. /// private ManipulationEventArgs CreateManipulationEventArgs(MouseEventArgs e) { return new ManipulationEventArgs(e.Location.ToScreenPoint()); } /// /// Gets the manipulator for the current mouse button and modifier keys. /// /// /// The event args. /// /// /// A manipulator or null if no gesture was recognized. /// private ManipulatorBase GetManipulator(MouseEventArgs e) { bool control = (ModifierKeys & Keys.Control) == Keys.Control; bool shift = (ModifierKeys & Keys.Shift) == Keys.Shift; bool alt = (ModifierKeys & Keys.Alt) == Keys.Alt; bool lmb = e.Button == MouseButtons.Left; bool rmb = e.Button == MouseButtons.Right; bool mmb = e.Button == MouseButtons.Middle; bool xb1 = e.Button == MouseButtons.XButton1; bool xb2 = e.Button == MouseButtons.XButton2; // MMB / control RMB / control+alt LMB if (mmb || (control && lmb) || (control && alt && rmb)) { return new ZoomRectangleManipulator(this); } // Right mouse button / alt+left mouse button if (lmb || (rmb && alt)) { if (e.Clicks == 2) { return new ResetManipulator(this); } return new PanManipulator(this); } // Left mouse button if (rmb) { return new TrackerManipulator(this) { Snap = !control, PointsOnly = shift }; } // XButtons are zoom-stepping if (xb1 || xb2) { double d = xb1 ? 0.05 : -0.05; return new ZoomStepManipulator(this, d, control); } return null; } /// /// The set clipboard text. /// /// /// The text. /// private void SetClipboardText(string text) { try { // todo: can't get the following solution to work // http://stackoverflow.com/questions/5707990/requested-clipboard-operation-did-not-succeed Clipboard.SetText(text); } catch (ExternalException ee) { // Requested Clipboard operation did not succeed. MessageBox.Show(this, ee.Message, "OxyPlot"); } } } }